Explorar el Código

decaffeinate: Convert ApplyingUpdatesToADocTests.coffee and 14 other files to JS

decaffeinate hace 6 años
padre
commit
adffde3059

+ 485 - 380
services/document-updater/test/acceptance/coffee/ApplyingUpdatesToADocTests.js

@@ -1,394 +1,499 @@
-sinon = require "sinon"
-chai = require("chai")
-chai.should()
-expect = chai.expect
-async = require "async"
-Settings = require('settings-sharelatex')
-rclient_history = require("redis-sharelatex").createClient(Settings.redis.history) # note: this is track changes, not project-history
-rclient_project_history = require("redis-sharelatex").createClient(Settings.redis.project_history)
-rclient_du = require("redis-sharelatex").createClient(Settings.redis.documentupdater)
-Keys = Settings.redis.documentupdater.key_schema
-HistoryKeys = Settings.redis.history.key_schema
-ProjectHistoryKeys = Settings.redis.project_history.key_schema
-
-MockTrackChangesApi = require "./helpers/MockTrackChangesApi"
-MockWebApi = require "./helpers/MockWebApi"
-DocUpdaterClient = require "./helpers/DocUpdaterClient"
-DocUpdaterApp = require "./helpers/DocUpdaterApp"
-
-describe "Applying updates to a doc", ->
-	before (done) ->
-		@lines = ["one", "two", "three"]
-		@version = 42
-		@update =
-			doc: @doc_id
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require("sinon");
+const chai = require("chai");
+chai.should();
+const {
+    expect
+} = chai;
+const async = require("async");
+const Settings = require('settings-sharelatex');
+const rclient_history = require("redis-sharelatex").createClient(Settings.redis.history); // note: this is track changes, not project-history
+const rclient_project_history = require("redis-sharelatex").createClient(Settings.redis.project_history);
+const rclient_du = require("redis-sharelatex").createClient(Settings.redis.documentupdater);
+const Keys = Settings.redis.documentupdater.key_schema;
+const HistoryKeys = Settings.redis.history.key_schema;
+const ProjectHistoryKeys = Settings.redis.project_history.key_schema;
+
+const MockTrackChangesApi = require("./helpers/MockTrackChangesApi");
+const MockWebApi = require("./helpers/MockWebApi");
+const DocUpdaterClient = require("./helpers/DocUpdaterClient");
+const DocUpdaterApp = require("./helpers/DocUpdaterApp");
+
+describe("Applying updates to a doc", function() {
+	before(function(done) {
+		this.lines = ["one", "two", "three"];
+		this.version = 42;
+		this.update = {
+			doc: this.doc_id,
 			op: [{
-				i: "one and a half\n"
+				i: "one and a half\n",
 				p: 4
-			}]
-			v: @version
-		@result = ["one", "one and a half", "two", "three"]
-		DocUpdaterApp.ensureRunning(done)
-
-	describe "when the document is not loaded", ->
-		before (done) ->
-			[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
-			sinon.spy MockWebApi, "getDocument"
-			@startTime = Date.now()
-			MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
-			DocUpdaterClient.sendUpdate @project_id, @doc_id, @update, (error) ->
-				throw error if error?
-				setTimeout done, 200
-			return null
-
-		after ->
-			MockWebApi.getDocument.restore()
-
-		it "should load the document from the web API", ->
-			MockWebApi.getDocument
-				.calledWith(@project_id, @doc_id)
-				.should.equal true
-
-		it "should update the doc", (done) ->
-			DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
-				doc.lines.should.deep.equal @result
-				done()
-			return null
-
-		it "should push the applied updates to the track changes api", (done) ->
-			rclient_history.lrange HistoryKeys.uncompressedHistoryOps({@doc_id}), 0, -1, (error, updates) =>
-				throw error if error?
-				JSON.parse(updates[0]).op.should.deep.equal @update.op
-				rclient_history.sismember HistoryKeys.docsWithHistoryOps({@project_id}), @doc_id, (error, result) =>
-					throw error if error?
-					result.should.equal 1
-					done()
-			return null
-
-		it "should push the applied updates to the project history changes api", (done) ->
-			rclient_project_history.lrange ProjectHistoryKeys.projectHistoryOps({@project_id}), 0, -1, (error, updates) =>
-				throw error if error?
-				JSON.parse(updates[0]).op.should.deep.equal @update.op
-				done()
-			return null
-
-		it "should set the first op timestamp", (done) ->
-			rclient_project_history.get ProjectHistoryKeys.projectHistoryFirstOpTimestamp({@project_id}), (error, result) =>
-				throw error if error?
-				result.should.be.within(@startTime, Date.now())
-				@firstOpTimestamp = result
-				done()
-			return null
-
-		describe "when sending another update", ->
-			before (done) ->
-				@timeout = 10000
-				@second_update = Object.create(@update)
-				@second_update.v = @version + 1
-				DocUpdaterClient.sendUpdate @project_id, @doc_id, @second_update, (error) ->
-					throw error if error?
-					setTimeout done, 200
-				return null
-
-			it "should not change the first op timestamp", (done) ->
-				rclient_project_history.get ProjectHistoryKeys.projectHistoryFirstOpTimestamp({@project_id}), (error, result) =>
-					throw error if error?
-					result.should.equal @firstOpTimestamp
-					done()
-				return null
-
-	describe "when the document is loaded", ->
-		before (done) ->
-			[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
-
-			MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
-			DocUpdaterClient.preloadDoc @project_id, @doc_id, (error) =>
-				throw error if error?
-				sinon.spy MockWebApi, "getDocument"
-				DocUpdaterClient.sendUpdate @project_id, @doc_id, @update, (error) ->
-					throw error if error?
-					setTimeout done, 200
-			return null
-
-		after ->
-			MockWebApi.getDocument.restore()
-
-		it "should not need to call the web api", ->
-			MockWebApi.getDocument.called.should.equal false
-
-		it "should update the doc", (done) ->
-			DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
-				doc.lines.should.deep.equal @result
-				done()
-			return null
-
-		it "should push the applied updates to the track changes api", (done) ->
-			rclient_history.lrange HistoryKeys.uncompressedHistoryOps({@doc_id}), 0, -1, (error, updates) =>
-				JSON.parse(updates[0]).op.should.deep.equal @update.op
-				rclient_history.sismember HistoryKeys.docsWithHistoryOps({@project_id}), @doc_id, (error, result) =>
-					result.should.equal 1
-					done()
-			return null
-
-		it "should push the applied updates to the project history changes api", (done) ->
-			rclient_project_history.lrange ProjectHistoryKeys.projectHistoryOps({@project_id}), 0, -1, (error, updates) =>
-				JSON.parse(updates[0]).op.should.deep.equal @update.op
-				done()
-			return null
-
-	describe "when the document is loaded and is using project-history only", ->
-		before (done) ->
-			[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
-
-			MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version, projectHistoryType: 'project-history'}
-			DocUpdaterClient.preloadDoc @project_id, @doc_id, (error) =>
-				throw error if error?
-				sinon.spy MockWebApi, "getDocument"
-				DocUpdaterClient.sendUpdate @project_id, @doc_id, @update, (error) ->
-					throw error if error?
-					setTimeout done, 200
-			return null
-
-		after ->
-			MockWebApi.getDocument.restore()
-
-		it "should update the doc", (done) ->
-			DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
-				doc.lines.should.deep.equal @result
-				done()
-			return null
-
-		it "should not push any applied updates to the track changes api", (done) ->
-			rclient_history.lrange HistoryKeys.uncompressedHistoryOps({@doc_id}), 0, -1, (error, updates) =>
-				updates.length.should.equal 0
-				done()
-			return null
-
-		it "should push the applied updates to the project history changes api", (done) ->
-			rclient_project_history.lrange ProjectHistoryKeys.projectHistoryOps({@project_id}), 0, -1, (error, updates) =>
-				JSON.parse(updates[0]).op.should.deep.equal @update.op
-				done()
-			return null
-
-	describe "when the document has been deleted", ->
-		describe "when the ops come in a single linear order", ->
-			before (done) ->
-				[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
-				lines = ["", "", ""]
-				MockWebApi.insertDoc @project_id, @doc_id, {lines: lines, version: 0}
-				@updates = [
-					{ doc_id: @doc_id, v: 0,  op: [i: "h", p: 0 ] }
-					{ doc_id: @doc_id, v: 1,  op: [i: "e", p: 1 ] }
-					{ doc_id: @doc_id, v: 2,  op: [i: "l", p: 2 ] }
-					{ doc_id: @doc_id, v: 3,  op: [i: "l", p: 3 ] }
-					{ doc_id: @doc_id, v: 4,  op: [i: "o", p: 4 ] }
-					{ doc_id: @doc_id, v: 5,  op: [i: " ", p: 5 ] }
-					{ doc_id: @doc_id, v: 6,  op: [i: "w", p: 6 ] }
-					{ doc_id: @doc_id, v: 7,  op: [i: "o", p: 7 ] }
-					{ doc_id: @doc_id, v: 8,  op: [i: "r", p: 8 ] }
-					{ doc_id: @doc_id, v: 9,  op: [i: "l", p: 9 ] }
-					{ doc_id: @doc_id, v: 10, op: [i: "d", p: 10] }
-				]
-				@my_result = ["hello world", "", ""]
-				done()
-
-			it "should be able to continue applying updates when the project has been deleted", (done) ->
-				actions = []
-				for update in @updates.slice(0,6)
-					do (update) =>
-						actions.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc_id, update, callback
-				actions.push (callback) => DocUpdaterClient.deleteDoc @project_id, @doc_id, callback
-				for update in @updates.slice(6)
-					do (update) =>
-						actions.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc_id, update, callback
-
-				async.series actions, (error) =>
-					throw error if error?
-					DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
-						doc.lines.should.deep.equal @my_result
-						done()
-				return null
-
-			it "should push the applied updates to the track changes api", (done) ->
-				rclient_history.lrange HistoryKeys.uncompressedHistoryOps({@doc_id}), 0, -1, (error, updates) =>
-					updates = (JSON.parse(u) for u in updates)
-					for appliedUpdate, i in @updates
-						appliedUpdate.op.should.deep.equal updates[i].op
-
-					rclient_history.sismember HistoryKeys.docsWithHistoryOps({@project_id}), @doc_id, (error, result) =>
-						result.should.equal 1
-						done()
-				return null
-
-			it "should store the doc ops in the correct order", (done) ->
-				rclient_du.lrange Keys.docOps({doc_id: @doc_id}), 0, -1, (error, updates) =>
-					updates = (JSON.parse(u) for u in updates)
-					for appliedUpdate, i in @updates
-						appliedUpdate.op.should.deep.equal updates[i].op
-					done()
-				return null
-
-		describe "when older ops come in after the delete", ->
-			before (done) ->
-				[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
-				lines = ["", "", ""]
-				MockWebApi.insertDoc @project_id, @doc_id, {lines: lines, version: 0}
-				@updates = [
-					{ doc_id: @doc_id, v: 0, op: [i: "h", p: 0 ] }
-					{ doc_id: @doc_id, v: 1, op: [i: "e", p: 1 ] }
-					{ doc_id: @doc_id, v: 2, op: [i: "l", p: 2 ] }
-					{ doc_id: @doc_id, v: 3, op: [i: "l", p: 3 ] }
-					{ doc_id: @doc_id, v: 4, op: [i: "o", p: 4 ] }
-					{ doc_id: @doc_id, v: 0, op: [i: "world", p: 1 ] }
-				]
-				@my_result = ["hello", "world", ""]
-				done()
-
-			it "should be able to continue applying updates when the project has been deleted", (done) ->
-				actions = []
-				for update in @updates.slice(0,5)
-					do (update) =>
-						actions.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc_id, update, callback
-				actions.push (callback) => DocUpdaterClient.deleteDoc @project_id, @doc_id, callback
-				for update in @updates.slice(5)
-					do (update) =>
-						actions.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc_id, update, callback
-
-				async.series actions, (error) =>
-					throw error if error?
-					DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
-						doc.lines.should.deep.equal @my_result
-						done()
-				return null
-
-	describe "with a broken update", ->
-		before (done) ->
-			[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
-			@broken_update = { doc_id: @doc_id, v: @version, op: [d: "not the correct content", p: 0 ] }
-			MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
-
-			DocUpdaterClient.subscribeToAppliedOps @messageCallback = sinon.stub()
-
-			DocUpdaterClient.sendUpdate @project_id, @doc_id, @broken_update, (error) ->
-				throw error if error?
-				setTimeout done, 200
-			return null
-
-		it "should not update the doc", (done) ->
-			DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
-				doc.lines.should.deep.equal @lines
-				done()
-			return null
-
-		it "should send a message with an error", ->
-			@messageCallback.called.should.equal true
-			[channel, message] = @messageCallback.args[0]
-			channel.should.equal "applied-ops"
-			JSON.parse(message).should.deep.include {
-				project_id: @project_id,
-				doc_id: @doc_id,
+			}],
+			v: this.version
+		};
+		this.result = ["one", "one and a half", "two", "three"];
+		return DocUpdaterApp.ensureRunning(done);
+	});
+
+	describe("when the document is not loaded", function() {
+		before(function(done) {
+			[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
+			sinon.spy(MockWebApi, "getDocument");
+			this.startTime = Date.now();
+			MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
+			DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, this.update, function(error) {
+				if (error != null) { throw error; }
+				return setTimeout(done, 200);
+			});
+			return null;
+		});
+
+		after(() => MockWebApi.getDocument.restore());
+
+		it("should load the document from the web API", function() {
+			return MockWebApi.getDocument
+				.calledWith(this.project_id, this.doc_id)
+				.should.equal(true);
+		});
+
+		it("should update the doc", function(done) {
+			DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
+				doc.lines.should.deep.equal(this.result);
+				return done();
+			});
+			return null;
+		});
+
+		it("should push the applied updates to the track changes api", function(done) {
+			rclient_history.lrange(HistoryKeys.uncompressedHistoryOps({doc_id: this.doc_id}), 0, -1, (error, updates) => {
+				if (error != null) { throw error; }
+				JSON.parse(updates[0]).op.should.deep.equal(this.update.op);
+				return rclient_history.sismember(HistoryKeys.docsWithHistoryOps({project_id: this.project_id}), this.doc_id, (error, result) => {
+					if (error != null) { throw error; }
+					result.should.equal(1);
+					return done();
+				});
+			});
+			return null;
+		});
+
+		it("should push the applied updates to the project history changes api", function(done) {
+			rclient_project_history.lrange(ProjectHistoryKeys.projectHistoryOps({project_id: this.project_id}), 0, -1, (error, updates) => {
+				if (error != null) { throw error; }
+				JSON.parse(updates[0]).op.should.deep.equal(this.update.op);
+				return done();
+			});
+			return null;
+		});
+
+		it("should set the first op timestamp", function(done) {
+			rclient_project_history.get(ProjectHistoryKeys.projectHistoryFirstOpTimestamp({project_id: this.project_id}), (error, result) => {
+				if (error != null) { throw error; }
+				result.should.be.within(this.startTime, Date.now());
+				this.firstOpTimestamp = result;
+				return done();
+			});
+			return null;
+		});
+
+		return describe("when sending another update", function() {
+			before(function(done) {
+				this.timeout = 10000;
+				this.second_update = Object.create(this.update);
+				this.second_update.v = this.version + 1;
+				DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, this.second_update, function(error) {
+					if (error != null) { throw error; }
+					return setTimeout(done, 200);
+				});
+				return null;
+			});
+
+			return it("should not change the first op timestamp", function(done) {
+				rclient_project_history.get(ProjectHistoryKeys.projectHistoryFirstOpTimestamp({project_id: this.project_id}), (error, result) => {
+					if (error != null) { throw error; }
+					result.should.equal(this.firstOpTimestamp);
+					return done();
+				});
+				return null;
+			});
+		});
+	});
+
+	describe("when the document is loaded", function() {
+		before(function(done) {
+			[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
+
+			MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
+			DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
+				if (error != null) { throw error; }
+				sinon.spy(MockWebApi, "getDocument");
+				return DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, this.update, function(error) {
+					if (error != null) { throw error; }
+					return setTimeout(done, 200);
+				});
+			});
+			return null;
+		});
+
+		after(() => MockWebApi.getDocument.restore());
+
+		it("should not need to call the web api", () => MockWebApi.getDocument.called.should.equal(false));
+
+		it("should update the doc", function(done) {
+			DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
+				doc.lines.should.deep.equal(this.result);
+				return done();
+			});
+			return null;
+		});
+
+		it("should push the applied updates to the track changes api", function(done) {
+			rclient_history.lrange(HistoryKeys.uncompressedHistoryOps({doc_id: this.doc_id}), 0, -1, (error, updates) => {
+				JSON.parse(updates[0]).op.should.deep.equal(this.update.op);
+				return rclient_history.sismember(HistoryKeys.docsWithHistoryOps({project_id: this.project_id}), this.doc_id, (error, result) => {
+					result.should.equal(1);
+					return done();
+				});
+			});
+			return null;
+		});
+
+		return it("should push the applied updates to the project history changes api", function(done) {
+			rclient_project_history.lrange(ProjectHistoryKeys.projectHistoryOps({project_id: this.project_id}), 0, -1, (error, updates) => {
+				JSON.parse(updates[0]).op.should.deep.equal(this.update.op);
+				return done();
+			});
+			return null;
+		});
+	});
+
+	describe("when the document is loaded and is using project-history only", function() {
+		before(function(done) {
+			[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
+
+			MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version, projectHistoryType: 'project-history'});
+			DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
+				if (error != null) { throw error; }
+				sinon.spy(MockWebApi, "getDocument");
+				return DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, this.update, function(error) {
+					if (error != null) { throw error; }
+					return setTimeout(done, 200);
+				});
+			});
+			return null;
+		});
+
+		after(() => MockWebApi.getDocument.restore());
+
+		it("should update the doc", function(done) {
+			DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
+				doc.lines.should.deep.equal(this.result);
+				return done();
+			});
+			return null;
+		});
+
+		it("should not push any applied updates to the track changes api", function(done) {
+			rclient_history.lrange(HistoryKeys.uncompressedHistoryOps({doc_id: this.doc_id}), 0, -1, (error, updates) => {
+				updates.length.should.equal(0);
+				return done();
+			});
+			return null;
+		});
+
+		return it("should push the applied updates to the project history changes api", function(done) {
+			rclient_project_history.lrange(ProjectHistoryKeys.projectHistoryOps({project_id: this.project_id}), 0, -1, (error, updates) => {
+				JSON.parse(updates[0]).op.should.deep.equal(this.update.op);
+				return done();
+			});
+			return null;
+		});
+	});
+
+	describe("when the document has been deleted", function() {
+		describe("when the ops come in a single linear order", function() {
+			before(function(done) {
+				[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
+				const lines = ["", "", ""];
+				MockWebApi.insertDoc(this.project_id, this.doc_id, {lines, version: 0});
+				this.updates = [
+					{ doc_id: this.doc_id, v: 0,  op: [{i: "h", p: 0} ] },
+					{ doc_id: this.doc_id, v: 1,  op: [{i: "e", p: 1} ] },
+					{ doc_id: this.doc_id, v: 2,  op: [{i: "l", p: 2} ] },
+					{ doc_id: this.doc_id, v: 3,  op: [{i: "l", p: 3} ] },
+					{ doc_id: this.doc_id, v: 4,  op: [{i: "o", p: 4} ] },
+					{ doc_id: this.doc_id, v: 5,  op: [{i: " ", p: 5} ] },
+					{ doc_id: this.doc_id, v: 6,  op: [{i: "w", p: 6} ] },
+					{ doc_id: this.doc_id, v: 7,  op: [{i: "o", p: 7} ] },
+					{ doc_id: this.doc_id, v: 8,  op: [{i: "r", p: 8} ] },
+					{ doc_id: this.doc_id, v: 9,  op: [{i: "l", p: 9} ] },
+					{ doc_id: this.doc_id, v: 10, op: [{i: "d", p: 10}] }
+				];
+				this.my_result = ["hello world", "", ""];
+				return done();
+			});
+
+			it("should be able to continue applying updates when the project has been deleted", function(done) {
+				let update;
+				const actions = [];
+				for (update of Array.from(this.updates.slice(0,6))) {
+					(update => {
+						return actions.push(callback => DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, update, callback));
+					})(update);
+				}
+				actions.push(callback => DocUpdaterClient.deleteDoc(this.project_id, this.doc_id, callback));
+				for (update of Array.from(this.updates.slice(6))) {
+					(update => {
+						return actions.push(callback => DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, update, callback));
+					})(update);
+				}
+
+				async.series(actions, error => {
+					if (error != null) { throw error; }
+					return DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
+						doc.lines.should.deep.equal(this.my_result);
+						return done();
+					});
+				});
+				return null;
+			});
+
+			it("should push the applied updates to the track changes api", function(done) {
+				rclient_history.lrange(HistoryKeys.uncompressedHistoryOps({doc_id: this.doc_id}), 0, -1, (error, updates) => {
+					updates = (Array.from(updates).map((u) => JSON.parse(u)));
+					for (let i = 0; i < this.updates.length; i++) {
+						const appliedUpdate = this.updates[i];
+						appliedUpdate.op.should.deep.equal(updates[i].op);
+					}
+
+					return rclient_history.sismember(HistoryKeys.docsWithHistoryOps({project_id: this.project_id}), this.doc_id, (error, result) => {
+						result.should.equal(1);
+						return done();
+					});
+				});
+				return null;
+			});
+
+			return it("should store the doc ops in the correct order", function(done) {
+				rclient_du.lrange(Keys.docOps({doc_id: this.doc_id}), 0, -1, (error, updates) => {
+					updates = (Array.from(updates).map((u) => JSON.parse(u)));
+					for (let i = 0; i < this.updates.length; i++) {
+						const appliedUpdate = this.updates[i];
+						appliedUpdate.op.should.deep.equal(updates[i].op);
+					}
+					return done();
+				});
+				return null;
+			});
+		});
+
+		return describe("when older ops come in after the delete", function() {
+			before(function(done) {
+				[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
+				const lines = ["", "", ""];
+				MockWebApi.insertDoc(this.project_id, this.doc_id, {lines, version: 0});
+				this.updates = [
+					{ doc_id: this.doc_id, v: 0, op: [{i: "h", p: 0} ] },
+					{ doc_id: this.doc_id, v: 1, op: [{i: "e", p: 1} ] },
+					{ doc_id: this.doc_id, v: 2, op: [{i: "l", p: 2} ] },
+					{ doc_id: this.doc_id, v: 3, op: [{i: "l", p: 3} ] },
+					{ doc_id: this.doc_id, v: 4, op: [{i: "o", p: 4} ] },
+					{ doc_id: this.doc_id, v: 0, op: [{i: "world", p: 1} ] }
+				];
+				this.my_result = ["hello", "world", ""];
+				return done();
+			});
+
+			return it("should be able to continue applying updates when the project has been deleted", function(done) {
+				let update;
+				const actions = [];
+				for (update of Array.from(this.updates.slice(0,5))) {
+					(update => {
+						return actions.push(callback => DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, update, callback));
+					})(update);
+				}
+				actions.push(callback => DocUpdaterClient.deleteDoc(this.project_id, this.doc_id, callback));
+				for (update of Array.from(this.updates.slice(5))) {
+					(update => {
+						return actions.push(callback => DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, update, callback));
+					})(update);
+				}
+
+				async.series(actions, error => {
+					if (error != null) { throw error; }
+					return DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
+						doc.lines.should.deep.equal(this.my_result);
+						return done();
+					});
+				});
+				return null;
+			});
+		});
+	});
+
+	describe("with a broken update", function() {
+		before(function(done) {
+			[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
+			this.broken_update = { doc_id: this.doc_id, v: this.version, op: [{d: "not the correct content", p: 0} ] };
+			MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
+
+			DocUpdaterClient.subscribeToAppliedOps(this.messageCallback = sinon.stub());
+
+			DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, this.broken_update, function(error) {
+				if (error != null) { throw error; }
+				return setTimeout(done, 200);
+			});
+			return null;
+		});
+
+		it("should not update the doc", function(done) {
+			DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
+				doc.lines.should.deep.equal(this.lines);
+				return done();
+			});
+			return null;
+		});
+
+		return it("should send a message with an error", function() {
+			this.messageCallback.called.should.equal(true);
+			const [channel, message] = Array.from(this.messageCallback.args[0]);
+			channel.should.equal("applied-ops");
+			return JSON.parse(message).should.deep.include({
+				project_id: this.project_id,
+				doc_id: this.doc_id,
 				error:'Delete component does not match'
+			});
+	});
+});
+
+	describe("with enough updates to flush to the track changes api", function() {
+		before(function(done) {
+			[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
+			const updates = [];
+			for (let v = 0; v <= 199; v++) { // Should flush after 100 ops
+				updates.push({
+					doc_id: this.doc_id,
+					op: [{i: v.toString(), p: 0}],
+					v
+				});
 			}
 
-	describe "with enough updates to flush to the track changes api", ->
-		before (done) ->
-			[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
-			updates = []
-			for v in [0..199] # Should flush after 100 ops
-				updates.push
-					doc_id: @doc_id,
-					op: [i: v.toString(), p: 0]
-					v: v
-
-			sinon.spy MockTrackChangesApi, "flushDoc"
-
-			MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: 0}
-
-			# Send updates in chunks to causes multiple flushes
-			actions = []
-			for i in [0..19]
-				do (i) =>
-					actions.push (cb) =>
-						DocUpdaterClient.sendUpdates @project_id, @doc_id, updates.slice(i*10, (i+1)*10), cb
-			async.series actions, (error) =>
-				throw error if error?
-				setTimeout done, 2000
-			return null
-
-		after ->
-			MockTrackChangesApi.flushDoc.restore()
-
-		it "should flush the doc twice", ->
-			MockTrackChangesApi.flushDoc.calledTwice.should.equal true
-
-	describe "when there is no version in Mongo", ->
-		before (done) ->
-			[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
-			MockWebApi.insertDoc @project_id, @doc_id, {
-				lines: @lines
-			}
+			sinon.spy(MockTrackChangesApi, "flushDoc");
+
+			MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: 0});
 
-			update =
-				doc: @doc_id
-				op: @update.op
+			// Send updates in chunks to causes multiple flushes
+			const actions = [];
+			for (let i = 0; i <= 19; i++) {
+				(i => {
+					return actions.push(cb => {
+						return DocUpdaterClient.sendUpdates(this.project_id, this.doc_id, updates.slice(i*10, (i+1)*10), cb);
+					});
+				})(i);
+			}
+			async.series(actions, error => {
+				if (error != null) { throw error; }
+				return setTimeout(done, 2000);
+			});
+			return null;
+		});
+
+		after(() => MockTrackChangesApi.flushDoc.restore());
+
+		return it("should flush the doc twice", () => MockTrackChangesApi.flushDoc.calledTwice.should.equal(true));
+	});
+
+	describe("when there is no version in Mongo", function() {
+		before(function(done) {
+			[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
+			MockWebApi.insertDoc(this.project_id, this.doc_id, {
+				lines: this.lines
+			});
+
+			const update = {
+				doc: this.doc_id,
+				op: this.update.op,
 				v: 0
-			DocUpdaterClient.sendUpdate @project_id, @doc_id, update, (error) ->
-				throw error if error?
-				setTimeout done, 200
-			return null
-
-		it "should update the doc (using version = 0)", (done) ->
-			DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
-				doc.lines.should.deep.equal @result
-				done()
-			return null
-
-	describe "when the sending duplicate ops", ->
-		before (done) ->
-			[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
-			MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
-
-			DocUpdaterClient.subscribeToAppliedOps @messageCallback = sinon.stub()
-
-			# One user delete 'one', the next turns it into 'once'. The second becomes a NOP.
-			DocUpdaterClient.sendUpdate @project_id, @doc_id, {
-				doc: @doc_id
+			};
+			DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, update, function(error) {
+				if (error != null) { throw error; }
+				return setTimeout(done, 200);
+			});
+			return null;
+		});
+
+		return it("should update the doc (using version = 0)", function(done) {
+			DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
+				doc.lines.should.deep.equal(this.result);
+				return done();
+			});
+			return null;
+		});
+	});
+
+	return describe("when the sending duplicate ops", function() {
+		before(function(done) {
+			[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
+			MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
+
+			DocUpdaterClient.subscribeToAppliedOps(this.messageCallback = sinon.stub());
+
+			// One user delete 'one', the next turns it into 'once'. The second becomes a NOP.
+			DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, {
+				doc: this.doc_id,
 				op: [{
-					i: "one and a half\n"
+					i: "one and a half\n",
 					p: 4
-				}]
-				v: @version
-				meta:
+				}],
+				v: this.version,
+				meta: {
 					source: "ikHceq3yfAdQYzBo4-xZ"
-			}, (error) =>
-				throw error if error?
-				setTimeout () =>
-					DocUpdaterClient.sendUpdate @project_id, @doc_id, {
-						doc: @doc_id
+				}
+			}, error => {
+				if (error != null) { throw error; }
+				return setTimeout(() => {
+					return DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, {
+						doc: this.doc_id,
 						op: [{
-							i: "one and a half\n"
+							i: "one and a half\n",
 							p: 4
-						}]
-						v: @version
-						dupIfSource: ["ikHceq3yfAdQYzBo4-xZ"]
-						meta:
+						}],
+						v: this.version,
+						dupIfSource: ["ikHceq3yfAdQYzBo4-xZ"],
+						meta: {
 							source: "ikHceq3yfAdQYzBo4-xZ"
-					}, (error) =>
-						throw error if error?
-						setTimeout done, 200
-				, 200
-			return null
-
-		it "should update the doc", (done) ->
-			DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
-				doc.lines.should.deep.equal @result
-				done()
-			return null
-
-		it "should return a message about duplicate ops", ->
-			@messageCallback.calledTwice.should.equal true
-			@messageCallback.args[0][0].should.equal "applied-ops"
-			expect(JSON.parse(@messageCallback.args[0][1]).op.dup).to.be.undefined
-			@messageCallback.args[1][0].should.equal "applied-ops"
-			expect(JSON.parse(@messageCallback.args[1][1]).op.dup).to.equal true
+						}
+					}, error => {
+						if (error != null) { throw error; }
+						return setTimeout(done, 200);
+					});
+				}
+				, 200);
+			});
+			return null;
+		});
+
+		it("should update the doc", function(done) {
+			DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
+				doc.lines.should.deep.equal(this.result);
+				return done();
+			});
+			return null;
+		});
+
+		return it("should return a message about duplicate ops", function() {
+			this.messageCallback.calledTwice.should.equal(true);
+			this.messageCallback.args[0][0].should.equal("applied-ops");
+			expect(JSON.parse(this.messageCallback.args[0][1]).op.dup).to.be.undefined;
+			this.messageCallback.args[1][0].should.equal("applied-ops");
+			return expect(JSON.parse(this.messageCallback.args[1][1]).op.dup).to.equal(true);
+		});
+	});
+});
 

+ 351 - 288
services/document-updater/test/acceptance/coffee/ApplyingUpdatesToProjectStructureTests.js

@@ -1,300 +1,363 @@
-sinon = require "sinon"
-chai = require("chai")
-chai.should()
-Settings = require('settings-sharelatex')
-rclient_project_history = require("redis-sharelatex").createClient(Settings.redis.project_history)
-ProjectHistoryKeys = Settings.redis.project_history.key_schema
-
-MockProjectHistoryApi = require "./helpers/MockProjectHistoryApi"
-MockWebApi = require "./helpers/MockWebApi"
-DocUpdaterClient = require "./helpers/DocUpdaterClient"
-DocUpdaterApp = require "./helpers/DocUpdaterApp"
-
-describe "Applying updates to a project's structure", ->
-	before ->
-		@user_id = 'user-id-123'
-		@version = 1234
-
-	describe "renaming a file", ->
-		before (done) ->
-			@project_id = DocUpdaterClient.randomId()
-			@fileUpdate =
-				id: DocUpdaterClient.randomId()
-				pathname: '/file-path'
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require("sinon");
+const chai = require("chai");
+chai.should();
+const Settings = require('settings-sharelatex');
+const rclient_project_history = require("redis-sharelatex").createClient(Settings.redis.project_history);
+const ProjectHistoryKeys = Settings.redis.project_history.key_schema;
+
+const MockProjectHistoryApi = require("./helpers/MockProjectHistoryApi");
+const MockWebApi = require("./helpers/MockWebApi");
+const DocUpdaterClient = require("./helpers/DocUpdaterClient");
+const DocUpdaterApp = require("./helpers/DocUpdaterApp");
+
+describe("Applying updates to a project's structure", function() {
+	before(function() {
+		this.user_id = 'user-id-123';
+		return this.version = 1234;
+	});
+
+	describe("renaming a file", function() {
+		before(function(done) {
+			this.project_id = DocUpdaterClient.randomId();
+			this.fileUpdate = {
+				id: DocUpdaterClient.randomId(),
+				pathname: '/file-path',
 				newPathname: '/new-file-path'
-			@fileUpdates = [ @fileUpdate ]
-			DocUpdaterApp.ensureRunning (error) =>
-				throw error if error?
-				DocUpdaterClient.sendProjectUpdate @project_id, @user_id, [], @fileUpdates, @version, (error) ->
-					throw error if error?
-					setTimeout done, 200
-
-		it "should push the applied file renames to the project history api", (done) ->
-			rclient_project_history.lrange ProjectHistoryKeys.projectHistoryOps({@project_id}), 0, -1, (error, updates) =>
-				throw error if error?
-
-				update = JSON.parse(updates[0])
-				update.file.should.equal @fileUpdate.id
-				update.pathname.should.equal '/file-path'
-				update.new_pathname.should.equal '/new-file-path'
-				update.meta.user_id.should.equal @user_id
-				update.meta.ts.should.be.a('string')
-				update.version.should.equal "#{@version}.0"
-
-				done()
-			return null
-
-	describe "renaming a document", ->
-		before ->
-			@docUpdate =
-				id: DocUpdaterClient.randomId()
-				pathname: '/doc-path'
+			};
+			this.fileUpdates = [ this.fileUpdate ];
+			return DocUpdaterApp.ensureRunning(error => {
+				if (error != null) { throw error; }
+				return DocUpdaterClient.sendProjectUpdate(this.project_id, this.user_id, [], this.fileUpdates, this.version, function(error) {
+					if (error != null) { throw error; }
+					return setTimeout(done, 200);
+				});
+			});
+		});
+
+		return it("should push the applied file renames to the project history api", function(done) {
+			rclient_project_history.lrange(ProjectHistoryKeys.projectHistoryOps({project_id: this.project_id}), 0, -1, (error, updates) => {
+				if (error != null) { throw error; }
+
+				const update = JSON.parse(updates[0]);
+				update.file.should.equal(this.fileUpdate.id);
+				update.pathname.should.equal('/file-path');
+				update.new_pathname.should.equal('/new-file-path');
+				update.meta.user_id.should.equal(this.user_id);
+				update.meta.ts.should.be.a('string');
+				update.version.should.equal(`${this.version}.0`);
+
+				return done();
+			});
+			return null;
+		});
+	});
+
+	describe("renaming a document", function() {
+		before(function() {
+			this.docUpdate = {
+				id: DocUpdaterClient.randomId(),
+				pathname: '/doc-path',
 				newPathname: '/new-doc-path'
-			@docUpdates = [ @docUpdate ]
-
-		describe "when the document is not loaded", ->
-			before (done) ->
-				@project_id = DocUpdaterClient.randomId()
-				DocUpdaterClient.sendProjectUpdate @project_id, @user_id, @docUpdates, [], @version, (error) ->
-					throw error if error?
-					setTimeout done, 200
-				return null
-
-			it "should push the applied doc renames to the project history api", (done) ->
-				rclient_project_history.lrange ProjectHistoryKeys.projectHistoryOps({@project_id}), 0, -1, (error, updates) =>
-					throw error if error?
-
-					update = JSON.parse(updates[0])
-					update.doc.should.equal @docUpdate.id
-					update.pathname.should.equal '/doc-path'
-					update.new_pathname.should.equal '/new-doc-path'
-					update.meta.user_id.should.equal @user_id
-					update.meta.ts.should.be.a('string')
-					update.version.should.equal "#{@version}.0"
-
-					done()
-				return null
-
-		describe "when the document is loaded", ->
-			before (done) ->
-				@project_id = DocUpdaterClient.randomId()
-				MockWebApi.insertDoc @project_id, @docUpdate.id, {}
-				DocUpdaterClient.preloadDoc @project_id, @docUpdate.id, (error) =>
-					throw error if error?
-					sinon.spy MockWebApi, "getDocument"
-					DocUpdaterClient.sendProjectUpdate @project_id, @user_id, @docUpdates, [], @version, (error) ->
-						throw error if error?
-						setTimeout done, 200
-				return null
-
-			after ->
-				MockWebApi.getDocument.restore()
-
-			it "should update the doc", (done) ->
-				DocUpdaterClient.getDoc @project_id, @docUpdate.id, (error, res, doc) =>
-					doc.pathname.should.equal @docUpdate.newPathname
-					done()
-				return null
-
-			it "should push the applied doc renames to the project history api", (done) ->
-				rclient_project_history.lrange ProjectHistoryKeys.projectHistoryOps({@project_id}), 0, -1, (error, updates) =>
-					throw error if error?
-
-					update = JSON.parse(updates[0])
-					update.doc.should.equal @docUpdate.id
-					update.pathname.should.equal '/doc-path'
-					update.new_pathname.should.equal '/new-doc-path'
-					update.meta.user_id.should.equal @user_id
-					update.meta.ts.should.be.a('string')
-					update.version.should.equal "#{@version}.0"
-
-					done()
-				return null
-
-	describe "renaming multiple documents and files", ->
-		before ->
-			@docUpdate0 =
-				id: DocUpdaterClient.randomId()
-				pathname: '/doc-path0'
+			};
+			return this.docUpdates = [ this.docUpdate ];});
+
+		describe("when the document is not loaded", function() {
+			before(function(done) {
+				this.project_id = DocUpdaterClient.randomId();
+				DocUpdaterClient.sendProjectUpdate(this.project_id, this.user_id, this.docUpdates, [], this.version, function(error) {
+					if (error != null) { throw error; }
+					return setTimeout(done, 200);
+				});
+				return null;
+			});
+
+			return it("should push the applied doc renames to the project history api", function(done) {
+				rclient_project_history.lrange(ProjectHistoryKeys.projectHistoryOps({project_id: this.project_id}), 0, -1, (error, updates) => {
+					if (error != null) { throw error; }
+
+					const update = JSON.parse(updates[0]);
+					update.doc.should.equal(this.docUpdate.id);
+					update.pathname.should.equal('/doc-path');
+					update.new_pathname.should.equal('/new-doc-path');
+					update.meta.user_id.should.equal(this.user_id);
+					update.meta.ts.should.be.a('string');
+					update.version.should.equal(`${this.version}.0`);
+
+					return done();
+				});
+				return null;
+			});
+		});
+
+		return describe("when the document is loaded", function() {
+			before(function(done) {
+				this.project_id = DocUpdaterClient.randomId();
+				MockWebApi.insertDoc(this.project_id, this.docUpdate.id, {});
+				DocUpdaterClient.preloadDoc(this.project_id, this.docUpdate.id, error => {
+					if (error != null) { throw error; }
+					sinon.spy(MockWebApi, "getDocument");
+					return DocUpdaterClient.sendProjectUpdate(this.project_id, this.user_id, this.docUpdates, [], this.version, function(error) {
+						if (error != null) { throw error; }
+						return setTimeout(done, 200);
+					});
+				});
+				return null;
+			});
+
+			after(() => MockWebApi.getDocument.restore());
+
+			it("should update the doc", function(done) {
+				DocUpdaterClient.getDoc(this.project_id, this.docUpdate.id, (error, res, doc) => {
+					doc.pathname.should.equal(this.docUpdate.newPathname);
+					return done();
+				});
+				return null;
+			});
+
+			return it("should push the applied doc renames to the project history api", function(done) {
+				rclient_project_history.lrange(ProjectHistoryKeys.projectHistoryOps({project_id: this.project_id}), 0, -1, (error, updates) => {
+					if (error != null) { throw error; }
+
+					const update = JSON.parse(updates[0]);
+					update.doc.should.equal(this.docUpdate.id);
+					update.pathname.should.equal('/doc-path');
+					update.new_pathname.should.equal('/new-doc-path');
+					update.meta.user_id.should.equal(this.user_id);
+					update.meta.ts.should.be.a('string');
+					update.version.should.equal(`${this.version}.0`);
+
+					return done();
+				});
+				return null;
+			});
+		});
+	});
+
+	describe("renaming multiple documents and files", function() {
+		before(function() {
+			this.docUpdate0 = {
+				id: DocUpdaterClient.randomId(),
+				pathname: '/doc-path0',
 				newPathname: '/new-doc-path0'
-			@docUpdate1 =
-				id: DocUpdaterClient.randomId()
-				pathname: '/doc-path1'
+			};
+			this.docUpdate1 = {
+				id: DocUpdaterClient.randomId(),
+				pathname: '/doc-path1',
 				newPathname: '/new-doc-path1'
-			@docUpdates = [ @docUpdate0, @docUpdate1 ]
-			@fileUpdate0 =
-				id: DocUpdaterClient.randomId()
-				pathname: '/file-path0'
+			};
+			this.docUpdates = [ this.docUpdate0, this.docUpdate1 ];
+			this.fileUpdate0 = {
+				id: DocUpdaterClient.randomId(),
+				pathname: '/file-path0',
 				newPathname: '/new-file-path0'
-			@fileUpdate1 =
-				id: DocUpdaterClient.randomId()
-				pathname: '/file-path1'
+			};
+			this.fileUpdate1 = {
+				id: DocUpdaterClient.randomId(),
+				pathname: '/file-path1',
 				newPathname: '/new-file-path1'
-			@fileUpdates = [ @fileUpdate0, @fileUpdate1 ]
-
-		describe "when the documents are not loaded", ->
-			before (done) ->
-				@project_id = DocUpdaterClient.randomId()
-				DocUpdaterClient.sendProjectUpdate @project_id, @user_id, @docUpdates, @fileUpdates, @version, (error) ->
-					throw error if error?
-					setTimeout done, 200
-				return null
-
-			it "should push the applied doc renames to the project history api", (done) ->
-				rclient_project_history.lrange ProjectHistoryKeys.projectHistoryOps({@project_id}), 0, -1, (error, updates) =>
-					throw error if error?
-
-					update = JSON.parse(updates[0])
-					update.doc.should.equal @docUpdate0.id
-					update.pathname.should.equal '/doc-path0'
-					update.new_pathname.should.equal '/new-doc-path0'
-					update.meta.user_id.should.equal @user_id
-					update.meta.ts.should.be.a('string')
-					update.version.should.equal "#{@version}.0"
-
-					update = JSON.parse(updates[1])
-					update.doc.should.equal @docUpdate1.id
-					update.pathname.should.equal '/doc-path1'
-					update.new_pathname.should.equal '/new-doc-path1'
-					update.meta.user_id.should.equal @user_id
-					update.meta.ts.should.be.a('string')
-					update.version.should.equal "#{@version}.1"
-
-					update = JSON.parse(updates[2])
-					update.file.should.equal @fileUpdate0.id
-					update.pathname.should.equal '/file-path0'
-					update.new_pathname.should.equal '/new-file-path0'
-					update.meta.user_id.should.equal @user_id
-					update.meta.ts.should.be.a('string')
-					update.version.should.equal "#{@version}.2"
-
-					update = JSON.parse(updates[3])
-					update.file.should.equal @fileUpdate1.id
-					update.pathname.should.equal '/file-path1'
-					update.new_pathname.should.equal '/new-file-path1'
-					update.meta.user_id.should.equal @user_id
-					update.meta.ts.should.be.a('string')
-					update.version.should.equal "#{@version}.3"
-
-					done()
-				return null
-
-
-	describe "adding a file", ->
-		before (done) ->
-			@project_id = DocUpdaterClient.randomId()
-			@fileUpdate =
-				id: DocUpdaterClient.randomId()
-				pathname: '/file-path'
+			};
+			return this.fileUpdates = [ this.fileUpdate0, this.fileUpdate1 ];});
+
+		return describe("when the documents are not loaded", function() {
+			before(function(done) {
+				this.project_id = DocUpdaterClient.randomId();
+				DocUpdaterClient.sendProjectUpdate(this.project_id, this.user_id, this.docUpdates, this.fileUpdates, this.version, function(error) {
+					if (error != null) { throw error; }
+					return setTimeout(done, 200);
+				});
+				return null;
+			});
+
+			return it("should push the applied doc renames to the project history api", function(done) {
+				rclient_project_history.lrange(ProjectHistoryKeys.projectHistoryOps({project_id: this.project_id}), 0, -1, (error, updates) => {
+					if (error != null) { throw error; }
+
+					let update = JSON.parse(updates[0]);
+					update.doc.should.equal(this.docUpdate0.id);
+					update.pathname.should.equal('/doc-path0');
+					update.new_pathname.should.equal('/new-doc-path0');
+					update.meta.user_id.should.equal(this.user_id);
+					update.meta.ts.should.be.a('string');
+					update.version.should.equal(`${this.version}.0`);
+
+					update = JSON.parse(updates[1]);
+					update.doc.should.equal(this.docUpdate1.id);
+					update.pathname.should.equal('/doc-path1');
+					update.new_pathname.should.equal('/new-doc-path1');
+					update.meta.user_id.should.equal(this.user_id);
+					update.meta.ts.should.be.a('string');
+					update.version.should.equal(`${this.version}.1`);
+
+					update = JSON.parse(updates[2]);
+					update.file.should.equal(this.fileUpdate0.id);
+					update.pathname.should.equal('/file-path0');
+					update.new_pathname.should.equal('/new-file-path0');
+					update.meta.user_id.should.equal(this.user_id);
+					update.meta.ts.should.be.a('string');
+					update.version.should.equal(`${this.version}.2`);
+
+					update = JSON.parse(updates[3]);
+					update.file.should.equal(this.fileUpdate1.id);
+					update.pathname.should.equal('/file-path1');
+					update.new_pathname.should.equal('/new-file-path1');
+					update.meta.user_id.should.equal(this.user_id);
+					update.meta.ts.should.be.a('string');
+					update.version.should.equal(`${this.version}.3`);
+
+					return done();
+				});
+				return null;
+			});
+		});
+	});
+
+
+	describe("adding a file", function() {
+		before(function(done) {
+			this.project_id = DocUpdaterClient.randomId();
+			this.fileUpdate = {
+				id: DocUpdaterClient.randomId(),
+				pathname: '/file-path',
 				url: 'filestore.example.com'
-			@fileUpdates = [ @fileUpdate ]
-			DocUpdaterClient.sendProjectUpdate @project_id, @user_id, [], @fileUpdates, @version, (error) ->
-				throw error if error?
-				setTimeout done, 200
-			return null
-
-		it "should push the file addition to the project history api", (done) ->
-			rclient_project_history.lrange ProjectHistoryKeys.projectHistoryOps({@project_id}), 0, -1, (error, updates) =>
-				throw error if error?
-
-				update = JSON.parse(updates[0])
-				update.file.should.equal @fileUpdate.id
-				update.pathname.should.equal '/file-path'
-				update.url.should.equal 'filestore.example.com'
-				update.meta.user_id.should.equal @user_id
-				update.meta.ts.should.be.a('string')
-				update.version.should.equal "#{@version}.0"
-
-				done()
-			return null
-
-	describe "adding a doc", ->
-		before (done) ->
-			@project_id = DocUpdaterClient.randomId()
-			@docUpdate =
-				id: DocUpdaterClient.randomId()
-				pathname: '/file-path'
+			};
+			this.fileUpdates = [ this.fileUpdate ];
+			DocUpdaterClient.sendProjectUpdate(this.project_id, this.user_id, [], this.fileUpdates, this.version, function(error) {
+				if (error != null) { throw error; }
+				return setTimeout(done, 200);
+			});
+			return null;
+		});
+
+		return it("should push the file addition to the project history api", function(done) {
+			rclient_project_history.lrange(ProjectHistoryKeys.projectHistoryOps({project_id: this.project_id}), 0, -1, (error, updates) => {
+				if (error != null) { throw error; }
+
+				const update = JSON.parse(updates[0]);
+				update.file.should.equal(this.fileUpdate.id);
+				update.pathname.should.equal('/file-path');
+				update.url.should.equal('filestore.example.com');
+				update.meta.user_id.should.equal(this.user_id);
+				update.meta.ts.should.be.a('string');
+				update.version.should.equal(`${this.version}.0`);
+
+				return done();
+			});
+			return null;
+		});
+	});
+
+	describe("adding a doc", function() {
+		before(function(done) {
+			this.project_id = DocUpdaterClient.randomId();
+			this.docUpdate = {
+				id: DocUpdaterClient.randomId(),
+				pathname: '/file-path',
 				docLines: 'a\nb'
-			@docUpdates = [ @docUpdate ]
-			DocUpdaterClient.sendProjectUpdate @project_id, @user_id, @docUpdates, [], @version, (error) ->
-				throw error if error?
-				setTimeout done, 200
-			return null
-
-		it "should push the doc addition to the project history api", (done) ->
-			rclient_project_history.lrange ProjectHistoryKeys.projectHistoryOps({@project_id}), 0, -1, (error, updates) =>
-				throw error if error?
-
-				update = JSON.parse(updates[0])
-				update.doc.should.equal @docUpdate.id
-				update.pathname.should.equal '/file-path'
-				update.docLines.should.equal 'a\nb'
-				update.meta.user_id.should.equal @user_id
-				update.meta.ts.should.be.a('string')
-				update.version.should.equal "#{@version}.0"
-
-				done()
-			return null
-
-	describe "with enough updates to flush to the history service", ->
-		before (done) ->
-			@project_id = DocUpdaterClient.randomId()
-			@user_id = DocUpdaterClient.randomId()
-			@version0 = 12345
-			@version1 = @version0 + 1
-			updates = []
-			for v in [0..599] # Should flush after 500 ops
-				updates.push
+			};
+			this.docUpdates = [ this.docUpdate ];
+			DocUpdaterClient.sendProjectUpdate(this.project_id, this.user_id, this.docUpdates, [], this.version, function(error) {
+				if (error != null) { throw error; }
+				return setTimeout(done, 200);
+			});
+			return null;
+		});
+
+		return it("should push the doc addition to the project history api", function(done) {
+			rclient_project_history.lrange(ProjectHistoryKeys.projectHistoryOps({project_id: this.project_id}), 0, -1, (error, updates) => {
+				if (error != null) { throw error; }
+
+				const update = JSON.parse(updates[0]);
+				update.doc.should.equal(this.docUpdate.id);
+				update.pathname.should.equal('/file-path');
+				update.docLines.should.equal('a\nb');
+				update.meta.user_id.should.equal(this.user_id);
+				update.meta.ts.should.be.a('string');
+				update.version.should.equal(`${this.version}.0`);
+
+				return done();
+			});
+			return null;
+		});
+	});
+
+	describe("with enough updates to flush to the history service", function() {
+		before(function(done) {
+			this.project_id = DocUpdaterClient.randomId();
+			this.user_id = DocUpdaterClient.randomId();
+			this.version0 = 12345;
+			this.version1 = this.version0 + 1;
+			const updates = [];
+			for (let v = 0; v <= 599; v++) { // Should flush after 500 ops
+				updates.push({
 					id: DocUpdaterClient.randomId(),
-					pathname: '/file-' + v
+					pathname: '/file-' + v,
 					docLines: 'a\nb'
-
-			sinon.spy MockProjectHistoryApi, "flushProject"
-
-			# Send updates in chunks to causes multiple flushes
-			projectId = @project_id
-			userId = @project_id
-			DocUpdaterClient.sendProjectUpdate projectId, userId, updates.slice(0, 250), [], @version0, (error) ->
-				throw error if error?
-				DocUpdaterClient.sendProjectUpdate projectId, userId, updates.slice(250), [], @version1, (error) ->
-					throw error if error?
-					setTimeout done, 2000
-			return null
-
-		after ->
-			MockProjectHistoryApi.flushProject.restore()
-
-		it "should flush project history", ->
-			MockProjectHistoryApi.flushProject.calledWith(@project_id).should.equal true
-
-	describe "with too few updates to flush to the history service", ->
-		before (done) ->
-			@project_id = DocUpdaterClient.randomId()
-			@user_id = DocUpdaterClient.randomId()
-			@version0 = 12345
-			@version1 = @version0 + 1
-
-			updates = []
-			for v in [0..42] # Should flush after 500 ops
-				updates.push
+				});
+			}
+
+			sinon.spy(MockProjectHistoryApi, "flushProject");
+
+			// Send updates in chunks to causes multiple flushes
+			const projectId = this.project_id;
+			const userId = this.project_id;
+			DocUpdaterClient.sendProjectUpdate(projectId, userId, updates.slice(0, 250), [], this.version0, function(error) {
+				if (error != null) { throw error; }
+				return DocUpdaterClient.sendProjectUpdate(projectId, userId, updates.slice(250), [], this.version1, function(error) {
+					if (error != null) { throw error; }
+					return setTimeout(done, 2000);
+				});
+			});
+			return null;
+		});
+
+		after(() => MockProjectHistoryApi.flushProject.restore());
+
+		return it("should flush project history", function() {
+			return MockProjectHistoryApi.flushProject.calledWith(this.project_id).should.equal(true);
+		});
+	});
+
+	return describe("with too few updates to flush to the history service", function() {
+		before(function(done) {
+			this.project_id = DocUpdaterClient.randomId();
+			this.user_id = DocUpdaterClient.randomId();
+			this.version0 = 12345;
+			this.version1 = this.version0 + 1;
+
+			const updates = [];
+			for (let v = 0; v <= 42; v++) { // Should flush after 500 ops
+				updates.push({
 					id: DocUpdaterClient.randomId(),
-					pathname: '/file-' + v
+					pathname: '/file-' + v,
 					docLines: 'a\nb'
-
-			sinon.spy MockProjectHistoryApi, "flushProject"
-
-			# Send updates in chunks
-			projectId = @project_id
-			userId = @project_id
-			DocUpdaterClient.sendProjectUpdate projectId, userId, updates.slice(0, 10), [], @version0, (error) ->
-				throw error if error?
-				DocUpdaterClient.sendProjectUpdate projectId, userId, updates.slice(10), [], @version1, (error) ->
-					throw error if error?
-					setTimeout done, 2000
-			return null
-
-		after ->
-			MockProjectHistoryApi.flushProject.restore()
-
-		it "should not flush project history", ->
-			MockProjectHistoryApi.flushProject.calledWith(@project_id).should.equal false
+				});
+			}
+
+			sinon.spy(MockProjectHistoryApi, "flushProject");
+
+			// Send updates in chunks
+			const projectId = this.project_id;
+			const userId = this.project_id;
+			DocUpdaterClient.sendProjectUpdate(projectId, userId, updates.slice(0, 10), [], this.version0, function(error) {
+				if (error != null) { throw error; }
+				return DocUpdaterClient.sendProjectUpdate(projectId, userId, updates.slice(10), [], this.version1, function(error) {
+					if (error != null) { throw error; }
+					return setTimeout(done, 2000);
+				});
+			});
+			return null;
+		});
+
+		after(() => MockProjectHistoryApi.flushProject.restore());
+
+		return it("should not flush project history", function() {
+			return MockProjectHistoryApi.flushProject.calledWith(this.project_id).should.equal(false);
+		});
+	});
+});

+ 137 - 105
services/document-updater/test/acceptance/coffee/DeletingADocumentTests.js

@@ -1,109 +1,141 @@
-sinon = require "sinon"
-chai = require("chai")
-chai.should()
-
-MockTrackChangesApi = require "./helpers/MockTrackChangesApi"
-MockProjectHistoryApi = require "./helpers/MockProjectHistoryApi"
-MockWebApi = require "./helpers/MockWebApi"
-DocUpdaterClient = require "./helpers/DocUpdaterClient"
-DocUpdaterApp = require "./helpers/DocUpdaterApp"
-
-describe "Deleting a document", ->
-	before (done) ->
-		@lines = ["one", "two", "three"]
-		@version = 42
-		@update =
-			doc: @doc_id
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require("sinon");
+const chai = require("chai");
+chai.should();
+
+const MockTrackChangesApi = require("./helpers/MockTrackChangesApi");
+const MockProjectHistoryApi = require("./helpers/MockProjectHistoryApi");
+const MockWebApi = require("./helpers/MockWebApi");
+const DocUpdaterClient = require("./helpers/DocUpdaterClient");
+const DocUpdaterApp = require("./helpers/DocUpdaterApp");
+
+describe("Deleting a document", function() {
+	before(function(done) {
+		this.lines = ["one", "two", "three"];
+		this.version = 42;
+		this.update = {
+			doc: this.doc_id,
 			op: [{
-				i: "one and a half\n"
+				i: "one and a half\n",
 				p: 4
-			}]
-			v: @version
-		@result = ["one", "one and a half", "two", "three"]
-
-		sinon.spy MockTrackChangesApi, "flushDoc"
-		sinon.spy MockProjectHistoryApi, "flushProject"
-		DocUpdaterApp.ensureRunning(done)
-
-	after ->
-		MockTrackChangesApi.flushDoc.restore()
-		MockProjectHistoryApi.flushProject.restore()
-
-	describe "when the updated doc exists in the doc updater", ->
-		before (done) ->
-			[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
-			sinon.spy MockWebApi, "setDocument"
-			sinon.spy MockWebApi, "getDocument"
-
-			MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
-			DocUpdaterClient.preloadDoc @project_id, @doc_id, (error) =>
-				throw error if error?
-				DocUpdaterClient.sendUpdate @project_id, @doc_id, @update, (error) =>
-					throw error if error?
-					setTimeout () =>
-						DocUpdaterClient.deleteDoc @project_id, @doc_id, (error, res, body) =>
-							@statusCode = res.statusCode
-							setTimeout done, 200
-					, 200
-
-		after ->
-			MockWebApi.setDocument.restore()
-			MockWebApi.getDocument.restore()
-
-		it "should return a 204 status code", ->
-			@statusCode.should.equal 204
-
-		it "should send the updated document and version to the web api", ->
-			MockWebApi.setDocument
-				.calledWith(@project_id, @doc_id, @result, @version + 1)
-				.should.equal true
-
-		it "should need to reload the doc if read again", (done) ->
-			MockWebApi.getDocument.called.should.equal.false
-			DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
+			}],
+			v: this.version
+		};
+		this.result = ["one", "one and a half", "two", "three"];
+
+		sinon.spy(MockTrackChangesApi, "flushDoc");
+		sinon.spy(MockProjectHistoryApi, "flushProject");
+		return DocUpdaterApp.ensureRunning(done);
+	});
+
+	after(function() {
+		MockTrackChangesApi.flushDoc.restore();
+		return MockProjectHistoryApi.flushProject.restore();
+	});
+
+	describe("when the updated doc exists in the doc updater", function() {
+		before(function(done) {
+			[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
+			sinon.spy(MockWebApi, "setDocument");
+			sinon.spy(MockWebApi, "getDocument");
+
+			MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
+			return DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
+				if (error != null) { throw error; }
+				return DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, this.update, error => {
+					if (error != null) { throw error; }
+					return setTimeout(() => {
+						return DocUpdaterClient.deleteDoc(this.project_id, this.doc_id, (error, res, body) => {
+							this.statusCode = res.statusCode;
+							return setTimeout(done, 200);
+						});
+					}
+					, 200);
+				});
+			});
+		});
+
+		after(function() {
+			MockWebApi.setDocument.restore();
+			return MockWebApi.getDocument.restore();
+		});
+
+		it("should return a 204 status code", function() {
+			return this.statusCode.should.equal(204);
+		});
+
+		it("should send the updated document and version to the web api", function() {
+			return MockWebApi.setDocument
+				.calledWith(this.project_id, this.doc_id, this.result, this.version + 1)
+				.should.equal(true);
+		});
+
+		it("should need to reload the doc if read again", function(done) {
+			MockWebApi.getDocument.called.should.equal.false;
+			return DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
 				MockWebApi.getDocument
-					.calledWith(@project_id, @doc_id)
-					.should.equal true
-				done()
-
-		it "should flush track changes", ->
-			MockTrackChangesApi.flushDoc.calledWith(@doc_id).should.equal true
-
-		it "should flush project history", ->
-			MockProjectHistoryApi.flushProject.calledWith(@project_id).should.equal true
-
-	describe "when the doc is not in the doc updater", ->
-		before (done) ->
-			[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
-			MockWebApi.insertDoc @project_id, @doc_id, {
-				lines: @lines
-			}
-			sinon.spy MockWebApi, "setDocument"
-			sinon.spy MockWebApi, "getDocument"
-			DocUpdaterClient.deleteDoc @project_id, @doc_id, (error, res, body) =>
-				@statusCode = res.statusCode
-				setTimeout done, 200
-
-		after ->
-			MockWebApi.setDocument.restore()
-			MockWebApi.getDocument.restore()
-
-		it "should return a 204 status code", ->
-			@statusCode.should.equal 204
-
-		it "should not need to send the updated document to the web api", ->
-			MockWebApi.setDocument.called.should.equal false
-
-		it "should need to reload the doc if read again", (done) ->
-			MockWebApi.getDocument.called.should.equal.false
-			DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
+					.calledWith(this.project_id, this.doc_id)
+					.should.equal(true);
+				return done();
+			});
+		});
+
+		it("should flush track changes", function() {
+			return MockTrackChangesApi.flushDoc.calledWith(this.doc_id).should.equal(true);
+		});
+
+		return it("should flush project history", function() {
+			return MockProjectHistoryApi.flushProject.calledWith(this.project_id).should.equal(true);
+		});
+	});
+
+	return describe("when the doc is not in the doc updater", function() {
+		before(function(done) {
+			[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
+			MockWebApi.insertDoc(this.project_id, this.doc_id, {
+				lines: this.lines
+			});
+			sinon.spy(MockWebApi, "setDocument");
+			sinon.spy(MockWebApi, "getDocument");
+			return DocUpdaterClient.deleteDoc(this.project_id, this.doc_id, (error, res, body) => {
+				this.statusCode = res.statusCode;
+				return setTimeout(done, 200);
+			});
+		});
+
+		after(function() {
+			MockWebApi.setDocument.restore();
+			return MockWebApi.getDocument.restore();
+		});
+
+		it("should return a 204 status code", function() {
+			return this.statusCode.should.equal(204);
+		});
+
+		it("should not need to send the updated document to the web api", () => MockWebApi.setDocument.called.should.equal(false));
+
+		it("should need to reload the doc if read again", function(done) {
+			MockWebApi.getDocument.called.should.equal.false;
+			return DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
 				MockWebApi.getDocument
-					.calledWith(@project_id, @doc_id)
-					.should.equal true
-				done()
-
-		it "should flush track changes", ->
-			MockTrackChangesApi.flushDoc.calledWith(@doc_id).should.equal true
-
-		it "should flush project history", ->
-			MockProjectHistoryApi.flushProject.calledWith(@project_id).should.equal true
+					.calledWith(this.project_id, this.doc_id)
+					.should.equal(true);
+				return done();
+			});
+		});
+
+		it("should flush track changes", function() {
+			return MockTrackChangesApi.flushDoc.calledWith(this.doc_id).should.equal(true);
+		});
+
+		return it("should flush project history", function() {
+			return MockProjectHistoryApi.flushProject.calledWith(this.project_id).should.equal(true);
+		});
+	});
+});

+ 204 - 161
services/document-updater/test/acceptance/coffee/DeletingAProjectTests.js

@@ -1,174 +1,217 @@
-sinon = require "sinon"
-chai = require("chai")
-chai.should()
-async = require "async"
-
-MockTrackChangesApi = require "./helpers/MockTrackChangesApi"
-MockProjectHistoryApi = require "./helpers/MockProjectHistoryApi"
-MockWebApi = require "./helpers/MockWebApi"
-DocUpdaterClient = require "./helpers/DocUpdaterClient"
-DocUpdaterApp = require "./helpers/DocUpdaterApp"
-
-describe "Deleting a project", ->
-	before (done) ->
-		@project_id = DocUpdaterClient.randomId()
-		@docs = [{
-			id: doc_id0 = DocUpdaterClient.randomId()
-			lines: ["one", "two", "three"]
-			update:
-				doc: doc_id0
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require("sinon");
+const chai = require("chai");
+chai.should();
+const async = require("async");
+
+const MockTrackChangesApi = require("./helpers/MockTrackChangesApi");
+const MockProjectHistoryApi = require("./helpers/MockProjectHistoryApi");
+const MockWebApi = require("./helpers/MockWebApi");
+const DocUpdaterClient = require("./helpers/DocUpdaterClient");
+const DocUpdaterApp = require("./helpers/DocUpdaterApp");
+
+describe("Deleting a project", function() {
+	before(function(done) {
+		let doc_id0, doc_id1;
+		this.project_id = DocUpdaterClient.randomId();
+		this.docs = [{
+			id: (doc_id0 = DocUpdaterClient.randomId()),
+			lines: ["one", "two", "three"],
+			update: {
+				doc: doc_id0,
 				op: [{
-					i: "one and a half\n"
+					i: "one and a half\n",
 					p: 4
-				}]
+				}],
 				v: 0
+			},
 			updatedLines: ["one", "one and a half", "two", "three"]
 		}, {
-			id: doc_id1 = DocUpdaterClient.randomId()
-			lines: ["four", "five", "six"]
-			update:
-				doc: doc_id1
+			id: (doc_id1 = DocUpdaterClient.randomId()),
+			lines: ["four", "five", "six"],
+			update: {
+				doc: doc_id1,
 				op: [{
-					i: "four and a half\n"
+					i: "four and a half\n",
 					p: 5
-				}]
+				}],
 				v: 0
+			},
 			updatedLines: ["four", "four and a half", "five", "six"]
-		}]
-		for doc in @docs
-			MockWebApi.insertDoc @project_id, doc.id, {
-				lines: doc.lines
+		}];
+		for (let doc of Array.from(this.docs)) {
+			MockWebApi.insertDoc(this.project_id, doc.id, {
+				lines: doc.lines,
 				version: doc.update.v
-			}
-
-		DocUpdaterApp.ensureRunning(done)
-
-
-	describe "with documents which have been updated", ->
-		before (done) ->
-			sinon.spy MockWebApi, "setDocument"
-			sinon.spy MockTrackChangesApi, "flushDoc"
-			sinon.spy MockProjectHistoryApi, "flushProject"
-
-			async.series @docs.map((doc) =>
-				(callback) =>
-					DocUpdaterClient.preloadDoc @project_id, doc.id, (error) =>
-						return callback(error) if error?
-						DocUpdaterClient.sendUpdate @project_id, doc.id, doc.update, (error) =>
-							callback(error)
-			), (error) =>
-				throw error if error?
-				setTimeout () =>
-					DocUpdaterClient.deleteProject @project_id, (error, res, body) =>
-						@statusCode = res.statusCode
-						done()
-				, 200
-
-		after ->
-			MockWebApi.setDocument.restore()
-			MockTrackChangesApi.flushDoc.restore()
-			MockProjectHistoryApi.flushProject.restore()
-
-		it "should return a 204 status code", ->
-			@statusCode.should.equal 204
-
-		it "should send each document to the web api", ->
-			for doc in @docs
+			});
+		}
+
+		return DocUpdaterApp.ensureRunning(done);
+	});
+
+
+	describe("with documents which have been updated", function() {
+		before(function(done) {
+			sinon.spy(MockWebApi, "setDocument");
+			sinon.spy(MockTrackChangesApi, "flushDoc");
+			sinon.spy(MockProjectHistoryApi, "flushProject");
+
+			return async.series(this.docs.map(doc => {
+				return callback => {
+					return DocUpdaterClient.preloadDoc(this.project_id, doc.id, error => {
+						if (error != null) { return callback(error); }
+						return DocUpdaterClient.sendUpdate(this.project_id, doc.id, doc.update, error => {
+							return callback(error);
+					});
+				});
+				};
+			}), error => {
+				if (error != null) { throw error; }
+				return setTimeout(() => {
+					return DocUpdaterClient.deleteProject(this.project_id, (error, res, body) => {
+						this.statusCode = res.statusCode;
+						return done();
+					});
+				}
+				, 200);
+			});
+		});
+
+		after(function() {
+			MockWebApi.setDocument.restore();
+			MockTrackChangesApi.flushDoc.restore();
+			return MockProjectHistoryApi.flushProject.restore();
+		});
+
+		it("should return a 204 status code", function() {
+			return this.statusCode.should.equal(204);
+		});
+
+		it("should send each document to the web api", function() {
+			return Array.from(this.docs).map((doc) =>
 				MockWebApi.setDocument
-					.calledWith(@project_id, doc.id, doc.updatedLines)
-					.should.equal true
-
-		it "should need to reload the docs if read again", (done) ->
-			sinon.spy MockWebApi, "getDocument"
-			async.series @docs.map((doc) =>
-				(callback) =>
-					MockWebApi.getDocument.calledWith(@project_id, doc.id).should.equal false
-					DocUpdaterClient.getDoc @project_id, doc.id, (error, res, returnedDoc) =>
-						MockWebApi.getDocument.calledWith(@project_id, doc.id).should.equal true
-						callback()
-			), () ->
-				MockWebApi.getDocument.restore()
-				done()
-
-		it "should flush each doc in track changes", ->
-			for doc in @docs
-				MockTrackChangesApi.flushDoc.calledWith(doc.id).should.equal true
-
-		it "should flush each doc in project history", ->
-			MockProjectHistoryApi.flushProject.calledWith(@project_id).should.equal true
-
-	describe "with the background=true parameter from realtime and no request to flush the queue", ->
-		before (done) ->
-			sinon.spy MockWebApi, "setDocument"
-			sinon.spy MockTrackChangesApi, "flushDoc"
-			sinon.spy MockProjectHistoryApi, "flushProject"
-
-			async.series @docs.map((doc) =>
-				(callback) =>
-					DocUpdaterClient.preloadDoc @project_id, doc.id, callback
-			), (error) =>
-				throw error if error?
-				setTimeout () =>
-					DocUpdaterClient.deleteProjectOnShutdown @project_id, (error, res, body) =>
-						@statusCode = res.statusCode
-						done()
-				, 200
-
-		after ->
-			MockWebApi.setDocument.restore()
-			MockTrackChangesApi.flushDoc.restore()
-			MockProjectHistoryApi.flushProject.restore()
-
-		it "should return a 204 status code", ->
-			@statusCode.should.equal 204
-
-		it "should not send any documents to the web api", ->
-			MockWebApi.setDocument.called.should.equal false
-
-		it "should not flush any docs in track changes", ->
-			MockTrackChangesApi.flushDoc.called.should.equal false
-
-		it "should not flush to project history", ->
-			MockProjectHistoryApi.flushProject.called.should.equal false
-
-	describe "with the background=true parameter from realtime and a request to flush the queue", ->
-		before (done) ->
-			sinon.spy MockWebApi, "setDocument"
-			sinon.spy MockTrackChangesApi, "flushDoc"
-			sinon.spy MockProjectHistoryApi, "flushProject"
-
-			async.series @docs.map((doc) =>
-				(callback) =>
-					DocUpdaterClient.preloadDoc @project_id, doc.id, callback
-			), (error) =>
-				throw error if error?
-				setTimeout () =>
-					DocUpdaterClient.deleteProjectOnShutdown @project_id, (error, res, body) =>
-						@statusCode = res.statusCode
-						# after deleting the project and putting it in the queue, flush the queue
-						setTimeout () ->
-							DocUpdaterClient.flushOldProjects done
-						, 2000
-				, 200
-
-		after ->
-			MockWebApi.setDocument.restore()
-			MockTrackChangesApi.flushDoc.restore()
-			MockProjectHistoryApi.flushProject.restore()
-
-		it "should return a 204 status code", ->
-			@statusCode.should.equal 204
-
-		it "should send each document to the web api", ->
-			for doc in @docs
+					.calledWith(this.project_id, doc.id, doc.updatedLines)
+					.should.equal(true));
+		});
+
+		it("should need to reload the docs if read again", function(done) {
+			sinon.spy(MockWebApi, "getDocument");
+			return async.series(this.docs.map(doc => {
+				return callback => {
+					MockWebApi.getDocument.calledWith(this.project_id, doc.id).should.equal(false);
+					return DocUpdaterClient.getDoc(this.project_id, doc.id, (error, res, returnedDoc) => {
+						MockWebApi.getDocument.calledWith(this.project_id, doc.id).should.equal(true);
+						return callback();
+				});
+				};
+			}), function() {
+				MockWebApi.getDocument.restore();
+				return done();
+			});
+		});
+
+		it("should flush each doc in track changes", function() {
+			return Array.from(this.docs).map((doc) =>
+				MockTrackChangesApi.flushDoc.calledWith(doc.id).should.equal(true));
+		});
+
+		return it("should flush each doc in project history", function() {
+			return MockProjectHistoryApi.flushProject.calledWith(this.project_id).should.equal(true);
+		});
+	});
+
+	describe("with the background=true parameter from realtime and no request to flush the queue", function() {
+		before(function(done) {
+			sinon.spy(MockWebApi, "setDocument");
+			sinon.spy(MockTrackChangesApi, "flushDoc");
+			sinon.spy(MockProjectHistoryApi, "flushProject");
+
+			return async.series(this.docs.map(doc => {
+				return callback => {
+					return DocUpdaterClient.preloadDoc(this.project_id, doc.id, callback);
+				};
+			}), error => {
+				if (error != null) { throw error; }
+				return setTimeout(() => {
+					return DocUpdaterClient.deleteProjectOnShutdown(this.project_id, (error, res, body) => {
+						this.statusCode = res.statusCode;
+						return done();
+					});
+				}
+				, 200);
+			});
+		});
+
+		after(function() {
+			MockWebApi.setDocument.restore();
+			MockTrackChangesApi.flushDoc.restore();
+			return MockProjectHistoryApi.flushProject.restore();
+		});
+
+		it("should return a 204 status code", function() {
+			return this.statusCode.should.equal(204);
+		});
+
+		it("should not send any documents to the web api", () => MockWebApi.setDocument.called.should.equal(false));
+
+		it("should not flush any docs in track changes", () => MockTrackChangesApi.flushDoc.called.should.equal(false));
+
+		return it("should not flush to project history", () => MockProjectHistoryApi.flushProject.called.should.equal(false));
+	});
+
+	return describe("with the background=true parameter from realtime and a request to flush the queue", function() {
+		before(function(done) {
+			sinon.spy(MockWebApi, "setDocument");
+			sinon.spy(MockTrackChangesApi, "flushDoc");
+			sinon.spy(MockProjectHistoryApi, "flushProject");
+
+			return async.series(this.docs.map(doc => {
+				return callback => {
+					return DocUpdaterClient.preloadDoc(this.project_id, doc.id, callback);
+				};
+			}), error => {
+				if (error != null) { throw error; }
+				return setTimeout(() => {
+					return DocUpdaterClient.deleteProjectOnShutdown(this.project_id, (error, res, body) => {
+						this.statusCode = res.statusCode;
+						// after deleting the project and putting it in the queue, flush the queue
+						return setTimeout(() => DocUpdaterClient.flushOldProjects(done)
+						, 2000);
+					});
+				}
+				, 200);
+			});
+		});
+
+		after(function() {
+			MockWebApi.setDocument.restore();
+			MockTrackChangesApi.flushDoc.restore();
+			return MockProjectHistoryApi.flushProject.restore();
+		});
+
+		it("should return a 204 status code", function() {
+			return this.statusCode.should.equal(204);
+		});
+
+		it("should send each document to the web api", function() {
+			return Array.from(this.docs).map((doc) =>
 				MockWebApi.setDocument
-					.calledWith(@project_id, doc.id, doc.updatedLines)
-					.should.equal true
-
-		it "should flush each doc in track changes", ->
-			for doc in @docs
-				MockTrackChangesApi.flushDoc.calledWith(doc.id).should.equal true
-
-		it "should flush to project history", ->
-			MockProjectHistoryApi.flushProject.called.should.equal true
+					.calledWith(this.project_id, doc.id, doc.updatedLines)
+					.should.equal(true));
+		});
+
+		it("should flush each doc in track changes", function() {
+			return Array.from(this.docs).map((doc) =>
+				MockTrackChangesApi.flushDoc.calledWith(doc.id).should.equal(true));
+		});
+
+		return it("should flush to project history", () => MockProjectHistoryApi.flushProject.called.should.equal(true));
+	});
+});
 

+ 85 - 60
services/document-updater/test/acceptance/coffee/FlushingAProjectTests.js

@@ -1,80 +1,105 @@
-sinon = require "sinon"
-chai = require("chai")
-chai.should()
-async = require "async"
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require("sinon");
+const chai = require("chai");
+chai.should();
+const async = require("async");
 
-MockWebApi = require "./helpers/MockWebApi"
-DocUpdaterClient = require "./helpers/DocUpdaterClient"
-DocUpdaterApp = require "./helpers/DocUpdaterApp"
+const MockWebApi = require("./helpers/MockWebApi");
+const DocUpdaterClient = require("./helpers/DocUpdaterClient");
+const DocUpdaterApp = require("./helpers/DocUpdaterApp");
 
-describe "Flushing a project", ->
-	before (done) ->
-		@project_id = DocUpdaterClient.randomId()
-		@docs = [{
-			id: doc_id0 = DocUpdaterClient.randomId()
-			lines: ["one", "two", "three"]
-			update:
-				doc: doc_id0
+describe("Flushing a project", function() {
+	before(function(done) {
+		let doc_id0, doc_id1;
+		this.project_id = DocUpdaterClient.randomId();
+		this.docs = [{
+			id: (doc_id0 = DocUpdaterClient.randomId()),
+			lines: ["one", "two", "three"],
+			update: {
+				doc: doc_id0,
 				op: [{
-					i: "one and a half\n"
+					i: "one and a half\n",
 					p: 4
-				}]
+				}],
 				v: 0
+			},
 			updatedLines: ["one", "one and a half", "two", "three"]
 		}, {
-			id: doc_id1 = DocUpdaterClient.randomId()
-			lines: ["four", "five", "six"]
-			update:
-				doc: doc_id1
+			id: (doc_id1 = DocUpdaterClient.randomId()),
+			lines: ["four", "five", "six"],
+			update: {
+				doc: doc_id1,
 				op: [{
-					i: "four and a half\n"
+					i: "four and a half\n",
 					p: 5
-				}]
+				}],
 				v: 0
+			},
 			updatedLines: ["four", "four and a half", "five", "six"]
-		}]
-		for doc in @docs
-			MockWebApi.insertDoc @project_id, doc.id, {
-				lines: doc.lines
+		}];
+		for (let doc of Array.from(this.docs)) {
+			MockWebApi.insertDoc(this.project_id, doc.id, {
+				lines: doc.lines,
 				version: doc.update.v
-			}
-		DocUpdaterApp.ensureRunning(done)
+			});
+		}
+		return DocUpdaterApp.ensureRunning(done);
+	});
 
-	describe "with documents which have been updated", ->
-		before (done) ->
-			sinon.spy MockWebApi, "setDocument"
+	return describe("with documents which have been updated", function() {
+		before(function(done) {
+			sinon.spy(MockWebApi, "setDocument");
 
-			async.series @docs.map((doc) =>
-				(callback) =>
-					DocUpdaterClient.preloadDoc @project_id, doc.id, (error) =>
-						return callback(error) if error?
-						DocUpdaterClient.sendUpdate @project_id, doc.id, doc.update, (error) =>
-							callback(error)
-			), (error) =>
-				throw error if error?
-				setTimeout () =>
-					DocUpdaterClient.flushProject @project_id, (error, res, body) =>
-						@statusCode = res.statusCode
-						done()
-				, 200
+			return async.series(this.docs.map(doc => {
+				return callback => {
+					return DocUpdaterClient.preloadDoc(this.project_id, doc.id, error => {
+						if (error != null) { return callback(error); }
+						return DocUpdaterClient.sendUpdate(this.project_id, doc.id, doc.update, error => {
+							return callback(error);
+					});
+				});
+				};
+			}), error => {
+				if (error != null) { throw error; }
+				return setTimeout(() => {
+					return DocUpdaterClient.flushProject(this.project_id, (error, res, body) => {
+						this.statusCode = res.statusCode;
+						return done();
+					});
+				}
+				, 200);
+			});
+		});
 
-		after ->
-			MockWebApi.setDocument.restore()
+		after(() => MockWebApi.setDocument.restore());
 
-		it "should return a 204 status code", ->
-			@statusCode.should.equal 204
+		it("should return a 204 status code", function() {
+			return this.statusCode.should.equal(204);
+		});
 
-		it "should send each document to the web api", ->
-			for doc in @docs
+		it("should send each document to the web api", function() {
+			return Array.from(this.docs).map((doc) =>
 				MockWebApi.setDocument
-					.calledWith(@project_id, doc.id, doc.updatedLines)
-					.should.equal true
+					.calledWith(this.project_id, doc.id, doc.updatedLines)
+					.should.equal(true));
+		});
 
-		it "should update the lines in the doc updater", (done) ->
-			async.series @docs.map((doc) =>
-				(callback) =>
-					DocUpdaterClient.getDoc @project_id, doc.id, (error, res, returnedDoc) =>
-						returnedDoc.lines.should.deep.equal doc.updatedLines
-						callback()
-			), done
+		return it("should update the lines in the doc updater", function(done) {
+			return async.series(this.docs.map(doc => {
+				return callback => {
+					return DocUpdaterClient.getDoc(this.project_id, doc.id, (error, res, returnedDoc) => {
+						returnedDoc.lines.should.deep.equal(doc.updatedLines);
+						return callback();
+				});
+				};
+			}), done);
+		});
+	});
+});
 

+ 96 - 74
services/document-updater/test/acceptance/coffee/FlushingDocsTests.js

@@ -1,90 +1,112 @@
-sinon = require "sinon"
-chai = require("chai")
-chai.should()
-expect = chai.expect
-async = require "async"
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require("sinon");
+const chai = require("chai");
+chai.should();
+const {
+    expect
+} = chai;
+const async = require("async");
 
-MockWebApi = require "./helpers/MockWebApi"
-DocUpdaterClient = require "./helpers/DocUpdaterClient"
-DocUpdaterApp = require "./helpers/DocUpdaterApp"
+const MockWebApi = require("./helpers/MockWebApi");
+const DocUpdaterClient = require("./helpers/DocUpdaterClient");
+const DocUpdaterApp = require("./helpers/DocUpdaterApp");
 
-describe "Flushing a doc to Mongo", ->
-	before (done) ->
-		@lines = ["one", "two", "three"]
-		@version = 42
-		@update =
-			doc: @doc_id
-			meta: { user_id: 'last-author-fake-id' }
+describe("Flushing a doc to Mongo", function() {
+	before(function(done) {
+		this.lines = ["one", "two", "three"];
+		this.version = 42;
+		this.update = {
+			doc: this.doc_id,
+			meta: { user_id: 'last-author-fake-id' },
 			op: [{
-				i: "one and a half\n"
+				i: "one and a half\n",
 				p: 4
-			}]
-			v: @version
-		@result = ["one", "one and a half", "two", "three"]
-		DocUpdaterApp.ensureRunning(done)
+			}],
+			v: this.version
+		};
+		this.result = ["one", "one and a half", "two", "three"];
+		return DocUpdaterApp.ensureRunning(done);
+	});
 
-	describe "when the updated doc exists in the doc updater", ->
-		before (done) ->
-			[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
-			sinon.spy MockWebApi, "setDocument"
+	describe("when the updated doc exists in the doc updater", function() {
+		before(function(done) {
+			[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
+			sinon.spy(MockWebApi, "setDocument");
 
-			MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
-			DocUpdaterClient.sendUpdates @project_id, @doc_id, [@update], (error) =>
-				throw error if error?
-				setTimeout () =>
-					DocUpdaterClient.flushDoc @project_id, @doc_id, done
-				, 200
+			MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
+			return DocUpdaterClient.sendUpdates(this.project_id, this.doc_id, [this.update], error => {
+				if (error != null) { throw error; }
+				return setTimeout(() => {
+					return DocUpdaterClient.flushDoc(this.project_id, this.doc_id, done);
+				}
+				, 200);
+			});
+		});
 
-		after ->
-			MockWebApi.setDocument.restore()
+		after(() => MockWebApi.setDocument.restore());
 
-		it "should flush the updated doc lines and version to the web api", ->
-			MockWebApi.setDocument
-				.calledWith(@project_id, @doc_id, @result, @version + 1)
-				.should.equal true
+		it("should flush the updated doc lines and version to the web api", function() {
+			return MockWebApi.setDocument
+				.calledWith(this.project_id, this.doc_id, this.result, this.version + 1)
+				.should.equal(true);
+		});
 
-		it "should flush the last update author and time to the web api", ->
-			lastUpdatedAt = MockWebApi.setDocument.lastCall.args[5]
-			parseInt(lastUpdatedAt).should.be.closeTo((new Date()).getTime(), 30000)
+		return it("should flush the last update author and time to the web api", function() {
+			const lastUpdatedAt = MockWebApi.setDocument.lastCall.args[5];
+			parseInt(lastUpdatedAt).should.be.closeTo((new Date()).getTime(), 30000);
 
-			lastUpdatedBy = MockWebApi.setDocument.lastCall.args[6]
-			lastUpdatedBy.should.equal 'last-author-fake-id'
+			const lastUpdatedBy = MockWebApi.setDocument.lastCall.args[6];
+			return lastUpdatedBy.should.equal('last-author-fake-id');
+		});
+	});
 
-	describe "when the doc does not exist in the doc updater", ->
-		before (done) ->
-			[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
-			MockWebApi.insertDoc @project_id, @doc_id, {
-				lines: @lines
-			}
-			sinon.spy MockWebApi, "setDocument"
-			DocUpdaterClient.flushDoc @project_id, @doc_id, done
+	describe("when the doc does not exist in the doc updater", function() {
+		before(function(done) {
+			[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
+			MockWebApi.insertDoc(this.project_id, this.doc_id, {
+				lines: this.lines
+			});
+			sinon.spy(MockWebApi, "setDocument");
+			return DocUpdaterClient.flushDoc(this.project_id, this.doc_id, done);
+		});
 
-		after ->
-			MockWebApi.setDocument.restore()
+		after(() => MockWebApi.setDocument.restore());
 
-		it "should not flush the doc to the web api", ->
-			MockWebApi.setDocument.called.should.equal false
+		return it("should not flush the doc to the web api", () => MockWebApi.setDocument.called.should.equal(false));
+	});
 
-	describe "when the web api http request takes a long time on first request", ->
-		before (done) ->
-			[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
-			MockWebApi.insertDoc @project_id, @doc_id, {
-				lines: @lines
-				version: @version
-			}
-			t = 30000
-			sinon.stub MockWebApi, "setDocument", (project_id, doc_id, lines, version, ranges, lastUpdatedAt, lastUpdatedBy, callback = (error) ->) ->
-				setTimeout callback, t
-				t = 0
-			DocUpdaterClient.preloadDoc @project_id, @doc_id, done
+	return describe("when the web api http request takes a long time on first request", function() {
+		before(function(done) {
+			[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
+			MockWebApi.insertDoc(this.project_id, this.doc_id, {
+				lines: this.lines,
+				version: this.version
+			});
+			let t = 30000;
+			sinon.stub(MockWebApi, "setDocument", function(project_id, doc_id, lines, version, ranges, lastUpdatedAt, lastUpdatedBy, callback) {
+				if (callback == null) { callback = function(error) {}; }
+				setTimeout(callback, t);
+				return t = 0;
+			});
+			return DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, done);
+		});
 
-		after ->
-			MockWebApi.setDocument.restore()
+		after(() => MockWebApi.setDocument.restore());
 		
-		it "should still work", (done) ->
-			start = Date.now()
-			DocUpdaterClient.flushDoc @project_id, @doc_id, (error, res, doc) =>
-				res.statusCode.should.equal 204
-				delta = Date.now() - start
-				expect(delta).to.be.below 20000
-				done()
+		return it("should still work", function(done) {
+			const start = Date.now();
+			return DocUpdaterClient.flushDoc(this.project_id, this.doc_id, (error, res, doc) => {
+				res.statusCode.should.equal(204);
+				const delta = Date.now() - start;
+				expect(delta).to.be.below(20000);
+				return done();
+			});
+		});
+	});
+});

+ 183 - 133
services/document-updater/test/acceptance/coffee/GettingADocumentTests.js

@@ -1,138 +1,188 @@
-sinon = require "sinon"
-chai = require("chai")
-chai.should()
-expect = chai.expect
-
-MockWebApi = require "./helpers/MockWebApi"
-DocUpdaterClient = require "./helpers/DocUpdaterClient"
-DocUpdaterApp = require "./helpers/DocUpdaterApp"
-
-describe "Getting a document", ->
-	before (done) ->
-		@lines = ["one", "two", "three"]
-		@version = 42
-		DocUpdaterApp.ensureRunning(done)
-
-	describe "when the document is not loaded", ->
-		before (done) ->
-			[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
-			sinon.spy MockWebApi, "getDocument"
-
-			MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
-
-			DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, @returnedDoc) => done()
-
-		after ->
-			MockWebApi.getDocument.restore()
-
-		it "should load the document from the web API", ->
-			MockWebApi.getDocument
-				.calledWith(@project_id, @doc_id)
-				.should.equal true
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require("sinon");
+const chai = require("chai");
+chai.should();
+const {
+    expect
+} = chai;
+
+const MockWebApi = require("./helpers/MockWebApi");
+const DocUpdaterClient = require("./helpers/DocUpdaterClient");
+const DocUpdaterApp = require("./helpers/DocUpdaterApp");
+
+describe("Getting a document", function() {
+	before(function(done) {
+		this.lines = ["one", "two", "three"];
+		this.version = 42;
+		return DocUpdaterApp.ensureRunning(done);
+	});
+
+	describe("when the document is not loaded", function() {
+		before(function(done) {
+			[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
+			sinon.spy(MockWebApi, "getDocument");
+
+			MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
+
+			return DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, returnedDoc) => { this.returnedDoc = returnedDoc; return done(); });
+		});
+
+		after(() => MockWebApi.getDocument.restore());
+
+		it("should load the document from the web API", function() {
+			return MockWebApi.getDocument
+				.calledWith(this.project_id, this.doc_id)
+				.should.equal(true);
+		});
 		
-		it "should return the document lines", ->
-			@returnedDoc.lines.should.deep.equal @lines
-
-		it "should return the document at its current version", ->
-			@returnedDoc.version.should.equal @version
-
-	describe "when the document is already loaded", ->
-		before (done) ->
-			[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
+		it("should return the document lines", function() {
+			return this.returnedDoc.lines.should.deep.equal(this.lines);
+		});
+
+		return it("should return the document at its current version", function() {
+			return this.returnedDoc.version.should.equal(this.version);
+		});
+	});
+
+	describe("when the document is already loaded", function() {
+		before(function(done) {
+			[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
 			
-			MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
-			DocUpdaterClient.preloadDoc @project_id, @doc_id, (error) =>
-				throw error if error?
-				sinon.spy MockWebApi, "getDocument"
-				DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, @returnedDoc) =>	done()
-
-		after ->
-			MockWebApi.getDocument.restore()
-
-		it "should not load the document from the web API", ->
-			MockWebApi.getDocument.called.should.equal false
-
-		it "should return the document lines", ->
-			@returnedDoc.lines.should.deep.equal @lines
-
-	describe "when the request asks for some recent ops", ->
-		before (done) ->
-			[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
-			MockWebApi.insertDoc @project_id, @doc_id, {
-				lines: @lines = ["one", "two", "three"]
-			}
-
-			@updates = for v in [0..199]
-				doc_id: @doc_id,
-				op: [i: v.toString(), p: 0]
-				v: v
-
-			DocUpdaterClient.sendUpdates @project_id, @doc_id, @updates, (error) =>
-				throw error if error?
-				sinon.spy MockWebApi, "getDocument"
-				done()
-
-		after ->
-			MockWebApi.getDocument.restore()
+			MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
+			return DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
+				if (error != null) { throw error; }
+				sinon.spy(MockWebApi, "getDocument");
+				return DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, returnedDoc) => {	this.returnedDoc = returnedDoc; return done(); });
+			});
+		});
+
+		after(() => MockWebApi.getDocument.restore());
+
+		it("should not load the document from the web API", () => MockWebApi.getDocument.called.should.equal(false));
+
+		return it("should return the document lines", function() {
+			return this.returnedDoc.lines.should.deep.equal(this.lines);
+		});
+	});
+
+	describe("when the request asks for some recent ops", function() {
+		before(function(done) {
+			[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
+			MockWebApi.insertDoc(this.project_id, this.doc_id, {
+				lines: (this.lines = ["one", "two", "three"])
+			});
+
+			this.updates = __range__(0, 199, true).map((v) => ({
+				doc_id: this.doc_id,
+				op: [{i: v.toString(), p: 0}],
+				v
+			}));
+
+			return DocUpdaterClient.sendUpdates(this.project_id, this.doc_id, this.updates, error => {
+				if (error != null) { throw error; }
+				sinon.spy(MockWebApi, "getDocument");
+				return done();
+			});
+		});
+
+		after(() => MockWebApi.getDocument.restore());
 			
-		describe "when the ops are loaded", ->
-			before (done) ->
-				DocUpdaterClient.getDocAndRecentOps @project_id, @doc_id, 190, (error, res, @returnedDoc) => done()
-
-			it "should return the recent ops", ->
-				@returnedDoc.ops.length.should.equal 10
-				for update, i in @updates.slice(190, -1)
-					@returnedDoc.ops[i].op.should.deep.equal update.op
+		describe("when the ops are loaded", function() {
+			before(function(done) {
+				return DocUpdaterClient.getDocAndRecentOps(this.project_id, this.doc_id, 190, (error, res, returnedDoc) => { this.returnedDoc = returnedDoc; return done(); });
+			});
+
+			return it("should return the recent ops", function() {
+				this.returnedDoc.ops.length.should.equal(10);
+				return Array.from(this.updates.slice(190, -1)).map((update, i) =>
+					this.returnedDoc.ops[i].op.should.deep.equal(update.op));
+			});
+		});
 					
-		describe "when the ops are not all loaded", ->
-			before (done) ->
-				# We only track 100 ops
-				DocUpdaterClient.getDocAndRecentOps @project_id, @doc_id, 10, (error, @res, @returnedDoc) => done()
-
-			it "should return UnprocessableEntity", ->
-				@res.statusCode.should.equal 422
-
-	describe "when the document does not exist", ->
-		before (done) ->
-			[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
-			DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
-				@statusCode = res.statusCode
-				done()
-
-		it "should return 404", ->
-			@statusCode.should.equal 404
-
-	describe "when the web api returns an error", ->
-		before (done) ->
-			[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
-			sinon.stub MockWebApi, "getDocument", (project_id, doc_id, callback = (error, doc) ->) ->
-				callback new Error("oops")
-			DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
-				@statusCode = res.statusCode
-				done()
-
-		after ->
-			MockWebApi.getDocument.restore()
-
-		it "should return 500", ->
-			@statusCode.should.equal 500
-
-	describe "when the web api http request takes a long time", ->
-		before (done) ->
-			@timeout = 10000
-			[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
-			sinon.stub MockWebApi, "getDocument", (project_id, doc_id, callback = (error, doc) ->) ->
-				setTimeout callback, 30000
-			done()
-
-		after ->
-			MockWebApi.getDocument.restore()
+		return describe("when the ops are not all loaded", function() {
+			before(function(done) {
+				// We only track 100 ops
+				return DocUpdaterClient.getDocAndRecentOps(this.project_id, this.doc_id, 10, (error, res, returnedDoc) => { this.res = res; this.returnedDoc = returnedDoc; return done(); });
+			});
+
+			return it("should return UnprocessableEntity", function() {
+				return this.res.statusCode.should.equal(422);
+			});
+		});
+	});
+
+	describe("when the document does not exist", function() {
+		before(function(done) {
+			[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
+			return DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
+				this.statusCode = res.statusCode;
+				return done();
+			});
+		});
+
+		return it("should return 404", function() {
+			return this.statusCode.should.equal(404);
+		});
+	});
+
+	describe("when the web api returns an error", function() {
+		before(function(done) {
+			[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
+			sinon.stub(MockWebApi, "getDocument", function(project_id, doc_id, callback) {
+				if (callback == null) { callback = function(error, doc) {}; }
+				return callback(new Error("oops"));
+			});
+			return DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
+				this.statusCode = res.statusCode;
+				return done();
+			});
+		});
+
+		after(() => MockWebApi.getDocument.restore());
+
+		return it("should return 500", function() {
+			return this.statusCode.should.equal(500);
+		});
+	});
+
+	return describe("when the web api http request takes a long time", function() {
+		before(function(done) {
+			this.timeout = 10000;
+			[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
+			sinon.stub(MockWebApi, "getDocument", function(project_id, doc_id, callback) {
+				if (callback == null) { callback = function(error, doc) {}; }
+				return setTimeout(callback, 30000);
+			});
+			return done();
+		});
+
+		after(() => MockWebApi.getDocument.restore());
 		
-		it "should return quickly(ish)", (done) ->
-			start = Date.now()
-			DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
-				res.statusCode.should.equal 500
-				delta = Date.now() - start
-				expect(delta).to.be.below 20000
-				done()
-
+		return it("should return quickly(ish)", function(done) {
+			const start = Date.now();
+			return DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
+				res.statusCode.should.equal(500);
+				const delta = Date.now() - start;
+				expect(delta).to.be.below(20000);
+				return done();
+			});
+		});
+	});
+});
+
+
+function __range__(left, right, inclusive) {
+  let range = [];
+  let ascending = left < right;
+  let end = !inclusive ? right : ascending ? right + 1 : right - 1;
+  for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) {
+    range.push(i);
+  }
+  return range;
+}

+ 95 - 55
services/document-updater/test/acceptance/coffee/GettingProjectDocsTests.js

@@ -1,69 +1,109 @@
-sinon = require "sinon"
-chai = require("chai")
-chai.should()
-expect = chai.expect
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require("sinon");
+const chai = require("chai");
+chai.should();
+const {
+    expect
+} = chai;
 
-MockWebApi = require "./helpers/MockWebApi"
-DocUpdaterClient = require "./helpers/DocUpdaterClient"
-DocUpdaterApp = require "./helpers/DocUpdaterApp"
+const MockWebApi = require("./helpers/MockWebApi");
+const DocUpdaterClient = require("./helpers/DocUpdaterClient");
+const DocUpdaterApp = require("./helpers/DocUpdaterApp");
 
-describe "Getting documents for project", ->
-	before (done) ->
-		@lines = ["one", "two", "three"]
-		@version = 42
-		DocUpdaterApp.ensureRunning(done)
+describe("Getting documents for project", function() {
+	before(function(done) {
+		this.lines = ["one", "two", "three"];
+		this.version = 42;
+		return DocUpdaterApp.ensureRunning(done);
+	});
 
-	describe "when project state hash does not match", ->
-		before (done) ->
-			@projectStateHash = DocUpdaterClient.randomId()
-			[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
+	describe("when project state hash does not match", function() {
+		before(function(done) {
+			this.projectStateHash = DocUpdaterClient.randomId();
+			[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
 
-			MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
-			DocUpdaterClient.preloadDoc @project_id, @doc_id, (error) =>
-				throw error if error?
-				DocUpdaterClient.getProjectDocs @project_id, @projectStateHash, (error, @res, @returnedDocs)	=>
-					done()
+			MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
+			return DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
+				if (error != null) { throw error; }
+				return DocUpdaterClient.getProjectDocs(this.project_id, this.projectStateHash, (error, res, returnedDocs)	=> {
+					this.res = res;
+					this.returnedDocs = returnedDocs;
+					return done();
+				});
+			});
+		});
 
-		it "should return a 409 Conflict response", ->
-			@res.statusCode.should.equal 409
+		return it("should return a 409 Conflict response", function() {
+			return this.res.statusCode.should.equal(409);
+		});
+	});
 
 
-	describe "when project state hash matches", ->
-		before (done) ->
-			@projectStateHash = DocUpdaterClient.randomId()
-			[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
+	describe("when project state hash matches", function() {
+		before(function(done) {
+			this.projectStateHash = DocUpdaterClient.randomId();
+			[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
 
-			MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
-			DocUpdaterClient.preloadDoc @project_id, @doc_id, (error) =>
-				throw error if error?
-				DocUpdaterClient.getProjectDocs @project_id, @projectStateHash, (error, @res0, @returnedDocs0)	=>
-					# set the hash
-					DocUpdaterClient.getProjectDocs @project_id, @projectStateHash, (error, @res, @returnedDocs)	=>
-						# the hash should now match
-						done()
+			MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
+			return DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
+				if (error != null) { throw error; }
+				return DocUpdaterClient.getProjectDocs(this.project_id, this.projectStateHash, (error, res0, returnedDocs0)	=> {
+					// set the hash
+					this.res0 = res0;
+					this.returnedDocs0 = returnedDocs0;
+					return DocUpdaterClient.getProjectDocs(this.project_id, this.projectStateHash, (error, res, returnedDocs)	=> {
+						// the hash should now match
+						this.res = res;
+						this.returnedDocs = returnedDocs;
+						return done();
+					});
+				});
+			});
+		});
 
-		it "should return a 200 response", ->
-			@res.statusCode.should.equal 200
+		it("should return a 200 response", function() {
+			return this.res.statusCode.should.equal(200);
+		});
 
-		it "should return the documents", ->
-			@returnedDocs.should.deep.equal [ {_id: @doc_id, lines: @lines, v: @version} ]
+		return it("should return the documents", function() {
+			return this.returnedDocs.should.deep.equal([ {_id: this.doc_id, lines: this.lines, v: this.version} ]);
+	});
+});
 
 
-	describe "when the doc has been removed", ->
-		before (done) ->
-			@projectStateHash = DocUpdaterClient.randomId()
-			[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
+	return describe("when the doc has been removed", function() {
+		before(function(done) {
+			this.projectStateHash = DocUpdaterClient.randomId();
+			[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
 
-			MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
-			DocUpdaterClient.preloadDoc @project_id, @doc_id, (error) =>
-				throw error if error?
-				DocUpdaterClient.getProjectDocs @project_id, @projectStateHash, (error, @res0, @returnedDocs0)	=>
-					# set the hash
-					DocUpdaterClient.deleteDoc @project_id, @doc_id, (error, res, body) =>
-					# delete the doc
-						DocUpdaterClient.getProjectDocs @project_id, @projectStateHash, (error, @res, @returnedDocs)	=>
-							# the hash would match, but the doc has been deleted
-							done()
+			MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
+			return DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
+				if (error != null) { throw error; }
+				return DocUpdaterClient.getProjectDocs(this.project_id, this.projectStateHash, (error, res0, returnedDocs0)	=> {
+					// set the hash
+					this.res0 = res0;
+					this.returnedDocs0 = returnedDocs0;
+					return DocUpdaterClient.deleteDoc(this.project_id, this.doc_id, (error, res, body) => {
+					// delete the doc
+						return DocUpdaterClient.getProjectDocs(this.project_id, this.projectStateHash, (error, res1, returnedDocs)	=> {
+							// the hash would match, but the doc has been deleted
+							this.res = res1;
+							this.returnedDocs = returnedDocs;
+							return done();
+						});
+					});
+				});
+			});
+		});
 
-		it "should return a 409 Conflict response", ->
-			@res.statusCode.should.equal 409
+		return it("should return a 409 Conflict response", function() {
+			return this.res.statusCode.should.equal(409);
+		});
+	});
+});

+ 411 - 316
services/document-updater/test/acceptance/coffee/RangesTests.js

@@ -1,372 +1,467 @@
-sinon = require "sinon"
-chai = require("chai")
-chai.should()
-expect = chai.expect
-async = require "async"
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require("sinon");
+const chai = require("chai");
+chai.should();
+const {
+    expect
+} = chai;
+const async = require("async");
 
-{db, ObjectId} = require "../../../app/js/mongojs"
-MockWebApi = require "./helpers/MockWebApi"
-DocUpdaterClient = require "./helpers/DocUpdaterClient"
-DocUpdaterApp = require "./helpers/DocUpdaterApp"
+const {db, ObjectId} = require("../../../app/js/mongojs");
+const MockWebApi = require("./helpers/MockWebApi");
+const DocUpdaterClient = require("./helpers/DocUpdaterClient");
+const DocUpdaterApp = require("./helpers/DocUpdaterApp");
 
-describe "Ranges", ->
-	before (done) ->
-		DocUpdaterApp.ensureRunning done
+describe("Ranges", function() {
+	before(done => DocUpdaterApp.ensureRunning(done));
 
-	describe "tracking changes from ops", ->
-		before (done) ->
-			@project_id = DocUpdaterClient.randomId()
-			@user_id = DocUpdaterClient.randomId()
-			@id_seed = "587357bd35e64f6157"
-			@doc = {
-				id: DocUpdaterClient.randomId()
+	describe("tracking changes from ops", function() {
+		before(function(done) {
+			this.project_id = DocUpdaterClient.randomId();
+			this.user_id = DocUpdaterClient.randomId();
+			this.id_seed = "587357bd35e64f6157";
+			this.doc = {
+				id: DocUpdaterClient.randomId(),
 				lines: ["aaa"]
-			}
-			@updates = [{
-				doc: @doc.id
-				op: [{ i: "123", p: 1 }]
-				v: 0
-				meta: { user_id: @user_id }
+			};
+			this.updates = [{
+				doc: this.doc.id,
+				op: [{ i: "123", p: 1 }],
+				v: 0,
+				meta: { user_id: this.user_id }
 			}, {
-				doc: @doc.id
-				op: [{ i: "456", p: 5 }]
-				v: 1
-				meta: { user_id: @user_id, tc: @id_seed }
+				doc: this.doc.id,
+				op: [{ i: "456", p: 5 }],
+				v: 1,
+				meta: { user_id: this.user_id, tc: this.id_seed }
 			}, {
-				doc: @doc.id
-				op: [{ d: "12", p: 1 }]
-				v: 2
-				meta: { user_id: @user_id }
-			}]
-			MockWebApi.insertDoc @project_id, @doc.id, {
-				lines: @doc.lines
+				doc: this.doc.id,
+				op: [{ d: "12", p: 1 }],
+				v: 2,
+				meta: { user_id: this.user_id }
+			}];
+			MockWebApi.insertDoc(this.project_id, this.doc.id, {
+				lines: this.doc.lines,
 				version: 0
+			});
+			const jobs = [];
+			for (let update of Array.from(this.updates)) {
+				(update => {
+					return jobs.push(callback => DocUpdaterClient.sendUpdate(this.project_id, this.doc.id, update, callback));
+				})(update);
 			}
-			jobs = []
-			for update in @updates
-				do (update) =>
-					jobs.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc.id, update, callback
 			
-			DocUpdaterApp.ensureRunning (error) =>
-				throw error if error?
-				DocUpdaterClient.preloadDoc @project_id, @doc.id, (error) =>
-					throw error if error?
-					async.series jobs, (error) ->
-						throw error if error?
-						done()
+			return DocUpdaterApp.ensureRunning(error => {
+				if (error != null) { throw error; }
+				return DocUpdaterClient.preloadDoc(this.project_id, this.doc.id, error => {
+					if (error != null) { throw error; }
+					return async.series(jobs, function(error) {
+						if (error != null) { throw error; }
+						return done();
+					});
+				});
+			});
+		});
 		
-		it "should update the ranges", (done) ->
-			DocUpdaterClient.getDoc @project_id, @doc.id, (error, res, data) =>
-				throw error if error?
-				ranges = data.ranges
-				change = ranges.changes[0]
-				change.op.should.deep.equal { i: "456", p: 3 }
-				change.id.should.equal @id_seed + "000001"
-				change.metadata.user_id.should.equal @user_id
-				done()
+		it("should update the ranges", function(done) {
+			return DocUpdaterClient.getDoc(this.project_id, this.doc.id, (error, res, data) => {
+				if (error != null) { throw error; }
+				const {
+                    ranges
+                } = data;
+				const change = ranges.changes[0];
+				change.op.should.deep.equal({ i: "456", p: 3 });
+				change.id.should.equal(this.id_seed + "000001");
+				change.metadata.user_id.should.equal(this.user_id);
+				return done();
+			});
+		});
 	
-		describe "Adding comments", ->
-			describe "standalone", ->
-				before (done) ->
-					@project_id = DocUpdaterClient.randomId()
-					@user_id = DocUpdaterClient.randomId()
-					@doc = {
-						id: DocUpdaterClient.randomId()
+		return describe("Adding comments", function() {
+			describe("standalone", function() {
+				before(function(done) {
+					this.project_id = DocUpdaterClient.randomId();
+					this.user_id = DocUpdaterClient.randomId();
+					this.doc = {
+						id: DocUpdaterClient.randomId(),
 						lines: ["foo bar baz"]
-					}
-					@updates = [{
-						doc: @doc.id
-						op: [{ c: "bar", p: 4, t: @tid = DocUpdaterClient.randomId() }]
+					};
+					this.updates = [{
+						doc: this.doc.id,
+						op: [{ c: "bar", p: 4, t: (this.tid = DocUpdaterClient.randomId()) }],
 						v: 0
-					}]
-					MockWebApi.insertDoc @project_id, @doc.id, {
-						lines: @doc.lines
+					}];
+					MockWebApi.insertDoc(this.project_id, this.doc.id, {
+						lines: this.doc.lines,
 						version: 0
+					});
+					const jobs = [];
+					for (let update of Array.from(this.updates)) {
+						(update => {
+							return jobs.push(callback => DocUpdaterClient.sendUpdate(this.project_id, this.doc.id, update, callback));
+						})(update);
 					}
-					jobs = []
-					for update in @updates
-						do (update) =>
-							jobs.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc.id, update, callback
-					DocUpdaterClient.preloadDoc @project_id, @doc.id, (error) =>
-						throw error if error?
-						async.series jobs, (error) ->
-							throw error if error?
-							setTimeout done, 200
+					return DocUpdaterClient.preloadDoc(this.project_id, this.doc.id, error => {
+						if (error != null) { throw error; }
+						return async.series(jobs, function(error) {
+							if (error != null) { throw error; }
+							return setTimeout(done, 200);
+						});
+					});
+				});
 				
-				it "should update the ranges", (done) ->
-					DocUpdaterClient.getDoc @project_id, @doc.id, (error, res, data) =>
-						throw error if error?
-						ranges = data.ranges
-						comment = ranges.comments[0]
-						comment.op.should.deep.equal { c: "bar", p: 4, t: @tid }
-						comment.id.should.equal @tid
-						done()
+				return it("should update the ranges", function(done) {
+					return DocUpdaterClient.getDoc(this.project_id, this.doc.id, (error, res, data) => {
+						if (error != null) { throw error; }
+						const {
+                            ranges
+                        } = data;
+						const comment = ranges.comments[0];
+						comment.op.should.deep.equal({ c: "bar", p: 4, t: this.tid });
+						comment.id.should.equal(this.tid);
+						return done();
+					});
+				});
+			});
 
-			describe "with conflicting ops needing OT", ->
-				before (done) ->
-					@project_id = DocUpdaterClient.randomId()
-					@user_id = DocUpdaterClient.randomId()
-					@doc = {
-						id: DocUpdaterClient.randomId()
+			return describe("with conflicting ops needing OT", function() {
+				before(function(done) {
+					this.project_id = DocUpdaterClient.randomId();
+					this.user_id = DocUpdaterClient.randomId();
+					this.doc = {
+						id: DocUpdaterClient.randomId(),
 						lines: ["foo bar baz"]
-					}
-					@updates = [{
-						doc: @doc.id
-						op: [{ i: "ABC", p: 3 }]
-						v: 0
-						meta: { user_id: @user_id }
+					};
+					this.updates = [{
+						doc: this.doc.id,
+						op: [{ i: "ABC", p: 3 }],
+						v: 0,
+						meta: { user_id: this.user_id }
 					}, {
-						doc: @doc.id
-						op: [{ c: "bar", p: 4, t: @tid = DocUpdaterClient.randomId() }]
+						doc: this.doc.id,
+						op: [{ c: "bar", p: 4, t: (this.tid = DocUpdaterClient.randomId()) }],
 						v: 0
-					}]
-					MockWebApi.insertDoc @project_id, @doc.id, {
-						lines: @doc.lines
+					}];
+					MockWebApi.insertDoc(this.project_id, this.doc.id, {
+						lines: this.doc.lines,
 						version: 0
+					});
+					const jobs = [];
+					for (let update of Array.from(this.updates)) {
+						(update => {
+							return jobs.push(callback => DocUpdaterClient.sendUpdate(this.project_id, this.doc.id, update, callback));
+						})(update);
 					}
-					jobs = []
-					for update in @updates
-						do (update) =>
-							jobs.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc.id, update, callback
-					DocUpdaterClient.preloadDoc @project_id, @doc.id, (error) =>
-						throw error if error?
-						async.series jobs, (error) ->
-							throw error if error?
-							setTimeout done, 200
+					return DocUpdaterClient.preloadDoc(this.project_id, this.doc.id, error => {
+						if (error != null) { throw error; }
+						return async.series(jobs, function(error) {
+							if (error != null) { throw error; }
+							return setTimeout(done, 200);
+						});
+					});
+				});
 				
-				it "should update the comments with the OT shifted comment", (done) ->
-					DocUpdaterClient.getDoc @project_id, @doc.id, (error, res, data) =>
-						throw error if error?
-						ranges = data.ranges
-						comment = ranges.comments[0]
-						comment.op.should.deep.equal { c: "bar", p: 7, t: @tid }
-						done()
+				return it("should update the comments with the OT shifted comment", function(done) {
+					return DocUpdaterClient.getDoc(this.project_id, this.doc.id, (error, res, data) => {
+						if (error != null) { throw error; }
+						const {
+                            ranges
+                        } = data;
+						const comment = ranges.comments[0];
+						comment.op.should.deep.equal({ c: "bar", p: 7, t: this.tid });
+						return done();
+					});
+				});
+			});
+		});
+	});
 
-	describe "Loading ranges from persistence layer", ->
-		before (done) ->
-			@project_id = DocUpdaterClient.randomId()
-			@user_id = DocUpdaterClient.randomId()
-			@id_seed = "587357bd35e64f6157"
-			@doc = {
-				id: DocUpdaterClient.randomId()
+	describe("Loading ranges from persistence layer", function() {
+		before(function(done) {
+			this.project_id = DocUpdaterClient.randomId();
+			this.user_id = DocUpdaterClient.randomId();
+			this.id_seed = "587357bd35e64f6157";
+			this.doc = {
+				id: DocUpdaterClient.randomId(),
 				lines: ["a123aa"]
-			}
-			@update = {
-				doc: @doc.id
-				op: [{ i: "456", p: 5 }]
-				v: 0
-				meta: { user_id: @user_id, tc: @id_seed }
-			}
-			MockWebApi.insertDoc @project_id, @doc.id, {
-				lines: @doc.lines
-				version: 0
+			};
+			this.update = {
+				doc: this.doc.id,
+				op: [{ i: "456", p: 5 }],
+				v: 0,
+				meta: { user_id: this.user_id, tc: this.id_seed }
+			};
+			MockWebApi.insertDoc(this.project_id, this.doc.id, {
+				lines: this.doc.lines,
+				version: 0,
 				ranges: {
 					changes: [{
-						op: { i: "123", p: 1 }
-						metadata:
-							user_id: @user_id
+						op: { i: "123", p: 1 },
+						metadata: {
+							user_id: this.user_id,
 							ts: new Date()
+						}
 					}]
 				}
-			}
-			DocUpdaterClient.preloadDoc @project_id, @doc.id, (error) =>
-				throw error if error?
-				DocUpdaterClient.sendUpdate @project_id, @doc.id, @update, (error) ->
-					throw error if error?
-					setTimeout done, 200
+			});
+			return DocUpdaterClient.preloadDoc(this.project_id, this.doc.id, error => {
+				if (error != null) { throw error; }
+				return DocUpdaterClient.sendUpdate(this.project_id, this.doc.id, this.update, function(error) {
+					if (error != null) { throw error; }
+					return setTimeout(done, 200);
+				});
+			});
+		});
 		
-		it "should have preloaded the existing ranges", (done) ->
-			DocUpdaterClient.getDoc @project_id, @doc.id, (error, res, data) =>
-				throw error if error?
-				{changes} = data.ranges
-				changes[0].op.should.deep.equal { i: "123", p: 1 }
-				changes[1].op.should.deep.equal { i: "456", p: 5 }
-				done()
+		it("should have preloaded the existing ranges", function(done) {
+			return DocUpdaterClient.getDoc(this.project_id, this.doc.id, (error, res, data) => {
+				if (error != null) { throw error; }
+				const {changes} = data.ranges;
+				changes[0].op.should.deep.equal({ i: "123", p: 1 });
+				changes[1].op.should.deep.equal({ i: "456", p: 5 });
+				return done();
+			});
+		});
 		
-		it "should flush the ranges to the persistence layer again", (done) ->
-			DocUpdaterClient.flushDoc @project_id, @doc.id, (error) =>
-				throw error if error?
-				MockWebApi.getDocument @project_id, @doc.id, (error, doc) =>
-					{changes} = doc.ranges
-					changes[0].op.should.deep.equal { i: "123", p: 1 }
-					changes[1].op.should.deep.equal { i: "456", p: 5 }
-					done()
+		return it("should flush the ranges to the persistence layer again", function(done) {
+			return DocUpdaterClient.flushDoc(this.project_id, this.doc.id, error => {
+				if (error != null) { throw error; }
+				return MockWebApi.getDocument(this.project_id, this.doc.id, (error, doc) => {
+					const {changes} = doc.ranges;
+					changes[0].op.should.deep.equal({ i: "123", p: 1 });
+					changes[1].op.should.deep.equal({ i: "456", p: 5 });
+					return done();
+				});
+			});
+		});
+	});
 
-	describe "accepting a change", ->
-		before (done) ->
-			@project_id = DocUpdaterClient.randomId()
-			@user_id = DocUpdaterClient.randomId()
-			@id_seed = "587357bd35e64f6157"
-			@doc = {
-				id: DocUpdaterClient.randomId()
+	describe("accepting a change", function() {
+		before(function(done) {
+			this.project_id = DocUpdaterClient.randomId();
+			this.user_id = DocUpdaterClient.randomId();
+			this.id_seed = "587357bd35e64f6157";
+			this.doc = {
+				id: DocUpdaterClient.randomId(),
 				lines: ["aaa"]
-			}
-			@update = {
-				doc: @doc.id
-				op: [{ i: "456", p: 1 }]
-				v: 0
-				meta: { user_id: @user_id, tc: @id_seed }
-			}
-			MockWebApi.insertDoc @project_id, @doc.id, {
-				lines: @doc.lines
+			};
+			this.update = {
+				doc: this.doc.id,
+				op: [{ i: "456", p: 1 }],
+				v: 0,
+				meta: { user_id: this.user_id, tc: this.id_seed }
+			};
+			MockWebApi.insertDoc(this.project_id, this.doc.id, {
+				lines: this.doc.lines,
 				version: 0
-			}
-			DocUpdaterClient.preloadDoc @project_id, @doc.id, (error) =>
-				throw error if error?
-				DocUpdaterClient.sendUpdate @project_id, @doc.id, @update, (error) =>
-					throw error if error?
-					setTimeout () =>
-						DocUpdaterClient.getDoc @project_id, @doc.id, (error, res, data) =>
-							throw error if error?
-							ranges = data.ranges
-							change = ranges.changes[0]
-							change.op.should.deep.equal { i: "456", p: 1 }
-							change.id.should.equal @id_seed + "000001"
-							change.metadata.user_id.should.equal @user_id
-							done()
-					, 200
+			});
+			return DocUpdaterClient.preloadDoc(this.project_id, this.doc.id, error => {
+				if (error != null) { throw error; }
+				return DocUpdaterClient.sendUpdate(this.project_id, this.doc.id, this.update, error => {
+					if (error != null) { throw error; }
+					return setTimeout(() => {
+						return DocUpdaterClient.getDoc(this.project_id, this.doc.id, (error, res, data) => {
+							if (error != null) { throw error; }
+							const {
+                                ranges
+                            } = data;
+							const change = ranges.changes[0];
+							change.op.should.deep.equal({ i: "456", p: 1 });
+							change.id.should.equal(this.id_seed + "000001");
+							change.metadata.user_id.should.equal(this.user_id);
+							return done();
+						});
+					}
+					, 200);
+				});
+			});
+		});
 		
-		it "should remove the change after accepting", (done) ->
-			DocUpdaterClient.acceptChange @project_id, @doc.id, @id_seed + "000001", (error) =>
-				throw error if error?
-				DocUpdaterClient.getDoc @project_id, @doc.id, (error, res, data) =>
-					throw error if error?
-					expect(data.ranges.changes).to.be.undefined
-					done()
+		return it("should remove the change after accepting", function(done) {
+			return DocUpdaterClient.acceptChange(this.project_id, this.doc.id, this.id_seed + "000001", error => {
+				if (error != null) { throw error; }
+				return DocUpdaterClient.getDoc(this.project_id, this.doc.id, (error, res, data) => {
+					if (error != null) { throw error; }
+					expect(data.ranges.changes).to.be.undefined;
+					return done();
+				});
+			});
+		});
+	});
 
-	describe "deleting a comment range", ->
-		before (done) ->
-			@project_id = DocUpdaterClient.randomId()
-			@user_id = DocUpdaterClient.randomId()
-			@doc = {
-				id: DocUpdaterClient.randomId()
+	describe("deleting a comment range", function() {
+		before(function(done) {
+			this.project_id = DocUpdaterClient.randomId();
+			this.user_id = DocUpdaterClient.randomId();
+			this.doc = {
+				id: DocUpdaterClient.randomId(),
 				lines: ["foo bar"]
-			}
-			@update = {
-				doc: @doc.id
-				op: [{ c: "bar", p: 4, t: @tid = DocUpdaterClient.randomId() }]
+			};
+			this.update = {
+				doc: this.doc.id,
+				op: [{ c: "bar", p: 4, t: (this.tid = DocUpdaterClient.randomId()) }],
 				v: 0
-			}
-			MockWebApi.insertDoc @project_id, @doc.id, {
-				lines: @doc.lines
+			};
+			MockWebApi.insertDoc(this.project_id, this.doc.id, {
+				lines: this.doc.lines,
 				version: 0
-			}
-			DocUpdaterClient.preloadDoc @project_id, @doc.id, (error) =>
-				throw error if error?
-				DocUpdaterClient.sendUpdate @project_id, @doc.id, @update, (error) =>
-					throw error if error?
-					setTimeout () =>
-						DocUpdaterClient.getDoc @project_id, @doc.id, (error, res, data) =>
-							throw error if error?
-							ranges = data.ranges
-							change = ranges.comments[0]
-							change.op.should.deep.equal { c: "bar", p: 4, t: @tid }
-							change.id.should.equal @tid
-							done()
-					, 200
+			});
+			return DocUpdaterClient.preloadDoc(this.project_id, this.doc.id, error => {
+				if (error != null) { throw error; }
+				return DocUpdaterClient.sendUpdate(this.project_id, this.doc.id, this.update, error => {
+					if (error != null) { throw error; }
+					return setTimeout(() => {
+						return DocUpdaterClient.getDoc(this.project_id, this.doc.id, (error, res, data) => {
+							if (error != null) { throw error; }
+							const {
+                                ranges
+                            } = data;
+							const change = ranges.comments[0];
+							change.op.should.deep.equal({ c: "bar", p: 4, t: this.tid });
+							change.id.should.equal(this.tid);
+							return done();
+						});
+					}
+					, 200);
+				});
+			});
+		});
 		
-		it "should remove the comment range", (done) ->
-			DocUpdaterClient.removeComment @project_id, @doc.id, @tid, (error, res) =>
-				throw error if error?
-				expect(res.statusCode).to.equal 204
-				DocUpdaterClient.getDoc @project_id, @doc.id, (error, res, data) =>
-					throw error if error?
-					expect(data.ranges.comments).to.be.undefined
-					done()
+		return it("should remove the comment range", function(done) {
+			return DocUpdaterClient.removeComment(this.project_id, this.doc.id, this.tid, (error, res) => {
+				if (error != null) { throw error; }
+				expect(res.statusCode).to.equal(204);
+				return DocUpdaterClient.getDoc(this.project_id, this.doc.id, (error, res, data) => {
+					if (error != null) { throw error; }
+					expect(data.ranges.comments).to.be.undefined;
+					return done();
+				});
+			});
+		});
+	});
 					
-	describe "tripping range size limit", ->
-		before (done) ->
-			@project_id = DocUpdaterClient.randomId()
-			@user_id = DocUpdaterClient.randomId()
-			@id_seed = DocUpdaterClient.randomId()
-			@doc = {
-				id: DocUpdaterClient.randomId()
+	describe("tripping range size limit", function() {
+		before(function(done) {
+			this.project_id = DocUpdaterClient.randomId();
+			this.user_id = DocUpdaterClient.randomId();
+			this.id_seed = DocUpdaterClient.randomId();
+			this.doc = {
+				id: DocUpdaterClient.randomId(),
 				lines: ["aaa"]
-			}
-			@i = new Array(3 * 1024 * 1024).join("a")
-			@updates = [{
-				doc: @doc.id
-				op: [{ i: @i, p: 1 }]
-				v: 0
-				meta: { user_id: @user_id, tc: @id_seed }
-			}]
-			MockWebApi.insertDoc @project_id, @doc.id, {
-				lines: @doc.lines
+			};
+			this.i = new Array(3 * 1024 * 1024).join("a");
+			this.updates = [{
+				doc: this.doc.id,
+				op: [{ i: this.i, p: 1 }],
+				v: 0,
+				meta: { user_id: this.user_id, tc: this.id_seed }
+			}];
+			MockWebApi.insertDoc(this.project_id, this.doc.id, {
+				lines: this.doc.lines,
 				version: 0
+			});
+			const jobs = [];
+			for (let update of Array.from(this.updates)) {
+				(update => {
+					return jobs.push(callback => DocUpdaterClient.sendUpdate(this.project_id, this.doc.id, update, callback));
+				})(update);
 			}
-			jobs = []
-			for update in @updates
-				do (update) =>
-					jobs.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc.id, update, callback
-			DocUpdaterClient.preloadDoc @project_id, @doc.id, (error) =>
-				throw error if error?
-				async.series jobs, (error) ->
-					throw error if error?
-					setTimeout done, 200
+			return DocUpdaterClient.preloadDoc(this.project_id, this.doc.id, error => {
+				if (error != null) { throw error; }
+				return async.series(jobs, function(error) {
+					if (error != null) { throw error; }
+					return setTimeout(done, 200);
+				});
+			});
+		});
 		
-		it "should not update the ranges", (done) ->
-			DocUpdaterClient.getDoc @project_id, @doc.id, (error, res, data) =>
-				throw error if error?
-				ranges = data.ranges
-				expect(ranges.changes).to.be.undefined
-				done()
+		return it("should not update the ranges", function(done) {
+			return DocUpdaterClient.getDoc(this.project_id, this.doc.id, (error, res, data) => {
+				if (error != null) { throw error; }
+				const {
+                    ranges
+                } = data;
+				expect(ranges.changes).to.be.undefined;
+				return done();
+			});
+		});
+	});
 
-	describe "deleting text surrounding a comment", ->
-		before (done) ->
-			@project_id = DocUpdaterClient.randomId()
-			@user_id = DocUpdaterClient.randomId()
-			@doc_id = DocUpdaterClient.randomId()
-			MockWebApi.insertDoc @project_id, @doc_id, {
-				lines: ["foo bar baz"]
-				version: 0
+	return describe("deleting text surrounding a comment", function() {
+		before(function(done) {
+			this.project_id = DocUpdaterClient.randomId();
+			this.user_id = DocUpdaterClient.randomId();
+			this.doc_id = DocUpdaterClient.randomId();
+			MockWebApi.insertDoc(this.project_id, this.doc_id, {
+				lines: ["foo bar baz"],
+				version: 0,
 				ranges: {
 					comments: [{
-						op: { c: "a", p: 5, tid: @tid = DocUpdaterClient.randomId() }
-						metadata:
-							user_id: @user_id
+						op: { c: "a", p: 5, tid: (this.tid = DocUpdaterClient.randomId()) },
+						metadata: {
+							user_id: this.user_id,
 							ts: new Date()
+						}
 					}]
 				}
-			}
-			@updates = [{
-				doc: @doc_id
-				op: [{ d: "foo ", p: 0 }]
-				v: 0
-				meta: { user_id: @user_id }
+			});
+			this.updates = [{
+				doc: this.doc_id,
+				op: [{ d: "foo ", p: 0 }],
+				v: 0,
+				meta: { user_id: this.user_id }
 			}, {
-				doc: @doc_id
-				op: [{ d: "bar ", p: 0 }]
-				v: 1
-				meta: { user_id: @user_id }
-			}]
-			jobs = []
-			for update in @updates
-				do (update) =>
-					jobs.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc_id, update, callback
-			DocUpdaterClient.preloadDoc @project_id, @doc_id, (error) =>
-				throw error if error?
-				async.series jobs, (error) ->
-					throw error if error?
-					setTimeout () =>
-						DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, data) =>
-							throw error if error?
-							done()
-					, 200
+				doc: this.doc_id,
+				op: [{ d: "bar ", p: 0 }],
+				v: 1,
+				meta: { user_id: this.user_id }
+			}];
+			const jobs = [];
+			for (let update of Array.from(this.updates)) {
+				(update => {
+					return jobs.push(callback => DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, update, callback));
+				})(update);
+			}
+			return DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
+				if (error != null) { throw error; }
+				return async.series(jobs, function(error) {
+					if (error != null) { throw error; }
+					return setTimeout(() => {
+						return DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, data) => {
+							if (error != null) { throw error; }
+							return done();
+						});
+					}
+					, 200);
+				});
+			});
+		});
 		
-		it "should write a snapshot from before the destructive change", (done) ->
-			DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, data) =>
-				return done(error) if error?
-				db.docSnapshots.find {
-					project_id: ObjectId(@project_id),
-					doc_id: ObjectId(@doc_id)
-				}, (error, docSnapshots) =>
-					return done(error) if error?
-					expect(docSnapshots.length).to.equal 1
-					expect(docSnapshots[0].version).to.equal 1
-					expect(docSnapshots[0].lines).to.deep.equal ["bar baz"]
-					expect(docSnapshots[0].ranges.comments[0].op).to.deep.equal {
+		return it("should write a snapshot from before the destructive change", function(done) {
+			return DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, data) => {
+				if (error != null) { return done(error); }
+				return db.docSnapshots.find({
+					project_id: ObjectId(this.project_id),
+					doc_id: ObjectId(this.doc_id)
+				}, (error, docSnapshots) => {
+					if (error != null) { return done(error); }
+					expect(docSnapshots.length).to.equal(1);
+					expect(docSnapshots[0].version).to.equal(1);
+					expect(docSnapshots[0].lines).to.deep.equal(["bar baz"]);
+					expect(docSnapshots[0].ranges.comments[0].op).to.deep.equal({
 						c: "a",
 						p: 1,
-						tid: @tid
-					}
-					done()
+						tid: this.tid
+					});
+					return done();
+				});
+			});
+		});
+	});
+});

+ 314 - 242
services/document-updater/test/acceptance/coffee/SettingADocumentTests.js

@@ -1,248 +1,320 @@
-sinon = require "sinon"
-chai = require("chai")
-chai.should()
-expect = require("chai").expect
-Settings = require('settings-sharelatex')
-rclient_du = require("redis-sharelatex").createClient(Settings.redis.documentupdater)
-Keys = Settings.redis.documentupdater.key_schema
-
-MockTrackChangesApi = require "./helpers/MockTrackChangesApi"
-MockProjectHistoryApi = require "./helpers/MockProjectHistoryApi"
-MockWebApi = require "./helpers/MockWebApi"
-DocUpdaterClient = require "./helpers/DocUpdaterClient"
-DocUpdaterApp = require "./helpers/DocUpdaterApp"
-
-describe "Setting a document", ->
-	before (done) ->
-		@lines = ["one", "two", "three"]
-		@version = 42
-		@update =
-			doc: @doc_id
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require("sinon");
+const chai = require("chai");
+chai.should();
+const {
+    expect
+} = require("chai");
+const Settings = require('settings-sharelatex');
+const rclient_du = require("redis-sharelatex").createClient(Settings.redis.documentupdater);
+const Keys = Settings.redis.documentupdater.key_schema;
+
+const MockTrackChangesApi = require("./helpers/MockTrackChangesApi");
+const MockProjectHistoryApi = require("./helpers/MockProjectHistoryApi");
+const MockWebApi = require("./helpers/MockWebApi");
+const DocUpdaterClient = require("./helpers/DocUpdaterClient");
+const DocUpdaterApp = require("./helpers/DocUpdaterApp");
+
+describe("Setting a document", function() {
+	before(function(done) {
+		this.lines = ["one", "two", "three"];
+		this.version = 42;
+		this.update = {
+			doc: this.doc_id,
 			op: [{
-				i: "one and a half\n"
+				i: "one and a half\n",
 				p: 4
-			}]
-			v: @version
-		@result = ["one", "one and a half", "two", "three"]
-		@newLines = ["these", "are", "the", "new", "lines"]
-		@source = "dropbox"
-		@user_id = "user-id-123"
-
-		sinon.spy MockTrackChangesApi, "flushDoc"
-		sinon.spy MockProjectHistoryApi, "flushProject"
-		sinon.spy MockWebApi, "setDocument"
-		DocUpdaterApp.ensureRunning(done)
-
-	after ->
-		MockTrackChangesApi.flushDoc.restore()
-		MockProjectHistoryApi.flushProject.restore()
-		MockWebApi.setDocument.restore()
-
-	describe "when the updated doc exists in the doc updater", ->
-		before (done) ->
-			[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
-			MockWebApi.insertDoc @project_id, @doc_id, lines: @lines, version: @version
-			DocUpdaterClient.preloadDoc @project_id, @doc_id, (error) =>
-				throw error if error?
-				DocUpdaterClient.sendUpdate @project_id, @doc_id, @update, (error) =>
-					throw error if error?
-					setTimeout () =>
-						DocUpdaterClient.setDocLines @project_id, @doc_id, @newLines, @source, @user_id, false, (error, res, body) =>
-							@statusCode = res.statusCode
-							done()
-					, 200
-			return null
-
-		after ->
-			MockTrackChangesApi.flushDoc.reset()
-			MockProjectHistoryApi.flushProject.reset()
-			MockWebApi.setDocument.reset()
-
-		it "should return a 204 status code", ->
-			@statusCode.should.equal 204
-
-		it "should send the updated doc lines and version to the web api", ->
-			MockWebApi.setDocument
-				.calledWith(@project_id, @doc_id, @newLines)
-				.should.equal true
-
-		it "should update the lines in the doc updater", (done) ->
-			DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
-				doc.lines.should.deep.equal @newLines
-				done()
-			return null
-
-		it "should bump the version in the doc updater", (done) ->
-			DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
-				doc.version.should.equal @version + 2
-				done()
-			return null
-
-		it "should leave the document in redis", (done) ->
-			rclient_du.get Keys.docLines({doc_id: @doc_id}), (error, lines) =>
-				throw error if error?
-				expect(JSON.parse(lines)).to.deep.equal @newLines
-				done()
-			return null
-
-	describe "when the updated doc does not exist in the doc updater", ->
-		before (done) ->
-			[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
-			MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
-			DocUpdaterClient.setDocLines @project_id, @doc_id, @newLines, @source, @user_id, false, (error, res, body) =>
-				@statusCode = res.statusCode
-				setTimeout done, 200
-			return null
-
-		after ->
-			MockTrackChangesApi.flushDoc.reset()
-			MockProjectHistoryApi.flushProject.reset()
-			MockWebApi.setDocument.reset()
-
-		it "should return a 204 status code", ->
-			@statusCode.should.equal 204
-
-		it "should send the updated doc lines to the web api", ->
-			MockWebApi.setDocument
-				.calledWith(@project_id, @doc_id, @newLines)
-				.should.equal true
-
-		it "should flush track changes", ->
-			MockTrackChangesApi.flushDoc.calledWith(@doc_id).should.equal true
-
-		it "should flush project history", ->
-			MockProjectHistoryApi.flushProject.calledWith(@project_id).should.equal true
-
-		it "should remove the document from redis", (done) ->
-			rclient_du.get Keys.docLines({doc_id: @doc_id}), (error, lines) =>
-				throw error if error?
-				expect(lines).to.not.exist
-				done()
-			return null
-
-	describe "when the updated doc is too large for the body parser", ->
-		before (done) ->
-			[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
-			MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
-			@newLines = []
-			while JSON.stringify(@newLines).length < Settings.max_doc_length + 64 * 1024
-				@newLines.push("(a long line of text)".repeat(10000))
-			DocUpdaterClient.setDocLines @project_id, @doc_id, @newLines, @source, @user_id, false, (error, res, body) =>
-				@statusCode = res.statusCode
-				setTimeout done, 200
-			return null
-
-		after ->
-			MockTrackChangesApi.flushDoc.reset()
-			MockProjectHistoryApi.flushProject.reset()
-			MockWebApi.setDocument.reset()
-
-		it "should return a 413 status code", ->
-			@statusCode.should.equal 413
-
-		it "should not send the updated doc lines to the web api", ->
-			MockWebApi.setDocument.called.should.equal false
-
-		it "should not flush track changes", ->
-			MockTrackChangesApi.flushDoc.called.should.equal false
-
-		it "should not flush project history", ->
-			MockProjectHistoryApi.flushProject.called.should.equal false
-
-	describe "when the updated doc is large but under the bodyParser and HTTPController size limit", ->
-		before (done) ->
-			[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
-			MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
-
-			@newLines = []
-			while JSON.stringify(@newLines).length < 2 * 1024 * 1024 # limit in HTTPController
-				@newLines.push("(a long line of text)".repeat(10000))
-			@newLines.pop() # remove the line which took it over the limit
-			DocUpdaterClient.setDocLines @project_id, @doc_id, @newLines, @source, @user_id, false, (error, res, body) =>
-				@statusCode = res.statusCode
-				setTimeout done, 200
-			return null
-
-		after ->
-			MockTrackChangesApi.flushDoc.reset()
-			MockProjectHistoryApi.flushProject.reset()
-			MockWebApi.setDocument.reset()
-
-		it "should return a 204 status code", ->
-			@statusCode.should.equal 204
-
-		it "should send the updated doc lines to the web api", ->
-			MockWebApi.setDocument
-				.calledWith(@project_id, @doc_id, @newLines)
-				.should.equal true
-
-	describe "with track changes", ->
-		before ->
-			@lines = ["one", "one and a half", "two", "three"]
-			@id_seed = "587357bd35e64f6157"
-			@update =
-				doc: @doc_id
+			}],
+			v: this.version
+		};
+		this.result = ["one", "one and a half", "two", "three"];
+		this.newLines = ["these", "are", "the", "new", "lines"];
+		this.source = "dropbox";
+		this.user_id = "user-id-123";
+
+		sinon.spy(MockTrackChangesApi, "flushDoc");
+		sinon.spy(MockProjectHistoryApi, "flushProject");
+		sinon.spy(MockWebApi, "setDocument");
+		return DocUpdaterApp.ensureRunning(done);
+	});
+
+	after(function() {
+		MockTrackChangesApi.flushDoc.restore();
+		MockProjectHistoryApi.flushProject.restore();
+		return MockWebApi.setDocument.restore();
+	});
+
+	describe("when the updated doc exists in the doc updater", function() {
+		before(function(done) {
+			[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
+			MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
+			DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
+				if (error != null) { throw error; }
+				return DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, this.update, error => {
+					if (error != null) { throw error; }
+					return setTimeout(() => {
+						return DocUpdaterClient.setDocLines(this.project_id, this.doc_id, this.newLines, this.source, this.user_id, false, (error, res, body) => {
+							this.statusCode = res.statusCode;
+							return done();
+						});
+					}
+					, 200);
+				});
+			});
+			return null;
+		});
+
+		after(function() {
+			MockTrackChangesApi.flushDoc.reset();
+			MockProjectHistoryApi.flushProject.reset();
+			return MockWebApi.setDocument.reset();
+		});
+
+		it("should return a 204 status code", function() {
+			return this.statusCode.should.equal(204);
+		});
+
+		it("should send the updated doc lines and version to the web api", function() {
+			return MockWebApi.setDocument
+				.calledWith(this.project_id, this.doc_id, this.newLines)
+				.should.equal(true);
+		});
+
+		it("should update the lines in the doc updater", function(done) {
+			DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
+				doc.lines.should.deep.equal(this.newLines);
+				return done();
+			});
+			return null;
+		});
+
+		it("should bump the version in the doc updater", function(done) {
+			DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
+				doc.version.should.equal(this.version + 2);
+				return done();
+			});
+			return null;
+		});
+
+		return it("should leave the document in redis", function(done) {
+			rclient_du.get(Keys.docLines({doc_id: this.doc_id}), (error, lines) => {
+				if (error != null) { throw error; }
+				expect(JSON.parse(lines)).to.deep.equal(this.newLines);
+				return done();
+			});
+			return null;
+		});
+	});
+
+	describe("when the updated doc does not exist in the doc updater", function() {
+		before(function(done) {
+			[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
+			MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
+			DocUpdaterClient.setDocLines(this.project_id, this.doc_id, this.newLines, this.source, this.user_id, false, (error, res, body) => {
+				this.statusCode = res.statusCode;
+				return setTimeout(done, 200);
+			});
+			return null;
+		});
+
+		after(function() {
+			MockTrackChangesApi.flushDoc.reset();
+			MockProjectHistoryApi.flushProject.reset();
+			return MockWebApi.setDocument.reset();
+		});
+
+		it("should return a 204 status code", function() {
+			return this.statusCode.should.equal(204);
+		});
+
+		it("should send the updated doc lines to the web api", function() {
+			return MockWebApi.setDocument
+				.calledWith(this.project_id, this.doc_id, this.newLines)
+				.should.equal(true);
+		});
+
+		it("should flush track changes", function() {
+			return MockTrackChangesApi.flushDoc.calledWith(this.doc_id).should.equal(true);
+		});
+
+		it("should flush project history", function() {
+			return MockProjectHistoryApi.flushProject.calledWith(this.project_id).should.equal(true);
+		});
+
+		return it("should remove the document from redis", function(done) {
+			rclient_du.get(Keys.docLines({doc_id: this.doc_id}), (error, lines) => {
+				if (error != null) { throw error; }
+				expect(lines).to.not.exist;
+				return done();
+			});
+			return null;
+		});
+	});
+
+	describe("when the updated doc is too large for the body parser", function() {
+		before(function(done) {
+			[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
+			MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
+			this.newLines = [];
+			while (JSON.stringify(this.newLines).length < (Settings.max_doc_length + (64 * 1024))) {
+				this.newLines.push("(a long line of text)".repeat(10000));
+			}
+			DocUpdaterClient.setDocLines(this.project_id, this.doc_id, this.newLines, this.source, this.user_id, false, (error, res, body) => {
+				this.statusCode = res.statusCode;
+				return setTimeout(done, 200);
+			});
+			return null;
+		});
+
+		after(function() {
+			MockTrackChangesApi.flushDoc.reset();
+			MockProjectHistoryApi.flushProject.reset();
+			return MockWebApi.setDocument.reset();
+		});
+
+		it("should return a 413 status code", function() {
+			return this.statusCode.should.equal(413);
+		});
+
+		it("should not send the updated doc lines to the web api", () => MockWebApi.setDocument.called.should.equal(false));
+
+		it("should not flush track changes", () => MockTrackChangesApi.flushDoc.called.should.equal(false));
+
+		return it("should not flush project history", () => MockProjectHistoryApi.flushProject.called.should.equal(false));
+	});
+
+	describe("when the updated doc is large but under the bodyParser and HTTPController size limit", function() {
+		before(function(done) {
+			[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
+			MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
+
+			this.newLines = [];
+			while (JSON.stringify(this.newLines).length < (2 * 1024 * 1024)) { // limit in HTTPController
+				this.newLines.push("(a long line of text)".repeat(10000));
+			}
+			this.newLines.pop(); // remove the line which took it over the limit
+			DocUpdaterClient.setDocLines(this.project_id, this.doc_id, this.newLines, this.source, this.user_id, false, (error, res, body) => {
+				this.statusCode = res.statusCode;
+				return setTimeout(done, 200);
+			});
+			return null;
+		});
+
+		after(function() {
+			MockTrackChangesApi.flushDoc.reset();
+			MockProjectHistoryApi.flushProject.reset();
+			return MockWebApi.setDocument.reset();
+		});
+
+		it("should return a 204 status code", function() {
+			return this.statusCode.should.equal(204);
+		});
+
+		return it("should send the updated doc lines to the web api", function() {
+			return MockWebApi.setDocument
+				.calledWith(this.project_id, this.doc_id, this.newLines)
+				.should.equal(true);
+		});
+	});
+
+	return describe("with track changes", function() {
+		before(function() {
+			this.lines = ["one", "one and a half", "two", "three"];
+			this.id_seed = "587357bd35e64f6157";
+			return this.update = {
+				doc: this.doc_id,
 				op: [{
-					d: "one and a half\n"
+					d: "one and a half\n",
 					p: 4
-				}]
-				meta:
-					tc: @id_seed
-					user_id: @user_id
-				v: @version
-
-		describe "with the undo flag", ->
-			before (done) ->
-				[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
-				MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
-				DocUpdaterClient.preloadDoc @project_id, @doc_id, (error) =>
-					throw error if error?
-					DocUpdaterClient.sendUpdate @project_id, @doc_id, @update, (error) =>
-						throw error if error?
-						# Go back to old lines, with undo flag
-						DocUpdaterClient.setDocLines @project_id, @doc_id, @lines, @source, @user_id, true, (error, res, body) =>
-							@statusCode = res.statusCode
-							setTimeout done, 200
-				return null
-
-			after ->
-				MockTrackChangesApi.flushDoc.reset()
-				MockProjectHistoryApi.flushProject.reset()
-				MockWebApi.setDocument.reset()
-
-			it "should undo the tracked changes", (done) ->
-				DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, data) =>
-					throw error if error?
-					ranges = data.ranges
-					expect(ranges.changes).to.be.undefined
-					done()
-				return null
-
-		describe "without the undo flag", ->
-			before (done) ->
-				[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
-				MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
-				DocUpdaterClient.preloadDoc @project_id, @doc_id, (error) =>
-					throw error if error?
-					DocUpdaterClient.sendUpdate @project_id, @doc_id, @update, (error) =>
-						throw error if error?
-						# Go back to old lines, without undo flag
-						DocUpdaterClient.setDocLines @project_id, @doc_id, @lines, @source, @user_id, false, (error, res, body) =>
-							@statusCode = res.statusCode
-							setTimeout done, 200
-				return null
-
-			after ->
-				MockTrackChangesApi.flushDoc.reset()
-				MockProjectHistoryApi.flushProject.reset()
-				MockWebApi.setDocument.reset()
-
-			it "should not undo the tracked changes", (done) ->
-				DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, data) =>
-					throw error if error?
-					ranges = data.ranges
-					expect(ranges.changes.length).to.equal 1
-					done()
-				return null
+				}],
+				meta: {
+					tc: this.id_seed,
+					user_id: this.user_id
+				},
+				v: this.version
+			};
+		});
+
+		describe("with the undo flag", function() {
+			before(function(done) {
+				[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
+				MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
+				DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
+					if (error != null) { throw error; }
+					return DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, this.update, error => {
+						if (error != null) { throw error; }
+						// Go back to old lines, with undo flag
+						return DocUpdaterClient.setDocLines(this.project_id, this.doc_id, this.lines, this.source, this.user_id, true, (error, res, body) => {
+							this.statusCode = res.statusCode;
+							return setTimeout(done, 200);
+						});
+					});
+				});
+				return null;
+			});
+
+			after(function() {
+				MockTrackChangesApi.flushDoc.reset();
+				MockProjectHistoryApi.flushProject.reset();
+				return MockWebApi.setDocument.reset();
+			});
+
+			return it("should undo the tracked changes", function(done) {
+				DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, data) => {
+					if (error != null) { throw error; }
+					const {
+                        ranges
+                    } = data;
+					expect(ranges.changes).to.be.undefined;
+					return done();
+				});
+				return null;
+			});
+		});
+
+		return describe("without the undo flag", function() {
+			before(function(done) {
+				[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
+				MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
+				DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
+					if (error != null) { throw error; }
+					return DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, this.update, error => {
+						if (error != null) { throw error; }
+						// Go back to old lines, without undo flag
+						return DocUpdaterClient.setDocLines(this.project_id, this.doc_id, this.lines, this.source, this.user_id, false, (error, res, body) => {
+							this.statusCode = res.statusCode;
+							return setTimeout(done, 200);
+						});
+					});
+				});
+				return null;
+			});
+
+			after(function() {
+				MockTrackChangesApi.flushDoc.reset();
+				MockProjectHistoryApi.flushProject.reset();
+				return MockWebApi.setDocument.reset();
+			});
+
+			return it("should not undo the tracked changes", function(done) {
+				DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, data) => {
+					if (error != null) { throw error; }
+					const {
+                        ranges
+                    } = data;
+					expect(ranges.changes.length).to.equal(1);
+					return done();
+				});
+				return null;
+			});
+		});
+	});
+});
 
 

+ 37 - 19
services/document-updater/test/acceptance/coffee/helpers/DocUpdaterApp.js

@@ -1,20 +1,38 @@
-app = require('../../../../app')
-require("logger-sharelatex").logger.level("fatal")
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS205: Consider reworking code to avoid use of IIFEs
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const app = require('../../../../app');
+require("logger-sharelatex").logger.level("fatal");
 
-module.exports =
-	running: false
-	initing: false
-	callbacks: []
-	ensureRunning: (callback = (error) ->) ->
-		if @running
-			return callback()
-		else if @initing
-			@callbacks.push callback
-		else
-			@initing = true
-			@callbacks.push callback
-			app.listen 3003, "localhost", (error) =>
-				throw error if error?
-				@running = true
-				for callback in @callbacks
-					callback()
+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(3003, "localhost", error => {
+				if (error != null) { throw error; }
+				this.running = true;
+				return (() => {
+					const result = [];
+					for (callback of Array.from(this.callbacks)) {
+						result.push(callback());
+					}
+					return result;
+				})();
+			});
+		}
+	}
+};

+ 168 - 108
services/document-updater/test/acceptance/coffee/helpers/DocUpdaterClient.js

@@ -1,111 +1,171 @@
-Settings = require('settings-sharelatex')
-rclient = require("redis-sharelatex").createClient(Settings.redis.documentupdater)
-keys = Settings.redis.documentupdater.key_schema
-request = require("request").defaults(jar: false)
-async = require "async"
-
-rclient_sub = require("redis-sharelatex").createClient(Settings.redis.pubsub)
-rclient_sub.subscribe "applied-ops"
-rclient_sub.setMaxListeners(0)
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let DocUpdaterClient;
+const Settings = require('settings-sharelatex');
+const rclient = require("redis-sharelatex").createClient(Settings.redis.documentupdater);
+const keys = Settings.redis.documentupdater.key_schema;
+const request = require("request").defaults({jar: false});
+const async = require("async");
+
+const rclient_sub = require("redis-sharelatex").createClient(Settings.redis.pubsub);
+rclient_sub.subscribe("applied-ops");
+rclient_sub.setMaxListeners(0);
 	
-module.exports = DocUpdaterClient =
-	randomId: () ->
-		chars = for i in [1..24]
-			Math.random().toString(16)[2]
-		return chars.join("")
+module.exports = (DocUpdaterClient = {
+	randomId() {
+		const chars = __range__(1, 24, true).map((i) =>
+			Math.random().toString(16)[2]);
+		return chars.join("");
+	},
 	
-	subscribeToAppliedOps: (callback = (message) ->) ->
-		rclient_sub.on "message", callback
-
-	sendUpdate: (project_id, doc_id, update, callback = (error) ->) ->
-		rclient.rpush keys.pendingUpdates({doc_id}), JSON.stringify(update), (error)->
-			return callback(error) if error?
-			doc_key = "#{project_id}:#{doc_id}"
-			rclient.sadd "DocsWithPendingUpdates", doc_key, (error) ->
-				return callback(error) if error?
-				rclient.rpush "pending-updates-list", doc_key, callback
-
-	sendUpdates: (project_id, doc_id, updates, callback = (error) ->) ->
-		DocUpdaterClient.preloadDoc project_id, doc_id, (error) ->
-			return callback(error) if error?
-			jobs = []
-			for update in updates
-				do (update) ->
-					jobs.push (callback) ->
-						DocUpdaterClient.sendUpdate project_id, doc_id, update, callback
-			async.series jobs, (err) ->
-				DocUpdaterClient.waitForPendingUpdates project_id, doc_id, callback
-
-	waitForPendingUpdates: (project_id, doc_id, callback) ->
-		async.retry {times: 30, interval: 100}, (cb) ->
-			rclient.llen keys.pendingUpdates({doc_id}), (err, length) ->
-				if length > 0
-					cb(new Error("updates still pending"))
-				else
-					cb()
-		, callback
-
-	getDoc: (project_id, doc_id, callback = (error, res, body) ->) ->
-		request.get "http://localhost:3003/project/#{project_id}/doc/#{doc_id}", (error, res, body) ->
-			if body? and res.statusCode >= 200 and res.statusCode < 300
-				body = JSON.parse(body)
-			callback error, res, body
-
-	getDocAndRecentOps: (project_id, doc_id, fromVersion, callback = (error, res, body) ->) ->
-		request.get "http://localhost:3003/project/#{project_id}/doc/#{doc_id}?fromVersion=#{fromVersion}", (error, res, body) ->
-			if body? and res.statusCode >= 200 and res.statusCode < 300
-				body = JSON.parse(body)
-			callback error, res, body
-
-	preloadDoc: (project_id, doc_id, callback = (error) ->) ->
-		DocUpdaterClient.getDoc project_id, doc_id, callback
-
-	flushDoc: (project_id, doc_id, callback = (error) ->) ->
-		request.post "http://localhost:3003/project/#{project_id}/doc/#{doc_id}/flush", (error, res, body) ->
-			callback error, res, body
-
-	setDocLines: (project_id, doc_id, lines, source, user_id, undoing, callback = (error) ->) ->
-		request.post {
-			url: "http://localhost:3003/project/#{project_id}/doc/#{doc_id}"
-			json:
-				lines: lines
-				source: source
-				user_id: user_id
-				undoing: undoing
-		}, (error, res, body) ->
-			callback error, res, body
-
-	deleteDoc: (project_id, doc_id, callback = (error) ->) ->
-		request.del "http://localhost:3003/project/#{project_id}/doc/#{doc_id}", (error, res, body) ->
-			callback error, res, body
-
-	flushProject: (project_id, callback = () ->) ->
-		request.post "http://localhost:3003/project/#{project_id}/flush", callback
-
-	deleteProject: (project_id, callback = () ->) ->
-		request.del "http://localhost:3003/project/#{project_id}", callback
-
-	deleteProjectOnShutdown: (project_id, callback = () ->) ->
-		request.del "http://localhost:3003/project/#{project_id}?background=true&shutdown=true", callback
-
-	flushOldProjects: (callback = () ->) ->
-		request.get "http://localhost:3003/flush_queued_projects?min_delete_age=1", callback
-
-	acceptChange: (project_id, doc_id, change_id, callback = () ->) ->
-		request.post "http://localhost:3003/project/#{project_id}/doc/#{doc_id}/change/#{change_id}/accept", callback
-
-	removeComment: (project_id, doc_id, comment, callback = () ->) ->
-		request.del "http://localhost:3003/project/#{project_id}/doc/#{doc_id}/comment/#{comment}", callback
-
-	getProjectDocs: (project_id, projectStateHash, callback = () ->) ->
-		request.get "http://localhost:3003/project/#{project_id}/doc?state=#{projectStateHash}", (error, res, body) ->
-			if body? and res.statusCode >= 200 and res.statusCode < 300
-				body = JSON.parse(body)
-			callback error, res, body
-
-	sendProjectUpdate: (project_id, userId, docUpdates, fileUpdates, version, callback = (error) ->) ->
-		request.post {
-			url: "http://localhost:3003/project/#{project_id}"
+	subscribeToAppliedOps(callback) {
+		if (callback == null) { callback = function(message) {}; }
+		return rclient_sub.on("message", callback);
+	},
+
+	sendUpdate(project_id, doc_id, update, callback) {
+		if (callback == null) { callback = function(error) {}; }
+		return rclient.rpush(keys.pendingUpdates({doc_id}), JSON.stringify(update), function(error){
+			if (error != null) { return callback(error); }
+			const doc_key = `${project_id}:${doc_id}`;
+			return rclient.sadd("DocsWithPendingUpdates", doc_key, function(error) {
+				if (error != null) { return callback(error); }
+				return rclient.rpush("pending-updates-list", doc_key, callback);
+			});
+		});
+	},
+
+	sendUpdates(project_id, doc_id, updates, callback) {
+		if (callback == null) { callback = function(error) {}; }
+		return DocUpdaterClient.preloadDoc(project_id, doc_id, function(error) {
+			if (error != null) { return callback(error); }
+			const jobs = [];
+			for (let update of Array.from(updates)) {
+				((update => jobs.push(callback => DocUpdaterClient.sendUpdate(project_id, doc_id, update, callback))))(update);
+			}
+			return async.series(jobs, err => DocUpdaterClient.waitForPendingUpdates(project_id, doc_id, callback));
+		});
+	},
+
+	waitForPendingUpdates(project_id, doc_id, callback) {
+		return async.retry({times: 30, interval: 100}, cb => rclient.llen(keys.pendingUpdates({doc_id}), function(err, length) {
+            if (length > 0) {
+                return cb(new Error("updates still pending"));
+            } else {
+                return cb();
+            }
+        })
+		, callback);
+	},
+
+	getDoc(project_id, doc_id, callback) {
+		if (callback == null) { callback = function(error, res, body) {}; }
+		return request.get(`http://localhost:3003/project/${project_id}/doc/${doc_id}`, function(error, res, body) {
+			if ((body != null) && (res.statusCode >= 200) && (res.statusCode < 300)) {
+				body = JSON.parse(body);
+			}
+			return callback(error, res, body);
+		});
+	},
+
+	getDocAndRecentOps(project_id, doc_id, fromVersion, callback) {
+		if (callback == null) { callback = function(error, res, body) {}; }
+		return request.get(`http://localhost:3003/project/${project_id}/doc/${doc_id}?fromVersion=${fromVersion}`, function(error, res, body) {
+			if ((body != null) && (res.statusCode >= 200) && (res.statusCode < 300)) {
+				body = JSON.parse(body);
+			}
+			return callback(error, res, body);
+		});
+	},
+
+	preloadDoc(project_id, doc_id, callback) {
+		if (callback == null) { callback = function(error) {}; }
+		return DocUpdaterClient.getDoc(project_id, doc_id, callback);
+	},
+
+	flushDoc(project_id, doc_id, callback) {
+		if (callback == null) { callback = function(error) {}; }
+		return request.post(`http://localhost:3003/project/${project_id}/doc/${doc_id}/flush`, (error, res, body) => callback(error, res, body));
+	},
+
+	setDocLines(project_id, doc_id, lines, source, user_id, undoing, callback) {
+		if (callback == null) { callback = function(error) {}; }
+		return request.post({
+			url: `http://localhost:3003/project/${project_id}/doc/${doc_id}`,
+			json: {
+				lines,
+				source,
+				user_id,
+				undoing
+			}
+		}, (error, res, body) => callback(error, res, body));
+	},
+
+	deleteDoc(project_id, doc_id, callback) {
+		if (callback == null) { callback = function(error) {}; }
+		return request.del(`http://localhost:3003/project/${project_id}/doc/${doc_id}`, (error, res, body) => callback(error, res, body));
+	},
+
+	flushProject(project_id, callback) {
+		if (callback == null) { callback = function() {}; }
+		return request.post(`http://localhost:3003/project/${project_id}/flush`, callback);
+	},
+
+	deleteProject(project_id, callback) {
+		if (callback == null) { callback = function() {}; }
+		return request.del(`http://localhost:3003/project/${project_id}`, callback);
+	},
+
+	deleteProjectOnShutdown(project_id, callback) {
+		if (callback == null) { callback = function() {}; }
+		return request.del(`http://localhost:3003/project/${project_id}?background=true&shutdown=true`, callback);
+	},
+
+	flushOldProjects(callback) {
+		if (callback == null) { callback = function() {}; }
+		return request.get("http://localhost:3003/flush_queued_projects?min_delete_age=1", callback);
+	},
+
+	acceptChange(project_id, doc_id, change_id, callback) {
+		if (callback == null) { callback = function() {}; }
+		return request.post(`http://localhost:3003/project/${project_id}/doc/${doc_id}/change/${change_id}/accept`, callback);
+	},
+
+	removeComment(project_id, doc_id, comment, callback) {
+		if (callback == null) { callback = function() {}; }
+		return request.del(`http://localhost:3003/project/${project_id}/doc/${doc_id}/comment/${comment}`, callback);
+	},
+
+	getProjectDocs(project_id, projectStateHash, callback) {
+		if (callback == null) { callback = function() {}; }
+		return request.get(`http://localhost:3003/project/${project_id}/doc?state=${projectStateHash}`, function(error, res, body) {
+			if ((body != null) && (res.statusCode >= 200) && (res.statusCode < 300)) {
+				body = JSON.parse(body);
+			}
+			return callback(error, res, body);
+		});
+	},
+
+	sendProjectUpdate(project_id, userId, docUpdates, fileUpdates, version, callback) {
+		if (callback == null) { callback = function(error) {}; }
+		return request.post({
+			url: `http://localhost:3003/project/${project_id}`,
 			json: { userId, docUpdates, fileUpdates, version }
-		}, (error, res, body) ->
-			callback error, res, body
+		}, (error, res, body) => callback(error, res, body));
+	}
+});
+
+function __range__(left, right, inclusive) {
+  let range = [];
+  let ascending = left < right;
+  let end = !inclusive ? right : ascending ? right + 1 : right - 1;
+  for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) {
+    range.push(i);
+  }
+  return range;
+}

+ 30 - 15
services/document-updater/test/acceptance/coffee/helpers/MockProjectHistoryApi.js

@@ -1,19 +1,34 @@
-express = require("express")
-app = express()
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let MockProjectHistoryApi;
+const express = require("express");
+const app = express();
 
-module.exports = MockProjectHistoryApi =
-	flushProject: (doc_id, callback = (error) ->) ->
-		callback()
+module.exports = (MockProjectHistoryApi = {
+	flushProject(doc_id, callback) {
+		if (callback == null) { callback = function(error) {}; }
+		return callback();
+	},
 
-	run: () ->
-		app.post "/project/:project_id/flush", (req, res, next) =>
-			@flushProject req.params.project_id, (error) ->
-				if error?
-					res.sendStatus 500
-				else
-					res.sendStatus 204
+	run() {
+		app.post("/project/:project_id/flush", (req, res, next) => {
+			return this.flushProject(req.params.project_id, function(error) {
+				if (error != null) {
+					return res.sendStatus(500);
+				} else {
+					return res.sendStatus(204);
+				}
+			});
+		});
 
-		app.listen 3054, (error) ->
-			throw error if error?
+		return app.listen(3054, function(error) {
+			if (error != null) { throw error; }
+		});
+	}
+});
 
-MockProjectHistoryApi.run()
+MockProjectHistoryApi.run();

+ 33 - 18
services/document-updater/test/acceptance/coffee/helpers/MockTrackChangesApi.js

@@ -1,23 +1,38 @@
-express = require("express")
-app = express()
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let MockTrackChangesApi;
+const express = require("express");
+const app = express();
 
-module.exports = MockTrackChangesApi =
-	flushDoc: (doc_id, callback = (error) ->) ->
-		callback()
+module.exports = (MockTrackChangesApi = {
+	flushDoc(doc_id, callback) {
+		if (callback == null) { callback = function(error) {}; }
+		return callback();
+	},
 
-	run: () ->
-		app.post "/project/:project_id/doc/:doc_id/flush", (req, res, next) =>
-			@flushDoc req.params.doc_id, (error) ->
-				if error?
-					res.sendStatus 500
-				else
-					res.sendStatus 204
+	run() {
+		app.post("/project/:project_id/doc/:doc_id/flush", (req, res, next) => {
+			return this.flushDoc(req.params.doc_id, function(error) {
+				if (error != null) {
+					return res.sendStatus(500);
+				} else {
+					return res.sendStatus(204);
+				}
+			});
+		});
 
-		app.listen 3015, (error) ->
-			throw error if error?
-		.on "error", (error) ->
-			console.error "error starting MockTrackChangesApi:", error.message
-			process.exit(1)
+		return app.listen(3015, function(error) {
+			if (error != null) { throw error; }
+	}).on("error", function(error) {
+			console.error("error starting MockTrackChangesApi:", error.message);
+			return process.exit(1);
+		});
+	}
+});
 
-MockTrackChangesApi.run()
+MockTrackChangesApi.run();
 

+ 74 - 53
services/document-updater/test/acceptance/coffee/helpers/MockWebApi.js

@@ -1,54 +1,75 @@
-express = require("express")
-bodyParser = require("body-parser")
-app = express()
-MAX_REQUEST_SIZE = 2*(2*1024*1024 + 64*1024)
-
-module.exports = MockWebApi =
-	docs: {}
-
-	clearDocs: () -> @docs = {}
-
-	insertDoc: (project_id, doc_id, doc) ->
-		doc.version ?= 0
-		doc.lines ?= []
-		doc.pathname = '/a/b/c.tex'
-		@docs["#{project_id}:#{doc_id}"] = doc
-
-	setDocument: (project_id, doc_id, lines, version, ranges, lastUpdatedAt, lastUpdatedBy, callback = (error) ->) ->
-		doc = @docs["#{project_id}:#{doc_id}"] ||= {}
-		doc.lines = lines
-		doc.version = version
-		doc.ranges = ranges
-		doc.pathname = '/a/b/c.tex'
-		doc.lastUpdatedAt = lastUpdatedAt
-		doc.lastUpdatedBy = lastUpdatedBy
-		callback null
-
-	getDocument: (project_id, doc_id, callback = (error, doc) ->) ->
-		callback null, @docs["#{project_id}:#{doc_id}"]
-
-	run: () ->
-		app.get "/project/:project_id/doc/:doc_id", (req, res, next) =>
-			@getDocument req.params.project_id, req.params.doc_id, (error, doc) ->
-				if error?
-					res.sendStatus 500
-				else if doc?
-					res.send JSON.stringify doc
-				else
-					res.sendStatus 404
-
-		app.post "/project/:project_id/doc/:doc_id", bodyParser.json({limit: MAX_REQUEST_SIZE}), (req, res, next) =>
-			MockWebApi.setDocument req.params.project_id, req.params.doc_id, req.body.lines, req.body.version, req.body.ranges, req.body.lastUpdatedAt, req.body.lastUpdatedBy, (error) ->
-				if error?
-					res.sendStatus 500
-				else
-					res.sendStatus 204
-
-		app.listen 3000, (error) ->
-			throw error if error?
-		.on "error", (error) ->
-			console.error "error starting MockWebApi:", error.message
-			process.exit(1)
-
-MockWebApi.run()
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let MockWebApi;
+const express = require("express");
+const bodyParser = require("body-parser");
+const app = express();
+const MAX_REQUEST_SIZE = 2*((2*1024*1024) + (64*1024));
+
+module.exports = (MockWebApi = {
+	docs: {},
+
+	clearDocs() { return this.docs = {}; },
+
+	insertDoc(project_id, doc_id, doc) {
+		if (doc.version == null) { doc.version = 0; }
+		if (doc.lines == null) { doc.lines = []; }
+		doc.pathname = '/a/b/c.tex';
+		return this.docs[`${project_id}:${doc_id}`] = doc;
+	},
+
+	setDocument(project_id, doc_id, lines, version, ranges, lastUpdatedAt, lastUpdatedBy, callback) {
+		if (callback == null) { callback = function(error) {}; }
+		const doc = this.docs[`${project_id}:${doc_id}`] || (this.docs[`${project_id}:${doc_id}`] = {});
+		doc.lines = lines;
+		doc.version = version;
+		doc.ranges = ranges;
+		doc.pathname = '/a/b/c.tex';
+		doc.lastUpdatedAt = lastUpdatedAt;
+		doc.lastUpdatedBy = lastUpdatedBy;
+		return callback(null);
+	},
+
+	getDocument(project_id, doc_id, callback) {
+		if (callback == null) { callback = function(error, doc) {}; }
+		return callback(null, this.docs[`${project_id}:${doc_id}`]);
+	},
+
+	run() {
+		app.get("/project/:project_id/doc/:doc_id", (req, res, next) => {
+			return this.getDocument(req.params.project_id, req.params.doc_id, function(error, doc) {
+				if (error != null) {
+					return res.sendStatus(500);
+				} else if (doc != null) {
+					return res.send(JSON.stringify(doc));
+				} else {
+					return res.sendStatus(404);
+				}
+			});
+		});
+
+		app.post("/project/:project_id/doc/:doc_id", bodyParser.json({limit: MAX_REQUEST_SIZE}), (req, res, next) => {
+			return MockWebApi.setDocument(req.params.project_id, req.params.doc_id, req.body.lines, req.body.version, req.body.ranges, req.body.lastUpdatedAt, req.body.lastUpdatedBy, function(error) {
+				if (error != null) {
+					return res.sendStatus(500);
+				} else {
+					return res.sendStatus(204);
+				}
+			});
+		});
+
+		return app.listen(3000, function(error) {
+			if (error != null) { throw error; }
+	}).on("error", function(error) {
+			console.error("error starting MockWebApi:", error.message);
+			return process.exit(1);
+		});
+	}
+});
+
+MockWebApi.run();