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

decaffeinate: Convert DiffCodecTests.coffee and 23 other files to JS

decaffeinate 6 лет назад
Родитель
Сommit
c781526af0
24 измененных файлов с 6098 добавлено и 4541 удалено
  1. 63 43
      services/document-updater/test/unit/coffee/DiffCodec/DiffCodecTests.js
  2. 127 90
      services/document-updater/test/unit/coffee/DispatchManager/DispatchManagerTests.js
  3. 694 537
      services/document-updater/test/unit/coffee/DocumentManager/DocumentManagerTests.js
  4. 300 234
      services/document-updater/test/unit/coffee/HistoryManager/HistoryManagerTests.js
  5. 73 46
      services/document-updater/test/unit/coffee/HistoryRedisManager/HistoryRedisManagerTests.js
  6. 779 587
      services/document-updater/test/unit/coffee/HttpController/HttpControllerTests.js
  7. 57 32
      services/document-updater/test/unit/coffee/LockManager/CheckingTheLock.js
  8. 74 45
      services/document-updater/test/unit/coffee/LockManager/ReleasingTheLock.js
  9. 107 65
      services/document-updater/test/unit/coffee/LockManager/getLockTests.js
  10. 116 70
      services/document-updater/test/unit/coffee/LockManager/tryLockTests.js
  11. 293 215
      services/document-updater/test/unit/coffee/PersistenceManager/PersistenceManagerTests.js
  12. 147 117
      services/document-updater/test/unit/coffee/ProjectHistoryRedisManager/ProjectHistoryRedisManagerTests.js
  13. 112 73
      services/document-updater/test/unit/coffee/ProjectManager/flushAndDeleteProjectTests.js
  14. 103 64
      services/document-updater/test/unit/coffee/ProjectManager/flushProjectTests.js
  15. 161 118
      services/document-updater/test/unit/coffee/ProjectManager/getProjectDocsTests.js
  16. 235 173
      services/document-updater/test/unit/coffee/ProjectManager/updateProjectTests.js
  17. 314 235
      services/document-updater/test/unit/coffee/RangesManager/RangesManagerTests.js
  18. 116 72
      services/document-updater/test/unit/coffee/RateLimitManager/RateLimitManager.js
  19. 125 91
      services/document-updater/test/unit/coffee/RealTimeRedisManager/RealTimeRedisManagerTests.js
  20. 981 786
      services/document-updater/test/unit/coffee/RedisManager/RedisManagerTests.js
  21. 342 267
      services/document-updater/test/unit/coffee/ShareJS/TextTransformTests.js
  22. 127 91
      services/document-updater/test/unit/coffee/ShareJsDB/ShareJsDBTests.js
  23. 178 128
      services/document-updater/test/unit/coffee/ShareJsUpdateManager/ShareJsUpdateManagerTests.js
  24. 474 362
      services/document-updater/test/unit/coffee/UpdateManager/UpdateManagerTests.js

+ 63 - 43
services/document-updater/test/unit/coffee/DiffCodec/DiffCodecTests.js

@@ -1,56 +1,76 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-expect = chai.expect
-modulePath = "../../../../app/js/DiffCodec.js"
-SandboxedModule = require('sandboxed-module')
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require('sinon');
+const chai = require('chai');
+const should = chai.should();
+const {
+    expect
+} = chai;
+const modulePath = "../../../../app/js/DiffCodec.js";
+const SandboxedModule = require('sandboxed-module');
 
-describe "DiffCodec", ->
-	beforeEach ->
-		@callback = sinon.stub()
-		@DiffCodec = SandboxedModule.require modulePath
+describe("DiffCodec", function() {
+	beforeEach(function() {
+		this.callback = sinon.stub();
+		return this.DiffCodec = SandboxedModule.require(modulePath);
+	});
 
-	describe "diffAsShareJsOps", ->
-		it "should insert new text correctly", (done) ->
-			@before = ["hello world"]
-			@after  = ["hello beautiful world"]
-			@DiffCodec.diffAsShareJsOp @before, @after, (error, ops) ->
-				expect(ops).to.deep.equal [
-					i: "beautiful "
+	return describe("diffAsShareJsOps", function() {
+		it("should insert new text correctly", function(done) {
+			this.before = ["hello world"];
+			this.after  = ["hello beautiful world"];
+			return this.DiffCodec.diffAsShareJsOp(this.before, this.after, function(error, ops) {
+				expect(ops).to.deep.equal([{
+					i: "beautiful ",
 					p: 6
-				]
-				done()
+				}
+				]);
+				return done();
+			});
+		});
 
-		it "should shift later inserts by previous inserts", (done) ->
-			@before = ["the boy played with the ball"]
-			@after  = ["the tall boy played with the red ball"]
-			@DiffCodec.diffAsShareJsOp @before, @after, (error, ops) ->
-				expect(ops).to.deep.equal [
-					{ i: "tall ", p: 4 }
+		it("should shift later inserts by previous inserts", function(done) {
+			this.before = ["the boy played with the ball"];
+			this.after  = ["the tall boy played with the red ball"];
+			return this.DiffCodec.diffAsShareJsOp(this.before, this.after, function(error, ops) {
+				expect(ops).to.deep.equal([
+					{ i: "tall ", p: 4 },
 					{ i: "red ", p: 29 }
-				]
-				done()
+				]);
+				return done();
+			});
+		});
 
-		it "should delete text correctly", (done) ->
-			@before  = ["hello beautiful world"]
-			@after = ["hello world"]
-			@DiffCodec.diffAsShareJsOp @before, @after, (error, ops) ->
-				expect(ops).to.deep.equal [
-					d: "beautiful "
+		it("should delete text correctly", function(done) {
+			this.before  = ["hello beautiful world"];
+			this.after = ["hello world"];
+			return this.DiffCodec.diffAsShareJsOp(this.before, this.after, function(error, ops) {
+				expect(ops).to.deep.equal([{
+					d: "beautiful ",
 					p: 6
-				]
-				done()
+				}
+				]);
+				return done();
+			});
+		});
 
 
-		it "should shift later deletes by the first deletes", (done) ->
-			@before = ["the tall boy played with the red ball"]
-			@after  = ["the boy played with the ball"]
-			@DiffCodec.diffAsShareJsOp @before, @after, (error, ops) ->
-				expect(ops).to.deep.equal [
-					{ d: "tall ", p: 4 }
+		return it("should shift later deletes by the first deletes", function(done) {
+			this.before = ["the tall boy played with the red ball"];
+			this.after  = ["the boy played with the ball"];
+			return this.DiffCodec.diffAsShareJsOp(this.before, this.after, function(error, ops) {
+				expect(ops).to.deep.equal([
+					{ d: "tall ", p: 4 },
 					{ d: "red ", p: 24 }
-				]
-				done()
+				]);
+				return done();
+			});
+		});
+	});
+});
 			
 			
 

+ 127 - 90
services/document-updater/test/unit/coffee/DispatchManager/DispatchManagerTests.js

@@ -1,110 +1,147 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-modulePath = "../../../../app/js/DispatchManager.js"
-SandboxedModule = require('sandboxed-module')
-Errors = require "../../../../app/js/Errors.js"
+/*
+ * 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');
+const should = chai.should();
+const modulePath = "../../../../app/js/DispatchManager.js";
+const SandboxedModule = require('sandboxed-module');
+const Errors = require("../../../../app/js/Errors.js");
 
-describe "DispatchManager", ->
-	beforeEach ->
-		@timeout(3000)
-		@DispatchManager = SandboxedModule.require modulePath, requires:
-			"./UpdateManager" : @UpdateManager = {}
-			"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() }
-			"settings-sharelatex": @settings =
-				redis:
+describe("DispatchManager", function() {
+	beforeEach(function() {
+		this.timeout(3000);
+		this.DispatchManager = SandboxedModule.require(modulePath, { requires: {
+			"./UpdateManager" : (this.UpdateManager = {}),
+			"logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() }),
+			"settings-sharelatex": (this.settings = {
+				redis: {
 					documentupdater: {}
-			"redis-sharelatex": @redis = {}
-			"./RateLimitManager": {}
-			"./Errors": Errors
-			"./Metrics":
-				Timer: ->
-					done: ->
-		@callback = sinon.stub()
-		@RateLimiter = { run: (task,cb) -> task(cb) } # run task without rate limit
+				}
+			}),
+			"redis-sharelatex": (this.redis = {}),
+			"./RateLimitManager": {},
+			"./Errors": Errors,
+			"./Metrics": {
+				Timer() {
+					return {done() {}};
+				}
+			}
+		}
+	}
+		);
+		this.callback = sinon.stub();
+		return this.RateLimiter = { run(task,cb) { return task(cb); } };}); // run task without rate limit
 
-	describe "each worker", ->
-		beforeEach ->
-			@client =
-				auth: sinon.stub()
-			@redis.createClient = sinon.stub().returns @client
-			@worker = @DispatchManager.createDispatcher(@RateLimiter)
+	return describe("each worker", function() {
+		beforeEach(function() {
+			this.client =
+				{auth: sinon.stub()};
+			this.redis.createClient = sinon.stub().returns(this.client);
+			return this.worker = this.DispatchManager.createDispatcher(this.RateLimiter);
+		});
 			
-		it "should create a new redis client", ->
-			@redis.createClient.called.should.equal true
+		it("should create a new redis client", function() {
+			return this.redis.createClient.called.should.equal(true);
+		});
 			
-		describe "_waitForUpdateThenDispatchWorker", ->
-			beforeEach ->
-				@project_id = "project-id-123"
-				@doc_id = "doc-id-123"
-				@doc_key = "#{@project_id}:#{@doc_id}"
-				@client.blpop = sinon.stub().callsArgWith(2, null, ["pending-updates-list", @doc_key])
+		describe("_waitForUpdateThenDispatchWorker", function() {
+			beforeEach(function() {
+				this.project_id = "project-id-123";
+				this.doc_id = "doc-id-123";
+				this.doc_key = `${this.project_id}:${this.doc_id}`;
+				return this.client.blpop = sinon.stub().callsArgWith(2, null, ["pending-updates-list", this.doc_key]);
+			});
 
-			describe "in the normal case", ->
-				beforeEach ->
-					@UpdateManager.processOutstandingUpdatesWithLock = sinon.stub().callsArg(2)
-					@worker._waitForUpdateThenDispatchWorker @callback
+			describe("in the normal case", function() {
+				beforeEach(function() {
+					this.UpdateManager.processOutstandingUpdatesWithLock = sinon.stub().callsArg(2);
+					return this.worker._waitForUpdateThenDispatchWorker(this.callback);
+				});
 
-				it "should call redis with BLPOP", ->
-					@client.blpop
+				it("should call redis with BLPOP", function() {
+					return this.client.blpop
 						.calledWith("pending-updates-list", 0)
-						.should.equal true
+						.should.equal(true);
+				});
 						
-				it "should call processOutstandingUpdatesWithLock", ->
-					@UpdateManager.processOutstandingUpdatesWithLock
-						.calledWith(@project_id, @doc_id)
-						.should.equal true
+				it("should call processOutstandingUpdatesWithLock", function() {
+					return this.UpdateManager.processOutstandingUpdatesWithLock
+						.calledWith(this.project_id, this.doc_id)
+						.should.equal(true);
+				});
 
-				it "should not log any errors", ->
-					@logger.error.called.should.equal false
-					@logger.warn.called.should.equal false
+				it("should not log any errors", function() {
+					this.logger.error.called.should.equal(false);
+					return this.logger.warn.called.should.equal(false);
+				});
 
-				it "should call the callback", ->
-					@callback.called.should.equal true
+				return it("should call the callback", function() {
+					return this.callback.called.should.equal(true);
+				});
+			});
 
-			describe "with an error", ->
-				beforeEach ->
-					@UpdateManager.processOutstandingUpdatesWithLock = sinon.stub().callsArgWith(2, new Error("a generic error"))
-					@worker._waitForUpdateThenDispatchWorker @callback
+			describe("with an error", function() {
+				beforeEach(function() {
+					this.UpdateManager.processOutstandingUpdatesWithLock = sinon.stub().callsArgWith(2, new Error("a generic error"));
+					return this.worker._waitForUpdateThenDispatchWorker(this.callback);
+				});
 
-				it "should log an error", ->
-					@logger.error.called.should.equal true
+				it("should log an error", function() {
+					return this.logger.error.called.should.equal(true);
+				});
 
-				it "should call the callback", ->
-					@callback.called.should.equal true
+				return it("should call the callback", function() {
+					return this.callback.called.should.equal(true);
+				});
+			});
 
-			describe "with a 'Delete component' error", ->
-				beforeEach ->
-					@UpdateManager.processOutstandingUpdatesWithLock = sinon.stub().callsArgWith(2, new Errors.DeleteMismatchError())
-					@worker._waitForUpdateThenDispatchWorker @callback
+			return describe("with a 'Delete component' error", function() {
+				beforeEach(function() {
+					this.UpdateManager.processOutstandingUpdatesWithLock = sinon.stub().callsArgWith(2, new Errors.DeleteMismatchError());
+					return this.worker._waitForUpdateThenDispatchWorker(this.callback);
+				});
 
-				it "should log a warning", ->
-					@logger.warn.called.should.equal true
+				it("should log a warning", function() {
+					return this.logger.warn.called.should.equal(true);
+				});
 
-				it "should call the callback", ->
-					@callback.called.should.equal true
+				return it("should call the callback", function() {
+					return this.callback.called.should.equal(true);
+				});
+			});
+		});
 
-		describe "run", ->
-			it "should call _waitForUpdateThenDispatchWorker until shutting down", (done) ->
-				callCount = 0
-				@worker._waitForUpdateThenDispatchWorker = (callback = (error) ->) =>
-					callCount++
-					if callCount == 3
-						@settings.shuttingDown = true
-					setTimeout () ->
-						callback()
-					, 10
-				sinon.spy @worker, "_waitForUpdateThenDispatchWorker"
-				
-			
-				@worker.run()
+		return describe("run", () => it("should call _waitForUpdateThenDispatchWorker until shutting down", function(done) {
+            let callCount = 0;
+            this.worker._waitForUpdateThenDispatchWorker = callback => {
+                if (callback == null) { callback = function(error) {}; }
+                callCount++;
+                if (callCount === 3) {
+                    this.settings.shuttingDown = true;
+                }
+                return setTimeout(() => callback()
+                , 10);
+            };
+            sinon.spy(this.worker, "_waitForUpdateThenDispatchWorker");
+            
+        
+            this.worker.run();
 
-				checkStatus = () =>
-					if not @settings.shuttingDown # retry until shutdown
-						setTimeout checkStatus, 100
-						return
-					else
-						@worker._waitForUpdateThenDispatchWorker.callCount.should.equal 3
-						done()
+            var checkStatus = () => {
+                if (!this.settings.shuttingDown) { // retry until shutdown
+                    setTimeout(checkStatus, 100);
+                    return;
+                } else {
+                    this.worker._waitForUpdateThenDispatchWorker.callCount.should.equal(3);
+                    return done();
+                }
+            };
 
-				checkStatus()
+            return checkStatus();
+        }));
+	});
+});

+ 694 - 537
services/document-updater/test/unit/coffee/DocumentManager/DocumentManagerTests.js

@@ -1,538 +1,695 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-modulePath = "../../../../app/js/DocumentManager.js"
-SandboxedModule = require('sandboxed-module')
-Errors = require "../../../../app/js/Errors"
-tk = require "timekeeper"
-
-describe "DocumentManager", ->
-	beforeEach ->
-		tk.freeze(new Date())
-		@DocumentManager = SandboxedModule.require modulePath, requires:
-			"./RedisManager": @RedisManager = {}
-			"./ProjectHistoryRedisManager": @ProjectHistoryRedisManager = {}
-			"./PersistenceManager": @PersistenceManager = {}
-			"./HistoryManager": @HistoryManager =
-				flushDocChangesAsync: sinon.stub()
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS206: Consider reworking classes to avoid initClass
+ * 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');
+const should = chai.should();
+const modulePath = "../../../../app/js/DocumentManager.js";
+const SandboxedModule = require('sandboxed-module');
+const Errors = require("../../../../app/js/Errors");
+const tk = require("timekeeper");
+
+describe("DocumentManager", function() {
+	beforeEach(function() {
+		let Timer;
+		tk.freeze(new Date());
+		this.DocumentManager = SandboxedModule.require(modulePath, { requires: {
+			"./RedisManager": (this.RedisManager = {}),
+			"./ProjectHistoryRedisManager": (this.ProjectHistoryRedisManager = {}),
+			"./PersistenceManager": (this.PersistenceManager = {}),
+			"./HistoryManager": (this.HistoryManager = {
+				flushDocChangesAsync: sinon.stub(),
 				flushProjectChangesAsync: sinon.stub()
-			"logger-sharelatex": @logger = {log: sinon.stub(), warn: sinon.stub()}
-			"./DocOpsManager": @DocOpsManager = {}
-			"./Metrics": @Metrics =
-				Timer: class Timer
-					done: sinon.stub()
-			"./RealTimeRedisManager": @RealTimeRedisManager = {}
-			"./DiffCodec": @DiffCodec = {}
-			"./UpdateManager": @UpdateManager = {}
-			"./RangesManager": @RangesManager = {}
-		@project_id = "project-id-123"
-		@projectHistoryId = "history-id-123"
-		@projectHistoryType = "project-history"
-		@doc_id = "doc-id-123"
-		@user_id = 1234
-		@callback = sinon.stub()
-		@lines = ["one", "two", "three"]
-		@version = 42
-		@ranges = { comments: "mock", entries: "mock" }
-		@pathname = '/a/b/c.tex'
-		@unflushedTime = Date.now()
-		@lastUpdatedAt = Date.now()
-		@lastUpdatedBy = 'last-author-id'
-
-	afterEach ->
-		tk.reset()
-
-	describe "flushAndDeleteDoc", ->
-		describe "successfully", ->
-			beforeEach ->
-				@RedisManager.removeDocFromMemory = sinon.stub().callsArg(2)
-				@DocumentManager.flushDocIfLoaded = sinon.stub().callsArgWith(2)
-				@DocumentManager.flushAndDeleteDoc @project_id, @doc_id, {}, @callback
-
-			it "should flush the doc", ->
-				@DocumentManager.flushDocIfLoaded
-					.calledWith(@project_id, @doc_id)
-					.should.equal true
-
-			it "should remove the doc from redis", ->
-				@RedisManager.removeDocFromMemory
-					.calledWith(@project_id, @doc_id)
-					.should.equal true
-
-			it "should call the callback without error", ->
-				@callback.calledWith(null).should.equal true
-
-			it "should time the execution", ->
-				@Metrics.Timer::done.called.should.equal true
-
-			it "should flush to the history api", ->
-				@HistoryManager.flushDocChangesAsync
-					.calledWithExactly(@project_id, @doc_id)
-					.should.equal true
-
-		describe "when a flush error occurs", ->
-			beforeEach ->
-				@DocumentManager.flushDocIfLoaded = sinon.stub().callsArgWith(2, new Error("boom!"))
-				@RedisManager.removeDocFromMemory = sinon.stub().callsArg(2)
-
-			it "should not remove the doc from redis", (done) ->
-				@DocumentManager.flushAndDeleteDoc @project_id, @doc_id, {}, (error) =>
-					error.should.exist
-					@RedisManager.removeDocFromMemory.called.should.equal false
-					done()
-
-			describe "when ignoring flush errors", ->
-				it "should remove the doc from redis", (done) ->
-					@DocumentManager.flushAndDeleteDoc @project_id, @doc_id, { ignoreFlushErrors: true }, (error) =>
-						if error?
-							return done(error)
-						@RedisManager.removeDocFromMemory.called.should.equal true
-						done()
-
-	describe "flushDocIfLoaded", ->
-		describe "when the doc is in Redis", ->
-			beforeEach ->
-				@RedisManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges, @pathname, @projectHistoryId, @unflushedTime, @lastUpdatedAt, @lastUpdatedBy)
-				@RedisManager.clearUnflushedTime = sinon.stub().callsArgWith(1, null)
-				@PersistenceManager.setDoc = sinon.stub().yields()
-				@DocumentManager.flushDocIfLoaded @project_id, @doc_id, @callback
-
-			it "should get the doc from redis", ->
-				@RedisManager.getDoc
-					.calledWith(@project_id, @doc_id)
-					.should.equal true
-
-			it "should write the doc lines to the persistence layer", ->
-				@PersistenceManager.setDoc
-					.calledWith(@project_id, @doc_id, @lines, @version, @ranges, @lastUpdatedAt, @lastUpdatedBy)
-					.should.equal true
-
-			it "should call the callback without error", ->
-				@callback.calledWith(null).should.equal true
-
-			it "should time the execution", ->
-				@Metrics.Timer::done.called.should.equal true
-
-		describe "when the document is not in Redis", ->
-			beforeEach ->
-				@RedisManager.getDoc = sinon.stub().callsArgWith(2, null, null, null, null)
-				@PersistenceManager.setDoc = sinon.stub().yields()
-				@DocOpsManager.flushDocOpsToMongo = sinon.stub().callsArgWith(2)
-				@DocumentManager.flushDocIfLoaded @project_id, @doc_id, @callback
-
-			it "should get the doc from redis", ->
-				@RedisManager.getDoc
-					.calledWith(@project_id, @doc_id)
-					.should.equal true
-
-			it "should not write anything to the persistence layer", ->
-				@PersistenceManager.setDoc.called.should.equal false
-				@DocOpsManager.flushDocOpsToMongo.called.should.equal false
-
-			it "should call the callback without error", ->
-				@callback.calledWith(null).should.equal true
-
-			it "should time the execution", ->
-				@Metrics.Timer::done.called.should.equal true
-
-	describe "getDocAndRecentOps", ->
-		describe "with a previous version specified", ->
-			beforeEach ->
-				@DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges, @pathname, @projectHistoryId)
-				@RedisManager.getPreviousDocOps = sinon.stub().callsArgWith(3, null, @ops)
-				@DocumentManager.getDocAndRecentOps @project_id, @doc_id, @fromVersion, @callback
-
-			it "should get the doc", ->
-				@DocumentManager.getDoc
-					.calledWith(@project_id, @doc_id)
-					.should.equal true
-
-			it "should get the doc ops", ->
-				@RedisManager.getPreviousDocOps
-					.calledWith(@doc_id, @fromVersion, @version)
-					.should.equal true
-
-			it "should call the callback with the doc info", ->
-				@callback.calledWith(null, @lines, @version, @ops, @ranges, @pathname, @projectHistoryId).should.equal true
-
-			it "should time the execution", ->
-				@Metrics.Timer::done.called.should.equal true
-
-		describe "with no previous version specified", ->
-			beforeEach ->
-				@DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges, @pathname, @projectHistoryId)
-				@RedisManager.getPreviousDocOps = sinon.stub().callsArgWith(3, null, @ops)
-				@DocumentManager.getDocAndRecentOps @project_id, @doc_id, -1, @callback
-
-			it "should get the doc", ->
-				@DocumentManager.getDoc
-					.calledWith(@project_id, @doc_id)
-					.should.equal true
-
-			it "should not need to get the doc ops", ->
-				@RedisManager.getPreviousDocOps.called.should.equal false
-
-			it "should call the callback with the doc info", ->
-				@callback.calledWith(null, @lines, @version, [], @ranges, @pathname, @projectHistoryId).should.equal true
-
-			it "should time the execution", ->
-				@Metrics.Timer::done.called.should.equal true
-
-	describe "getDoc", ->
-		describe "when the doc exists in Redis", ->
-			beforeEach ->
-				@RedisManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges, @pathname, @projectHistoryId, @unflushedTime)
-				@DocumentManager.getDoc @project_id, @doc_id, @callback
-
-			it "should get the doc from Redis", ->
-				@RedisManager.getDoc
-					.calledWith(@project_id, @doc_id)
-					.should.equal true
-
-			it "should call the callback with the doc info", ->
-				@callback.calledWith(null, @lines, @version, @ranges, @pathname, @projectHistoryId, @unflushedTime, true).should.equal true
-
-			it "should time the execution", ->
-				@Metrics.Timer::done.called.should.equal true
-
-		describe "when the doc does not exist in Redis", ->
-			beforeEach ->
-				@RedisManager.getDoc = sinon.stub().callsArgWith(2, null, null, null, null, null, null)
-				@PersistenceManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges, @pathname, @projectHistoryId, @projectHistoryType)
-				@RedisManager.putDocInMemory = sinon.stub().yields()
-				@RedisManager.setHistoryType = sinon.stub().yields()
-				@DocumentManager.getDoc @project_id, @doc_id, @callback
-
-			it "should try to get the doc from Redis", ->
-				@RedisManager.getDoc
-					.calledWith(@project_id, @doc_id)
-					.should.equal true
-
-			it "should get the doc from the PersistenceManager", ->
-				@PersistenceManager.getDoc
-					.calledWith(@project_id, @doc_id)
-					.should.equal true
-
-			it "should set the doc in Redis", ->
-				@RedisManager.putDocInMemory
-					.calledWith(@project_id, @doc_id, @lines, @version, @ranges, @pathname, @projectHistoryId)
-					.should.equal true
-
-			it "should set the history type in Redis", ->
-				@RedisManager.setHistoryType
-					.calledWith(@doc_id, @projectHistoryType)
-					.should.equal true
-
-			it "should call the callback with the doc info", ->
-				@callback.calledWith(null, @lines, @version, @ranges, @pathname, @projectHistoryId, null, false).should.equal true
-
-			it "should time the execution", ->
-				@Metrics.Timer::done.called.should.equal true
-
-	describe "setDoc", ->
-		describe "with plain tex lines", ->
-			beforeEach ->
-				@beforeLines = ["before", "lines"]
-				@afterLines = ["after", "lines"]
-				@ops = [{ i: "foo", p: 4 }, { d: "bar", p: 42 }]
-				@DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, @beforeLines, @version, @ranges, @pathname, @projectHistoryId, @unflushedTime, true)
-				@DiffCodec.diffAsShareJsOp = sinon.stub().callsArgWith(2, null, @ops)
-				@UpdateManager.applyUpdate = sinon.stub().callsArgWith(3, null)
-				@DocumentManager.flushDocIfLoaded = sinon.stub().callsArg(2)
-				@DocumentManager.flushAndDeleteDoc = sinon.stub().callsArg(3)
-
-			describe "when already loaded", ->
-				beforeEach ->
-					@DocumentManager.setDoc @project_id, @doc_id, @afterLines, @source, @user_id, false, @callback
-
-				it "should get the current doc lines", ->
-					@DocumentManager.getDoc
-						.calledWith(@project_id, @doc_id)
-						.should.equal true
-
-				it "should return a diff of the old and new lines", ->
-					@DiffCodec.diffAsShareJsOp
-						.calledWith(@beforeLines, @afterLines)
-						.should.equal true
-
-				it "should apply the diff as a ShareJS op", ->
-					@UpdateManager.applyUpdate
-						.calledWith(
-							@project_id,
-							@doc_id,
-							{
-								doc: @doc_id,
-								v: @version,
-								op: @ops,
-								meta: {
-									type: "external"
-									source: @source
-									user_id: @user_id
-								}
-							}
-						)
-						.should.equal true
-
-				it "should flush the doc to Mongo", ->
-					@DocumentManager.flushDocIfLoaded
-						.calledWith(@project_id, @doc_id)
-						.should.equal true
-
-				it "should not flush the project history", ->
-					@HistoryManager.flushProjectChangesAsync
-						.called.should.equal false
-
-				it "should call the callback", ->
-					@callback.calledWith(null).should.equal true
-
-				it "should time the execution", ->
-					@Metrics.Timer::done.called.should.equal true
-
-			describe "when not already loaded", ->
-				beforeEach ->
-					@DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, @beforeLines, @version, @pathname, null, false)
-					@DocumentManager.setDoc @project_id, @doc_id, @afterLines, @source, @user_id, false, @callback
-
-				it "should flush and delete the doc from the doc updater", ->
-					@DocumentManager.flushAndDeleteDoc
-						.calledWith(@project_id, @doc_id, {})
-						.should.equal true
-
-				it "should not flush the project history", ->
-					@HistoryManager.flushProjectChangesAsync
-						.calledWithExactly(@project_id)
-						.should.equal true
-
-			describe "without new lines", ->
-				beforeEach ->
-					@DocumentManager.setDoc @project_id, @doc_id, null, @source, @user_id, false, @callback
-
-				it "should return the callback with an error", ->
-					@callback.calledWith(new Error("No lines were passed to setDoc"))
-
-				it "should not try to get the doc lines", ->
-					@DocumentManager.getDoc.called.should.equal false
-
-			describe "with the undoing flag", ->
-				beforeEach ->
-					# Copy ops so we don't interfere with other tests
-					@ops = [{ i: "foo", p: 4 }, { d: "bar", p: 42 }]
-					@DiffCodec.diffAsShareJsOp = sinon.stub().callsArgWith(2, null, @ops)
-					@DocumentManager.setDoc @project_id, @doc_id, @afterLines, @source, @user_id, true, @callback
-
-				it "should set the undo flag on each op", ->
-					for op in @ops
-						op.u.should.equal true
-
-	describe "acceptChanges", ->
-		beforeEach ->
-			@change_id = "mock-change-id"
-			@change_ids = [ "mock-change-id-1", "mock-change-id-2", "mock-change-id-3", "mock-change-id-4" ]
-			@version = 34
-			@lines = ["original", "lines"]
-			@ranges = { entries: "mock", comments: "mock" }
-			@updated_ranges = { entries: "updated", comments: "updated" }
-			@DocumentManager.getDoc = sinon.stub().yields(null, @lines, @version, @ranges)
-			@RangesManager.acceptChanges = sinon.stub().yields(null, @updated_ranges)
-			@RedisManager.updateDocument = sinon.stub().yields()
-
-		describe "successfully with a single change", ->
-			beforeEach ->
-				@DocumentManager.acceptChanges @project_id, @doc_id, [ @change_id ], @callback
-
-			it "should get the document's current ranges", ->
-				@DocumentManager.getDoc
-					.calledWith(@project_id, @doc_id)
-					.should.equal true
-
-			it "should apply the accept change to the ranges", ->
-				@RangesManager.acceptChanges
-					.calledWith([ @change_id ], @ranges)
-					.should.equal true
-
-			it "should save the updated ranges", ->
-				@RedisManager.updateDocument
-					.calledWith(@project_id, @doc_id, @lines, @version, [], @updated_ranges, {})
-					.should.equal true
-
-			it "should call the callback", ->
-				@callback.called.should.equal true
-
-		describe "successfully with multiple changes", ->
-			beforeEach ->
-				@DocumentManager.acceptChanges @project_id, @doc_id, @change_ids, @callback
-
-			it "should apply the accept change to the ranges", ->
-				@RangesManager.acceptChanges
-					.calledWith(@change_ids, @ranges)
-					.should.equal true
-
-		describe "when the doc is not found", ->
-			beforeEach ->
-				@DocumentManager.getDoc = sinon.stub().yields(null, null, null, null)
-				@DocumentManager.acceptChanges @project_id, @doc_id, [ @change_id ], @callback
-
-			it "should not save anything", ->
-				@RedisManager.updateDocument.called.should.equal false
-
-			it "should call the callback with a not found error", ->
-				error = new Errors.NotFoundError("document not found: #{@doc_id}")
-				@callback.calledWith(error).should.equal true
-
-	describe "deleteComment", ->
-		beforeEach ->
-			@comment_id = "mock-comment-id"
-			@version = 34
-			@lines = ["original", "lines"]
-			@ranges = { comments: ["one", "two", "three"] }
-			@updated_ranges = { comments: ["one", "three"] }
-			@DocumentManager.getDoc = sinon.stub().yields(null, @lines, @version, @ranges)
-			@RangesManager.deleteComment = sinon.stub().yields(null, @updated_ranges)
-			@RedisManager.updateDocument = sinon.stub().yields()
-
-		describe "successfully", ->
-			beforeEach ->
-				@DocumentManager.deleteComment @project_id, @doc_id, @comment_id, @callback
-
-			it "should get the document's current ranges", ->
-				@DocumentManager.getDoc
-					.calledWith(@project_id, @doc_id)
-					.should.equal true
-
-			it "should delete the comment from the ranges", ->
-				@RangesManager.deleteComment
-					.calledWith(@comment_id, @ranges)
-					.should.equal true
-
-			it "should save the updated ranges", ->
-				@RedisManager.updateDocument
-					.calledWith(@project_id, @doc_id, @lines, @version, [], @updated_ranges, {})
-					.should.equal true
-
-			it "should call the callback", ->
-				@callback.called.should.equal true
-
-		describe "when the doc is not found", ->
-			beforeEach ->
-				@DocumentManager.getDoc = sinon.stub().yields(null, null, null, null)
-				@DocumentManager.acceptChanges @project_id, @doc_id, [ @comment_id ], @callback
-
-			it "should not save anything", ->
-				@RedisManager.updateDocument.called.should.equal false
-
-			it "should call the callback with a not found error", ->
-				error = new Errors.NotFoundError("document not found: #{@doc_id}")
-				@callback.calledWith(error).should.equal true
-
-	describe "getDocAndFlushIfOld", ->
-		beforeEach ->
-			@DocumentManager.flushDocIfLoaded = sinon.stub().callsArg(2)
-
-		describe "when the doc is in Redis", ->
-			describe "and has changes to be flushed", ->
-				beforeEach ->
-					@DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges, @projectHistoryId, @pathname, Date.now() - 1e9, true)
-					@DocumentManager.getDocAndFlushIfOld @project_id, @doc_id, @callback
-
-				it "should get the doc", ->
-					@DocumentManager.getDoc
-						.calledWith(@project_id, @doc_id)
-						.should.equal true
-
-				it "should flush the doc", ->
-					@DocumentManager.flushDocIfLoaded
-					.calledWith(@project_id, @doc_id)
-					.should.equal true
-
-				it "should call the callback with the lines and versions", ->
-					@callback.calledWith(null, @lines, @version).should.equal true
-
-			describe "and has only changes that don't need to be flushed", ->
-				beforeEach ->
-					@DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges, @pathname, Date.now() - 100, true)
-					@DocumentManager.getDocAndFlushIfOld @project_id, @doc_id, @callback
-
-				it "should get the doc", ->
-					@DocumentManager.getDoc
-						.calledWith(@project_id, @doc_id)
-						.should.equal true
-
-				it "should not flush the doc", ->
-					@DocumentManager.flushDocIfLoaded
-					.called.should.equal false
-
-				it "should call the callback with the lines and versions", ->
-					@callback.calledWith(null, @lines, @version).should.equal true
-
-		describe "when the doc is not in Redis", ->
-			beforeEach ->
-				@DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges, null, false)
-				@DocumentManager.getDocAndFlushIfOld @project_id, @doc_id, @callback
-
-			it "should get the doc", ->
-				@DocumentManager.getDoc
-					.calledWith(@project_id, @doc_id)
-					.should.equal true
-
-			it "should not flush the doc", ->
-				@DocumentManager.flushDocIfLoaded
-				.called.should.equal false
-
-			it "should call the callback with the lines and versions", ->
-				@callback.calledWith(null, @lines, @version).should.equal true
-
-	describe "renameDoc", ->
-		beforeEach ->
-			@update = 'some-update'
-			@RedisManager.renameDoc = sinon.stub().yields()
-
-		describe "successfully", ->
-			beforeEach ->
-				@DocumentManager.renameDoc @project_id, @doc_id, @user_id, @update, @projectHistoryId, @callback
-
-			it "should rename the document", ->
-				@RedisManager.renameDoc
-					.calledWith(@project_id, @doc_id, @user_id, @update, @projectHistoryId)
-					.should.equal true
-
-			it "should call the callback", ->
-				@callback.called.should.equal true
-
-	describe "resyncDocContents", ->
-		describe "when doc is loaded in redis", ->
-			beforeEach ->
-				@RedisManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges, @pathname, @projectHistoryId)
-				@ProjectHistoryRedisManager.queueResyncDocContent = sinon.stub()
-				@DocumentManager.resyncDocContents @project_id, @doc_id, @callback
-
-			it "gets the doc contents from redis", ->
-				@RedisManager.getDoc
-					.calledWith(@project_id, @doc_id)
-					.should.equal true
-
-			it "queues a resync doc content update", ->
-				@ProjectHistoryRedisManager.queueResyncDocContent
-					.calledWith(@project_id, @projectHistoryId, @doc_id, @lines, @version, @pathname, @callback)
-					.should.equal true
-
-		describe "when doc is not loaded in redis", ->
-			beforeEach ->
-				@RedisManager.getDoc = sinon.stub().callsArgWith(2, null)
-				@PersistenceManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges, @pathname, @projectHistoryId)
-				@ProjectHistoryRedisManager.queueResyncDocContent = sinon.stub()
-				@DocumentManager.resyncDocContents @project_id, @doc_id, @callback
-
-			it "tries to get the doc contents from redis", ->
-				@RedisManager.getDoc
-					.calledWith(@project_id, @doc_id)
-					.should.equal true
-
-			it "gets the doc contents from web", ->
-				@PersistenceManager.getDoc
-					.calledWith(@project_id, @doc_id)
-					.should.equal true
-
-			it "queues a resync doc content update", ->
-				@ProjectHistoryRedisManager.queueResyncDocContent
-					.calledWith(@project_id, @projectHistoryId, @doc_id, @lines, @version, @pathname, @callback)
-					.should.equal true
+			}),
+			"logger-sharelatex": (this.logger = {log: sinon.stub(), warn: sinon.stub()}),
+			"./DocOpsManager": (this.DocOpsManager = {}),
+			"./Metrics": (this.Metrics = {
+				Timer: (Timer = (function() {
+					Timer = class Timer {
+						static initClass() {
+							this.prototype.done = sinon.stub();
+						}
+					};
+					Timer.initClass();
+					return Timer;
+				})())
+			}),
+			"./RealTimeRedisManager": (this.RealTimeRedisManager = {}),
+			"./DiffCodec": (this.DiffCodec = {}),
+			"./UpdateManager": (this.UpdateManager = {}),
+			"./RangesManager": (this.RangesManager = {})
+		}
+	});
+		this.project_id = "project-id-123";
+		this.projectHistoryId = "history-id-123";
+		this.projectHistoryType = "project-history";
+		this.doc_id = "doc-id-123";
+		this.user_id = 1234;
+		this.callback = sinon.stub();
+		this.lines = ["one", "two", "three"];
+		this.version = 42;
+		this.ranges = { comments: "mock", entries: "mock" };
+		this.pathname = '/a/b/c.tex';
+		this.unflushedTime = Date.now();
+		this.lastUpdatedAt = Date.now();
+		return this.lastUpdatedBy = 'last-author-id';
+	});
+
+	afterEach(() => tk.reset());
+
+	describe("flushAndDeleteDoc", function() {
+		describe("successfully", function() {
+			beforeEach(function() {
+				this.RedisManager.removeDocFromMemory = sinon.stub().callsArg(2);
+				this.DocumentManager.flushDocIfLoaded = sinon.stub().callsArgWith(2);
+				return this.DocumentManager.flushAndDeleteDoc(this.project_id, this.doc_id, {}, this.callback);
+			});
+
+			it("should flush the doc", function() {
+				return this.DocumentManager.flushDocIfLoaded
+					.calledWith(this.project_id, this.doc_id)
+					.should.equal(true);
+			});
+
+			it("should remove the doc from redis", function() {
+				return this.RedisManager.removeDocFromMemory
+					.calledWith(this.project_id, this.doc_id)
+					.should.equal(true);
+			});
+
+			it("should call the callback without error", function() {
+				return this.callback.calledWith(null).should.equal(true);
+			});
+
+			it("should time the execution", function() {
+				return this.Metrics.Timer.prototype.done.called.should.equal(true);
+			});
+
+			return it("should flush to the history api", function() {
+				return this.HistoryManager.flushDocChangesAsync
+					.calledWithExactly(this.project_id, this.doc_id)
+					.should.equal(true);
+			});
+		});
+
+		return describe("when a flush error occurs", function() {
+			beforeEach(function() {
+				this.DocumentManager.flushDocIfLoaded = sinon.stub().callsArgWith(2, new Error("boom!"));
+				return this.RedisManager.removeDocFromMemory = sinon.stub().callsArg(2);
+			});
+
+			it("should not remove the doc from redis", function(done) {
+				return this.DocumentManager.flushAndDeleteDoc(this.project_id, this.doc_id, {}, error => {
+					error.should.exist;
+					this.RedisManager.removeDocFromMemory.called.should.equal(false);
+					return done();
+				});
+			});
+
+			return describe("when ignoring flush errors", () => it("should remove the doc from redis", function(done) {
+                return this.DocumentManager.flushAndDeleteDoc(this.project_id, this.doc_id, { ignoreFlushErrors: true }, error => {
+                    if (error != null) {
+                        return done(error);
+                    }
+                    this.RedisManager.removeDocFromMemory.called.should.equal(true);
+                    return done();
+                });
+            }));
+		});
+	});
+
+	describe("flushDocIfLoaded", function() {
+		describe("when the doc is in Redis", function() {
+			beforeEach(function() {
+				this.RedisManager.getDoc = sinon.stub().callsArgWith(2, null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId, this.unflushedTime, this.lastUpdatedAt, this.lastUpdatedBy);
+				this.RedisManager.clearUnflushedTime = sinon.stub().callsArgWith(1, null);
+				this.PersistenceManager.setDoc = sinon.stub().yields();
+				return this.DocumentManager.flushDocIfLoaded(this.project_id, this.doc_id, this.callback);
+			});
+
+			it("should get the doc from redis", function() {
+				return this.RedisManager.getDoc
+					.calledWith(this.project_id, this.doc_id)
+					.should.equal(true);
+			});
+
+			it("should write the doc lines to the persistence layer", function() {
+				return this.PersistenceManager.setDoc
+					.calledWith(this.project_id, this.doc_id, this.lines, this.version, this.ranges, this.lastUpdatedAt, this.lastUpdatedBy)
+					.should.equal(true);
+			});
+
+			it("should call the callback without error", function() {
+				return this.callback.calledWith(null).should.equal(true);
+			});
+
+			return it("should time the execution", function() {
+				return this.Metrics.Timer.prototype.done.called.should.equal(true);
+			});
+		});
+
+		return describe("when the document is not in Redis", function() {
+			beforeEach(function() {
+				this.RedisManager.getDoc = sinon.stub().callsArgWith(2, null, null, null, null);
+				this.PersistenceManager.setDoc = sinon.stub().yields();
+				this.DocOpsManager.flushDocOpsToMongo = sinon.stub().callsArgWith(2);
+				return this.DocumentManager.flushDocIfLoaded(this.project_id, this.doc_id, this.callback);
+			});
+
+			it("should get the doc from redis", function() {
+				return this.RedisManager.getDoc
+					.calledWith(this.project_id, this.doc_id)
+					.should.equal(true);
+			});
+
+			it("should not write anything to the persistence layer", function() {
+				this.PersistenceManager.setDoc.called.should.equal(false);
+				return this.DocOpsManager.flushDocOpsToMongo.called.should.equal(false);
+			});
+
+			it("should call the callback without error", function() {
+				return this.callback.calledWith(null).should.equal(true);
+			});
+
+			return it("should time the execution", function() {
+				return this.Metrics.Timer.prototype.done.called.should.equal(true);
+			});
+		});
+	});
+
+	describe("getDocAndRecentOps", function() {
+		describe("with a previous version specified", function() {
+			beforeEach(function() {
+				this.DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId);
+				this.RedisManager.getPreviousDocOps = sinon.stub().callsArgWith(3, null, this.ops);
+				return this.DocumentManager.getDocAndRecentOps(this.project_id, this.doc_id, this.fromVersion, this.callback);
+			});
+
+			it("should get the doc", function() {
+				return this.DocumentManager.getDoc
+					.calledWith(this.project_id, this.doc_id)
+					.should.equal(true);
+			});
+
+			it("should get the doc ops", function() {
+				return this.RedisManager.getPreviousDocOps
+					.calledWith(this.doc_id, this.fromVersion, this.version)
+					.should.equal(true);
+			});
+
+			it("should call the callback with the doc info", function() {
+				return this.callback.calledWith(null, this.lines, this.version, this.ops, this.ranges, this.pathname, this.projectHistoryId).should.equal(true);
+			});
+
+			return it("should time the execution", function() {
+				return this.Metrics.Timer.prototype.done.called.should.equal(true);
+			});
+		});
+
+		return describe("with no previous version specified", function() {
+			beforeEach(function() {
+				this.DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId);
+				this.RedisManager.getPreviousDocOps = sinon.stub().callsArgWith(3, null, this.ops);
+				return this.DocumentManager.getDocAndRecentOps(this.project_id, this.doc_id, -1, this.callback);
+			});
+
+			it("should get the doc", function() {
+				return this.DocumentManager.getDoc
+					.calledWith(this.project_id, this.doc_id)
+					.should.equal(true);
+			});
+
+			it("should not need to get the doc ops", function() {
+				return this.RedisManager.getPreviousDocOps.called.should.equal(false);
+			});
+
+			it("should call the callback with the doc info", function() {
+				return this.callback.calledWith(null, this.lines, this.version, [], this.ranges, this.pathname, this.projectHistoryId).should.equal(true);
+			});
+
+			return it("should time the execution", function() {
+				return this.Metrics.Timer.prototype.done.called.should.equal(true);
+			});
+		});
+	});
+
+	describe("getDoc", function() {
+		describe("when the doc exists in Redis", function() {
+			beforeEach(function() {
+				this.RedisManager.getDoc = sinon.stub().callsArgWith(2, null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId, this.unflushedTime);
+				return this.DocumentManager.getDoc(this.project_id, this.doc_id, this.callback);
+			});
+
+			it("should get the doc from Redis", function() {
+				return this.RedisManager.getDoc
+					.calledWith(this.project_id, this.doc_id)
+					.should.equal(true);
+			});
+
+			it("should call the callback with the doc info", function() {
+				return this.callback.calledWith(null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId, this.unflushedTime, true).should.equal(true);
+			});
+
+			return it("should time the execution", function() {
+				return this.Metrics.Timer.prototype.done.called.should.equal(true);
+			});
+		});
+
+		return describe("when the doc does not exist in Redis", function() {
+			beforeEach(function() {
+				this.RedisManager.getDoc = sinon.stub().callsArgWith(2, null, null, null, null, null, null);
+				this.PersistenceManager.getDoc = sinon.stub().callsArgWith(2, null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId, this.projectHistoryType);
+				this.RedisManager.putDocInMemory = sinon.stub().yields();
+				this.RedisManager.setHistoryType = sinon.stub().yields();
+				return this.DocumentManager.getDoc(this.project_id, this.doc_id, this.callback);
+			});
+
+			it("should try to get the doc from Redis", function() {
+				return this.RedisManager.getDoc
+					.calledWith(this.project_id, this.doc_id)
+					.should.equal(true);
+			});
+
+			it("should get the doc from the PersistenceManager", function() {
+				return this.PersistenceManager.getDoc
+					.calledWith(this.project_id, this.doc_id)
+					.should.equal(true);
+			});
+
+			it("should set the doc in Redis", function() {
+				return this.RedisManager.putDocInMemory
+					.calledWith(this.project_id, this.doc_id, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId)
+					.should.equal(true);
+			});
+
+			it("should set the history type in Redis", function() {
+				return this.RedisManager.setHistoryType
+					.calledWith(this.doc_id, this.projectHistoryType)
+					.should.equal(true);
+			});
+
+			it("should call the callback with the doc info", function() {
+				return this.callback.calledWith(null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId, null, false).should.equal(true);
+			});
+
+			return it("should time the execution", function() {
+				return this.Metrics.Timer.prototype.done.called.should.equal(true);
+			});
+		});
+	});
+
+	describe("setDoc", () => describe("with plain tex lines", function() {
+        beforeEach(function() {
+            this.beforeLines = ["before", "lines"];
+            this.afterLines = ["after", "lines"];
+            this.ops = [{ i: "foo", p: 4 }, { d: "bar", p: 42 }];
+            this.DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, this.beforeLines, this.version, this.ranges, this.pathname, this.projectHistoryId, this.unflushedTime, true);
+            this.DiffCodec.diffAsShareJsOp = sinon.stub().callsArgWith(2, null, this.ops);
+            this.UpdateManager.applyUpdate = sinon.stub().callsArgWith(3, null);
+            this.DocumentManager.flushDocIfLoaded = sinon.stub().callsArg(2);
+            return this.DocumentManager.flushAndDeleteDoc = sinon.stub().callsArg(3);
+        });
+
+        describe("when already loaded", function() {
+            beforeEach(function() {
+                return this.DocumentManager.setDoc(this.project_id, this.doc_id, this.afterLines, this.source, this.user_id, false, this.callback);
+            });
+
+            it("should get the current doc lines", function() {
+                return this.DocumentManager.getDoc
+                    .calledWith(this.project_id, this.doc_id)
+                    .should.equal(true);
+            });
+
+            it("should return a diff of the old and new lines", function() {
+                return this.DiffCodec.diffAsShareJsOp
+                    .calledWith(this.beforeLines, this.afterLines)
+                    .should.equal(true);
+            });
+
+            it("should apply the diff as a ShareJS op", function() {
+                return this.UpdateManager.applyUpdate
+                    .calledWith(
+                        this.project_id,
+                        this.doc_id,
+                        {
+                            doc: this.doc_id,
+                            v: this.version,
+                            op: this.ops,
+                            meta: {
+                                type: "external",
+                                source: this.source,
+                                user_id: this.user_id
+                            }
+                        }
+                    )
+                    .should.equal(true);
+            });
+
+            it("should flush the doc to Mongo", function() {
+                return this.DocumentManager.flushDocIfLoaded
+                    .calledWith(this.project_id, this.doc_id)
+                    .should.equal(true);
+            });
+
+            it("should not flush the project history", function() {
+                return this.HistoryManager.flushProjectChangesAsync
+                    .called.should.equal(false);
+            });
+
+            it("should call the callback", function() {
+                return this.callback.calledWith(null).should.equal(true);
+            });
+
+            return it("should time the execution", function() {
+                return this.Metrics.Timer.prototype.done.called.should.equal(true);
+            });
+        });
+
+        describe("when not already loaded", function() {
+            beforeEach(function() {
+                this.DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, this.beforeLines, this.version, this.pathname, null, false);
+                return this.DocumentManager.setDoc(this.project_id, this.doc_id, this.afterLines, this.source, this.user_id, false, this.callback);
+            });
+
+            it("should flush and delete the doc from the doc updater", function() {
+                return this.DocumentManager.flushAndDeleteDoc
+                    .calledWith(this.project_id, this.doc_id, {})
+                    .should.equal(true);
+            });
+
+            return it("should not flush the project history", function() {
+                return this.HistoryManager.flushProjectChangesAsync
+                    .calledWithExactly(this.project_id)
+                    .should.equal(true);
+            });
+        });
+
+        describe("without new lines", function() {
+            beforeEach(function() {
+                return this.DocumentManager.setDoc(this.project_id, this.doc_id, null, this.source, this.user_id, false, this.callback);
+            });
+
+            it("should return the callback with an error", function() {
+                return this.callback.calledWith(new Error("No lines were passed to setDoc"));
+            });
+
+            return it("should not try to get the doc lines", function() {
+                return this.DocumentManager.getDoc.called.should.equal(false);
+            });
+        });
+
+        return describe("with the undoing flag", function() {
+            beforeEach(function() {
+                // Copy ops so we don't interfere with other tests
+                this.ops = [{ i: "foo", p: 4 }, { d: "bar", p: 42 }];
+                this.DiffCodec.diffAsShareJsOp = sinon.stub().callsArgWith(2, null, this.ops);
+                return this.DocumentManager.setDoc(this.project_id, this.doc_id, this.afterLines, this.source, this.user_id, true, this.callback);
+            });
+
+            return it("should set the undo flag on each op", function() {
+                return Array.from(this.ops).map((op) =>
+                    op.u.should.equal(true));
+            });
+        });
+    }));
+
+	describe("acceptChanges", function() {
+		beforeEach(function() {
+			this.change_id = "mock-change-id";
+			this.change_ids = [ "mock-change-id-1", "mock-change-id-2", "mock-change-id-3", "mock-change-id-4" ];
+			this.version = 34;
+			this.lines = ["original", "lines"];
+			this.ranges = { entries: "mock", comments: "mock" };
+			this.updated_ranges = { entries: "updated", comments: "updated" };
+			this.DocumentManager.getDoc = sinon.stub().yields(null, this.lines, this.version, this.ranges);
+			this.RangesManager.acceptChanges = sinon.stub().yields(null, this.updated_ranges);
+			return this.RedisManager.updateDocument = sinon.stub().yields();
+		});
+
+		describe("successfully with a single change", function() {
+			beforeEach(function() {
+				return this.DocumentManager.acceptChanges(this.project_id, this.doc_id, [ this.change_id ], this.callback);
+			});
+
+			it("should get the document's current ranges", function() {
+				return this.DocumentManager.getDoc
+					.calledWith(this.project_id, this.doc_id)
+					.should.equal(true);
+			});
+
+			it("should apply the accept change to the ranges", function() {
+				return this.RangesManager.acceptChanges
+					.calledWith([ this.change_id ], this.ranges)
+					.should.equal(true);
+			});
+
+			it("should save the updated ranges", function() {
+				return this.RedisManager.updateDocument
+					.calledWith(this.project_id, this.doc_id, this.lines, this.version, [], this.updated_ranges, {})
+					.should.equal(true);
+			});
+
+			return it("should call the callback", function() {
+				return this.callback.called.should.equal(true);
+			});
+		});
+
+		describe("successfully with multiple changes", function() {
+			beforeEach(function() {
+				return this.DocumentManager.acceptChanges(this.project_id, this.doc_id, this.change_ids, this.callback);
+			});
+
+			return it("should apply the accept change to the ranges", function() {
+				return this.RangesManager.acceptChanges
+					.calledWith(this.change_ids, this.ranges)
+					.should.equal(true);
+			});
+		});
+
+		return describe("when the doc is not found", function() {
+			beforeEach(function() {
+				this.DocumentManager.getDoc = sinon.stub().yields(null, null, null, null);
+				return this.DocumentManager.acceptChanges(this.project_id, this.doc_id, [ this.change_id ], this.callback);
+			});
+
+			it("should not save anything", function() {
+				return this.RedisManager.updateDocument.called.should.equal(false);
+			});
+
+			return it("should call the callback with a not found error", function() {
+				const error = new Errors.NotFoundError(`document not found: ${this.doc_id}`);
+				return this.callback.calledWith(error).should.equal(true);
+			});
+		});
+	});
+
+	describe("deleteComment", function() {
+		beforeEach(function() {
+			this.comment_id = "mock-comment-id";
+			this.version = 34;
+			this.lines = ["original", "lines"];
+			this.ranges = { comments: ["one", "two", "three"] };
+			this.updated_ranges = { comments: ["one", "three"] };
+			this.DocumentManager.getDoc = sinon.stub().yields(null, this.lines, this.version, this.ranges);
+			this.RangesManager.deleteComment = sinon.stub().yields(null, this.updated_ranges);
+			return this.RedisManager.updateDocument = sinon.stub().yields();
+		});
+
+		describe("successfully", function() {
+			beforeEach(function() {
+				return this.DocumentManager.deleteComment(this.project_id, this.doc_id, this.comment_id, this.callback);
+			});
+
+			it("should get the document's current ranges", function() {
+				return this.DocumentManager.getDoc
+					.calledWith(this.project_id, this.doc_id)
+					.should.equal(true);
+			});
+
+			it("should delete the comment from the ranges", function() {
+				return this.RangesManager.deleteComment
+					.calledWith(this.comment_id, this.ranges)
+					.should.equal(true);
+			});
+
+			it("should save the updated ranges", function() {
+				return this.RedisManager.updateDocument
+					.calledWith(this.project_id, this.doc_id, this.lines, this.version, [], this.updated_ranges, {})
+					.should.equal(true);
+			});
+
+			return it("should call the callback", function() {
+				return this.callback.called.should.equal(true);
+			});
+		});
+
+		return describe("when the doc is not found", function() {
+			beforeEach(function() {
+				this.DocumentManager.getDoc = sinon.stub().yields(null, null, null, null);
+				return this.DocumentManager.acceptChanges(this.project_id, this.doc_id, [ this.comment_id ], this.callback);
+			});
+
+			it("should not save anything", function() {
+				return this.RedisManager.updateDocument.called.should.equal(false);
+			});
+
+			return it("should call the callback with a not found error", function() {
+				const error = new Errors.NotFoundError(`document not found: ${this.doc_id}`);
+				return this.callback.calledWith(error).should.equal(true);
+			});
+		});
+	});
+
+	describe("getDocAndFlushIfOld", function() {
+		beforeEach(function() {
+			return this.DocumentManager.flushDocIfLoaded = sinon.stub().callsArg(2);
+		});
+
+		describe("when the doc is in Redis", function() {
+			describe("and has changes to be flushed", function() {
+				beforeEach(function() {
+					this.DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, this.lines, this.version, this.ranges, this.projectHistoryId, this.pathname, Date.now() - 1e9, true);
+					return this.DocumentManager.getDocAndFlushIfOld(this.project_id, this.doc_id, this.callback);
+				});
+
+				it("should get the doc", function() {
+					return this.DocumentManager.getDoc
+						.calledWith(this.project_id, this.doc_id)
+						.should.equal(true);
+				});
+
+				it("should flush the doc", function() {
+					return this.DocumentManager.flushDocIfLoaded
+					.calledWith(this.project_id, this.doc_id)
+					.should.equal(true);
+				});
+
+				return it("should call the callback with the lines and versions", function() {
+					return this.callback.calledWith(null, this.lines, this.version).should.equal(true);
+				});
+			});
+
+			return describe("and has only changes that don't need to be flushed", function() {
+				beforeEach(function() {
+					this.DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, this.lines, this.version, this.ranges, this.pathname, Date.now() - 100, true);
+					return this.DocumentManager.getDocAndFlushIfOld(this.project_id, this.doc_id, this.callback);
+				});
+
+				it("should get the doc", function() {
+					return this.DocumentManager.getDoc
+						.calledWith(this.project_id, this.doc_id)
+						.should.equal(true);
+				});
+
+				it("should not flush the doc", function() {
+					return this.DocumentManager.flushDocIfLoaded
+					.called.should.equal(false);
+				});
+
+				return it("should call the callback with the lines and versions", function() {
+					return this.callback.calledWith(null, this.lines, this.version).should.equal(true);
+				});
+			});
+		});
+
+		return describe("when the doc is not in Redis", function() {
+			beforeEach(function() {
+				this.DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, this.lines, this.version, this.ranges, null, false);
+				return this.DocumentManager.getDocAndFlushIfOld(this.project_id, this.doc_id, this.callback);
+			});
+
+			it("should get the doc", function() {
+				return this.DocumentManager.getDoc
+					.calledWith(this.project_id, this.doc_id)
+					.should.equal(true);
+			});
+
+			it("should not flush the doc", function() {
+				return this.DocumentManager.flushDocIfLoaded
+				.called.should.equal(false);
+			});
+
+			return it("should call the callback with the lines and versions", function() {
+				return this.callback.calledWith(null, this.lines, this.version).should.equal(true);
+			});
+		});
+	});
+
+	describe("renameDoc", function() {
+		beforeEach(function() {
+			this.update = 'some-update';
+			return this.RedisManager.renameDoc = sinon.stub().yields();
+		});
+
+		return describe("successfully", function() {
+			beforeEach(function() {
+				return this.DocumentManager.renameDoc(this.project_id, this.doc_id, this.user_id, this.update, this.projectHistoryId, this.callback);
+			});
+
+			it("should rename the document", function() {
+				return this.RedisManager.renameDoc
+					.calledWith(this.project_id, this.doc_id, this.user_id, this.update, this.projectHistoryId)
+					.should.equal(true);
+			});
+
+			return it("should call the callback", function() {
+				return this.callback.called.should.equal(true);
+			});
+		});
+	});
+
+	return describe("resyncDocContents", function() {
+		describe("when doc is loaded in redis", function() {
+			beforeEach(function() {
+				this.RedisManager.getDoc = sinon.stub().callsArgWith(2, null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId);
+				this.ProjectHistoryRedisManager.queueResyncDocContent = sinon.stub();
+				return this.DocumentManager.resyncDocContents(this.project_id, this.doc_id, this.callback);
+			});
+
+			it("gets the doc contents from redis", function() {
+				return this.RedisManager.getDoc
+					.calledWith(this.project_id, this.doc_id)
+					.should.equal(true);
+			});
+
+			return it("queues a resync doc content update", function() {
+				return this.ProjectHistoryRedisManager.queueResyncDocContent
+					.calledWith(this.project_id, this.projectHistoryId, this.doc_id, this.lines, this.version, this.pathname, this.callback)
+					.should.equal(true);
+			});
+		});
+
+		return describe("when doc is not loaded in redis", function() {
+			beforeEach(function() {
+				this.RedisManager.getDoc = sinon.stub().callsArgWith(2, null);
+				this.PersistenceManager.getDoc = sinon.stub().callsArgWith(2, null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId);
+				this.ProjectHistoryRedisManager.queueResyncDocContent = sinon.stub();
+				return this.DocumentManager.resyncDocContents(this.project_id, this.doc_id, this.callback);
+			});
+
+			it("tries to get the doc contents from redis", function() {
+				return this.RedisManager.getDoc
+					.calledWith(this.project_id, this.doc_id)
+					.should.equal(true);
+			});
+
+			it("gets the doc contents from web", function() {
+				return this.PersistenceManager.getDoc
+					.calledWith(this.project_id, this.doc_id)
+					.should.equal(true);
+			});
+
+			return it("queues a resync doc content update", function() {
+				return this.ProjectHistoryRedisManager.queueResyncDocContent
+					.calledWith(this.project_id, this.projectHistoryId, this.doc_id, this.lines, this.version, this.pathname, this.callback)
+					.should.equal(true);
+			});
+		});
+	});
+});

+ 300 - 234
services/document-updater/test/unit/coffee/HistoryManager/HistoryManagerTests.js

@@ -1,241 +1,307 @@
-SandboxedModule = require('sandboxed-module')
-sinon = require('sinon')
-require('chai').should()
-modulePath = require('path').join __dirname, '../../../../app/js/HistoryManager'
-
-describe "HistoryManager", ->
-	beforeEach ->
-		@HistoryManager = SandboxedModule.require modulePath, requires:
-			"request": @request = {}
-			"settings-sharelatex": @Settings = {
-				apis:
-					project_history:
-						enabled: true
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const SandboxedModule = require('sandboxed-module');
+const sinon = require('sinon');
+require('chai').should();
+const modulePath = require('path').join(__dirname, '../../../../app/js/HistoryManager');
+
+describe("HistoryManager", function() {
+	beforeEach(function() {
+		this.HistoryManager = SandboxedModule.require(modulePath, { requires: {
+			"request": (this.request = {}),
+			"settings-sharelatex": (this.Settings = {
+				apis: {
+					project_history: {
+						enabled: true,
 						url: "http://project_history.example.com"
-					trackchanges:
+					},
+					trackchanges: {
 						url: "http://trackchanges.example.com"
-			}
-			"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), debug: sinon.stub() }
-			"./DocumentManager": @DocumentManager = {}
-			"./HistoryRedisManager": @HistoryRedisManager = {}
-			"./RedisManager": @RedisManager = {}
-			"./ProjectHistoryRedisManager": @ProjectHistoryRedisManager = {}
-			"./Metrics": @metrics = {inc: sinon.stub()}
-		@project_id = "mock-project-id"
-		@doc_id = "mock-doc-id"
-		@callback = sinon.stub()
-
-	describe "flushDocChangesAsync", ->
-		beforeEach ->
-			@request.post = sinon.stub().callsArgWith(1, null, statusCode: 204)
-
-		describe "when the project uses track changes", ->
-			beforeEach ->
-				@RedisManager.getHistoryType = sinon.stub().yields(null, 'track-changes')
-				@HistoryManager.flushDocChangesAsync @project_id, @doc_id
-
-			it "should send a request to the track changes api", ->
-				@request.post
-					.calledWith("#{@Settings.apis.trackchanges.url}/project/#{@project_id}/doc/#{@doc_id}/flush")
-					.should.equal true
-
-		describe "when the project uses project history and double flush is not disabled", ->
-			beforeEach ->
-				@RedisManager.getHistoryType = sinon.stub().yields(null, 'project-history')
-				@HistoryManager.flushDocChangesAsync @project_id, @doc_id
-
-			it "should send a request to the track changes api", ->
-				@request.post
+					}
+				}
+			}),
+			"logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub(), debug: sinon.stub() }),
+			"./DocumentManager": (this.DocumentManager = {}),
+			"./HistoryRedisManager": (this.HistoryRedisManager = {}),
+			"./RedisManager": (this.RedisManager = {}),
+			"./ProjectHistoryRedisManager": (this.ProjectHistoryRedisManager = {}),
+			"./Metrics": (this.metrics = {inc: sinon.stub()})
+		}
+	});
+		this.project_id = "mock-project-id";
+		this.doc_id = "mock-doc-id";
+		return this.callback = sinon.stub();
+	});
+
+	describe("flushDocChangesAsync", function() {
+		beforeEach(function() {
+			return this.request.post = sinon.stub().callsArgWith(1, null, {statusCode: 204});
+		});
+
+		describe("when the project uses track changes", function() {
+			beforeEach(function() {
+				this.RedisManager.getHistoryType = sinon.stub().yields(null, 'track-changes');
+				return this.HistoryManager.flushDocChangesAsync(this.project_id, this.doc_id);
+			});
+
+			return it("should send a request to the track changes api", function() {
+				return this.request.post
+					.calledWith(`${this.Settings.apis.trackchanges.url}/project/${this.project_id}/doc/${this.doc_id}/flush`)
+					.should.equal(true);
+			});
+		});
+
+		describe("when the project uses project history and double flush is not disabled", function() {
+			beforeEach(function() {
+				this.RedisManager.getHistoryType = sinon.stub().yields(null, 'project-history');
+				return this.HistoryManager.flushDocChangesAsync(this.project_id, this.doc_id);
+			});
+
+			return it("should send a request to the track changes api", function() {
+				return this.request.post
 					.called
-					.should.equal true
-
-		describe "when the project uses project history and double flush is disabled", ->
-			beforeEach ->
-				@Settings.disableDoubleFlush = true
-				@RedisManager.getHistoryType = sinon.stub().yields(null, 'project-history')
-				@HistoryManager.flushDocChangesAsync @project_id, @doc_id
-
-			it "should not send a request to the track changes api", ->
-				@request.post
+					.should.equal(true);
+			});
+		});
+
+		return describe("when the project uses project history and double flush is disabled", function() {
+			beforeEach(function() {
+				this.Settings.disableDoubleFlush = true;
+				this.RedisManager.getHistoryType = sinon.stub().yields(null, 'project-history');
+				return this.HistoryManager.flushDocChangesAsync(this.project_id, this.doc_id);
+			});
+
+			return it("should not send a request to the track changes api", function() {
+				return this.request.post
 					.called
-					.should.equal false
-
-
-	describe "flushProjectChangesAsync", ->
-		beforeEach ->
-			@request.post = sinon.stub().callsArgWith(1, null, statusCode: 204)
-
-			@HistoryManager.flushProjectChangesAsync @project_id
-
-		it "should send a request to the project history api", ->
-			@request.post
-				.calledWith({url: "#{@Settings.apis.project_history.url}/project/#{@project_id}/flush", qs:{background:true}})
-				.should.equal true
-
-	describe "flushProjectChanges", ->
-
-		describe "in the normal case", ->
-			beforeEach ->
-				@request.post = sinon.stub().callsArgWith(1, null, statusCode: 204)
-				@HistoryManager.flushProjectChanges @project_id, {background:true}
-
-			it "should send a request to the project history api", ->
-				@request.post
-					.calledWith({url: "#{@Settings.apis.project_history.url}/project/#{@project_id}/flush", qs:{background:true}})
-					.should.equal true
-
-		describe "with the skip_history_flush option", ->
-			beforeEach ->
-				@request.post = sinon.stub()
-				@HistoryManager.flushProjectChanges @project_id, {skip_history_flush:true}
-
-			it "should not send a request to the project history api", ->
-				@request.post
+					.should.equal(false);
+			});
+		});
+	});
+
+
+	describe("flushProjectChangesAsync", function() {
+		beforeEach(function() {
+			this.request.post = sinon.stub().callsArgWith(1, null, {statusCode: 204});
+
+			return this.HistoryManager.flushProjectChangesAsync(this.project_id);
+		});
+
+		return it("should send a request to the project history api", function() {
+			return this.request.post
+				.calledWith({url: `${this.Settings.apis.project_history.url}/project/${this.project_id}/flush`, qs:{background:true}})
+				.should.equal(true);
+		});
+	});
+
+	describe("flushProjectChanges", function() {
+
+		describe("in the normal case", function() {
+			beforeEach(function() {
+				this.request.post = sinon.stub().callsArgWith(1, null, {statusCode: 204});
+				return this.HistoryManager.flushProjectChanges(this.project_id, {background:true});});
+
+			return it("should send a request to the project history api", function() {
+				return this.request.post
+					.calledWith({url: `${this.Settings.apis.project_history.url}/project/${this.project_id}/flush`, qs:{background:true}})
+					.should.equal(true);
+			});
+		});
+
+		return describe("with the skip_history_flush option", function() {
+			beforeEach(function() {
+				this.request.post = sinon.stub();
+				return this.HistoryManager.flushProjectChanges(this.project_id, {skip_history_flush:true});});
+
+			return it("should not send a request to the project history api", function() {
+				return this.request.post
 					.called
-					.should.equal false
-
-	describe "recordAndFlushHistoryOps", ->
-		beforeEach ->
-			@ops = [ 'mock-ops' ]
-			@project_ops_length = 10
-			@doc_ops_length = 5
-
-			@HistoryManager.flushProjectChangesAsync = sinon.stub()
-			@HistoryRedisManager.recordDocHasHistoryOps = sinon.stub().callsArg(3)
-			@HistoryManager.flushDocChangesAsync = sinon.stub()
-
-		describe "with no ops", ->
-			beforeEach ->
-				@HistoryManager.recordAndFlushHistoryOps(
-					@project_id, @doc_id, [], @doc_ops_length, @project_ops_length, @callback
-				)
-
-			it "should not flush project changes", ->
-				@HistoryManager.flushProjectChangesAsync.called.should.equal false
-
-			it "should not record doc has history ops", ->
-				@HistoryRedisManager.recordDocHasHistoryOps.called.should.equal false
-
-			it "should not flush doc changes", ->
-				@HistoryManager.flushDocChangesAsync.called.should.equal false
-
-			it "should call the callback", ->
-				@callback.called.should.equal true
-
-		describe "with enough ops to flush project changes", ->
-			beforeEach ->
-				@HistoryManager.shouldFlushHistoryOps = sinon.stub()
-				@HistoryManager.shouldFlushHistoryOps.withArgs(@project_ops_length).returns(true)
-				@HistoryManager.shouldFlushHistoryOps.withArgs(@doc_ops_length).returns(false)
-
-				@HistoryManager.recordAndFlushHistoryOps(
-					@project_id, @doc_id, @ops, @doc_ops_length, @project_ops_length, @callback
-				)
-
-			it "should flush project changes", ->
-				@HistoryManager.flushProjectChangesAsync
-					.calledWith(@project_id)
-					.should.equal true
-
-			it "should record doc has history ops", ->
-				@HistoryRedisManager.recordDocHasHistoryOps
-					.calledWith(@project_id, @doc_id, @ops)
-
-			it "should not flush doc changes", ->
-				@HistoryManager.flushDocChangesAsync.called.should.equal false
-
-			it "should call the callback", ->
-				@callback.called.should.equal true
-
-		describe "with enough ops to flush doc changes", ->
-			beforeEach ->
-				@HistoryManager.shouldFlushHistoryOps = sinon.stub()
-				@HistoryManager.shouldFlushHistoryOps.withArgs(@project_ops_length).returns(false)
-				@HistoryManager.shouldFlushHistoryOps.withArgs(@doc_ops_length).returns(true)
-
-				@HistoryManager.recordAndFlushHistoryOps(
-					@project_id, @doc_id, @ops, @doc_ops_length, @project_ops_length, @callback
-				)
-
-			it "should not flush project changes", ->
-				@HistoryManager.flushProjectChangesAsync.called.should.equal false
-
-			it "should record doc has history ops", ->
-				@HistoryRedisManager.recordDocHasHistoryOps
-					.calledWith(@project_id, @doc_id, @ops)
-
-			it "should flush doc changes", ->
-				@HistoryManager.flushDocChangesAsync
-					.calledWith(@project_id, @doc_id)
-					.should.equal true
-
-			it "should call the callback", ->
-				@callback.called.should.equal true
-
-		describe "when recording doc has history ops errors", ->
-			beforeEach ->
-				@error = new Error("error")
-				@HistoryRedisManager.recordDocHasHistoryOps =
-					sinon.stub().callsArgWith(3, @error)
-
-				@HistoryManager.recordAndFlushHistoryOps(
-					@project_id, @doc_id, @ops, @doc_ops_length, @project_ops_length, @callback
-				)
-
-			it "should not flush doc changes", ->
-				@HistoryManager.flushDocChangesAsync.called.should.equal false
-
-			it "should call the callback with the error", ->
-				@callback.calledWith(@error).should.equal true
-
-		describe "shouldFlushHistoryOps", ->
-			it "should return false if the number of ops is not known", ->
-				@HistoryManager.shouldFlushHistoryOps(null, ['a', 'b', 'c'].length, 1).should.equal false
-
-			it "should return false if the updates didn't take us past the threshold", ->
-				# Currently there are 14 ops
-				# Previously we were on 11 ops
-				# We didn't pass over a multiple of 5
-				@HistoryManager.shouldFlushHistoryOps(14, ['a', 'b', 'c'].length, 5).should.equal false
-
-		  it "should return true if the updates took to the threshold", ->
-				# Currently there are 15 ops
-				# Previously we were on 12 ops
-				# We've reached a new multiple of 5
-				@HistoryManager.shouldFlushHistoryOps(15, ['a', 'b', 'c'].length, 5).should.equal true
-
-			it "should return true if the updates took past the threshold", ->
-				# Currently there are 19 ops
-				# Previously we were on 16 ops
-				# We didn't pass over a multiple of 5
-				@HistoryManager.shouldFlushHistoryOps(17, ['a', 'b', 'c'].length, 5).should.equal true
-
-	describe "resyncProjectHistory", ->
-		beforeEach ->
-			@projectHistoryId = 'history-id-1234'
-			@docs = [
-				doc: @doc_id
+					.should.equal(false);
+			});
+		});
+	});
+
+	describe("recordAndFlushHistoryOps", function() {
+		beforeEach(function() {
+			this.ops = [ 'mock-ops' ];
+			this.project_ops_length = 10;
+			this.doc_ops_length = 5;
+
+			this.HistoryManager.flushProjectChangesAsync = sinon.stub();
+			this.HistoryRedisManager.recordDocHasHistoryOps = sinon.stub().callsArg(3);
+			return this.HistoryManager.flushDocChangesAsync = sinon.stub();
+		});
+
+		describe("with no ops", function() {
+			beforeEach(function() {
+				return this.HistoryManager.recordAndFlushHistoryOps(
+					this.project_id, this.doc_id, [], this.doc_ops_length, this.project_ops_length, this.callback
+				);
+			});
+
+			it("should not flush project changes", function() {
+				return this.HistoryManager.flushProjectChangesAsync.called.should.equal(false);
+			});
+
+			it("should not record doc has history ops", function() {
+				return this.HistoryRedisManager.recordDocHasHistoryOps.called.should.equal(false);
+			});
+
+			it("should not flush doc changes", function() {
+				return this.HistoryManager.flushDocChangesAsync.called.should.equal(false);
+			});
+
+			return it("should call the callback", function() {
+				return this.callback.called.should.equal(true);
+			});
+		});
+
+		describe("with enough ops to flush project changes", function() {
+			beforeEach(function() {
+				this.HistoryManager.shouldFlushHistoryOps = sinon.stub();
+				this.HistoryManager.shouldFlushHistoryOps.withArgs(this.project_ops_length).returns(true);
+				this.HistoryManager.shouldFlushHistoryOps.withArgs(this.doc_ops_length).returns(false);
+
+				return this.HistoryManager.recordAndFlushHistoryOps(
+					this.project_id, this.doc_id, this.ops, this.doc_ops_length, this.project_ops_length, this.callback
+				);
+			});
+
+			it("should flush project changes", function() {
+				return this.HistoryManager.flushProjectChangesAsync
+					.calledWith(this.project_id)
+					.should.equal(true);
+			});
+
+			it("should record doc has history ops", function() {
+				return this.HistoryRedisManager.recordDocHasHistoryOps
+					.calledWith(this.project_id, this.doc_id, this.ops);
+			});
+
+			it("should not flush doc changes", function() {
+				return this.HistoryManager.flushDocChangesAsync.called.should.equal(false);
+			});
+
+			return it("should call the callback", function() {
+				return this.callback.called.should.equal(true);
+			});
+		});
+
+		describe("with enough ops to flush doc changes", function() {
+			beforeEach(function() {
+				this.HistoryManager.shouldFlushHistoryOps = sinon.stub();
+				this.HistoryManager.shouldFlushHistoryOps.withArgs(this.project_ops_length).returns(false);
+				this.HistoryManager.shouldFlushHistoryOps.withArgs(this.doc_ops_length).returns(true);
+
+				return this.HistoryManager.recordAndFlushHistoryOps(
+					this.project_id, this.doc_id, this.ops, this.doc_ops_length, this.project_ops_length, this.callback
+				);
+			});
+
+			it("should not flush project changes", function() {
+				return this.HistoryManager.flushProjectChangesAsync.called.should.equal(false);
+			});
+
+			it("should record doc has history ops", function() {
+				return this.HistoryRedisManager.recordDocHasHistoryOps
+					.calledWith(this.project_id, this.doc_id, this.ops);
+			});
+
+			it("should flush doc changes", function() {
+				return this.HistoryManager.flushDocChangesAsync
+					.calledWith(this.project_id, this.doc_id)
+					.should.equal(true);
+			});
+
+			return it("should call the callback", function() {
+				return this.callback.called.should.equal(true);
+			});
+		});
+
+		describe("when recording doc has history ops errors", function() {
+			beforeEach(function() {
+				this.error = new Error("error");
+				this.HistoryRedisManager.recordDocHasHistoryOps =
+					sinon.stub().callsArgWith(3, this.error);
+
+				return this.HistoryManager.recordAndFlushHistoryOps(
+					this.project_id, this.doc_id, this.ops, this.doc_ops_length, this.project_ops_length, this.callback
+				);
+			});
+
+			it("should not flush doc changes", function() {
+				return this.HistoryManager.flushDocChangesAsync.called.should.equal(false);
+			});
+
+			return it("should call the callback with the error", function() {
+				return this.callback.calledWith(this.error).should.equal(true);
+			});
+		});
+
+		return describe("shouldFlushHistoryOps", function() {
+			it("should return false if the number of ops is not known", function() {
+				return this.HistoryManager.shouldFlushHistoryOps(null, ['a', 'b', 'c'].length, 1).should.equal(false);
+			});
+
+			it("should return false if the updates didn't take us past the threshold", function() {
+				// Currently there are 14 ops
+				// Previously we were on 11 ops
+				// We didn't pass over a multiple of 5
+				this.HistoryManager.shouldFlushHistoryOps(14, ['a', 'b', 'c'].length, 5).should.equal(false);
+
+		  it("should return true if the updates took to the threshold", function() {});
+				// Currently there are 15 ops
+				// Previously we were on 12 ops
+				// We've reached a new multiple of 5
+				return this.HistoryManager.shouldFlushHistoryOps(15, ['a', 'b', 'c'].length, 5).should.equal(true);
+			});
+
+			return it("should return true if the updates took past the threshold", function() {
+				// Currently there are 19 ops
+				// Previously we were on 16 ops
+				// We didn't pass over a multiple of 5
+				return this.HistoryManager.shouldFlushHistoryOps(17, ['a', 'b', 'c'].length, 5).should.equal(true);
+			});
+		});
+	});
+
+	return describe("resyncProjectHistory", function() {
+		beforeEach(function() {
+			this.projectHistoryId = 'history-id-1234';
+			this.docs = [{
+				doc: this.doc_id,
 				path: 'main.tex'
-			]
-			@files = [
-				file: 'mock-file-id'
-				path: 'universe.png'
-				url: "www.filestore.test/#{@project_id}/mock-file-id"
-			]
-			@ProjectHistoryRedisManager.queueResyncProjectStructure = sinon.stub().yields()
-			@DocumentManager.resyncDocContentsWithLock = sinon.stub().yields()
-			@HistoryManager.resyncProjectHistory @project_id, @projectHistoryId, @docs, @files, @callback
-
-		it "should queue a project structure reync", ->
-			@ProjectHistoryRedisManager.queueResyncProjectStructure
-				.calledWith(@project_id, @projectHistoryId, @docs, @files)
-				.should.equal true
-
-		it "should queue doc content reyncs", ->
-			@DocumentManager
+			}
+			];
+			this.files = [{
+				file: 'mock-file-id',
+				path: 'universe.png',
+				url: `www.filestore.test/${this.project_id}/mock-file-id`
+			}
+			];
+			this.ProjectHistoryRedisManager.queueResyncProjectStructure = sinon.stub().yields();
+			this.DocumentManager.resyncDocContentsWithLock = sinon.stub().yields();
+			return this.HistoryManager.resyncProjectHistory(this.project_id, this.projectHistoryId, this.docs, this.files, this.callback);
+		});
+
+		it("should queue a project structure reync", function() {
+			return this.ProjectHistoryRedisManager.queueResyncProjectStructure
+				.calledWith(this.project_id, this.projectHistoryId, this.docs, this.files)
+				.should.equal(true);
+		});
+
+		it("should queue doc content reyncs", function() {
+			return this.DocumentManager
 				.resyncDocContentsWithLock
-				.calledWith(@project_id, @doc_id)
-				.should.equal true
-
-		it "should call the callback", ->
-			@callback.called.should.equal true
+				.calledWith(this.project_id, this.doc_id)
+				.should.equal(true);
+		});
+
+		return it("should call the callback", function() {
+			return this.callback.called.should.equal(true);
+		});
+	});
+});

+ 73 - 46
services/document-updater/test/unit/coffee/HistoryRedisManager/HistoryRedisManagerTests.js

@@ -1,55 +1,82 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-modulePath = "../../../../app/js/HistoryRedisManager.js"
-SandboxedModule = require('sandboxed-module')
-Errors = require "../../../../app/js/Errors"
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require('sinon');
+const chai = require('chai');
+const should = chai.should();
+const modulePath = "../../../../app/js/HistoryRedisManager.js";
+const SandboxedModule = require('sandboxed-module');
+const Errors = require("../../../../app/js/Errors");
 
-describe "HistoryRedisManager", ->
-	beforeEach ->
-		@rclient =
-			auth: () ->
+describe("HistoryRedisManager", function() {
+	beforeEach(function() {
+		this.rclient = {
+			auth() {},
 			exec: sinon.stub()
-		@rclient.multi = () => @rclient
-		@HistoryRedisManager = SandboxedModule.require modulePath, requires:
-			"redis-sharelatex": createClient: () => @rclient
-			"settings-sharelatex":
-				redis:
-					history: @settings =
-						key_schema:
-							uncompressedHistoryOps: ({doc_id}) -> "UncompressedHistoryOps:#{doc_id}"
-							docsWithHistoryOps: ({project_id}) -> "DocsWithHistoryOps:#{project_id}"
-			"logger-sharelatex": { log: () -> }
-		@doc_id = "doc-id-123"
-		@project_id = "project-id-123"
-		@callback = sinon.stub()
+		};
+		this.rclient.multi = () => this.rclient;
+		this.HistoryRedisManager = SandboxedModule.require(modulePath, { requires: {
+			"redis-sharelatex": { createClient: () => this.rclient
+		},
+			"settings-sharelatex": {
+				redis: {
+					history: (this.settings = {
+						key_schema: {
+							uncompressedHistoryOps({doc_id}) { return `UncompressedHistoryOps:${doc_id}`; },
+							docsWithHistoryOps({project_id}) { return `DocsWithHistoryOps:${project_id}`; }
+						}
+					})
+				}
+			},
+			"logger-sharelatex": { log() {} }
+		}
+	});
+		this.doc_id = "doc-id-123";
+		this.project_id = "project-id-123";
+		return this.callback = sinon.stub();
+	});
 
-	describe "recordDocHasHistoryOps", ->
-		beforeEach ->
-			@ops = [{ op: [{ i: "foo", p: 4 }] },{ op: [{ i: "bar", p: 56 }] }]
-			@rclient.sadd = sinon.stub().yields()
+	return describe("recordDocHasHistoryOps", function() {
+		beforeEach(function() {
+			this.ops = [{ op: [{ i: "foo", p: 4 }] },{ op: [{ i: "bar", p: 56 }] }];
+			return this.rclient.sadd = sinon.stub().yields();
+		});
 		
-		describe "with ops", ->
-			beforeEach (done) ->
-				@HistoryRedisManager.recordDocHasHistoryOps @project_id, @doc_id, @ops, (args...) =>
-					@callback(args...)
-					done()
+		describe("with ops", function() {
+			beforeEach(function(done) {
+				return this.HistoryRedisManager.recordDocHasHistoryOps(this.project_id, this.doc_id, this.ops, (...args) => {
+					this.callback(...Array.from(args || []));
+					return done();
+				});
+			});
 			
-			it "should add the doc_id to the set of which records the project docs", ->
-				@rclient.sadd
-					.calledWith("DocsWithHistoryOps:#{@project_id}", @doc_id)
-					.should.equal true
+			return it("should add the doc_id to the set of which records the project docs", function() {
+				return this.rclient.sadd
+					.calledWith(`DocsWithHistoryOps:${this.project_id}`, this.doc_id)
+					.should.equal(true);
+			});
+		});
 
-		describe "with no ops", ->
-			beforeEach (done) ->
-				@HistoryRedisManager.recordDocHasHistoryOps @project_id, @doc_id, [], (args...) =>
-					@callback(args...)
-					done()
+		return describe("with no ops", function() {
+			beforeEach(function(done) {
+				return this.HistoryRedisManager.recordDocHasHistoryOps(this.project_id, this.doc_id, [], (...args) => {
+					this.callback(...Array.from(args || []));
+					return done();
+				});
+			});
 			
-			it "should not add the doc_id to the set of which records the project docs", ->
-				@rclient.sadd
+			it("should not add the doc_id to the set of which records the project docs", function() {
+				return this.rclient.sadd
 					.called
-					.should.equal false
+					.should.equal(false);
+			});
 
-			it "should call the callback with an error", ->
-				@callback.calledWith(new Error("cannot push no ops")).should.equal true
+			return it("should call the callback with an error", function() {
+				return this.callback.calledWith(new Error("cannot push no ops")).should.equal(true);
+			});
+		});
+	});
+});

+ 779 - 587
services/document-updater/test/unit/coffee/HttpController/HttpControllerTests.js

@@ -1,617 +1,809 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-modulePath = "../../../../app/js/HttpController.js"
-SandboxedModule = require('sandboxed-module')
-Errors = require "../../../../app/js/Errors.js"
-
-describe "HttpController", ->
-	beforeEach ->
-		@HttpController = SandboxedModule.require modulePath, requires:
-			"./DocumentManager": @DocumentManager = {}
-			"./HistoryManager": @HistoryManager =
-				flushProjectChangesAsync: sinon.stub()
-			"./ProjectManager": @ProjectManager = {}
-			"logger-sharelatex" : @logger = { log: sinon.stub() }
-			"./ProjectFlusher": {flushAllProjects:->}
-			"./DeleteQueueManager": @DeleteQueueManager = {}
-			"./Metrics": @Metrics = {}
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS206: Consider reworking classes to avoid initClass
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require('sinon');
+const chai = require('chai');
+const should = chai.should();
+const modulePath = "../../../../app/js/HttpController.js";
+const SandboxedModule = require('sandboxed-module');
+const Errors = require("../../../../app/js/Errors.js");
+
+describe("HttpController", function() {
+	beforeEach(function() {
+		let Timer;
+		this.HttpController = SandboxedModule.require(modulePath, { requires: {
+			"./DocumentManager": (this.DocumentManager = {}),
+			"./HistoryManager": (this.HistoryManager =
+				{flushProjectChangesAsync: sinon.stub()}),
+			"./ProjectManager": (this.ProjectManager = {}),
+			"logger-sharelatex" : (this.logger = { log: sinon.stub() }),
+			"./ProjectFlusher": {flushAllProjects() {}},
+			"./DeleteQueueManager": (this.DeleteQueueManager = {}),
+			"./Metrics": (this.Metrics = {}),
 			"./Errors" : Errors
-		@Metrics.Timer = class Timer
-			done: sinon.stub()
-		@project_id = "project-id-123"
-		@doc_id = "doc-id-123"
-		@next = sinon.stub()
-		@res =
-			send: sinon.stub()
-			sendStatus: sinon.stub()
+		}
+	}
+		);
+		this.Metrics.Timer = (Timer = (function() {
+			Timer = class Timer {
+				static initClass() {
+					this.prototype.done = sinon.stub();
+				}
+			};
+			Timer.initClass();
+			return Timer;
+		})());
+		this.project_id = "project-id-123";
+		this.doc_id = "doc-id-123";
+		this.next = sinon.stub();
+		return this.res = {
+			send: sinon.stub(),
+			sendStatus: sinon.stub(),
 			json: sinon.stub()
-
-	describe "getDoc", ->
-		beforeEach ->
-			@lines = ["one", "two", "three"]
-			@ops = ["mock-op-1", "mock-op-2"]
-			@version = 42
-			@fromVersion = 42
-			@ranges = { changes: "mock", comments: "mock" }
-			@pathname = '/a/b/c'
-			@req =
-				params:
-					project_id: @project_id
-					doc_id: @doc_id
-
-		describe "when the document exists and no recent ops are requested", ->
-			beforeEach ->
-				@DocumentManager.getDocAndRecentOpsWithLock = sinon.stub().callsArgWith(3, null, @lines, @version, [], @ranges, @pathname)
-				@HttpController.getDoc(@req, @res, @next)
-
-			it "should get the doc", ->
-				@DocumentManager.getDocAndRecentOpsWithLock
-					.calledWith(@project_id, @doc_id, -1)
-					.should.equal true
-
-			it "should return the doc as JSON", ->
-				@res.json
+		};
+	});
+
+	describe("getDoc", function() {
+		beforeEach(function() {
+			this.lines = ["one", "two", "three"];
+			this.ops = ["mock-op-1", "mock-op-2"];
+			this.version = 42;
+			this.fromVersion = 42;
+			this.ranges = { changes: "mock", comments: "mock" };
+			this.pathname = '/a/b/c';
+			return this.req = {
+				params: {
+					project_id: this.project_id,
+					doc_id: this.doc_id
+				}
+			};
+		});
+
+		describe("when the document exists and no recent ops are requested", function() {
+			beforeEach(function() {
+				this.DocumentManager.getDocAndRecentOpsWithLock = sinon.stub().callsArgWith(3, null, this.lines, this.version, [], this.ranges, this.pathname);
+				return this.HttpController.getDoc(this.req, this.res, this.next);
+			});
+
+			it("should get the doc", function() {
+				return this.DocumentManager.getDocAndRecentOpsWithLock
+					.calledWith(this.project_id, this.doc_id, -1)
+					.should.equal(true);
+			});
+
+			it("should return the doc as JSON", function() {
+				return this.res.json
 					.calledWith({
-						id: @doc_id
-						lines: @lines
-						version: @version
-						ops: []
-						ranges: @ranges
-						pathname: @pathname
+						id: this.doc_id,
+						lines: this.lines,
+						version: this.version,
+						ops: [],
+						ranges: this.ranges,
+						pathname: this.pathname
 					})
-					.should.equal true
-
-			it "should log the request", ->
-				@logger.log
-					.calledWith(doc_id: @doc_id, project_id: @project_id, "getting doc via http")
-					.should.equal true
-
-			it "should time the request", ->
-				@Metrics.Timer::done.called.should.equal true
-
-		describe "when recent ops are requested", ->
-			beforeEach ->
-				@DocumentManager.getDocAndRecentOpsWithLock = sinon.stub().callsArgWith(3, null, @lines, @version, @ops, @ranges, @pathname)
-				@req.query = fromVersion: "#{@fromVersion}"
-				@HttpController.getDoc(@req, @res, @next)
-
-			it "should get the doc", ->
-				@DocumentManager.getDocAndRecentOpsWithLock
-					.calledWith(@project_id, @doc_id, @fromVersion)
-					.should.equal true
-
-			it "should return the doc as JSON", ->
-				@res.json
+					.should.equal(true);
+			});
+
+			it("should log the request", function() {
+				return this.logger.log
+					.calledWith({doc_id: this.doc_id, project_id: this.project_id}, "getting doc via http")
+					.should.equal(true);
+			});
+
+			return it("should time the request", function() {
+				return this.Metrics.Timer.prototype.done.called.should.equal(true);
+			});
+		});
+
+		describe("when recent ops are requested", function() {
+			beforeEach(function() {
+				this.DocumentManager.getDocAndRecentOpsWithLock = sinon.stub().callsArgWith(3, null, this.lines, this.version, this.ops, this.ranges, this.pathname);
+				this.req.query = {fromVersion: `${this.fromVersion}`};
+				return this.HttpController.getDoc(this.req, this.res, this.next);
+			});
+
+			it("should get the doc", function() {
+				return this.DocumentManager.getDocAndRecentOpsWithLock
+					.calledWith(this.project_id, this.doc_id, this.fromVersion)
+					.should.equal(true);
+			});
+
+			it("should return the doc as JSON", function() {
+				return this.res.json
 					.calledWith({
-						id: @doc_id
-						lines: @lines
-						version: @version
-						ops: @ops
-						ranges: @ranges
-						pathname: @pathname
+						id: this.doc_id,
+						lines: this.lines,
+						version: this.version,
+						ops: this.ops,
+						ranges: this.ranges,
+						pathname: this.pathname
 					})
-					.should.equal true
-
-			it "should log the request", ->
-				@logger.log
-					.calledWith(doc_id: @doc_id, project_id: @project_id, "getting doc via http")
-					.should.equal true
-
-			it "should time the request", ->
-				@Metrics.Timer::done.called.should.equal true
-
-		describe "when the document does not exist", ->
-			beforeEach ->
-				@DocumentManager.getDocAndRecentOpsWithLock = sinon.stub().callsArgWith(3, null, null, null)
-				@HttpController.getDoc(@req, @res, @next)
-
-			it "should call next with NotFoundError", ->
-				@next
+					.should.equal(true);
+			});
+
+			it("should log the request", function() {
+				return this.logger.log
+					.calledWith({doc_id: this.doc_id, project_id: this.project_id}, "getting doc via http")
+					.should.equal(true);
+			});
+
+			return it("should time the request", function() {
+				return this.Metrics.Timer.prototype.done.called.should.equal(true);
+			});
+		});
+
+		describe("when the document does not exist", function() {
+			beforeEach(function() {
+				this.DocumentManager.getDocAndRecentOpsWithLock = sinon.stub().callsArgWith(3, null, null, null);
+				return this.HttpController.getDoc(this.req, this.res, this.next);
+			});
+
+			return it("should call next with NotFoundError", function() {
+				return this.next
 					.calledWith(new Errors.NotFoundError("not found"))
-					.should.equal true
-
-		describe "when an errors occurs", ->
-			beforeEach ->
-				@DocumentManager.getDocAndRecentOpsWithLock = sinon.stub().callsArgWith(3, new Error("oops"), null, null)
-				@HttpController.getDoc(@req, @res, @next)
-
-			it "should call next with the error", ->
-				@next
+					.should.equal(true);
+			});
+		});
+
+		return describe("when an errors occurs", function() {
+			beforeEach(function() {
+				this.DocumentManager.getDocAndRecentOpsWithLock = sinon.stub().callsArgWith(3, new Error("oops"), null, null);
+				return this.HttpController.getDoc(this.req, this.res, this.next);
+			});
+
+			return it("should call next with the error", function() {
+				return this.next
 					.calledWith(new Error("oops"))
-					.should.equal true
-
-	describe "setDoc", ->
-		beforeEach ->
-			@lines = ["one", "two", "three"]
-			@source = "dropbox"
-			@user_id = "user-id-123"
-			@req =
-				headers: {}
-				params:
-					project_id: @project_id
-					doc_id: @doc_id
-				body:
-					lines: @lines
-					source: @source
-					user_id: @user_id
-					undoing: @undoing = true
-
-		describe "successfully", ->
-			beforeEach ->
-				@DocumentManager.setDocWithLock = sinon.stub().callsArgWith(6)
-				@HttpController.setDoc(@req, @res, @next)
-
-			it "should set the doc", ->
-				@DocumentManager.setDocWithLock
-					.calledWith(@project_id, @doc_id, @lines, @source, @user_id, @undoing)
-					.should.equal true
-
-			it "should return a successful No Content response", ->
-				@res.sendStatus
+					.should.equal(true);
+			});
+		});
+	});
+
+	describe("setDoc", function() {
+		beforeEach(function() {
+			this.lines = ["one", "two", "three"];
+			this.source = "dropbox";
+			this.user_id = "user-id-123";
+			return this.req = {
+				headers: {},
+				params: {
+					project_id: this.project_id,
+					doc_id: this.doc_id
+				},
+				body: {
+					lines: this.lines,
+					source: this.source,
+					user_id: this.user_id,
+					undoing: (this.undoing = true)
+				}
+			};
+		});
+
+		describe("successfully", function() {
+			beforeEach(function() {
+				this.DocumentManager.setDocWithLock = sinon.stub().callsArgWith(6);
+				return this.HttpController.setDoc(this.req, this.res, this.next);
+			});
+
+			it("should set the doc", function() {
+				return this.DocumentManager.setDocWithLock
+					.calledWith(this.project_id, this.doc_id, this.lines, this.source, this.user_id, this.undoing)
+					.should.equal(true);
+			});
+
+			it("should return a successful No Content response", function() {
+				return this.res.sendStatus
 					.calledWith(204)
-					.should.equal true
-
-			it "should log the request", ->
-				@logger.log
-					.calledWith(doc_id: @doc_id, project_id: @project_id, lines: @lines, source: @source, user_id: @user_id, undoing: @undoing, "setting doc via http")
-					.should.equal true
-
-			it "should time the request", ->
-				@Metrics.Timer::done.called.should.equal true
-
-		describe "when an errors occurs", ->
-			beforeEach ->
-				@DocumentManager.setDocWithLock = sinon.stub().callsArgWith(6, new Error("oops"))
-				@HttpController.setDoc(@req, @res, @next)
-
-			it "should call next with the error", ->
-				@next
+					.should.equal(true);
+			});
+
+			it("should log the request", function() {
+				return this.logger.log
+					.calledWith({doc_id: this.doc_id, project_id: this.project_id, lines: this.lines, source: this.source, user_id: this.user_id, undoing: this.undoing}, "setting doc via http")
+					.should.equal(true);
+			});
+
+			return it("should time the request", function() {
+				return this.Metrics.Timer.prototype.done.called.should.equal(true);
+			});
+		});
+
+		describe("when an errors occurs", function() {
+			beforeEach(function() {
+				this.DocumentManager.setDocWithLock = sinon.stub().callsArgWith(6, new Error("oops"));
+				return this.HttpController.setDoc(this.req, this.res, this.next);
+			});
+
+			return it("should call next with the error", function() {
+				return this.next
 					.calledWith(new Error("oops"))
-					.should.equal true
-
-		describe "when the payload is too large", ->
-			beforeEach ->
-				lines = []
-				for _ in [0..200000]
-					lines.push "test test test"
-				@req.body.lines = lines
-				@DocumentManager.setDocWithLock = sinon.stub().callsArgWith(6)
-				@HttpController.setDoc(@req, @res, @next)
-
-			it 'should send back a 406 response', ->
-				@res.sendStatus.calledWith(406).should.equal true
-
-			it 'should not call setDocWithLock', ->
-				@DocumentManager.setDocWithLock.callCount.should.equal 0
-
-	describe "flushProject", ->
-		beforeEach ->
-			@req =
-				params:
-					project_id: @project_id
-
-		describe "successfully", ->
-			beforeEach ->
-				@ProjectManager.flushProjectWithLocks = sinon.stub().callsArgWith(1)
-				@HttpController.flushProject(@req, @res, @next)
-
-			it "should flush the project", ->
-				@ProjectManager.flushProjectWithLocks
-					.calledWith(@project_id)
-					.should.equal true
-
-			it "should return a successful No Content response", ->
-				@res.sendStatus
+					.should.equal(true);
+			});
+		});
+
+		return describe("when the payload is too large", function() {
+			beforeEach(function() {
+				const lines = [];
+				for (let _ = 0; _ <= 200000; _++) {
+					lines.push("test test test");
+				}
+				this.req.body.lines = lines;
+				this.DocumentManager.setDocWithLock = sinon.stub().callsArgWith(6);
+				return this.HttpController.setDoc(this.req, this.res, this.next);
+			});
+
+			it('should send back a 406 response', function() {
+				return this.res.sendStatus.calledWith(406).should.equal(true);
+			});
+
+			return it('should not call setDocWithLock', function() {
+				return this.DocumentManager.setDocWithLock.callCount.should.equal(0);
+			});
+		});
+	});
+
+	describe("flushProject", function() {
+		beforeEach(function() {
+			return this.req = {
+				params: {
+					project_id: this.project_id
+				}
+			};
+		});
+
+		describe("successfully", function() {
+			beforeEach(function() {
+				this.ProjectManager.flushProjectWithLocks = sinon.stub().callsArgWith(1);
+				return this.HttpController.flushProject(this.req, this.res, this.next);
+			});
+
+			it("should flush the project", function() {
+				return this.ProjectManager.flushProjectWithLocks
+					.calledWith(this.project_id)
+					.should.equal(true);
+			});
+
+			it("should return a successful No Content response", function() {
+				return this.res.sendStatus
 					.calledWith(204)
-					.should.equal true
-
-			it "should log the request", ->
-				@logger.log
-					.calledWith(project_id: @project_id, "flushing project via http")
-					.should.equal true
-
-			it "should time the request", ->
-				@Metrics.Timer::done.called.should.equal true
-
-		describe "when an errors occurs", ->
-			beforeEach ->
-				@ProjectManager.flushProjectWithLocks = sinon.stub().callsArgWith(1, new Error("oops"))
-				@HttpController.flushProject(@req, @res, @next)
-
-			it "should call next with the error", ->
-				@next
+					.should.equal(true);
+			});
+
+			it("should log the request", function() {
+				return this.logger.log
+					.calledWith({project_id: this.project_id}, "flushing project via http")
+					.should.equal(true);
+			});
+
+			return it("should time the request", function() {
+				return this.Metrics.Timer.prototype.done.called.should.equal(true);
+			});
+		});
+
+		return describe("when an errors occurs", function() {
+			beforeEach(function() {
+				this.ProjectManager.flushProjectWithLocks = sinon.stub().callsArgWith(1, new Error("oops"));
+				return this.HttpController.flushProject(this.req, this.res, this.next);
+			});
+
+			return it("should call next with the error", function() {
+				return this.next
 					.calledWith(new Error("oops"))
-					.should.equal true
-
-	describe "flushDocIfLoaded", ->
-		beforeEach ->
-			@lines = ["one", "two", "three"]
-			@version = 42
-			@req =
-				params:
-					project_id: @project_id
-					doc_id: @doc_id
-
-		describe "successfully", ->
-			beforeEach ->
-				@DocumentManager.flushDocIfLoadedWithLock = sinon.stub().callsArgWith(2)
-				@HttpController.flushDocIfLoaded(@req, @res, @next)
-
-			it "should flush the doc", ->
-				@DocumentManager.flushDocIfLoadedWithLock
-					.calledWith(@project_id, @doc_id)
-					.should.equal true
-
-			it "should return a successful No Content response", ->
-				@res.sendStatus
+					.should.equal(true);
+			});
+		});
+	});
+
+	describe("flushDocIfLoaded", function() {
+		beforeEach(function() {
+			this.lines = ["one", "two", "three"];
+			this.version = 42;
+			return this.req = {
+				params: {
+					project_id: this.project_id,
+					doc_id: this.doc_id
+				}
+			};
+		});
+
+		describe("successfully", function() {
+			beforeEach(function() {
+				this.DocumentManager.flushDocIfLoadedWithLock = sinon.stub().callsArgWith(2);
+				return this.HttpController.flushDocIfLoaded(this.req, this.res, this.next);
+			});
+
+			it("should flush the doc", function() {
+				return this.DocumentManager.flushDocIfLoadedWithLock
+					.calledWith(this.project_id, this.doc_id)
+					.should.equal(true);
+			});
+
+			it("should return a successful No Content response", function() {
+				return this.res.sendStatus
 					.calledWith(204)
-					.should.equal true
-
-			it "should log the request", ->
-				@logger.log
-					.calledWith(doc_id: @doc_id, project_id: @project_id, "flushing doc via http")
-					.should.equal true
-
-			it "should time the request", ->
-				@Metrics.Timer::done.called.should.equal true
-
-		describe "when an errors occurs", ->
-			beforeEach ->
-				@DocumentManager.flushDocIfLoadedWithLock = sinon.stub().callsArgWith(2, new Error("oops"))
-				@HttpController.flushDocIfLoaded(@req, @res, @next)
-
-			it "should call next with the error", ->
-				@next
+					.should.equal(true);
+			});
+
+			it("should log the request", function() {
+				return this.logger.log
+					.calledWith({doc_id: this.doc_id, project_id: this.project_id}, "flushing doc via http")
+					.should.equal(true);
+			});
+
+			return it("should time the request", function() {
+				return this.Metrics.Timer.prototype.done.called.should.equal(true);
+			});
+		});
+
+		return describe("when an errors occurs", function() {
+			beforeEach(function() {
+				this.DocumentManager.flushDocIfLoadedWithLock = sinon.stub().callsArgWith(2, new Error("oops"));
+				return this.HttpController.flushDocIfLoaded(this.req, this.res, this.next);
+			});
+
+			return it("should call next with the error", function() {
+				return this.next
 					.calledWith(new Error("oops"))
-					.should.equal true
-
-	describe "deleteDoc", ->
-		beforeEach ->
-			@req =
-				params:
-					project_id: @project_id
-					doc_id: @doc_id
+					.should.equal(true);
+			});
+		});
+	});
+
+	describe("deleteDoc", function() {
+		beforeEach(function() {
+			return this.req = {
+				params: {
+					project_id: this.project_id,
+					doc_id: this.doc_id
+				},
 				query: {}
-
-		describe "successfully", ->
-			beforeEach ->
-				@DocumentManager.flushAndDeleteDocWithLock = sinon.stub().callsArgWith(3)
-				@HttpController.deleteDoc(@req, @res, @next)
-
-			it "should flush and delete the doc", ->
-				@DocumentManager.flushAndDeleteDocWithLock
-					.calledWith(@project_id, @doc_id, { ignoreFlushErrors: false })
-					.should.equal true
-
-			it "should flush project history", ->
-				@HistoryManager.flushProjectChangesAsync
-					.calledWithExactly(@project_id)
-					.should.equal true
-
-			it "should return a successful No Content response", ->
-				@res.sendStatus
+			};});
+
+		describe("successfully", function() {
+			beforeEach(function() {
+				this.DocumentManager.flushAndDeleteDocWithLock = sinon.stub().callsArgWith(3);
+				return this.HttpController.deleteDoc(this.req, this.res, this.next);
+			});
+
+			it("should flush and delete the doc", function() {
+				return this.DocumentManager.flushAndDeleteDocWithLock
+					.calledWith(this.project_id, this.doc_id, { ignoreFlushErrors: false })
+					.should.equal(true);
+			});
+
+			it("should flush project history", function() {
+				return this.HistoryManager.flushProjectChangesAsync
+					.calledWithExactly(this.project_id)
+					.should.equal(true);
+			});
+
+			it("should return a successful No Content response", function() {
+				return this.res.sendStatus
 					.calledWith(204)
-					.should.equal true
-
-			it "should log the request", ->
-				@logger.log
-					.calledWith(doc_id: @doc_id, project_id: @project_id, "deleting doc via http")
-					.should.equal true
-
-			it "should time the request", ->
-				@Metrics.Timer::done.called.should.equal true
-
-		describe "ignoring errors", ->
-			beforeEach ->
-				@req.query.ignore_flush_errors = 'true'
-				@DocumentManager.flushAndDeleteDocWithLock = sinon.stub().yields()
-				@HttpController.deleteDoc(@req, @res, @next)
-
-			it "should delete the doc", ->
-				@DocumentManager.flushAndDeleteDocWithLock
-					.calledWith(@project_id, @doc_id, { ignoreFlushErrors: true })
-					.should.equal true
-
-			it "should return a successful No Content response", ->
-				@res.sendStatus.calledWith(204).should.equal true
-
-		describe "when an errors occurs", ->
-			beforeEach ->
-				@DocumentManager.flushAndDeleteDocWithLock = sinon.stub().callsArgWith(3, new Error("oops"))
-				@HttpController.deleteDoc(@req, @res, @next)
-
-			it "should flush project history", ->
-				@HistoryManager.flushProjectChangesAsync
-					.calledWithExactly(@project_id)
-					.should.equal true
-
-			it "should call next with the error", ->
-				@next
+					.should.equal(true);
+			});
+
+			it("should log the request", function() {
+				return this.logger.log
+					.calledWith({doc_id: this.doc_id, project_id: this.project_id}, "deleting doc via http")
+					.should.equal(true);
+			});
+
+			return it("should time the request", function() {
+				return this.Metrics.Timer.prototype.done.called.should.equal(true);
+			});
+		});
+
+		describe("ignoring errors", function() {
+			beforeEach(function() {
+				this.req.query.ignore_flush_errors = 'true';
+				this.DocumentManager.flushAndDeleteDocWithLock = sinon.stub().yields();
+				return this.HttpController.deleteDoc(this.req, this.res, this.next);
+			});
+
+			it("should delete the doc", function() {
+				return this.DocumentManager.flushAndDeleteDocWithLock
+					.calledWith(this.project_id, this.doc_id, { ignoreFlushErrors: true })
+					.should.equal(true);
+			});
+
+			return it("should return a successful No Content response", function() {
+				return this.res.sendStatus.calledWith(204).should.equal(true);
+			});
+		});
+
+		return describe("when an errors occurs", function() {
+			beforeEach(function() {
+				this.DocumentManager.flushAndDeleteDocWithLock = sinon.stub().callsArgWith(3, new Error("oops"));
+				return this.HttpController.deleteDoc(this.req, this.res, this.next);
+			});
+
+			it("should flush project history", function() {
+				return this.HistoryManager.flushProjectChangesAsync
+					.calledWithExactly(this.project_id)
+					.should.equal(true);
+			});
+
+			return it("should call next with the error", function() {
+				return this.next
 					.calledWith(new Error("oops"))
-					.should.equal true
-
-	describe "deleteProject", ->
-		beforeEach ->
-			@req =
-				params:
-					project_id: @project_id
-
-		describe "successfully", ->
-			beforeEach ->
-				@ProjectManager.flushAndDeleteProjectWithLocks = sinon.stub().callsArgWith(2)
-				@HttpController.deleteProject(@req, @res, @next)
-
-			it "should delete the project", ->
-				@ProjectManager.flushAndDeleteProjectWithLocks
-					.calledWith(@project_id)
-					.should.equal true
-
-			it "should return a successful No Content response", ->
-				@res.sendStatus
+					.should.equal(true);
+			});
+		});
+	});
+
+	describe("deleteProject", function() {
+		beforeEach(function() {
+			return this.req = {
+				params: {
+					project_id: this.project_id
+				}
+			};
+		});
+
+		describe("successfully", function() {
+			beforeEach(function() {
+				this.ProjectManager.flushAndDeleteProjectWithLocks = sinon.stub().callsArgWith(2);
+				return this.HttpController.deleteProject(this.req, this.res, this.next);
+			});
+
+			it("should delete the project", function() {
+				return this.ProjectManager.flushAndDeleteProjectWithLocks
+					.calledWith(this.project_id)
+					.should.equal(true);
+			});
+
+			it("should return a successful No Content response", function() {
+				return this.res.sendStatus
 					.calledWith(204)
-					.should.equal true
-
-			it "should log the request", ->
-				@logger.log
-					.calledWith(project_id: @project_id, "deleting project via http")
-					.should.equal true
-
-			it "should time the request", ->
-				@Metrics.Timer::done.called.should.equal true
-
-		describe "with the background=true option from realtime", ->
-			beforeEach ->
-				@ProjectManager.queueFlushAndDeleteProject = sinon.stub().callsArgWith(1)
-				@req.query = {background:true, shutdown:true}
-				@HttpController.deleteProject(@req, @res, @next)
-
-			it "should queue the flush and delete", ->
-				@ProjectManager.queueFlushAndDeleteProject
-					.calledWith(@project_id)
-					.should.equal true
-
-		describe "when an errors occurs", ->
-			beforeEach ->
-				@ProjectManager.flushAndDeleteProjectWithLocks = sinon.stub().callsArgWith(2, new Error("oops"))
-				@HttpController.deleteProject(@req, @res, @next)
-
-			it "should call next with the error", ->
-				@next
+					.should.equal(true);
+			});
+
+			it("should log the request", function() {
+				return this.logger.log
+					.calledWith({project_id: this.project_id}, "deleting project via http")
+					.should.equal(true);
+			});
+
+			return it("should time the request", function() {
+				return this.Metrics.Timer.prototype.done.called.should.equal(true);
+			});
+		});
+
+		describe("with the background=true option from realtime", function() {
+			beforeEach(function() {
+				this.ProjectManager.queueFlushAndDeleteProject = sinon.stub().callsArgWith(1);
+				this.req.query = {background:true, shutdown:true};
+				return this.HttpController.deleteProject(this.req, this.res, this.next);
+			});
+
+			return it("should queue the flush and delete", function() {
+				return this.ProjectManager.queueFlushAndDeleteProject
+					.calledWith(this.project_id)
+					.should.equal(true);
+			});
+		});
+
+		return describe("when an errors occurs", function() {
+			beforeEach(function() {
+				this.ProjectManager.flushAndDeleteProjectWithLocks = sinon.stub().callsArgWith(2, new Error("oops"));
+				return this.HttpController.deleteProject(this.req, this.res, this.next);
+			});
+
+			return it("should call next with the error", function() {
+				return this.next
 					.calledWith(new Error("oops"))
-					.should.equal true
-
-	describe "acceptChanges", ->
-		beforeEach ->
-			@req =
-				params:
-					project_id: @project_id
-					doc_id: @doc_id
-					change_id: @change_id = "mock-change-od-1"
-
-		describe "successfully with a single change", ->
-			beforeEach ->
-				@DocumentManager.acceptChangesWithLock = sinon.stub().callsArgWith(3)
-				@HttpController.acceptChanges(@req, @res, @next)
-
-			it "should accept the change", ->
-				@DocumentManager.acceptChangesWithLock
-					.calledWith(@project_id, @doc_id, [ @change_id ])
-					.should.equal true
-
-			it "should return a successful No Content response", ->
-				@res.sendStatus
+					.should.equal(true);
+			});
+		});
+	});
+
+	describe("acceptChanges", function() {
+		beforeEach(function() {
+			return this.req = {
+				params: {
+					project_id: this.project_id,
+					doc_id: this.doc_id,
+					change_id: (this.change_id = "mock-change-od-1")
+				}
+			};
+		});
+
+		describe("successfully with a single change", function() {
+			beforeEach(function() {
+				this.DocumentManager.acceptChangesWithLock = sinon.stub().callsArgWith(3);
+				return this.HttpController.acceptChanges(this.req, this.res, this.next);
+			});
+
+			it("should accept the change", function() {
+				return this.DocumentManager.acceptChangesWithLock
+					.calledWith(this.project_id, this.doc_id, [ this.change_id ])
+					.should.equal(true);
+			});
+
+			it("should return a successful No Content response", function() {
+				return this.res.sendStatus
 					.calledWith(204)
-					.should.equal true
-
-			it "should log the request", ->
-				@logger.log
-					.calledWith({@project_id, @doc_id}, "accepting 1 changes via http")
-					.should.equal true
-
-			it "should time the request", ->
-				@Metrics.Timer::done.called.should.equal true
-
-		describe "succesfully with with multiple changes", ->
-			beforeEach ->
-				@change_ids = [ "mock-change-od-1", "mock-change-od-2", "mock-change-od-3", "mock-change-od-4" ]
-				@req.body =
-					change_ids: @change_ids
-				@DocumentManager.acceptChangesWithLock = sinon.stub().callsArgWith(3)
-				@HttpController.acceptChanges(@req, @res, @next)
-
-			it "should accept the changes in the body payload", ->
-				@DocumentManager.acceptChangesWithLock
-					.calledWith(@project_id, @doc_id, @change_ids)
-					.should.equal true
-
-			it "should log the request with the correct number of changes", ->
-				@logger.log
-					.calledWith({@project_id, @doc_id}, "accepting #{ @change_ids.length } changes via http")
-					.should.equal true
-
-		describe "when an errors occurs", ->
-			beforeEach ->
-				@DocumentManager.acceptChangesWithLock = sinon.stub().callsArgWith(3, new Error("oops"))
-				@HttpController.acceptChanges(@req, @res, @next)
-
-			it "should call next with the error", ->
-				@next
+					.should.equal(true);
+			});
+
+			it("should log the request", function() {
+				return this.logger.log
+					.calledWith({project_id: this.project_id, doc_id: this.doc_id}, "accepting 1 changes via http")
+					.should.equal(true);
+			});
+
+			return it("should time the request", function() {
+				return this.Metrics.Timer.prototype.done.called.should.equal(true);
+			});
+		});
+
+		describe("succesfully with with multiple changes", function() {
+			beforeEach(function() {
+				this.change_ids = [ "mock-change-od-1", "mock-change-od-2", "mock-change-od-3", "mock-change-od-4" ];
+				this.req.body =
+					{change_ids: this.change_ids};
+				this.DocumentManager.acceptChangesWithLock = sinon.stub().callsArgWith(3);
+				return this.HttpController.acceptChanges(this.req, this.res, this.next);
+			});
+
+			it("should accept the changes in the body payload", function() {
+				return this.DocumentManager.acceptChangesWithLock
+					.calledWith(this.project_id, this.doc_id, this.change_ids)
+					.should.equal(true);
+			});
+
+			return it("should log the request with the correct number of changes", function() {
+				return this.logger.log
+					.calledWith({project_id: this.project_id, doc_id: this.doc_id}, `accepting ${ this.change_ids.length } changes via http`)
+					.should.equal(true);
+			});
+		});
+
+		return describe("when an errors occurs", function() {
+			beforeEach(function() {
+				this.DocumentManager.acceptChangesWithLock = sinon.stub().callsArgWith(3, new Error("oops"));
+				return this.HttpController.acceptChanges(this.req, this.res, this.next);
+			});
+
+			return it("should call next with the error", function() {
+				return this.next
 					.calledWith(new Error("oops"))
-					.should.equal true
-
-	describe "deleteComment", ->
-		beforeEach ->
-			@req =
-				params:
-					project_id: @project_id
-					doc_id: @doc_id
-					comment_id: @comment_id = "mock-comment-id"
-
-		describe "successfully", ->
-			beforeEach ->
-				@DocumentManager.deleteCommentWithLock = sinon.stub().callsArgWith(3)
-				@HttpController.deleteComment(@req, @res, @next)
-
-			it "should accept the change", ->
-				@DocumentManager.deleteCommentWithLock
-					.calledWith(@project_id, @doc_id, @comment_id)
-					.should.equal true
-
-			it "should return a successful No Content response", ->
-				@res.sendStatus
+					.should.equal(true);
+			});
+		});
+	});
+
+	describe("deleteComment", function() {
+		beforeEach(function() {
+			return this.req = {
+				params: {
+					project_id: this.project_id,
+					doc_id: this.doc_id,
+					comment_id: (this.comment_id = "mock-comment-id")
+				}
+			};
+		});
+
+		describe("successfully", function() {
+			beforeEach(function() {
+				this.DocumentManager.deleteCommentWithLock = sinon.stub().callsArgWith(3);
+				return this.HttpController.deleteComment(this.req, this.res, this.next);
+			});
+
+			it("should accept the change", function() {
+				return this.DocumentManager.deleteCommentWithLock
+					.calledWith(this.project_id, this.doc_id, this.comment_id)
+					.should.equal(true);
+			});
+
+			it("should return a successful No Content response", function() {
+				return this.res.sendStatus
 					.calledWith(204)
-					.should.equal true
-
-			it "should log the request", ->
-				@logger.log
-					.calledWith({@project_id, @doc_id, @comment_id}, "deleting comment via http")
-					.should.equal true
-
-			it "should time the request", ->
-				@Metrics.Timer::done.called.should.equal true
-
-		describe "when an errors occurs", ->
-			beforeEach ->
-				@DocumentManager.deleteCommentWithLock = sinon.stub().callsArgWith(3, new Error("oops"))
-				@HttpController.deleteComment(@req, @res, @next)
-
-			it "should call next with the error", ->
-				@next
+					.should.equal(true);
+			});
+
+			it("should log the request", function() {
+				return this.logger.log
+					.calledWith({project_id: this.project_id, doc_id: this.doc_id, comment_id: this.comment_id}, "deleting comment via http")
+					.should.equal(true);
+			});
+
+			return it("should time the request", function() {
+				return this.Metrics.Timer.prototype.done.called.should.equal(true);
+			});
+		});
+
+		return describe("when an errors occurs", function() {
+			beforeEach(function() {
+				this.DocumentManager.deleteCommentWithLock = sinon.stub().callsArgWith(3, new Error("oops"));
+				return this.HttpController.deleteComment(this.req, this.res, this.next);
+			});
+
+			return it("should call next with the error", function() {
+				return this.next
 					.calledWith(new Error("oops"))
-					.should.equal true
-
-	describe "getProjectDocsAndFlushIfOld", ->
-		beforeEach ->
-			@state = "01234567890abcdef"
-			@docs = [{_id: "1234", lines: "hello", v: 23}, {_id: "4567", lines: "world", v: 45}]
-			@req =
-				params:
-					project_id: @project_id
-				query:
-					state: @state
-
-		describe "successfully", ->
-			beforeEach ->
-				@ProjectManager.getProjectDocsAndFlushIfOld = sinon.stub().callsArgWith(3,null, @docs)
-				@HttpController.getProjectDocsAndFlushIfOld(@req, @res, @next)
-
-			it "should get docs from the project manager", ->
-				@ProjectManager.getProjectDocsAndFlushIfOld
-					.calledWith(@project_id, @state, {})
-					.should.equal true
-
-			it "should return a successful response", ->
-				@res.send
-					.calledWith(@docs)
-					.should.equal true
-
-			it "should log the request", ->
-				@logger.log
-					.calledWith({project_id: @project_id, exclude: []}, "getting docs via http")
-					.should.equal true
-
-			it "should log the response", ->
-				@logger.log
-					.calledWith({project_id: @project_id, result: ["1234:23", "4567:45"]}, "got docs via http")
-					.should.equal true
-
-			it "should time the request", ->
-				@Metrics.Timer::done.called.should.equal true
-
-		describe "when there is a conflict", ->
-			beforeEach ->
-				@ProjectManager.getProjectDocsAndFlushIfOld = sinon.stub().callsArgWith(3, new Errors.ProjectStateChangedError("project state changed"))
-				@HttpController.getProjectDocsAndFlushIfOld(@req, @res, @next)
-
-			it "should return an HTTP 409 Conflict response", ->
-				@res.sendStatus
+					.should.equal(true);
+			});
+		});
+	});
+
+	describe("getProjectDocsAndFlushIfOld", function() {
+		beforeEach(function() {
+			this.state = "01234567890abcdef";
+			this.docs = [{_id: "1234", lines: "hello", v: 23}, {_id: "4567", lines: "world", v: 45}];
+			return this.req = {
+				params: {
+					project_id: this.project_id
+				},
+				query: {
+					state: this.state
+				}
+			};
+		});
+
+		describe("successfully", function() {
+			beforeEach(function() {
+				this.ProjectManager.getProjectDocsAndFlushIfOld = sinon.stub().callsArgWith(3,null, this.docs);
+				return this.HttpController.getProjectDocsAndFlushIfOld(this.req, this.res, this.next);
+			});
+
+			it("should get docs from the project manager", function() {
+				return this.ProjectManager.getProjectDocsAndFlushIfOld
+					.calledWith(this.project_id, this.state, {})
+					.should.equal(true);
+			});
+
+			it("should return a successful response", function() {
+				return this.res.send
+					.calledWith(this.docs)
+					.should.equal(true);
+			});
+
+			it("should log the request", function() {
+				return this.logger.log
+					.calledWith({project_id: this.project_id, exclude: []}, "getting docs via http")
+					.should.equal(true);
+			});
+
+			it("should log the response", function() {
+				return this.logger.log
+					.calledWith({project_id: this.project_id, result: ["1234:23", "4567:45"]}, "got docs via http")
+					.should.equal(true);
+			});
+
+			return it("should time the request", function() {
+				return this.Metrics.Timer.prototype.done.called.should.equal(true);
+			});
+		});
+
+		describe("when there is a conflict", function() {
+			beforeEach(function() {
+				this.ProjectManager.getProjectDocsAndFlushIfOld = sinon.stub().callsArgWith(3, new Errors.ProjectStateChangedError("project state changed"));
+				return this.HttpController.getProjectDocsAndFlushIfOld(this.req, this.res, this.next);
+			});
+
+			return it("should return an HTTP 409 Conflict response", function() {
+				return this.res.sendStatus
 					.calledWith(409)
-					.should.equal true
-
-		describe "when an error occurs", ->
-			beforeEach ->
-				@ProjectManager.getProjectDocsAndFlushIfOld = sinon.stub().callsArgWith(3, new Error("oops"))
-				@HttpController.getProjectDocsAndFlushIfOld(@req, @res, @next)
-
-			it "should call next with the error", ->
-				@next
+					.should.equal(true);
+			});
+		});
+
+		return describe("when an error occurs", function() {
+			beforeEach(function() {
+				this.ProjectManager.getProjectDocsAndFlushIfOld = sinon.stub().callsArgWith(3, new Error("oops"));
+				return this.HttpController.getProjectDocsAndFlushIfOld(this.req, this.res, this.next);
+			});
+
+			return it("should call next with the error", function() {
+				return this.next
 					.calledWith(new Error("oops"))
-					.should.equal true
-
-	describe "updateProject", ->
-		beforeEach ->
-			@projectHistoryId = "history-id-123"
-			@userId = "user-id-123"
-			@docUpdates = sinon.stub()
-			@fileUpdates = sinon.stub()
-			@version = 1234567
-			@req =
-				body: {@projectHistoryId, @userId, @docUpdates, @fileUpdates, @version}
-				params:
-					project_id: @project_id
-
-		describe "successfully", ->
-			beforeEach ->
-				@ProjectManager.updateProjectWithLocks = sinon.stub().callsArgWith(6)
-				@HttpController.updateProject(@req, @res, @next)
-
-			it "should accept the change", ->
-				@ProjectManager.updateProjectWithLocks
-					.calledWith(@project_id, @projectHistoryId, @userId, @docUpdates, @fileUpdates, @version)
-					.should.equal true
-
-			it "should return a successful No Content response", ->
-				@res.sendStatus
+					.should.equal(true);
+			});
+		});
+	});
+
+	describe("updateProject", function() {
+		beforeEach(function() {
+			this.projectHistoryId = "history-id-123";
+			this.userId = "user-id-123";
+			this.docUpdates = sinon.stub();
+			this.fileUpdates = sinon.stub();
+			this.version = 1234567;
+			return this.req = {
+				body: {projectHistoryId: this.projectHistoryId, userId: this.userId, docUpdates: this.docUpdates, fileUpdates: this.fileUpdates, version: this.version},
+				params: {
+					project_id: this.project_id
+				}
+			};
+		});
+
+		describe("successfully", function() {
+			beforeEach(function() {
+				this.ProjectManager.updateProjectWithLocks = sinon.stub().callsArgWith(6);
+				return this.HttpController.updateProject(this.req, this.res, this.next);
+			});
+
+			it("should accept the change", function() {
+				return this.ProjectManager.updateProjectWithLocks
+					.calledWith(this.project_id, this.projectHistoryId, this.userId, this.docUpdates, this.fileUpdates, this.version)
+					.should.equal(true);
+			});
+
+			it("should return a successful No Content response", function() {
+				return this.res.sendStatus
 					.calledWith(204)
-					.should.equal true
-
-			it "should time the request", ->
-				@Metrics.Timer::done.called.should.equal true
-
-		describe "when an errors occurs", ->
-			beforeEach ->
-				@ProjectManager.updateProjectWithLocks = sinon.stub().callsArgWith(6, new Error("oops"))
-				@HttpController.updateProject(@req, @res, @next)
-
-			it "should call next with the error", ->
-				@next
+					.should.equal(true);
+			});
+
+			return it("should time the request", function() {
+				return this.Metrics.Timer.prototype.done.called.should.equal(true);
+			});
+		});
+
+		return describe("when an errors occurs", function() {
+			beforeEach(function() {
+				this.ProjectManager.updateProjectWithLocks = sinon.stub().callsArgWith(6, new Error("oops"));
+				return this.HttpController.updateProject(this.req, this.res, this.next);
+			});
+
+			return it("should call next with the error", function() {
+				return this.next
 					.calledWith(new Error("oops"))
-					.should.equal true
-
-	describe "resyncProjectHistory", ->
-		beforeEach ->
-			@projectHistoryId = "history-id-123"
-			@docs = sinon.stub()
-			@files = sinon.stub()
-			@fileUpdates = sinon.stub()
-			@req =
+					.should.equal(true);
+			});
+		});
+	});
+
+	return describe("resyncProjectHistory", function() {
+		beforeEach(function() {
+			this.projectHistoryId = "history-id-123";
+			this.docs = sinon.stub();
+			this.files = sinon.stub();
+			this.fileUpdates = sinon.stub();
+			return this.req = {
 				body:
-					{@projectHistoryId, @docs, @files}
-				params:
-					project_id: @project_id
-
-		describe "successfully", ->
-			beforeEach ->
-				@HistoryManager.resyncProjectHistory = sinon.stub().callsArgWith(4)
-				@HttpController.resyncProjectHistory(@req, @res, @next)
-
-			it "should accept the change", ->
-				@HistoryManager.resyncProjectHistory
-					.calledWith(@project_id, @projectHistoryId, @docs, @files)
-					.should.equal true
-
-			it "should return a successful No Content response", ->
-				@res.sendStatus
+					{projectHistoryId: this.projectHistoryId, docs: this.docs, files: this.files},
+				params: {
+					project_id: this.project_id
+				}
+			};
+		});
+
+		describe("successfully", function() {
+			beforeEach(function() {
+				this.HistoryManager.resyncProjectHistory = sinon.stub().callsArgWith(4);
+				return this.HttpController.resyncProjectHistory(this.req, this.res, this.next);
+			});
+
+			it("should accept the change", function() {
+				return this.HistoryManager.resyncProjectHistory
+					.calledWith(this.project_id, this.projectHistoryId, this.docs, this.files)
+					.should.equal(true);
+			});
+
+			return it("should return a successful No Content response", function() {
+				return this.res.sendStatus
 					.calledWith(204)
-					.should.equal true
-
-		describe "when an errors occurs", ->
-			beforeEach ->
-				@HistoryManager.resyncProjectHistory = sinon.stub().callsArgWith(4, new Error("oops"))
-				@HttpController.resyncProjectHistory(@req, @res, @next)
-
-			it "should call next with the error", ->
-				@next
+					.should.equal(true);
+			});
+		});
+
+		return describe("when an errors occurs", function() {
+			beforeEach(function() {
+				this.HistoryManager.resyncProjectHistory = sinon.stub().callsArgWith(4, new Error("oops"));
+				return this.HttpController.resyncProjectHistory(this.req, this.res, this.next);
+			});
+
+			return it("should call next with the error", function() {
+				return this.next
 					.calledWith(new Error("oops"))
-					.should.equal true
+					.should.equal(true);
+			});
+		});
+	});
+});

+ 57 - 32
services/document-updater/test/unit/coffee/LockManager/CheckingTheLock.js

@@ -1,37 +1,62 @@
-require('coffee-script')
-sinon = require('sinon')
-assert = require('assert')
-path = require('path')
-modulePath = path.join __dirname, '../../../../app/js/LockManager.js'
-project_id = 1234
-doc_id     = 5678
-blockingKey = "Blocking:#{doc_id}"
-SandboxedModule = require('sandboxed-module')
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS206: Consider reworking classes to avoid initClass
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+require('coffee-script');
+const sinon = require('sinon');
+const assert = require('assert');
+const path = require('path');
+const modulePath = path.join(__dirname, '../../../../app/js/LockManager.js');
+const project_id = 1234;
+const doc_id     = 5678;
+const blockingKey = `Blocking:${doc_id}`;
+const SandboxedModule = require('sandboxed-module');
 
-describe 'LockManager - checking the lock', ()->
+describe('LockManager - checking the lock', function(){
 
-	existsStub = sinon.stub()
+	let Profiler;
+	const existsStub = sinon.stub();
 	
-	mocks =
-		"logger-sharelatex": log:->
-		"redis-sharelatex":
-			createClient : ()->
-				auth:->
-				exists: existsStub
-		"./Metrics": {inc: () ->}
-		"./Profiler": class Profiler
-			log: sinon.stub().returns { end: sinon.stub() }
-			end: sinon.stub()
-	LockManager = SandboxedModule.require(modulePath, requires: mocks)
+	const mocks = {
+		"logger-sharelatex": { log() {}
+	},
+		"redis-sharelatex": {
+			createClient(){
+				return {
+					auth() {},
+					exists: existsStub
+				};
+			}
+		},
+		"./Metrics": {inc() {}},
+		"./Profiler": (Profiler = (function() {
+			Profiler = class Profiler {
+				static initClass() {
+					this.prototype.log = sinon.stub().returns({ end: sinon.stub() });
+					this.prototype.end = sinon.stub();
+				}
+			};
+			Profiler.initClass();
+			return Profiler;
+		})())
+	};
+	const LockManager = SandboxedModule.require(modulePath, {requires: mocks});
 
-	it 'should return true if the key does not exists', (done)->
-		existsStub.yields(null, "0")
-		LockManager.checkLock doc_id, (err, free)->
-			free.should.equal true
-			done()
+	it('should return true if the key does not exists', function(done){
+		existsStub.yields(null, "0");
+		return LockManager.checkLock(doc_id, function(err, free){
+			free.should.equal(true);
+			return done();
+		});
+	});
 
-	it 'should return false if the key does exists', (done)->
-		existsStub.yields(null, "1")
-		LockManager.checkLock doc_id, (err, free)->
-			free.should.equal false
-			done()
+	return it('should return false if the key does exists', function(done){
+		existsStub.yields(null, "1");
+		return LockManager.checkLock(doc_id, function(err, free){
+			free.should.equal(false);
+			return done();
+		});
+	});
+});

+ 74 - 45
services/document-updater/test/unit/coffee/LockManager/ReleasingTheLock.js

@@ -1,53 +1,82 @@
-require('coffee-script')
-sinon = require('sinon')
-assert = require('assert')
-path = require('path')
-modulePath = path.join __dirname, '../../../../app/js/LockManager.js'
-project_id = 1234
-doc_id     = 5678
-SandboxedModule = require('sandboxed-module')
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS206: Consider reworking classes to avoid initClass
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+require('coffee-script');
+const sinon = require('sinon');
+const assert = require('assert');
+const path = require('path');
+const modulePath = path.join(__dirname, '../../../../app/js/LockManager.js');
+const project_id = 1234;
+const doc_id     = 5678;
+const SandboxedModule = require('sandboxed-module');
 
-describe 'LockManager - releasing the lock', ()->
-	beforeEach ->
-		@client = {
-			auth: ->
+describe('LockManager - releasing the lock', function(){
+	beforeEach(function() {
+		let Profiler;
+		this.client = {
+			auth() {},
 			eval: sinon.stub()
-		}
-		mocks =
-			"logger-sharelatex":
-				log:->
-				error:->
-			"redis-sharelatex":
-				createClient : () => @client
+		};
+		const mocks = {
+			"logger-sharelatex": {
+				log() {},
+				error() {}
+			},
+			"redis-sharelatex": {
+				createClient : () => this.client
+			},
 			"settings-sharelatex": {
-				redis:
-					lock:
-						key_schema:
-							blockingKey: ({doc_id}) -> "Blocking:#{doc_id}"
-			}
-			"./Metrics": {inc: () ->}
-			"./Profiler": class Profiler
-				log: sinon.stub().returns { end: sinon.stub() }
-				end: sinon.stub()
-		@LockManager = SandboxedModule.require(modulePath, requires: mocks)
-		@lockValue = "lock-value-stub"
-		@callback = sinon.stub()
+				redis: {
+					lock: {
+						key_schema: {
+							blockingKey({doc_id}) { return `Blocking:${doc_id}`; }
+						}
+					}
+				}
+			},
+			"./Metrics": {inc() {}},
+			"./Profiler": (Profiler = (function() {
+				Profiler = class Profiler {
+					static initClass() {
+						this.prototype.log = sinon.stub().returns({ end: sinon.stub() });
+						this.prototype.end = sinon.stub();
+					}
+				};
+				Profiler.initClass();
+				return Profiler;
+			})())
+		};
+		this.LockManager = SandboxedModule.require(modulePath, {requires: mocks});
+		this.lockValue = "lock-value-stub";
+		return this.callback = sinon.stub();
+	});
 
-	describe "when the lock is current", ->
-		beforeEach ->
-			@client.eval = sinon.stub().yields(null, 1)
-			@LockManager.releaseLock doc_id, @lockValue, @callback
+	describe("when the lock is current", function() {
+		beforeEach(function() {
+			this.client.eval = sinon.stub().yields(null, 1);
+			return this.LockManager.releaseLock(doc_id, this.lockValue, this.callback);
+		});
 
-		it 'should clear the data from redis', ->
-			@client.eval.calledWith(@LockManager.unlockScript, 1, "Blocking:#{doc_id}", @lockValue).should.equal true
+		it('should clear the data from redis', function() {
+			return this.client.eval.calledWith(this.LockManager.unlockScript, 1, `Blocking:${doc_id}`, this.lockValue).should.equal(true);
+		});
 
-		it 'should call the callback', ->
-			@callback.called.should.equal true
+		return it('should call the callback', function() {
+			return this.callback.called.should.equal(true);
+		});
+	});
 
-	describe "when the lock has expired", ->
-		beforeEach ->
-			@client.eval = sinon.stub().yields(null, 0)
-			@LockManager.releaseLock doc_id, @lockValue, @callback
+	return describe("when the lock has expired", function() {
+		beforeEach(function() {
+			this.client.eval = sinon.stub().yields(null, 0);
+			return this.LockManager.releaseLock(doc_id, this.lockValue, this.callback);
+		});
 
-		it 'should return an error if the lock has expired', ->
-			@callback.calledWith(new Error("tried to release timed out lock")).should.equal true
+		return it('should return an error if the lock has expired', function() {
+			return this.callback.calledWith(new Error("tried to release timed out lock")).should.equal(true);
+		});
+	});
+});

+ 107 - 65
services/document-updater/test/unit/coffee/LockManager/getLockTests.js

@@ -1,79 +1,121 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-modulePath = "../../../../app/js/LockManager.js"
-SandboxedModule = require('sandboxed-module')
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS206: Consider reworking classes to avoid initClass
+ * 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');
+const should = chai.should();
+const modulePath = "../../../../app/js/LockManager.js";
+const SandboxedModule = require('sandboxed-module');
 
-describe 'LockManager - getting the lock', ->
-	beforeEach ->
-		@LockManager = SandboxedModule.require modulePath, requires:
-			"logger-sharelatex": log:->
-			"redis-sharelatex":
-				createClient : () =>
-					auth:->
-			"./Metrics": {inc: () ->}
-			"./Profiler": class Profiler
-				log: sinon.stub().returns { end: sinon.stub() }
-				end: sinon.stub()
-		@callback = sinon.stub()
-		@doc_id = "doc-id-123"
+describe('LockManager - getting the lock', function() {
+	beforeEach(function() {
+		let Profiler;
+		this.LockManager = SandboxedModule.require(modulePath, { requires: {
+			"logger-sharelatex": { log() {}
+		},
+			"redis-sharelatex": {
+				createClient : () => {
+					return {auth() {}};
+				}
+			},
+			"./Metrics": {inc() {}},
+			"./Profiler": (Profiler = (function() {
+				Profiler = class Profiler {
+					static initClass() {
+						this.prototype.log = sinon.stub().returns({ end: sinon.stub() });
+						this.prototype.end = sinon.stub();
+					}
+				};
+				Profiler.initClass();
+				return Profiler;
+			})())
+		}
+	}
+		);
+		this.callback = sinon.stub();
+		return this.doc_id = "doc-id-123";
+	});
 	
-	describe "when the lock is not set", ->
-		beforeEach (done) ->
-			@lockValue = "mock-lock-value"
-			@LockManager.tryLock = sinon.stub().callsArgWith(1, null, true, @lockValue)
-			@LockManager.getLock @doc_id, (args...) =>
-				@callback(args...)
-				done()
+	describe("when the lock is not set", function() {
+		beforeEach(function(done) {
+			this.lockValue = "mock-lock-value";
+			this.LockManager.tryLock = sinon.stub().callsArgWith(1, null, true, this.lockValue);
+			return this.LockManager.getLock(this.doc_id, (...args) => {
+				this.callback(...Array.from(args || []));
+				return done();
+			});
+		});
 
-		it "should try to get the lock", ->
-			@LockManager.tryLock
-				.calledWith(@doc_id)
-				.should.equal true
+		it("should try to get the lock", function() {
+			return this.LockManager.tryLock
+				.calledWith(this.doc_id)
+				.should.equal(true);
+		});
 
-		it "should only need to try once", ->
-			@LockManager.tryLock.callCount.should.equal 1
+		it("should only need to try once", function() {
+			return this.LockManager.tryLock.callCount.should.equal(1);
+		});
 
-		it "should return the callback with the lock value", ->
-			@callback.calledWith(null, @lockValue).should.equal true
+		return it("should return the callback with the lock value", function() {
+			return this.callback.calledWith(null, this.lockValue).should.equal(true);
+		});
+	});
 
-	describe "when the lock is initially set", ->
-		beforeEach (done) ->
-			@lockValue = "mock-lock-value"
-			startTime = Date.now()
-			tries = 0
-			@LockManager.LOCK_TEST_INTERVAL = 5
-			@LockManager.tryLock = (doc_id, callback = (error, isFree) ->) =>
-				if (Date.now() - startTime < 20) or (tries < 2)
-					tries = tries + 1
-					callback null, false
-				else
-					callback null, true, @lockValue
-			sinon.spy @LockManager, "tryLock"
+	describe("when the lock is initially set", function() {
+		beforeEach(function(done) {
+			this.lockValue = "mock-lock-value";
+			const startTime = Date.now();
+			let tries = 0;
+			this.LockManager.LOCK_TEST_INTERVAL = 5;
+			this.LockManager.tryLock = (doc_id, callback) => {
+				if (callback == null) { callback = function(error, isFree) {}; }
+				if (((Date.now() - startTime) < 20) || (tries < 2)) {
+					tries = tries + 1;
+					return callback(null, false);
+				} else {
+					return callback(null, true, this.lockValue);
+				}
+			};
+			sinon.spy(this.LockManager, "tryLock");
 
-			@LockManager.getLock @doc_id, (args...) =>
-				@callback(args...)
-				done()
+			return this.LockManager.getLock(this.doc_id, (...args) => {
+				this.callback(...Array.from(args || []));
+				return done();
+			});
+		});
 
-		it "should call tryLock multiple times until free", ->
-			(@LockManager.tryLock.callCount > 1).should.equal true
+		it("should call tryLock multiple times until free", function() {
+			return (this.LockManager.tryLock.callCount > 1).should.equal(true);
+		});
 
-		it "should return the callback with the lock value", ->
-			@callback.calledWith(null, @lockValue).should.equal true
+		return it("should return the callback with the lock value", function() {
+			return this.callback.calledWith(null, this.lockValue).should.equal(true);
+		});
+	});
 
-	describe "when the lock times out", ->
-		beforeEach (done) ->
-			time = Date.now()
-			@LockManager.MAX_LOCK_WAIT_TIME = 5
-			@LockManager.tryLock = sinon.stub().callsArgWith(1, null, false)
-			@LockManager.getLock @doc_id, (args...) =>
-				@callback(args...)
-				done()
+	return describe("when the lock times out", function() {
+		beforeEach(function(done) {
+			const time = Date.now();
+			this.LockManager.MAX_LOCK_WAIT_TIME = 5;
+			this.LockManager.tryLock = sinon.stub().callsArgWith(1, null, false);
+			return this.LockManager.getLock(this.doc_id, (...args) => {
+				this.callback(...Array.from(args || []));
+				return done();
+			});
+		});
 
-		it "should return the callback with an error", ->
-			e = new Error("Timeout")
-			e.doc_id = @doc_id
-			@callback.calledWith(e).should.equal true
+		return it("should return the callback with an error", function() {
+			const e = new Error("Timeout");
+			e.doc_id = this.doc_id;
+			return this.callback.calledWith(e).should.equal(true);
+		});
+	});
+});
 		
 
 

+ 116 - 70
services/document-updater/test/unit/coffee/LockManager/tryLockTests.js

@@ -1,86 +1,132 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-modulePath = "../../../../app/js/LockManager.js"
-SandboxedModule = require('sandboxed-module')
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS206: Consider reworking classes to avoid initClass
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require('sinon');
+const chai = require('chai');
+const should = chai.should();
+const modulePath = "../../../../app/js/LockManager.js";
+const SandboxedModule = require('sandboxed-module');
 
-describe 'LockManager - trying the lock', ->
-	beforeEach ->
-		@LockManager = SandboxedModule.require modulePath, requires:
-			"logger-sharelatex": log:->
-			"redis-sharelatex":
-				createClient : () =>
-					auth:->
-					set: @set = sinon.stub()
-			"./Metrics": {inc: () ->}
+describe('LockManager - trying the lock', function() {
+	beforeEach(function() {
+		let Profiler;
+		this.LockManager = SandboxedModule.require(modulePath, { requires: {
+			"logger-sharelatex": { log() {}
+		},
+			"redis-sharelatex": {
+				createClient : () => {
+					return {
+						auth() {},
+						set: (this.set = sinon.stub())
+					};
+				}
+			},
+			"./Metrics": {inc() {}},
 			"settings-sharelatex": {
-				redis:
-					lock:
-						key_schema:
-							blockingKey: ({doc_id}) -> "Blocking:#{doc_id}"
-			}
-			"./Profiler": @Profiler = class Profiler
-				log: sinon.stub().returns { end: sinon.stub() }
-				end: sinon.stub()
+				redis: {
+					lock: {
+						key_schema: {
+							blockingKey({doc_id}) { return `Blocking:${doc_id}`; }
+						}
+					}
+				}
+			},
+			"./Profiler": (this.Profiler = (Profiler = (function() {
+				Profiler = class Profiler {
+					static initClass() {
+						this.prototype.log = sinon.stub().returns({ end: sinon.stub() });
+						this.prototype.end = sinon.stub();
+					}
+				};
+				Profiler.initClass();
+				return Profiler;
+			})()))
+		}
+	}
+		);
 
-		@callback = sinon.stub()
-		@doc_id = "doc-id-123"
+		this.callback = sinon.stub();
+		return this.doc_id = "doc-id-123";
+	});
 	
-	describe "when the lock is not set", ->
-		beforeEach ->
-			@lockValue = "mock-lock-value"
-			@LockManager.randomLock = sinon.stub().returns @lockValue
-			@set.callsArgWith(5, null, "OK")
-			@LockManager.tryLock @doc_id, @callback
+	describe("when the lock is not set", function() {
+		beforeEach(function() {
+			this.lockValue = "mock-lock-value";
+			this.LockManager.randomLock = sinon.stub().returns(this.lockValue);
+			this.set.callsArgWith(5, null, "OK");
+			return this.LockManager.tryLock(this.doc_id, this.callback);
+		});
 
-		it "should set the lock key with an expiry if it is not set", ->
-			@set.calledWith("Blocking:#{@doc_id}", @lockValue, "EX", 30, "NX")
-				.should.equal true
+		it("should set the lock key with an expiry if it is not set", function() {
+			return this.set.calledWith(`Blocking:${this.doc_id}`, this.lockValue, "EX", 30, "NX")
+				.should.equal(true);
+		});
 
-		it "should return the callback with true and the lock value", ->
-			@callback.calledWith(null, true, @lockValue).should.equal true
+		return it("should return the callback with true and the lock value", function() {
+			return this.callback.calledWith(null, true, this.lockValue).should.equal(true);
+		});
+	});
 
-	describe "when the lock is already set", ->
-		beforeEach ->
-			@set.callsArgWith(5, null, null)
-			@LockManager.tryLock @doc_id, @callback
+	describe("when the lock is already set", function() {
+		beforeEach(function() {
+			this.set.callsArgWith(5, null, null);
+			return this.LockManager.tryLock(this.doc_id, this.callback);
+		});
 
-		it "should return the callback with false", ->
-			@callback.calledWith(null, false).should.equal true
+		return it("should return the callback with false", function() {
+			return this.callback.calledWith(null, false).should.equal(true);
+		});
+	});
 
-	describe "when it takes a long time for redis to set the lock", ->
-		beforeEach ->
-			@Profiler.prototype.end = () -> 7000 # take a long time
-			@Profiler.prototype.log = sinon.stub().returns { end: @Profiler.prototype.end }
-			@lockValue = "mock-lock-value"
-			@LockManager.randomLock = sinon.stub().returns @lockValue
-			@LockManager.releaseLock = sinon.stub().callsArgWith(2,null)
-			@set.callsArgWith(5, null, "OK")
+	return describe("when it takes a long time for redis to set the lock", function() {
+		beforeEach(function() {
+			this.Profiler.prototype.end = () => 7000; // take a long time
+			this.Profiler.prototype.log = sinon.stub().returns({ end: this.Profiler.prototype.end });
+			this.lockValue = "mock-lock-value";
+			this.LockManager.randomLock = sinon.stub().returns(this.lockValue);
+			this.LockManager.releaseLock = sinon.stub().callsArgWith(2,null);
+			return this.set.callsArgWith(5, null, "OK");
+		});
 
-		describe "in all cases", ->
-			beforeEach ->
-				@LockManager.tryLock @doc_id, @callback
+		describe("in all cases", function() {
+			beforeEach(function() {
+				return this.LockManager.tryLock(this.doc_id, this.callback);
+			});
 
-			it "should set the lock key with an expiry if it is not set", ->
-				@set.calledWith("Blocking:#{@doc_id}", @lockValue, "EX", 30, "NX")
-					.should.equal true
+			it("should set the lock key with an expiry if it is not set", function() {
+				return this.set.calledWith(`Blocking:${this.doc_id}`, this.lockValue, "EX", 30, "NX")
+					.should.equal(true);
+			});
 
-			it "should try to release the lock", ->
-				@LockManager.releaseLock.calledWith(@doc_id, @lockValue).should.equal true
+			return it("should try to release the lock", function() {
+				return this.LockManager.releaseLock.calledWith(this.doc_id, this.lockValue).should.equal(true);
+			});
+		});
 
-		describe "if the lock is released successfully", ->
-			beforeEach ->
-				@LockManager.releaseLock = sinon.stub().callsArgWith(2,null)
-				@LockManager.tryLock @doc_id, @callback
+		describe("if the lock is released successfully", function() {
+			beforeEach(function() {
+				this.LockManager.releaseLock = sinon.stub().callsArgWith(2,null);
+				return this.LockManager.tryLock(this.doc_id, this.callback);
+			});
 
-			it "should return the callback with false", ->
-				@callback.calledWith(null, false).should.equal true
+			return it("should return the callback with false", function() {
+				return this.callback.calledWith(null, false).should.equal(true);
+			});
+		});
 
-		describe "if the lock has already timed out", ->
-			beforeEach ->
-				@LockManager.releaseLock = sinon.stub().callsArgWith(2, new Error("tried to release timed out lock"))
-				@LockManager.tryLock @doc_id, @callback
+		return describe("if the lock has already timed out", function() {
+			beforeEach(function() {
+				this.LockManager.releaseLock = sinon.stub().callsArgWith(2, new Error("tried to release timed out lock"));
+				return this.LockManager.tryLock(this.doc_id, this.callback);
+			});
 
-			it "should return the callback with an error", ->
-				e = new Error("tried to release timed out lock")
-				@callback.calledWith(e).should.equal true
+			return it("should return the callback with an error", function() {
+				const e = new Error("tried to release timed out lock");
+				return this.callback.calledWith(e).should.equal(true);
+			});
+		});
+	});
+});

+ 293 - 215
services/document-updater/test/unit/coffee/PersistenceManager/PersistenceManagerTests.js

@@ -1,226 +1,304 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-modulePath = "../../../../app/js/PersistenceManager.js"
-SandboxedModule = require('sandboxed-module')
-Errors = require "../../../../app/js/Errors"
-
-describe "PersistenceManager", ->
-	beforeEach ->
-		@request = sinon.stub()
-		@request.defaults = () => @request
-		@PersistenceManager = SandboxedModule.require modulePath, requires:
-			"requestretry": @request
-			"settings-sharelatex": @Settings = {}
-			"./Metrics": @Metrics =
-				Timer: class Timer
-					done: sinon.stub()
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS206: Consider reworking classes to avoid initClass
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require('sinon');
+const chai = require('chai');
+const should = chai.should();
+const modulePath = "../../../../app/js/PersistenceManager.js";
+const SandboxedModule = require('sandboxed-module');
+const Errors = require("../../../../app/js/Errors");
+
+describe("PersistenceManager", function() {
+	beforeEach(function() {
+		let Timer;
+		this.request = sinon.stub();
+		this.request.defaults = () => this.request;
+		this.PersistenceManager = SandboxedModule.require(modulePath, { requires: {
+			"requestretry": this.request,
+			"settings-sharelatex": (this.Settings = {}),
+			"./Metrics": (this.Metrics = {
+				Timer: (Timer = (function() {
+					Timer = class Timer {
+						static initClass() {
+							this.prototype.done = sinon.stub();
+						}
+					};
+					Timer.initClass();
+					return Timer;
+				})()),
 				inc: sinon.stub()
-			"logger-sharelatex": @logger = {log: sinon.stub(), err: sinon.stub()}
-		@project_id = "project-id-123"
-		@projectHistoryId = "history-id-123"
-		@doc_id = "doc-id-123"
-		@lines = ["one", "two", "three"]
-		@version = 42
-		@callback = sinon.stub()
-		@ranges = { comments: "mock", entries: "mock" }
-		@pathname = '/a/b/c.tex'
-		@lastUpdatedAt = Date.now()
-		@lastUpdatedBy = 'last-author-id'
-		@Settings.apis =
-			web:
-				url: @url = "www.example.com"
-				user: @user = "sharelatex"
-				pass: @pass = "password"
-
-	describe "getDoc", ->
-		beforeEach ->
-			@webResponse = {
-				lines: @lines,
-				version: @version,
-				ranges: @ranges
-				pathname: @pathname,
-				projectHistoryId: @projectHistoryId
+			}),
+			"logger-sharelatex": (this.logger = {log: sinon.stub(), err: sinon.stub()})
+		}
+	});
+		this.project_id = "project-id-123";
+		this.projectHistoryId = "history-id-123";
+		this.doc_id = "doc-id-123";
+		this.lines = ["one", "two", "three"];
+		this.version = 42;
+		this.callback = sinon.stub();
+		this.ranges = { comments: "mock", entries: "mock" };
+		this.pathname = '/a/b/c.tex';
+		this.lastUpdatedAt = Date.now();
+		this.lastUpdatedBy = 'last-author-id';
+		return this.Settings.apis = {
+			web: {
+				url: (this.url = "www.example.com"),
+				user: (this.user = "sharelatex"),
+				pass: (this.pass = "password")
 			}
-
-		describe "with a successful response from the web api", ->
-			beforeEach ->
-				@request.callsArgWith(1, null, {statusCode: 200}, JSON.stringify(@webResponse))
-				@PersistenceManager.getDoc(@project_id, @doc_id, @callback)
-
-			it "should call the web api", ->
-				@request
+		};
+	});
+
+	describe("getDoc", function() {
+		beforeEach(function() {
+			return this.webResponse = {
+				lines: this.lines,
+				version: this.version,
+				ranges: this.ranges,
+				pathname: this.pathname,
+				projectHistoryId: this.projectHistoryId
+			};});
+
+		describe("with a successful response from the web api", function() {
+			beforeEach(function() {
+				this.request.callsArgWith(1, null, {statusCode: 200}, JSON.stringify(this.webResponse));
+				return this.PersistenceManager.getDoc(this.project_id, this.doc_id, this.callback);
+			});
+
+			it("should call the web api", function() {
+				return this.request
 					.calledWith({
-						url: "#{@url}/project/#{@project_id}/doc/#{@doc_id}"
-						method: "GET"
-						headers:
+						url: `${this.url}/project/${this.project_id}/doc/${this.doc_id}`,
+						method: "GET",
+						headers: {
 							"accept": "application/json"
-						auth:
-							user: @user
-							pass: @pass
+						},
+						auth: {
+							user: this.user,
+							pass: this.pass,
 							sendImmediately: true
-						jar: false
+						},
+						jar: false,
 						timeout: 5000
 					})
-					.should.equal true
-
-			it "should call the callback with the doc lines, version and ranges", ->
-				@callback
-					.calledWith(null, @lines, @version, @ranges, @pathname, @projectHistoryId)
-					.should.equal true
-
-			it "should time the execution", ->
-				@Metrics.Timer::done.called.should.equal true
-
-			it "should increment the metric", ->
-				@Metrics.inc.calledWith("getDoc", 1, {status: 200}).should.equal true
-
-		describe "when request returns an error", ->
-			beforeEach ->
-				@error = new Error("oops")
-				@error.code = "EOOPS"
-				@request.callsArgWith(1, @error, null, null)
-				@PersistenceManager.getDoc(@project_id, @doc_id, @callback)
-
-			it "should return the error", ->
-				@callback.calledWith(@error).should.equal true
-
-			it "should time the execution", ->
-				@Metrics.Timer::done.called.should.equal true
-
-			it "should increment the metric", ->
-				@Metrics.inc.calledWith("getDoc", 1, {status: "EOOPS"}).should.equal true
-
-		describe "when the request returns 404", ->
-			beforeEach ->
-				@request.callsArgWith(1, null, {statusCode: 404}, "")
-				@PersistenceManager.getDoc(@project_id, @doc_id, @callback)
-
-			it "should return a NotFoundError", ->
-				@callback.calledWith(new Errors.NotFoundError("not found")).should.equal true
-
-			it "should time the execution", ->
-				@Metrics.Timer::done.called.should.equal true
-
-			it "should increment the metric", ->
-				@Metrics.inc.calledWith("getDoc", 1, {status: 404}).should.equal true
-
-		describe "when the request returns an error status code", ->
-			beforeEach ->
-				@request.callsArgWith(1, null, {statusCode: 500}, "")
-				@PersistenceManager.getDoc(@project_id, @doc_id, @callback)
-
-			it "should return an error", ->
-				@callback.calledWith(new Error("web api error")).should.equal true
-
-			it "should time the execution", ->
-				@Metrics.Timer::done.called.should.equal true
-
-			it "should increment the metric", ->
-				@Metrics.inc.calledWith("getDoc", 1, {status: 500}).should.equal true
-
-		describe "when request returns an doc without lines", ->
-			beforeEach ->
-				delete @webResponse.lines
-				@request.callsArgWith(1, null, {statusCode: 200}, JSON.stringify(@webResponse))
-				@PersistenceManager.getDoc(@project_id, @doc_id, @callback)
-
-			it "should return and error", ->
-				@callback.calledWith(new Error("web API response had no doc lines")).should.equal true
-
-		describe "when request returns an doc without a version", ->
-			beforeEach ->
-				delete @webResponse.version
-				@request.callsArgWith(1, null, {statusCode: 200}, JSON.stringify(@webResponse))
-				@PersistenceManager.getDoc(@project_id, @doc_id, @callback)
-
-			it "should return and error", ->
-				@callback.calledWith(new Error("web API response had no valid doc version")).should.equal true
-
-		describe "when request returns an doc without a pathname", ->
-			beforeEach ->
-				delete @webResponse.pathname
-				@request.callsArgWith(1, null, {statusCode: 200}, JSON.stringify(@webResponse))
-				@PersistenceManager.getDoc(@project_id, @doc_id, @callback)
-
-			it "should return and error", ->
-				@callback.calledWith(new Error("web API response had no valid doc pathname")).should.equal true
-
-	describe "setDoc", ->
-		describe "with a successful response from the web api", ->
-			beforeEach ->
-				@request.callsArgWith(1, null, {statusCode: 200})
-				@PersistenceManager.setDoc(@project_id, @doc_id, @lines, @version, @ranges, @lastUpdatedAt, @lastUpdatedBy, @callback)
-
-			it "should call the web api", ->
-				@request
+					.should.equal(true);
+			});
+
+			it("should call the callback with the doc lines, version and ranges", function() {
+				return this.callback
+					.calledWith(null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId)
+					.should.equal(true);
+			});
+
+			it("should time the execution", function() {
+				return this.Metrics.Timer.prototype.done.called.should.equal(true);
+			});
+
+			return it("should increment the metric", function() {
+				return this.Metrics.inc.calledWith("getDoc", 1, {status: 200}).should.equal(true);
+			});
+		});
+
+		describe("when request returns an error", function() {
+			beforeEach(function() {
+				this.error = new Error("oops");
+				this.error.code = "EOOPS";
+				this.request.callsArgWith(1, this.error, null, null);
+				return this.PersistenceManager.getDoc(this.project_id, this.doc_id, this.callback);
+			});
+
+			it("should return the error", function() {
+				return this.callback.calledWith(this.error).should.equal(true);
+			});
+
+			it("should time the execution", function() {
+				return this.Metrics.Timer.prototype.done.called.should.equal(true);
+			});
+
+			return it("should increment the metric", function() {
+				return this.Metrics.inc.calledWith("getDoc", 1, {status: "EOOPS"}).should.equal(true);
+			});
+		});
+
+		describe("when the request returns 404", function() {
+			beforeEach(function() {
+				this.request.callsArgWith(1, null, {statusCode: 404}, "");
+				return this.PersistenceManager.getDoc(this.project_id, this.doc_id, this.callback);
+			});
+
+			it("should return a NotFoundError", function() {
+				return this.callback.calledWith(new Errors.NotFoundError("not found")).should.equal(true);
+			});
+
+			it("should time the execution", function() {
+				return this.Metrics.Timer.prototype.done.called.should.equal(true);
+			});
+
+			return it("should increment the metric", function() {
+				return this.Metrics.inc.calledWith("getDoc", 1, {status: 404}).should.equal(true);
+			});
+		});
+
+		describe("when the request returns an error status code", function() {
+			beforeEach(function() {
+				this.request.callsArgWith(1, null, {statusCode: 500}, "");
+				return this.PersistenceManager.getDoc(this.project_id, this.doc_id, this.callback);
+			});
+
+			it("should return an error", function() {
+				return this.callback.calledWith(new Error("web api error")).should.equal(true);
+			});
+
+			it("should time the execution", function() {
+				return this.Metrics.Timer.prototype.done.called.should.equal(true);
+			});
+
+			return it("should increment the metric", function() {
+				return this.Metrics.inc.calledWith("getDoc", 1, {status: 500}).should.equal(true);
+			});
+		});
+
+		describe("when request returns an doc without lines", function() {
+			beforeEach(function() {
+				delete this.webResponse.lines;
+				this.request.callsArgWith(1, null, {statusCode: 200}, JSON.stringify(this.webResponse));
+				return this.PersistenceManager.getDoc(this.project_id, this.doc_id, this.callback);
+			});
+
+			return it("should return and error", function() {
+				return this.callback.calledWith(new Error("web API response had no doc lines")).should.equal(true);
+			});
+		});
+
+		describe("when request returns an doc without a version", function() {
+			beforeEach(function() {
+				delete this.webResponse.version;
+				this.request.callsArgWith(1, null, {statusCode: 200}, JSON.stringify(this.webResponse));
+				return this.PersistenceManager.getDoc(this.project_id, this.doc_id, this.callback);
+			});
+
+			return it("should return and error", function() {
+				return this.callback.calledWith(new Error("web API response had no valid doc version")).should.equal(true);
+			});
+		});
+
+		return describe("when request returns an doc without a pathname", function() {
+			beforeEach(function() {
+				delete this.webResponse.pathname;
+				this.request.callsArgWith(1, null, {statusCode: 200}, JSON.stringify(this.webResponse));
+				return this.PersistenceManager.getDoc(this.project_id, this.doc_id, this.callback);
+			});
+
+			return it("should return and error", function() {
+				return this.callback.calledWith(new Error("web API response had no valid doc pathname")).should.equal(true);
+			});
+		});
+	});
+
+	return describe("setDoc", function() {
+		describe("with a successful response from the web api", function() {
+			beforeEach(function() {
+				this.request.callsArgWith(1, null, {statusCode: 200});
+				return this.PersistenceManager.setDoc(this.project_id, this.doc_id, this.lines, this.version, this.ranges, this.lastUpdatedAt, this.lastUpdatedBy, this.callback);
+			});
+
+			it("should call the web api", function() {
+				return this.request
 					.calledWith({
-						url: "#{@url}/project/#{@project_id}/doc/#{@doc_id}"
-						json:
-							lines: @lines
-							version: @version
-							ranges: @ranges
-							lastUpdatedAt: @lastUpdatedAt
-							lastUpdatedBy: @lastUpdatedBy
-						method: "POST"
-						auth:
-							user: @user
-							pass: @pass
+						url: `${this.url}/project/${this.project_id}/doc/${this.doc_id}`,
+						json: {
+							lines: this.lines,
+							version: this.version,
+							ranges: this.ranges,
+							lastUpdatedAt: this.lastUpdatedAt,
+							lastUpdatedBy: this.lastUpdatedBy
+						},
+						method: "POST",
+						auth: {
+							user: this.user,
+							pass: this.pass,
 							sendImmediately: true
-						jar: false
+						},
+						jar: false,
 						timeout: 5000
 					})
-					.should.equal true
-
-			it "should call the callback without error", ->
-				@callback.calledWith(null).should.equal true
-
-			it "should time the execution", ->
-				@Metrics.Timer::done.called.should.equal true
-
-			it "should increment the metric", ->
-				@Metrics.inc.calledWith("setDoc", 1, {status: 200}).should.equal true
-
-		describe "when request returns an error", ->
-			beforeEach ->
-				@error = new Error("oops")
-				@error.code = "EOOPS"
-				@request.callsArgWith(1, @error, null, null)
-				@PersistenceManager.setDoc(@project_id, @doc_id, @lines, @version, @ranges, @lastUpdatedAt, @lastUpdatedBy, @callback)
-
-			it "should return the error", ->
-				@callback.calledWith(@error).should.equal true
-
-			it "should time the execution", ->
-				@Metrics.Timer::done.called.should.equal true
-
-			it "should increment the metric", ->
-				@Metrics.inc.calledWith("setDoc", 1, {status: "EOOPS"}).should.equal true
-
-		describe "when the request returns 404", ->
-			beforeEach ->
-				@request.callsArgWith(1, null, {statusCode: 404}, "")
-				@PersistenceManager.setDoc(@project_id, @doc_id, @lines, @version, @ranges, @lastUpdatedAt, @lastUpdatedBy, @callback)
-
-			it "should return a NotFoundError", ->
-				@callback.calledWith(new Errors.NotFoundError("not found")).should.equal true
-
-			it "should time the execution", ->
-				@Metrics.Timer::done.called.should.equal true
-
-			it "should increment the metric", ->
-				@Metrics.inc.calledWith("setDoc", 1, {status: 404}).should.equal true
-
-		describe "when the request returns an error status code", ->
-			beforeEach ->
-				@request.callsArgWith(1, null, {statusCode: 500}, "")
-				@PersistenceManager.setDoc(@project_id, @doc_id, @lines, @version, @ranges, @lastUpdatedAt, @lastUpdatedBy, @callback)
-
-			it "should return an error", ->
-				@callback.calledWith(new Error("web api error")).should.equal true
-
-			it "should time the execution", ->
-				@Metrics.Timer::done.called.should.equal true
-
-			it "should increment the metric", ->
-				@Metrics.inc.calledWith("setDoc", 1, {status: 500}).should.equal true
+					.should.equal(true);
+			});
+
+			it("should call the callback without error", function() {
+				return this.callback.calledWith(null).should.equal(true);
+			});
+
+			it("should time the execution", function() {
+				return this.Metrics.Timer.prototype.done.called.should.equal(true);
+			});
+
+			return it("should increment the metric", function() {
+				return this.Metrics.inc.calledWith("setDoc", 1, {status: 200}).should.equal(true);
+			});
+		});
+
+		describe("when request returns an error", function() {
+			beforeEach(function() {
+				this.error = new Error("oops");
+				this.error.code = "EOOPS";
+				this.request.callsArgWith(1, this.error, null, null);
+				return this.PersistenceManager.setDoc(this.project_id, this.doc_id, this.lines, this.version, this.ranges, this.lastUpdatedAt, this.lastUpdatedBy, this.callback);
+			});
+
+			it("should return the error", function() {
+				return this.callback.calledWith(this.error).should.equal(true);
+			});
+
+			it("should time the execution", function() {
+				return this.Metrics.Timer.prototype.done.called.should.equal(true);
+			});
+
+			return it("should increment the metric", function() {
+				return this.Metrics.inc.calledWith("setDoc", 1, {status: "EOOPS"}).should.equal(true);
+			});
+		});
+
+		describe("when the request returns 404", function() {
+			beforeEach(function() {
+				this.request.callsArgWith(1, null, {statusCode: 404}, "");
+				return this.PersistenceManager.setDoc(this.project_id, this.doc_id, this.lines, this.version, this.ranges, this.lastUpdatedAt, this.lastUpdatedBy, this.callback);
+			});
+
+			it("should return a NotFoundError", function() {
+				return this.callback.calledWith(new Errors.NotFoundError("not found")).should.equal(true);
+			});
+
+			it("should time the execution", function() {
+				return this.Metrics.Timer.prototype.done.called.should.equal(true);
+			});
+
+			return it("should increment the metric", function() {
+				return this.Metrics.inc.calledWith("setDoc", 1, {status: 404}).should.equal(true);
+			});
+		});
+
+		return describe("when the request returns an error status code", function() {
+			beforeEach(function() {
+				this.request.callsArgWith(1, null, {statusCode: 500}, "");
+				return this.PersistenceManager.setDoc(this.project_id, this.doc_id, this.lines, this.version, this.ranges, this.lastUpdatedAt, this.lastUpdatedBy, this.callback);
+			});
+
+			it("should return an error", function() {
+				return this.callback.calledWith(new Error("web api error")).should.equal(true);
+			});
+
+			it("should time the execution", function() {
+				return this.Metrics.Timer.prototype.done.called.should.equal(true);
+			});
+
+			return it("should increment the metric", function() {
+				return this.Metrics.inc.calledWith("setDoc", 1, {status: 500}).should.equal(true);
+			});
+		});
+	});
+});

+ 147 - 117
services/document-updater/test/unit/coffee/ProjectHistoryRedisManager/ProjectHistoryRedisManagerTests.js

@@ -1,122 +1,152 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-modulePath = "../../../../app/js/ProjectHistoryRedisManager.js"
-SandboxedModule = require('sandboxed-module')
-tk = require "timekeeper"
-
-describe "ProjectHistoryRedisManager", ->
-	beforeEach ->
-		@project_id = "project-id-123"
-		@projectHistoryId = "history-id-123"
-		@user_id = "user-id-123"
-		@callback = sinon.stub()
-		@rclient = {}
-		tk.freeze(new Date())
-		@ProjectHistoryRedisManager = SandboxedModule.require modulePath,
-			requires:
-				"settings-sharelatex": @settings = {
-					redis:
-						project_history:
-							key_schema:
-								projectHistoryOps: ({project_id}) -> "ProjectHistory:Ops:#{project_id}"
-								projectHistoryFirstOpTimestamp: ({project_id}) -> "ProjectHistory:FirstOpTimestamp:#{project_id}"
-				}
-				"redis-sharelatex":
-					createClient: () => @rclient
-				"logger-sharelatex":
-					log:->
-				"./Metrics": @metrics = { summary: sinon.stub()}
-			globals:
-				JSON: @JSON = JSON
-
-	afterEach ->
-		tk.reset()
-
-	describe "queueOps", ->
-		beforeEach ->
-			@ops = ["mock-op-1", "mock-op-2"]
-			@multi = exec: sinon.stub()
-			@multi.rpush = sinon.stub()
-			@multi.setnx = sinon.stub()
-			@rclient.multi = () => @multi
-			# @rclient = multi: () => @multi
-			@ProjectHistoryRedisManager.queueOps @project_id, @ops..., @callback
-
-		it "should queue an update", ->
-			@multi.rpush
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require('sinon');
+const chai = require('chai');
+const should = chai.should();
+const modulePath = "../../../../app/js/ProjectHistoryRedisManager.js";
+const SandboxedModule = require('sandboxed-module');
+const tk = require("timekeeper");
+
+describe("ProjectHistoryRedisManager", function() {
+	beforeEach(function() {
+		this.project_id = "project-id-123";
+		this.projectHistoryId = "history-id-123";
+		this.user_id = "user-id-123";
+		this.callback = sinon.stub();
+		this.rclient = {};
+		tk.freeze(new Date());
+		return this.ProjectHistoryRedisManager = SandboxedModule.require(modulePath, {
+			requires: {
+				"settings-sharelatex": (this.settings = {
+					redis: {
+						project_history: {
+							key_schema: {
+								projectHistoryOps({project_id}) { return `ProjectHistory:Ops:${project_id}`; },
+								projectHistoryFirstOpTimestamp({project_id}) { return `ProjectHistory:FirstOpTimestamp:${project_id}`; }
+							}
+						}
+					}
+				}),
+				"redis-sharelatex": {
+					createClient: () => this.rclient
+				},
+				"logger-sharelatex": {
+					log() {}
+				},
+				"./Metrics": (this.metrics = { summary: sinon.stub()})
+			},
+			globals: {
+				JSON: (this.JSON = JSON)
+			}
+		}
+		);
+	});
+
+	afterEach(() => tk.reset());
+
+	describe("queueOps", function() {
+		beforeEach(function() {
+			this.ops = ["mock-op-1", "mock-op-2"];
+			this.multi = {exec: sinon.stub()};
+			this.multi.rpush = sinon.stub();
+			this.multi.setnx = sinon.stub();
+			this.rclient.multi = () => this.multi;
+			// @rclient = multi: () => @multi
+			return this.ProjectHistoryRedisManager.queueOps(this.project_id, ...Array.from(this.ops), this.callback);
+		});
+
+		it("should queue an update", function() {
+			return this.multi.rpush
 				.calledWithExactly(
-					"ProjectHistory:Ops:#{@project_id}"
-					@ops[0]
-					@ops[1]
-				).should.equal true
-
-		it "should set the queue timestamp if not present", ->
-			@multi.setnx
+					`ProjectHistory:Ops:${this.project_id}`,
+					this.ops[0],
+					this.ops[1]
+				).should.equal(true);
+		});
+
+		return it("should set the queue timestamp if not present", function() {
+			return this.multi.setnx
 				.calledWithExactly(
-					"ProjectHistory:FirstOpTimestamp:#{@project_id}"
+					`ProjectHistory:FirstOpTimestamp:${this.project_id}`,
 					Date.now()
-				).should.equal true
-
-	describe "queueRenameEntity", ->
-		beforeEach () ->
-			@file_id = 1234
-
-			@rawUpdate =
-				pathname: @pathname = '/old'
-				newPathname: @newPathname = '/new'
-				version: @version = 2
-
-			@ProjectHistoryRedisManager.queueOps = sinon.stub()
-			@ProjectHistoryRedisManager.queueRenameEntity @project_id, @projectHistoryId, 'file', @file_id, @user_id, @rawUpdate, @callback
-
-		it "should queue an update", ->
-			update =
-				pathname: @pathname
-				new_pathname: @newPathname
-				meta:
-					user_id: @user_id
+				).should.equal(true);
+		});
+	});
+
+	describe("queueRenameEntity", function() {
+		beforeEach(function() {
+			this.file_id = 1234;
+
+			this.rawUpdate = {
+				pathname: (this.pathname = '/old'),
+				newPathname: (this.newPathname = '/new'),
+				version: (this.version = 2)
+			};
+
+			this.ProjectHistoryRedisManager.queueOps = sinon.stub();
+			return this.ProjectHistoryRedisManager.queueRenameEntity(this.project_id, this.projectHistoryId, 'file', this.file_id, this.user_id, this.rawUpdate, this.callback);
+		});
+
+		return it("should queue an update", function() {
+			const update = {
+				pathname: this.pathname,
+				new_pathname: this.newPathname,
+				meta: {
+					user_id: this.user_id,
 					ts: new Date()
-				version: @version
-				projectHistoryId: @projectHistoryId
-				file: @file_id
-
-			@ProjectHistoryRedisManager.queueOps
-				.calledWithExactly(@project_id, @JSON.stringify(update), @callback)
-				.should.equal true
-
-	describe "queueAddEntity", ->
-		beforeEach () ->
-			@rclient.rpush = sinon.stub().yields()
-			@doc_id = 1234
-
-			@rawUpdate =
-				pathname: @pathname = '/old'
-				docLines: @docLines = 'a\nb'
-				version: @version = 2
-				url: @url = 'filestore.example.com'
-
-			@ProjectHistoryRedisManager.queueOps = sinon.stub()
-			@ProjectHistoryRedisManager.queueAddEntity @project_id, @projectHistoryId, 'doc', @doc_id, @user_id, @rawUpdate, @callback
-
-		it "should queue an update", ->
-			update =
-				pathname: @pathname
-				docLines: @docLines
-				url: @url
-				meta:
-					user_id: @user_id
+				},
+				version: this.version,
+				projectHistoryId: this.projectHistoryId,
+				file: this.file_id
+			};
+
+			return this.ProjectHistoryRedisManager.queueOps
+				.calledWithExactly(this.project_id, this.JSON.stringify(update), this.callback)
+				.should.equal(true);
+		});
+	});
+
+	return describe("queueAddEntity", function() {
+		beforeEach(function() {
+			this.rclient.rpush = sinon.stub().yields();
+			this.doc_id = 1234;
+
+			this.rawUpdate = {
+				pathname: (this.pathname = '/old'),
+				docLines: (this.docLines = 'a\nb'),
+				version: (this.version = 2),
+				url: (this.url = 'filestore.example.com')
+			};
+
+			this.ProjectHistoryRedisManager.queueOps = sinon.stub();
+			return this.ProjectHistoryRedisManager.queueAddEntity(this.project_id, this.projectHistoryId, 'doc', this.doc_id, this.user_id, this.rawUpdate, this.callback);
+		});
+
+		it("should queue an update", function() {
+			const update = {
+				pathname: this.pathname,
+				docLines: this.docLines,
+				url: this.url,
+				meta: {
+					user_id: this.user_id,
 					ts: new Date()
-				version: @version
-				projectHistoryId: @projectHistoryId
-				doc: @doc_id
-
-			@ProjectHistoryRedisManager.queueOps
-				.calledWithExactly(@project_id, @JSON.stringify(update), @callback)
-				.should.equal true
-
-		describe "queueResyncProjectStructure", ->
-			it "should queue an update", ->
-
-		describe "queueResyncDocContent", ->
-			it "should queue an update", ->
+				},
+				version: this.version,
+				projectHistoryId: this.projectHistoryId,
+				doc: this.doc_id
+			};
+
+			return this.ProjectHistoryRedisManager.queueOps
+				.calledWithExactly(this.project_id, this.JSON.stringify(update), this.callback)
+				.should.equal(true);
+		});
+
+		describe("queueResyncProjectStructure", () => it("should queue an update", function() {}));
+
+		return describe("queueResyncDocContent", () => it("should queue an update", function() {}));
+	});
+});

+ 112 - 73
services/document-updater/test/unit/coffee/ProjectManager/flushAndDeleteProjectTests.js

@@ -1,86 +1,125 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-modulePath = "../../../../app/js/ProjectManager.js"
-SandboxedModule = require('sandboxed-module')
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS206: Consider reworking classes to avoid initClass
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require('sinon');
+const chai = require('chai');
+const should = chai.should();
+const modulePath = "../../../../app/js/ProjectManager.js";
+const SandboxedModule = require('sandboxed-module');
 
-describe "ProjectManager - flushAndDeleteProject", ->
-	beforeEach ->
-		@ProjectManager = SandboxedModule.require modulePath, requires:
-			"./RedisManager": @RedisManager = {}
-			"./ProjectHistoryRedisManager": @ProjectHistoryRedisManager = {}
-			"./DocumentManager": @DocumentManager = {}
-			"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
-			"./HistoryManager": @HistoryManager =
-				flushProjectChanges: sinon.stub().callsArg(2)
-			"./Metrics": @Metrics =
-				Timer: class Timer
-					done: sinon.stub()
-		@project_id = "project-id-123"
-		@callback = sinon.stub()
+describe("ProjectManager - flushAndDeleteProject", function() {
+	beforeEach(function() {
+		let Timer;
+		this.ProjectManager = SandboxedModule.require(modulePath, { requires: {
+			"./RedisManager": (this.RedisManager = {}),
+			"./ProjectHistoryRedisManager": (this.ProjectHistoryRedisManager = {}),
+			"./DocumentManager": (this.DocumentManager = {}),
+			"logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }),
+			"./HistoryManager": (this.HistoryManager =
+				{flushProjectChanges: sinon.stub().callsArg(2)}),
+			"./Metrics": (this.Metrics = {
+				Timer: (Timer = (function() {
+					Timer = class Timer {
+						static initClass() {
+							this.prototype.done = sinon.stub();
+						}
+					};
+					Timer.initClass();
+					return Timer;
+				})())
+			})
+		}
+	}
+		);
+		this.project_id = "project-id-123";
+		return this.callback = sinon.stub();
+	});
 
-	describe "successfully", ->
-		beforeEach (done) ->
-			@doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"]
-			@RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, @doc_ids)
-			@DocumentManager.flushAndDeleteDocWithLock = sinon.stub().callsArg(3)
-			@ProjectManager.flushAndDeleteProjectWithLocks @project_id, {}, (error) =>
-				@callback(error)
-				done()
+	describe("successfully", function() {
+		beforeEach(function(done) {
+			this.doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"];
+			this.RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, this.doc_ids);
+			this.DocumentManager.flushAndDeleteDocWithLock = sinon.stub().callsArg(3);
+			return this.ProjectManager.flushAndDeleteProjectWithLocks(this.project_id, {}, error => {
+				this.callback(error);
+				return done();
+			});
+		});
 
-		it "should get the doc ids in the project", ->
-			@RedisManager.getDocIdsInProject
-				.calledWith(@project_id)
-				.should.equal true
+		it("should get the doc ids in the project", function() {
+			return this.RedisManager.getDocIdsInProject
+				.calledWith(this.project_id)
+				.should.equal(true);
+		});
 
-		it "should delete each doc in the project", ->
-			for doc_id in @doc_ids
-				@DocumentManager.flushAndDeleteDocWithLock
-					.calledWith(@project_id, doc_id, {})
-					.should.equal true
+		it("should delete each doc in the project", function() {
+			return Array.from(this.doc_ids).map((doc_id) =>
+				this.DocumentManager.flushAndDeleteDocWithLock
+					.calledWith(this.project_id, doc_id, {})
+					.should.equal(true));
+		});
 
-		it "should flush project history", ->
-			@HistoryManager.flushProjectChanges
-				.calledWith(@project_id, {})
-				.should.equal true
+		it("should flush project history", function() {
+			return this.HistoryManager.flushProjectChanges
+				.calledWith(this.project_id, {})
+				.should.equal(true);
+		});
 
-		it "should call the callback without error", ->
-			@callback.calledWith(null).should.equal true
+		it("should call the callback without error", function() {
+			return this.callback.calledWith(null).should.equal(true);
+		});
 
-		it "should time the execution", ->
-			@Metrics.Timer::done.called.should.equal true
+		return it("should time the execution", function() {
+			return this.Metrics.Timer.prototype.done.called.should.equal(true);
+		});
+	});
 
-	describe "when a doc errors", ->
-		beforeEach (done) ->
-			@doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"]
-			@RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, @doc_ids)
-			@DocumentManager.flushAndDeleteDocWithLock = sinon.spy (project_id, doc_id, options, callback) =>
-				if doc_id == "doc-id-1"
-					callback(@error = new Error("oops, something went wrong"))
-				else
-					callback()
-			@ProjectManager.flushAndDeleteProjectWithLocks @project_id, {}, (error) =>
-				@callback(error)
-				done()
+	return describe("when a doc errors", function() {
+		beforeEach(function(done) {
+			this.doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"];
+			this.RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, this.doc_ids);
+			this.DocumentManager.flushAndDeleteDocWithLock = sinon.spy((project_id, doc_id, options, callback) => {
+				if (doc_id === "doc-id-1") {
+					return callback(this.error = new Error("oops, something went wrong"));
+				} else {
+					return callback();
+				}
+			});
+			return this.ProjectManager.flushAndDeleteProjectWithLocks(this.project_id, {}, error => {
+				this.callback(error);
+				return done();
+			});
+		});
 
-		it "should still flush each doc in the project", ->
-			for doc_id in @doc_ids
-				@DocumentManager.flushAndDeleteDocWithLock
-					.calledWith(@project_id, doc_id, {})
-					.should.equal true
+		it("should still flush each doc in the project", function() {
+			return Array.from(this.doc_ids).map((doc_id) =>
+				this.DocumentManager.flushAndDeleteDocWithLock
+					.calledWith(this.project_id, doc_id, {})
+					.should.equal(true));
+		});
 
-		it "should still flush project history", ->
-			@HistoryManager.flushProjectChanges
-				.calledWith(@project_id, {})
-				.should.equal true
+		it("should still flush project history", function() {
+			return this.HistoryManager.flushProjectChanges
+				.calledWith(this.project_id, {})
+				.should.equal(true);
+		});
 
-		it "should record the error", ->
-			@logger.error
-				.calledWith(err: @error, project_id: @project_id, doc_id: "doc-id-1", "error deleting doc")
-				.should.equal true
+		it("should record the error", function() {
+			return this.logger.error
+				.calledWith({err: this.error, project_id: this.project_id, doc_id: "doc-id-1"}, "error deleting doc")
+				.should.equal(true);
+		});
 
-		it "should call the callback with an error", ->
-			@callback.calledWith(new Error()).should.equal true
+		it("should call the callback with an error", function() {
+			return this.callback.calledWith(new Error()).should.equal(true);
+		});
 
-		it "should time the execution", ->
-			@Metrics.Timer::done.called.should.equal true
+		return it("should time the execution", function() {
+			return this.Metrics.Timer.prototype.done.called.should.equal(true);
+		});
+	});
+});

+ 103 - 64
services/document-updater/test/unit/coffee/ProjectManager/flushProjectTests.js

@@ -1,75 +1,114 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-modulePath = "../../../../app/js/ProjectManager.js"
-SandboxedModule = require('sandboxed-module')
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS206: Consider reworking classes to avoid initClass
+ * 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');
+const should = chai.should();
+const modulePath = "../../../../app/js/ProjectManager.js";
+const SandboxedModule = require('sandboxed-module');
 
-describe "ProjectManager - flushProject", ->
-	beforeEach ->
-		@ProjectManager = SandboxedModule.require modulePath, requires:
-			"./RedisManager": @RedisManager = {}
-			"./ProjectHistoryRedisManager": @ProjectHistoryRedisManager = {}
-			"./DocumentManager": @DocumentManager = {}
-			"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
-			"./HistoryManager": @HistoryManager = {}
-			"./Metrics": @Metrics =
-				Timer: class Timer
-					done: sinon.stub()
-		@project_id = "project-id-123"
-		@callback = sinon.stub()
+describe("ProjectManager - flushProject", function() {
+	beforeEach(function() {
+		let Timer;
+		this.ProjectManager = SandboxedModule.require(modulePath, { requires: {
+			"./RedisManager": (this.RedisManager = {}),
+			"./ProjectHistoryRedisManager": (this.ProjectHistoryRedisManager = {}),
+			"./DocumentManager": (this.DocumentManager = {}),
+			"logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }),
+			"./HistoryManager": (this.HistoryManager = {}),
+			"./Metrics": (this.Metrics = {
+				Timer: (Timer = (function() {
+					Timer = class Timer {
+						static initClass() {
+							this.prototype.done = sinon.stub();
+						}
+					};
+					Timer.initClass();
+					return Timer;
+				})())
+			})
+		}
+	}
+		);
+		this.project_id = "project-id-123";
+		return this.callback = sinon.stub();
+	});
 
-	describe "successfully", ->
-		beforeEach (done) ->
-			@doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"]
-			@RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, @doc_ids)
-			@DocumentManager.flushDocIfLoadedWithLock = sinon.stub().callsArg(2)
-			@ProjectManager.flushProjectWithLocks @project_id, (error) =>
-				@callback(error)
-				done()
+	describe("successfully", function() {
+		beforeEach(function(done) {
+			this.doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"];
+			this.RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, this.doc_ids);
+			this.DocumentManager.flushDocIfLoadedWithLock = sinon.stub().callsArg(2);
+			return this.ProjectManager.flushProjectWithLocks(this.project_id, error => {
+				this.callback(error);
+				return done();
+			});
+		});
 
-		it "should get the doc ids in the project", ->
-			@RedisManager.getDocIdsInProject
-				.calledWith(@project_id)
-				.should.equal true
+		it("should get the doc ids in the project", function() {
+			return this.RedisManager.getDocIdsInProject
+				.calledWith(this.project_id)
+				.should.equal(true);
+		});
 		
-		it "should flush each doc in the project", ->
-			for doc_id in @doc_ids
-				@DocumentManager.flushDocIfLoadedWithLock
-					.calledWith(@project_id, doc_id)
-					.should.equal true
+		it("should flush each doc in the project", function() {
+			return Array.from(this.doc_ids).map((doc_id) =>
+				this.DocumentManager.flushDocIfLoadedWithLock
+					.calledWith(this.project_id, doc_id)
+					.should.equal(true));
+		});
 
-		it "should call the callback without error", ->
-			@callback.calledWith(null).should.equal true
+		it("should call the callback without error", function() {
+			return this.callback.calledWith(null).should.equal(true);
+		});
 
-		it "should time the execution", ->
-			@Metrics.Timer::done.called.should.equal true
+		return it("should time the execution", function() {
+			return this.Metrics.Timer.prototype.done.called.should.equal(true);
+		});
+	});
 
-	describe "when a doc errors", ->
-		beforeEach (done) ->
-			@doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"]
-			@RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, @doc_ids)
-			@DocumentManager.flushDocIfLoadedWithLock = sinon.spy (project_id, doc_id, callback = (error) ->) =>
-				if doc_id == "doc-id-1"
-					callback(@error = new Error("oops, something went wrong"))
-				else
-					callback()
-			@ProjectManager.flushProjectWithLocks @project_id, (error) =>
-				@callback(error)
-				done()
+	return describe("when a doc errors", function() {
+		beforeEach(function(done) {
+			this.doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"];
+			this.RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, this.doc_ids);
+			this.DocumentManager.flushDocIfLoadedWithLock = sinon.spy((project_id, doc_id, callback) => {
+				if (callback == null) { callback = function(error) {}; }
+				if (doc_id === "doc-id-1") {
+					return callback(this.error = new Error("oops, something went wrong"));
+				} else {
+					return callback();
+				}
+			});
+			return this.ProjectManager.flushProjectWithLocks(this.project_id, error => {
+				this.callback(error);
+				return done();
+			});
+		});
 			
-		it "should still flush each doc in the project", ->
-			for doc_id in @doc_ids
-				@DocumentManager.flushDocIfLoadedWithLock
-					.calledWith(@project_id, doc_id)
-					.should.equal true
+		it("should still flush each doc in the project", function() {
+			return Array.from(this.doc_ids).map((doc_id) =>
+				this.DocumentManager.flushDocIfLoadedWithLock
+					.calledWith(this.project_id, doc_id)
+					.should.equal(true));
+		});
 
-		it "should record the error", ->
-			@logger.error
-				.calledWith(err: @error, project_id: @project_id, doc_id: "doc-id-1", "error flushing doc")
-				.should.equal true
+		it("should record the error", function() {
+			return this.logger.error
+				.calledWith({err: this.error, project_id: this.project_id, doc_id: "doc-id-1"}, "error flushing doc")
+				.should.equal(true);
+		});
 
-		it "should call the callback with an error", ->
-			@callback.calledWith(new Error()).should.equal true
+		it("should call the callback with an error", function() {
+			return this.callback.calledWith(new Error()).should.equal(true);
+		});
 
-		it "should time the execution", ->
-			@Metrics.Timer::done.called.should.equal true
+		return it("should time the execution", function() {
+			return this.Metrics.Timer.prototype.done.called.should.equal(true);
+		});
+	});
+});

+ 161 - 118
services/document-updater/test/unit/coffee/ProjectManager/getProjectDocsTests.js

@@ -1,118 +1,161 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-modulePath = "../../../../app/js/ProjectManager.js"
-SandboxedModule = require('sandboxed-module')
-Errors = require "../../../../app/js/Errors.js"
-
-describe "ProjectManager - getProjectDocsAndFlushIfOld", ->
-	beforeEach ->
-		@ProjectManager = SandboxedModule.require modulePath, requires:
-			"./RedisManager": @RedisManager = {}
-			"./ProjectHistoryRedisManager": @ProjectHistoryRedisManager = {}
-			"./DocumentManager": @DocumentManager = {}
-			"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
-			"./HistoryManager": @HistoryManager = {}
-			"./Metrics": @Metrics =
-				Timer: class Timer
-					done: sinon.stub()
-		@project_id = "project-id-123"
-		@callback = sinon.stub()
-		@doc_versions = [111, 222, 333]
-
-	describe "successfully", ->
-		beforeEach (done) ->
-			@doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"]
-			@doc_lines = [["aaa","aaa"],["bbb","bbb"],["ccc","ccc"]]
-			@docs = [
-				{_id: @doc_ids[0], lines: @doc_lines[0], v: @doc_versions[0]}
-				{_id: @doc_ids[1], lines: @doc_lines[1], v: @doc_versions[1]}
-				{_id: @doc_ids[2], lines: @doc_lines[2], v: @doc_versions[2]}
-			]
-			@RedisManager.checkOrSetProjectState = sinon.stub().callsArgWith(2, null)
-			@RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, @doc_ids)
-			@DocumentManager.getDocAndFlushIfOldWithLock = sinon.stub()
-			@DocumentManager.getDocAndFlushIfOldWithLock.withArgs(@project_id, @doc_ids[0])
-				.callsArgWith(2, null, @doc_lines[0], @doc_versions[0])
-			@DocumentManager.getDocAndFlushIfOldWithLock.withArgs(@project_id, @doc_ids[1])
-				.callsArgWith(2, null, @doc_lines[1], @doc_versions[1])
-			@DocumentManager.getDocAndFlushIfOldWithLock.withArgs(@project_id, @doc_ids[2])
-				.callsArgWith(2, null, @doc_lines[2], @doc_versions[2])
-			@ProjectManager.getProjectDocsAndFlushIfOld @project_id, @projectStateHash, @excludeVersions,  (error, docs) =>
-				@callback(error, docs)
-				done()
-
-		it "should check the project state", ->
-			@RedisManager.checkOrSetProjectState
-				.calledWith(@project_id, @projectStateHash)
-				.should.equal true
-
-		it "should get the doc ids in the project", ->
-			@RedisManager.getDocIdsInProject
-				.calledWith(@project_id)
-				.should.equal true
-
-		it "should call the callback without error", ->
-			@callback.calledWith(null, @docs).should.equal true
-
-		it "should time the execution", ->
-			@Metrics.Timer::done.called.should.equal true
-
-	describe "when the state does not match", ->
-		beforeEach (done) ->
-			@doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"]
-			@RedisManager.checkOrSetProjectState = sinon.stub().callsArgWith(2, null, true)
-			@ProjectManager.getProjectDocsAndFlushIfOld @project_id, @projectStateHash, @excludeVersions,  (error, docs) =>
-				@callback(error, docs)
-				done()
-
-		it "should check the project state", ->
-			@RedisManager.checkOrSetProjectState
-				.calledWith(@project_id, @projectStateHash)
-				.should.equal true
-
-		it "should call the callback with an error", ->
-			@callback.calledWith(new Errors.ProjectStateChangedError("project state changed")).should.equal true
-
-		it "should time the execution", ->
-			@Metrics.Timer::done.called.should.equal true
-
-	describe "when a doc errors", ->
-		beforeEach (done) ->
-			@doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"]
-			@RedisManager.checkOrSetProjectState = sinon.stub().callsArgWith(2, null)
-			@RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, @doc_ids)
-			@DocumentManager.getDocAndFlushIfOldWithLock = sinon.stub()
-			@DocumentManager.getDocAndFlushIfOldWithLock.withArgs(@project_id, "doc-id-1")
-				.callsArgWith(2, null, ["test doc content"], @doc_versions[1])
-			@DocumentManager.getDocAndFlushIfOldWithLock.withArgs(@project_id, "doc-id-2")
-				.callsArgWith(2, @error = new Error("oops")) # trigger an error
-			@ProjectManager.getProjectDocsAndFlushIfOld @project_id, @projectStateHash, @excludeVersions, (error, docs) =>
-				@callback(error)
-				done()
-
-		it "should record the error", ->
-			@logger.error
-				.calledWith(err: @error, project_id: @project_id, doc_id: "doc-id-2", "error getting project doc lines in getProjectDocsAndFlushIfOld")
-				.should.equal true
-
-		it "should call the callback with an error", ->
-			@callback.calledWith(new Error("oops")).should.equal true
-
-		it "should time the execution", ->
-			@Metrics.Timer::done.called.should.equal true
-
-	describe "clearing the project state with clearProjectState", ->
-		beforeEach (done) ->
-			@RedisManager.clearProjectState = sinon.stub().callsArg(1)
-			@ProjectManager.clearProjectState @project_id, (error) =>
-				@callback(error)
-				done()
-
-		it "should clear the project state", ->
-			@RedisManager.clearProjectState
-			.calledWith(@project_id)
-			.should.equal true
-
-		it "should call the callback", ->
-			@callback.called.should.equal true
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS206: Consider reworking classes to avoid initClass
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require('sinon');
+const chai = require('chai');
+const should = chai.should();
+const modulePath = "../../../../app/js/ProjectManager.js";
+const SandboxedModule = require('sandboxed-module');
+const Errors = require("../../../../app/js/Errors.js");
+
+describe("ProjectManager - getProjectDocsAndFlushIfOld", function() {
+	beforeEach(function() {
+		let Timer;
+		this.ProjectManager = SandboxedModule.require(modulePath, { requires: {
+			"./RedisManager": (this.RedisManager = {}),
+			"./ProjectHistoryRedisManager": (this.ProjectHistoryRedisManager = {}),
+			"./DocumentManager": (this.DocumentManager = {}),
+			"logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }),
+			"./HistoryManager": (this.HistoryManager = {}),
+			"./Metrics": (this.Metrics = {
+				Timer: (Timer = (function() {
+					Timer = class Timer {
+						static initClass() {
+							this.prototype.done = sinon.stub();
+						}
+					};
+					Timer.initClass();
+					return Timer;
+				})())
+			})
+		}
+	}
+		);
+		this.project_id = "project-id-123";
+		this.callback = sinon.stub();
+		return this.doc_versions = [111, 222, 333];});
+
+	describe("successfully", function() {
+		beforeEach(function(done) {
+			this.doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"];
+			this.doc_lines = [["aaa","aaa"],["bbb","bbb"],["ccc","ccc"]];
+			this.docs = [
+				{_id: this.doc_ids[0], lines: this.doc_lines[0], v: this.doc_versions[0]},
+				{_id: this.doc_ids[1], lines: this.doc_lines[1], v: this.doc_versions[1]},
+				{_id: this.doc_ids[2], lines: this.doc_lines[2], v: this.doc_versions[2]}
+			];
+			this.RedisManager.checkOrSetProjectState = sinon.stub().callsArgWith(2, null);
+			this.RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, this.doc_ids);
+			this.DocumentManager.getDocAndFlushIfOldWithLock = sinon.stub();
+			this.DocumentManager.getDocAndFlushIfOldWithLock.withArgs(this.project_id, this.doc_ids[0])
+				.callsArgWith(2, null, this.doc_lines[0], this.doc_versions[0]);
+			this.DocumentManager.getDocAndFlushIfOldWithLock.withArgs(this.project_id, this.doc_ids[1])
+				.callsArgWith(2, null, this.doc_lines[1], this.doc_versions[1]);
+			this.DocumentManager.getDocAndFlushIfOldWithLock.withArgs(this.project_id, this.doc_ids[2])
+				.callsArgWith(2, null, this.doc_lines[2], this.doc_versions[2]);
+			return this.ProjectManager.getProjectDocsAndFlushIfOld(this.project_id, this.projectStateHash, this.excludeVersions,  (error, docs) => {
+				this.callback(error, docs);
+				return done();
+			});
+		});
+
+		it("should check the project state", function() {
+			return this.RedisManager.checkOrSetProjectState
+				.calledWith(this.project_id, this.projectStateHash)
+				.should.equal(true);
+		});
+
+		it("should get the doc ids in the project", function() {
+			return this.RedisManager.getDocIdsInProject
+				.calledWith(this.project_id)
+				.should.equal(true);
+		});
+
+		it("should call the callback without error", function() {
+			return this.callback.calledWith(null, this.docs).should.equal(true);
+		});
+
+		return it("should time the execution", function() {
+			return this.Metrics.Timer.prototype.done.called.should.equal(true);
+		});
+	});
+
+	describe("when the state does not match", function() {
+		beforeEach(function(done) {
+			this.doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"];
+			this.RedisManager.checkOrSetProjectState = sinon.stub().callsArgWith(2, null, true);
+			return this.ProjectManager.getProjectDocsAndFlushIfOld(this.project_id, this.projectStateHash, this.excludeVersions,  (error, docs) => {
+				this.callback(error, docs);
+				return done();
+			});
+		});
+
+		it("should check the project state", function() {
+			return this.RedisManager.checkOrSetProjectState
+				.calledWith(this.project_id, this.projectStateHash)
+				.should.equal(true);
+		});
+
+		it("should call the callback with an error", function() {
+			return this.callback.calledWith(new Errors.ProjectStateChangedError("project state changed")).should.equal(true);
+		});
+
+		return it("should time the execution", function() {
+			return this.Metrics.Timer.prototype.done.called.should.equal(true);
+		});
+	});
+
+	describe("when a doc errors", function() {
+		beforeEach(function(done) {
+			this.doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"];
+			this.RedisManager.checkOrSetProjectState = sinon.stub().callsArgWith(2, null);
+			this.RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, this.doc_ids);
+			this.DocumentManager.getDocAndFlushIfOldWithLock = sinon.stub();
+			this.DocumentManager.getDocAndFlushIfOldWithLock.withArgs(this.project_id, "doc-id-1")
+				.callsArgWith(2, null, ["test doc content"], this.doc_versions[1]);
+			this.DocumentManager.getDocAndFlushIfOldWithLock.withArgs(this.project_id, "doc-id-2")
+				.callsArgWith(2, (this.error = new Error("oops"))); // trigger an error
+			return this.ProjectManager.getProjectDocsAndFlushIfOld(this.project_id, this.projectStateHash, this.excludeVersions, (error, docs) => {
+				this.callback(error);
+				return done();
+			});
+		});
+
+		it("should record the error", function() {
+			return this.logger.error
+				.calledWith({err: this.error, project_id: this.project_id, doc_id: "doc-id-2"}, "error getting project doc lines in getProjectDocsAndFlushIfOld")
+				.should.equal(true);
+		});
+
+		it("should call the callback with an error", function() {
+			return this.callback.calledWith(new Error("oops")).should.equal(true);
+		});
+
+		return it("should time the execution", function() {
+			return this.Metrics.Timer.prototype.done.called.should.equal(true);
+		});
+	});
+
+	return describe("clearing the project state with clearProjectState", function() {
+		beforeEach(function(done) {
+			this.RedisManager.clearProjectState = sinon.stub().callsArg(1);
+			return this.ProjectManager.clearProjectState(this.project_id, error => {
+				this.callback(error);
+				return done();
+			});
+		});
+
+		it("should clear the project state", function() {
+			return this.RedisManager.clearProjectState
+			.calledWith(this.project_id)
+			.should.equal(true);
+		});
+
+		return it("should call the callback", function() {
+			return this.callback.called.should.equal(true);
+		});
+	});
+});

+ 235 - 173
services/document-updater/test/unit/coffee/ProjectManager/updateProjectTests.js

@@ -1,180 +1,242 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-modulePath = "../../../../app/js/ProjectManager.js"
-SandboxedModule = require('sandboxed-module')
-_ = require('lodash')
-
-describe "ProjectManager", ->
-	beforeEach ->
-		@ProjectManager = SandboxedModule.require modulePath, requires:
-			"./RedisManager": @RedisManager = {}
-			"./ProjectHistoryRedisManager": @ProjectHistoryRedisManager = {}
-			"./DocumentManager": @DocumentManager = {}
-			"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
-			"./HistoryManager": @HistoryManager = {}
-			"./Metrics": @Metrics =
-				Timer: class Timer
-					done: sinon.stub()
-
-		@project_id = "project-id-123"
-		@projectHistoryId = 'history-id-123'
-		@user_id = "user-id-123"
-		@version = 1234567
-		@HistoryManager.shouldFlushHistoryOps = sinon.stub().returns(false)
-		@HistoryManager.flushProjectChangesAsync = sinon.stub()
-		@callback = sinon.stub()
-
-	describe "updateProjectWithLocks", ->
-		describe "rename operations", ->
-			beforeEach ->
-				@firstDocUpdate =
-					id: 1
-					pathname: 'foo'
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS206: Consider reworking classes to avoid initClass
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require('sinon');
+const chai = require('chai');
+const should = chai.should();
+const modulePath = "../../../../app/js/ProjectManager.js";
+const SandboxedModule = require('sandboxed-module');
+const _ = require('lodash');
+
+describe("ProjectManager", function() {
+	beforeEach(function() {
+		let Timer;
+		this.ProjectManager = SandboxedModule.require(modulePath, { requires: {
+			"./RedisManager": (this.RedisManager = {}),
+			"./ProjectHistoryRedisManager": (this.ProjectHistoryRedisManager = {}),
+			"./DocumentManager": (this.DocumentManager = {}),
+			"logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }),
+			"./HistoryManager": (this.HistoryManager = {}),
+			"./Metrics": (this.Metrics = {
+				Timer: (Timer = (function() {
+					Timer = class Timer {
+						static initClass() {
+							this.prototype.done = sinon.stub();
+						}
+					};
+					Timer.initClass();
+					return Timer;
+				})())
+			})
+		}
+	}
+		);
+
+		this.project_id = "project-id-123";
+		this.projectHistoryId = 'history-id-123';
+		this.user_id = "user-id-123";
+		this.version = 1234567;
+		this.HistoryManager.shouldFlushHistoryOps = sinon.stub().returns(false);
+		this.HistoryManager.flushProjectChangesAsync = sinon.stub();
+		return this.callback = sinon.stub();
+	});
+
+	return describe("updateProjectWithLocks", function() {
+		describe("rename operations", function() {
+			beforeEach(function() {
+				this.firstDocUpdate = {
+					id: 1,
+					pathname: 'foo',
 					newPathname: 'foo'
-				@secondDocUpdate =
-					id: 2
-					pathname: 'bar'
+				};
+				this.secondDocUpdate = {
+					id: 2,
+					pathname: 'bar',
 					newPathname: 'bar2'
-				@docUpdates = [ @firstDocUpdate, @secondDocUpdate ]
-				@firstFileUpdate =
-					id: 2
-					pathname: 'bar'
+				};
+				this.docUpdates = [ this.firstDocUpdate, this.secondDocUpdate ];
+				this.firstFileUpdate = {
+					id: 2,
+					pathname: 'bar',
 					newPathname: 'bar2'
-				@fileUpdates = [ @firstFileUpdate ]
-				@DocumentManager.renameDocWithLock = sinon.stub().yields()
-				@ProjectHistoryRedisManager.queueRenameEntity = sinon.stub().yields()
-
-			describe "successfully", ->
-				beforeEach ->
-					@ProjectManager.updateProjectWithLocks @project_id, @projectHistoryId, @user_id, @docUpdates, @fileUpdates, @version, @callback
-
-				it "should rename the docs in the updates", ->
-					firstDocUpdateWithVersion = _.extend({}, @firstDocUpdate, {version: "#{@version}.0"})
-					secondDocUpdateWithVersion = _.extend({}, @secondDocUpdate, {version: "#{@version}.1"})
-					@DocumentManager.renameDocWithLock
-						.calledWith(@project_id, @firstDocUpdate.id, @user_id, firstDocUpdateWithVersion, @projectHistoryId)
-						.should.equal true
-					@DocumentManager.renameDocWithLock
-						.calledWith(@project_id, @secondDocUpdate.id, @user_id, secondDocUpdateWithVersion, @projectHistoryId)
-						.should.equal true
-
-				it "should rename the files in the updates", ->
-					firstFileUpdateWithVersion = _.extend({}, @firstFileUpdate, {version: "#{@version}.2"})
-					@ProjectHistoryRedisManager.queueRenameEntity
-						.calledWith(@project_id, @projectHistoryId, 'file', @firstFileUpdate.id, @user_id, firstFileUpdateWithVersion)
-						.should.equal true
-
-				it "should not flush the history", ->
-					@HistoryManager.flushProjectChangesAsync
-						.calledWith(@project_id)
-						.should.equal false
-
-				it "should call the callback", ->
-					@callback.called.should.equal true
-
-			describe "when renaming a doc fails", ->
-				beforeEach ->
-					@error = new Error('error')
-					@DocumentManager.renameDocWithLock = sinon.stub().yields(@error)
-					@ProjectManager.updateProjectWithLocks @project_id, @projectHistoryId, @user_id, @docUpdates, @fileUpdates, @version, @callback
-
-				it "should call the callback with the error", ->
-					@callback.calledWith(@error).should.equal true
-
-			describe "when renaming a file fails", ->
-				beforeEach ->
-					@error = new Error('error')
-					@ProjectHistoryRedisManager.queueRenameEntity = sinon.stub().yields(@error)
-					@ProjectManager.updateProjectWithLocks @project_id, @projectHistoryId, @user_id, @docUpdates, @fileUpdates, @version, @callback
-
-				it "should call the callback with the error", ->
-					@callback.calledWith(@error).should.equal true
-
-			describe "with enough ops to flush", ->
-				beforeEach ->
-					@HistoryManager.shouldFlushHistoryOps = sinon.stub().returns(true)
-					@ProjectManager.updateProjectWithLocks @project_id, @projectHistoryId, @user_id, @docUpdates, @fileUpdates, @version, @callback
-
-				it "should flush the history", ->
-					@HistoryManager.flushProjectChangesAsync
-						.calledWith(@project_id)
-						.should.equal true
-
-		describe "add operations", ->
-			beforeEach ->
-				@firstDocUpdate =
-					id: 1
+				};
+				this.fileUpdates = [ this.firstFileUpdate ];
+				this.DocumentManager.renameDocWithLock = sinon.stub().yields();
+				return this.ProjectHistoryRedisManager.queueRenameEntity = sinon.stub().yields();
+			});
+
+			describe("successfully", function() {
+				beforeEach(function() {
+					return this.ProjectManager.updateProjectWithLocks(this.project_id, this.projectHistoryId, this.user_id, this.docUpdates, this.fileUpdates, this.version, this.callback);
+				});
+
+				it("should rename the docs in the updates", function() {
+					const firstDocUpdateWithVersion = _.extend({}, this.firstDocUpdate, {version: `${this.version}.0`});
+					const secondDocUpdateWithVersion = _.extend({}, this.secondDocUpdate, {version: `${this.version}.1`});
+					this.DocumentManager.renameDocWithLock
+						.calledWith(this.project_id, this.firstDocUpdate.id, this.user_id, firstDocUpdateWithVersion, this.projectHistoryId)
+						.should.equal(true);
+					return this.DocumentManager.renameDocWithLock
+						.calledWith(this.project_id, this.secondDocUpdate.id, this.user_id, secondDocUpdateWithVersion, this.projectHistoryId)
+						.should.equal(true);
+				});
+
+				it("should rename the files in the updates", function() {
+					const firstFileUpdateWithVersion = _.extend({}, this.firstFileUpdate, {version: `${this.version}.2`});
+					return this.ProjectHistoryRedisManager.queueRenameEntity
+						.calledWith(this.project_id, this.projectHistoryId, 'file', this.firstFileUpdate.id, this.user_id, firstFileUpdateWithVersion)
+						.should.equal(true);
+				});
+
+				it("should not flush the history", function() {
+					return this.HistoryManager.flushProjectChangesAsync
+						.calledWith(this.project_id)
+						.should.equal(false);
+				});
+
+				return it("should call the callback", function() {
+					return this.callback.called.should.equal(true);
+				});
+			});
+
+			describe("when renaming a doc fails", function() {
+				beforeEach(function() {
+					this.error = new Error('error');
+					this.DocumentManager.renameDocWithLock = sinon.stub().yields(this.error);
+					return this.ProjectManager.updateProjectWithLocks(this.project_id, this.projectHistoryId, this.user_id, this.docUpdates, this.fileUpdates, this.version, this.callback);
+				});
+
+				return it("should call the callback with the error", function() {
+					return this.callback.calledWith(this.error).should.equal(true);
+				});
+			});
+
+			describe("when renaming a file fails", function() {
+				beforeEach(function() {
+					this.error = new Error('error');
+					this.ProjectHistoryRedisManager.queueRenameEntity = sinon.stub().yields(this.error);
+					return this.ProjectManager.updateProjectWithLocks(this.project_id, this.projectHistoryId, this.user_id, this.docUpdates, this.fileUpdates, this.version, this.callback);
+				});
+
+				return it("should call the callback with the error", function() {
+					return this.callback.calledWith(this.error).should.equal(true);
+				});
+			});
+
+			return describe("with enough ops to flush", function() {
+				beforeEach(function() {
+					this.HistoryManager.shouldFlushHistoryOps = sinon.stub().returns(true);
+					return this.ProjectManager.updateProjectWithLocks(this.project_id, this.projectHistoryId, this.user_id, this.docUpdates, this.fileUpdates, this.version, this.callback);
+				});
+
+				return it("should flush the history", function() {
+					return this.HistoryManager.flushProjectChangesAsync
+						.calledWith(this.project_id)
+						.should.equal(true);
+				});
+			});
+		});
+
+		return describe("add operations", function() {
+			beforeEach(function() {
+				this.firstDocUpdate = {
+					id: 1,
 					docLines: "a\nb"
-				@secondDocUpdate =
-					id: 2
+				};
+				this.secondDocUpdate = {
+					id: 2,
 					docLines: "a\nb"
-				@docUpdates = [ @firstDocUpdate, @secondDocUpdate ]
-				@firstFileUpdate =
-					id: 3
+				};
+				this.docUpdates = [ this.firstDocUpdate, this.secondDocUpdate ];
+				this.firstFileUpdate = {
+					id: 3,
 					url: 'filestore.example.com/2'
-				@secondFileUpdate =
-					id: 4
+				};
+				this.secondFileUpdate = {
+					id: 4,
 					url: 'filestore.example.com/3'
-				@fileUpdates = [ @firstFileUpdate, @secondFileUpdate ]
-				@ProjectHistoryRedisManager.queueAddEntity = sinon.stub().yields()
-
-			describe "successfully", ->
-				beforeEach ->
-					@ProjectManager.updateProjectWithLocks @project_id, @projectHistoryId, @user_id, @docUpdates, @fileUpdates, @version, @callback
-
-				it "should add the docs in the updates", ->
-					firstDocUpdateWithVersion = _.extend({}, @firstDocUpdate, {version: "#{@version}.0"})
-					secondDocUpdateWithVersion = _.extend({}, @secondDocUpdate, {version: "#{@version}.1"})
-					@ProjectHistoryRedisManager.queueAddEntity.getCall(0)
-						.calledWith(@project_id, @projectHistoryId, 'doc', @firstDocUpdate.id, @user_id, firstDocUpdateWithVersion)
-						.should.equal true
-					@ProjectHistoryRedisManager.queueAddEntity.getCall(1)
-						.calledWith(@project_id, @projectHistoryId, 'doc', @secondDocUpdate.id, @user_id, secondDocUpdateWithVersion)
-						.should.equal true
-
-				it "should add the files in the updates", ->
-					firstFileUpdateWithVersion = _.extend({}, @firstFileUpdate, {version: "#{@version}.2"})
-					secondFileUpdateWithVersion = _.extend({}, @secondFileUpdate, {version: "#{@version}.3"})
-					@ProjectHistoryRedisManager.queueAddEntity.getCall(2)
-						.calledWith(@project_id, @projectHistoryId, 'file', @firstFileUpdate.id, @user_id, firstFileUpdateWithVersion)
-						.should.equal true
-					@ProjectHistoryRedisManager.queueAddEntity.getCall(3)
-						.calledWith(@project_id, @projectHistoryId, 'file', @secondFileUpdate.id, @user_id, secondFileUpdateWithVersion)
-						.should.equal true
-
-				it "should not flush the history", ->
-					@HistoryManager.flushProjectChangesAsync
-						.calledWith(@project_id)
-						.should.equal false
-
-				it "should call the callback", ->
-					@callback.called.should.equal true
-
-			describe "when adding a doc fails", ->
-				beforeEach ->
-					@error = new Error('error')
-					@ProjectHistoryRedisManager.queueAddEntity = sinon.stub().yields(@error)
-					@ProjectManager.updateProjectWithLocks @project_id, @projectHistoryId, @user_id, @docUpdates, @fileUpdates, @version, @callback
-
-				it "should call the callback with the error", ->
-					@callback.calledWith(@error).should.equal true
-
-			describe "when adding a file fails", ->
-				beforeEach ->
-					@error = new Error('error')
-					@ProjectHistoryRedisManager.queueAddEntity = sinon.stub().yields(@error)
-					@ProjectManager.updateProjectWithLocks @project_id, @projectHistoryId, @user_id, @docUpdates, @fileUpdates, @version, @callback
-
-				it "should call the callback with the error", ->
-					@callback.calledWith(@error).should.equal true
-
-			describe "with enough ops to flush", ->
-				beforeEach ->
-					@HistoryManager.shouldFlushHistoryOps = sinon.stub().returns(true)
-					@ProjectManager.updateProjectWithLocks @project_id, @projectHistoryId, @user_id, @docUpdates, @fileUpdates, @version, @callback
-
-				it "should flush the history", ->
-					@HistoryManager.flushProjectChangesAsync
-						.calledWith(@project_id)
-						.should.equal true
+				};
+				this.fileUpdates = [ this.firstFileUpdate, this.secondFileUpdate ];
+				return this.ProjectHistoryRedisManager.queueAddEntity = sinon.stub().yields();
+			});
+
+			describe("successfully", function() {
+				beforeEach(function() {
+					return this.ProjectManager.updateProjectWithLocks(this.project_id, this.projectHistoryId, this.user_id, this.docUpdates, this.fileUpdates, this.version, this.callback);
+				});
+
+				it("should add the docs in the updates", function() {
+					const firstDocUpdateWithVersion = _.extend({}, this.firstDocUpdate, {version: `${this.version}.0`});
+					const secondDocUpdateWithVersion = _.extend({}, this.secondDocUpdate, {version: `${this.version}.1`});
+					this.ProjectHistoryRedisManager.queueAddEntity.getCall(0)
+						.calledWith(this.project_id, this.projectHistoryId, 'doc', this.firstDocUpdate.id, this.user_id, firstDocUpdateWithVersion)
+						.should.equal(true);
+					return this.ProjectHistoryRedisManager.queueAddEntity.getCall(1)
+						.calledWith(this.project_id, this.projectHistoryId, 'doc', this.secondDocUpdate.id, this.user_id, secondDocUpdateWithVersion)
+						.should.equal(true);
+				});
+
+				it("should add the files in the updates", function() {
+					const firstFileUpdateWithVersion = _.extend({}, this.firstFileUpdate, {version: `${this.version}.2`});
+					const secondFileUpdateWithVersion = _.extend({}, this.secondFileUpdate, {version: `${this.version}.3`});
+					this.ProjectHistoryRedisManager.queueAddEntity.getCall(2)
+						.calledWith(this.project_id, this.projectHistoryId, 'file', this.firstFileUpdate.id, this.user_id, firstFileUpdateWithVersion)
+						.should.equal(true);
+					return this.ProjectHistoryRedisManager.queueAddEntity.getCall(3)
+						.calledWith(this.project_id, this.projectHistoryId, 'file', this.secondFileUpdate.id, this.user_id, secondFileUpdateWithVersion)
+						.should.equal(true);
+				});
+
+				it("should not flush the history", function() {
+					return this.HistoryManager.flushProjectChangesAsync
+						.calledWith(this.project_id)
+						.should.equal(false);
+				});
+
+				return it("should call the callback", function() {
+					return this.callback.called.should.equal(true);
+				});
+			});
+
+			describe("when adding a doc fails", function() {
+				beforeEach(function() {
+					this.error = new Error('error');
+					this.ProjectHistoryRedisManager.queueAddEntity = sinon.stub().yields(this.error);
+					return this.ProjectManager.updateProjectWithLocks(this.project_id, this.projectHistoryId, this.user_id, this.docUpdates, this.fileUpdates, this.version, this.callback);
+				});
+
+				return it("should call the callback with the error", function() {
+					return this.callback.calledWith(this.error).should.equal(true);
+				});
+			});
+
+			describe("when adding a file fails", function() {
+				beforeEach(function() {
+					this.error = new Error('error');
+					this.ProjectHistoryRedisManager.queueAddEntity = sinon.stub().yields(this.error);
+					return this.ProjectManager.updateProjectWithLocks(this.project_id, this.projectHistoryId, this.user_id, this.docUpdates, this.fileUpdates, this.version, this.callback);
+				});
+
+				return it("should call the callback with the error", function() {
+					return this.callback.calledWith(this.error).should.equal(true);
+				});
+			});
+
+			return describe("with enough ops to flush", function() {
+				beforeEach(function() {
+					this.HistoryManager.shouldFlushHistoryOps = sinon.stub().returns(true);
+					return this.ProjectManager.updateProjectWithLocks(this.project_id, this.projectHistoryId, this.user_id, this.docUpdates, this.fileUpdates, this.version, this.callback);
+				});
+
+				return it("should flush the history", function() {
+					return this.HistoryManager.flushProjectChangesAsync
+						.calledWith(this.project_id)
+						.should.equal(true);
+				});
+			});
+		});
+	});
+});

+ 314 - 235
services/document-updater/test/unit/coffee/RangesManager/RangesManagerTests.js

@@ -1,316 +1,395 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-expect = chai.expect
-modulePath = "../../../../app/js/RangesManager.js"
-SandboxedModule = require('sandboxed-module')
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require('sinon');
+const chai = require('chai');
+const should = chai.should();
+const {
+    expect
+} = chai;
+const modulePath = "../../../../app/js/RangesManager.js";
+const SandboxedModule = require('sandboxed-module');
 
-describe "RangesManager", ->
-	beforeEach ->
-		@RangesManager = SandboxedModule.require modulePath,
-			requires:
-				"logger-sharelatex": @logger = { error: sinon.stub(), log: sinon.stub(), warn: sinon.stub() }
+describe("RangesManager", function() {
+	beforeEach(function() {
+		this.RangesManager = SandboxedModule.require(modulePath, {
+			requires: {
+				"logger-sharelatex": (this.logger = { error: sinon.stub(), log: sinon.stub(), warn: sinon.stub() })
+			}
+		});
 
-		@doc_id = "doc-id-123"
-		@project_id = "project-id-123"
-		@user_id = "user-id-123"
-		@callback = sinon.stub()
+		this.doc_id = "doc-id-123";
+		this.project_id = "project-id-123";
+		this.user_id = "user-id-123";
+		return this.callback = sinon.stub();
+	});
 
-	describe "applyUpdate", ->
-		beforeEach ->
-			@updates = [{
-				meta:
-					user_id: @user_id
+	describe("applyUpdate", function() {
+		beforeEach(function() {
+			this.updates = [{
+				meta: {
+					user_id: this.user_id
+				},
 				op: [{
-					i: "two "
+					i: "two ",
 					p: 4
 				}]
-			}]
-			@entries = {
+			}];
+			this.entries = {
 				comments: [{
-					op:
-						c: "three "
+					op: {
+						c: "three ",
 						p: 4
-					metadata:
-						user_id: @user_id
-				}]
+					},
+					metadata: {
+						user_id: this.user_id
+					}
+				}],
 				changes: [{
-					op:
-						i: "five"
+					op: {
+						i: "five",
 						p: 15
-					metadata:
-						user_id: @user_id
+					},
+					metadata: {
+						user_id: this.user_id
+					}
 				}]
-			}
-			@newDocLines = ["one two three four five"] # old is "one three four five"
+			};
+			return this.newDocLines = ["one two three four five"];}); // old is "one three four five"
 		
-		describe "successfully", ->
-			beforeEach ->
-				@RangesManager.applyUpdate @project_id, @doc_id, @entries, @updates, @newDocLines, @callback
+		describe("successfully", function() {
+			beforeEach(function() {
+				return this.RangesManager.applyUpdate(this.project_id, this.doc_id, this.entries, this.updates, this.newDocLines, this.callback);
+			});
 			
-			it "should return the modified the comments and changes", ->
-				@callback.called.should.equal true
-				[error, entries, ranges_were_collapsed] = @callback.args[0]
-				expect(error).to.be.null
-				expect(ranges_were_collapsed).to.equal false
-				entries.comments[0].op.should.deep.equal {
-					c: "three "
+			return it("should return the modified the comments and changes", function() {
+				this.callback.called.should.equal(true);
+				const [error, entries, ranges_were_collapsed] = Array.from(this.callback.args[0]);
+				expect(error).to.be.null;
+				expect(ranges_were_collapsed).to.equal(false);
+				entries.comments[0].op.should.deep.equal({
+					c: "three ",
 					p: 8
-				}
-				entries.changes[0].op.should.deep.equal {
-					i: "five"
+				});
+				return entries.changes[0].op.should.deep.equal({
+					i: "five",
 					p: 19
-				}
+				});
+		});
+	});
 		
-		describe "with empty comments", ->
-			beforeEach ->
-				@entries.comments = []
-				@RangesManager.applyUpdate @project_id, @doc_id, @entries, @updates, @newDocLines, @callback
+		describe("with empty comments", function() {
+			beforeEach(function() {
+				this.entries.comments = [];
+				return this.RangesManager.applyUpdate(this.project_id, this.doc_id, this.entries, this.updates, this.newDocLines, this.callback);
+			});
 			
-			it "should return an object with no comments", ->
-				# Save space in redis and don't store just {}
-				@callback.called.should.equal true
-				[error, entries] = @callback.args[0]
-				expect(error).to.be.null
-				expect(entries.comments).to.be.undefined
+			return it("should return an object with no comments", function() {
+				// Save space in redis and don't store just {}
+				this.callback.called.should.equal(true);
+				const [error, entries] = Array.from(this.callback.args[0]);
+				expect(error).to.be.null;
+				return expect(entries.comments).to.be.undefined;
+			});
+		});
 		
-		describe "with empty changes", ->
-			beforeEach ->
-				@entries.changes = []
-				@RangesManager.applyUpdate @project_id, @doc_id, @entries, @updates, @newDocLines, @callback
+		describe("with empty changes", function() {
+			beforeEach(function() {
+				this.entries.changes = [];
+				return this.RangesManager.applyUpdate(this.project_id, this.doc_id, this.entries, this.updates, this.newDocLines, this.callback);
+			});
 			
-			it "should return an object with no changes", ->
-				# Save space in redis and don't store just {}
-				@callback.called.should.equal true
-				[error, entries] = @callback.args[0]
-				expect(error).to.be.null
-				expect(entries.changes).to.be.undefined
+			return it("should return an object with no changes", function() {
+				// Save space in redis and don't store just {}
+				this.callback.called.should.equal(true);
+				const [error, entries] = Array.from(this.callback.args[0]);
+				expect(error).to.be.null;
+				return expect(entries.changes).to.be.undefined;
+			});
+		});
 	
-		describe "with too many comments", ->
-			beforeEach ->
-				@RangesManager.MAX_COMMENTS = 2
-				@updates = [{
-					meta:
-						user_id: @user_id
+		describe("with too many comments", function() {
+			beforeEach(function() {
+				this.RangesManager.MAX_COMMENTS = 2;
+				this.updates = [{
+					meta: {
+						user_id: this.user_id
+					},
 					op: [{
-						c: "one"
-						p: 0
+						c: "one",
+						p: 0,
 						t: "thread-id-1"
 					}]
-				}]
-				@entries = {
+				}];
+				this.entries = {
 					comments: [{
-						op:
-							c: "three "
-							p: 4
+						op: {
+							c: "three ",
+							p: 4,
 							t: "thread-id-2"
-						metadata:
-							user_id: @user_id
+						},
+						metadata: {
+							user_id: this.user_id
+						}
 					}, {
-						op:
-							c: "four "
-							p: 10
+						op: {
+							c: "four ",
+							p: 10,
 							t: "thread-id-3"
-						metadata:
-							user_id: @user_id
-					}]
+						},
+						metadata: {
+							user_id: this.user_id
+						}
+					}],
 					changes: []
-				}
-				@RangesManager.applyUpdate @project_id, @doc_id, @entries, @updates, @newDocLines, @callback
+				};
+				return this.RangesManager.applyUpdate(this.project_id, this.doc_id, this.entries, this.updates, this.newDocLines, this.callback);
+			});
 			
-			it "should return an error", ->
-				@callback.called.should.equal true
-				[error, entries] = @callback.args[0]
-				expect(error).to.not.be.null
-				expect(error.message).to.equal("too many comments or tracked changes")
+			return it("should return an error", function() {
+				this.callback.called.should.equal(true);
+				const [error, entries] = Array.from(this.callback.args[0]);
+				expect(error).to.not.be.null;
+				return expect(error.message).to.equal("too many comments or tracked changes");
+			});
+		});
 		
-		describe "with too many changes", ->
-			beforeEach ->
-				@RangesManager.MAX_CHANGES = 2
-				@updates = [{
-					meta:
-						user_id: @user_id
+		describe("with too many changes", function() {
+			beforeEach(function() {
+				this.RangesManager.MAX_CHANGES = 2;
+				this.updates = [{
+					meta: {
+						user_id: this.user_id,
 						tc: "track-changes-id-yes"
+					},
 					op: [{
-						i: "one "
+						i: "one ",
 						p: 0
 					}]
-				}]
-				@entries = {
+				}];
+				this.entries = {
 					changes: [{
-						op:
-							i: "three"
+						op: {
+							i: "three",
 							p: 4
-						metadata:
-							user_id: @user_id
+						},
+						metadata: {
+							user_id: this.user_id
+						}
 					}, {
-						op:
-							i: "four"
+						op: {
+							i: "four",
 							p: 10
-						metadata:
-							user_id: @user_id
-					}]
+						},
+						metadata: {
+							user_id: this.user_id
+						}
+					}],
 					comments: []
-				}
-				@newDocLines = ["one two three four"]
-				@RangesManager.applyUpdate @project_id, @doc_id, @entries, @updates, @newDocLines, @callback
+				};
+				this.newDocLines = ["one two three four"];
+				return this.RangesManager.applyUpdate(this.project_id, this.doc_id, this.entries, this.updates, this.newDocLines, this.callback);
+			});
 			
-			it "should return an error", ->
-				# Save space in redis and don't store just {}
-				@callback.called.should.equal true
-				[error, entries] = @callback.args[0]
-				expect(error).to.not.be.null
-				expect(error.message).to.equal("too many comments or tracked changes")
+			return it("should return an error", function() {
+				// Save space in redis and don't store just {}
+				this.callback.called.should.equal(true);
+				const [error, entries] = Array.from(this.callback.args[0]);
+				expect(error).to.not.be.null;
+				return expect(error.message).to.equal("too many comments or tracked changes");
+			});
+		});
 		
-		describe "inconsistent changes", ->
-			beforeEach ->
-				@updates = [{
-					meta:
-						user_id: @user_id
+		describe("inconsistent changes", function() {
+			beforeEach(function() {
+				this.updates = [{
+					meta: {
+						user_id: this.user_id
+					},
 					op: [{
-						c: "doesn't match"
+						c: "doesn't match",
 						p: 0
 					}]
-				}]
-				@RangesManager.applyUpdate @project_id, @doc_id, @entries, @updates, @newDocLines, @callback
+				}];
+				return this.RangesManager.applyUpdate(this.project_id, this.doc_id, this.entries, this.updates, this.newDocLines, this.callback);
+			});
 			
-			it "should return an error", ->
-				# Save space in redis and don't store just {}
-				@callback.called.should.equal true
-				[error, entries] = @callback.args[0]
-				expect(error).to.not.be.null
-				expect(error.message).to.equal("Change ({\"op\":{\"i\":\"five\",\"p\":15},\"metadata\":{\"user_id\":\"user-id-123\"}}) doesn't match text (\"our \")")
+			return it("should return an error", function() {
+				// Save space in redis and don't store just {}
+				this.callback.called.should.equal(true);
+				const [error, entries] = Array.from(this.callback.args[0]);
+				expect(error).to.not.be.null;
+				return expect(error.message).to.equal("Change ({\"op\":{\"i\":\"five\",\"p\":15},\"metadata\":{\"user_id\":\"user-id-123\"}}) doesn't match text (\"our \")");
+			});
+		});
 
 
-		describe "with an update that collapses a range", ->
-			beforeEach ->
-				@updates = [{
-					meta:
-						user_id: @user_id
+		return describe("with an update that collapses a range", function() {
+			beforeEach(function() {
+				this.updates = [{
+					meta: {
+						user_id: this.user_id
+					},
 					op: [{
-						d: "one"
-						p: 0
+						d: "one",
+						p: 0,
 						t: "thread-id-1"
 					}]
-				}]
-				@entries = {
+				}];
+				this.entries = {
 					comments: [{
-						op:
-							c: "n"
-							p: 1
+						op: {
+							c: "n",
+							p: 1,
 							t: "thread-id-2"
-						metadata:
-							user_id: @user_id
-					}]
+						},
+						metadata: {
+							user_id: this.user_id
+						}
+					}],
 					changes: []
-				}
-				@RangesManager.applyUpdate @project_id, @doc_id, @entries, @updates, @newDocLines, @callback
+				};
+				return this.RangesManager.applyUpdate(this.project_id, this.doc_id, this.entries, this.updates, this.newDocLines, this.callback);
+			});
 
-			it "should return ranges_were_collapsed == true", ->
-				@callback.called.should.equal true
-				[error, entries, ranges_were_collapsed] = @callback.args[0]
-				expect(ranges_were_collapsed).to.equal true
+			return it("should return ranges_were_collapsed == true", function() {
+				this.callback.called.should.equal(true);
+				const [error, entries, ranges_were_collapsed] = Array.from(this.callback.args[0]);
+				return expect(ranges_were_collapsed).to.equal(true);
+			});
+		});
+	});
 
-	describe "acceptChanges", ->
-		beforeEach ->
-			@RangesManager = SandboxedModule.require modulePath,
-				requires:
-					"logger-sharelatex": @logger = { error: sinon.stub(), log: sinon.stub(), warn: sinon.stub() }
-					"./RangesTracker":@RangesTracker = SandboxedModule.require "../../../../app/js/RangesTracker.js"
+	return describe("acceptChanges", function() {
+		beforeEach(function() {
+			this.RangesManager = SandboxedModule.require(modulePath, {
+				requires: {
+					"logger-sharelatex": (this.logger = { error: sinon.stub(), log: sinon.stub(), warn: sinon.stub() }),
+					"./RangesTracker":(this.RangesTracker = SandboxedModule.require("../../../../app/js/RangesTracker.js"))
+				}
+			}
+			);
 
-			@ranges = {
-				comments: []
+			this.ranges = {
+				comments: [],
 				changes: [{
-					id: "a1"
-					op:
-						i: "lorem"
+					id: "a1",
+					op: {
+						i: "lorem",
 						p: 0
+					}
 				}, {
-					id: "a2"
-					op:
-						i: "ipsum"
+					id: "a2",
+					op: {
+						i: "ipsum",
 						p: 10
+					}
 				}, {
-					id: "a3"
-					op:
-						i: "dolor"
+					id: "a3",
+					op: {
+						i: "dolor",
 						p: 20
+					}
 				}, {
-					id: "a4"
-					op:
-						i: "sit"
+					id: "a4",
+					op: {
+						i: "sit",
 						p: 30
+					}
 				}, {
-					id: "a5"
-					op:
-						i: "amet"
+					id: "a5",
+					op: {
+						i: "amet",
 						p: 40
+					}
 				}]
-			}
-			@removeChangeIdsSpy = sinon.spy @RangesTracker.prototype, "removeChangeIds"
+			};
+			return this.removeChangeIdsSpy = sinon.spy(this.RangesTracker.prototype, "removeChangeIds");
+		});
 
-		describe "successfully with a single change", ->
-			beforeEach (done) ->
-				@change_ids = [ @ranges.changes[1].id ]
-				@RangesManager.acceptChanges @change_ids, @ranges, (err, ranges) => 
-					@rangesResponse = ranges
-					done()
+		describe("successfully with a single change", function() {
+			beforeEach(function(done) {
+				this.change_ids = [ this.ranges.changes[1].id ];
+				return this.RangesManager.acceptChanges(this.change_ids, this.ranges, (err, ranges) => { 
+					this.rangesResponse = ranges;
+					return done();
+				});
+			});
 
-			it "should log the call with the correct number of changes", ->
-				@logger.log
+			it("should log the call with the correct number of changes", function() {
+				return this.logger.log
 					.calledWith("accepting 1 changes in ranges")
-					.should.equal true
+					.should.equal(true);
+			});
 
-			it "should delegate the change removal to the ranges tracker", ->
-				@removeChangeIdsSpy
-					.calledWith(@change_ids)
-					.should.equal true
+			it("should delegate the change removal to the ranges tracker", function() {
+				return this.removeChangeIdsSpy
+					.calledWith(this.change_ids)
+					.should.equal(true);
+			});
 
-			it "should remove the change", ->
-				expect(@rangesResponse.changes
-					.find((change) => change.id ==  @ranges.changes[1].id))
-					.to.be.undefined
+			it("should remove the change", function() {
+				return expect(this.rangesResponse.changes
+					.find(change => change.id ===  this.ranges.changes[1].id))
+					.to.be.undefined;
+			});
 
-			it "should return the original number of changes minus 1", ->
-				@rangesResponse.changes.length
-					.should.equal @ranges.changes.length - 1
+			it("should return the original number of changes minus 1", function() {
+				return this.rangesResponse.changes.length
+					.should.equal(this.ranges.changes.length - 1);
+			});
 					
-			it "should not touch other changes", ->
-				for i in [ 0, 2, 3, 4]
-					expect(@rangesResponse.changes
-						.find((change) => change.id ==  @ranges.changes[i].id))
-						.to.deep.equal @ranges.changes[i]
+			return it("should not touch other changes", function() {
+				return [ 0, 2, 3, 4].map((i) =>
+					expect(this.rangesResponse.changes
+						.find(change => change.id ===  this.ranges.changes[i].id))
+						.to.deep.equal(this.ranges.changes[i]));
+		});
+	});
 
-		describe "successfully with multiple changes", ->
-			beforeEach (done) ->
-				@change_ids = [ @ranges.changes[1].id, @ranges.changes[3].id, @ranges.changes[4].id ]
-				@RangesManager.acceptChanges @change_ids, @ranges, (err, ranges) => 
-					@rangesResponse = ranges
-					done()
+		return describe("successfully with multiple changes", function() {
+			beforeEach(function(done) {
+				this.change_ids = [ this.ranges.changes[1].id, this.ranges.changes[3].id, this.ranges.changes[4].id ];
+				return this.RangesManager.acceptChanges(this.change_ids, this.ranges, (err, ranges) => { 
+					this.rangesResponse = ranges;
+					return done();
+				});
+			});
 
-			it "should log the call with the correct number of changes", ->
-				@logger.log
-					.calledWith("accepting #{ @change_ids.length } changes in ranges")
-					.should.equal true
+			it("should log the call with the correct number of changes", function() {
+				return this.logger.log
+					.calledWith(`accepting ${ this.change_ids.length } changes in ranges`)
+					.should.equal(true);
+			});
 
-			it "should delegate the change removal to the ranges tracker", ->
-				@removeChangeIdsSpy
-					.calledWith(@change_ids)
-					.should.equal true
+			it("should delegate the change removal to the ranges tracker", function() {
+				return this.removeChangeIdsSpy
+					.calledWith(this.change_ids)
+					.should.equal(true);
+			});
 
-			it "should remove the changes", ->
-				for i in [ 1, 3, 4]
-					expect(@rangesResponse.changes
-						.find((change) => change.id ==  @ranges.changes[1].id))
-						.to.be.undefined
+			it("should remove the changes", function() {
+				return [ 1, 3, 4].map((i) =>
+					expect(this.rangesResponse.changes
+						.find(change => change.id ===  this.ranges.changes[1].id))
+						.to.be.undefined);
+			});
 			
-			it "should return the original number of changes minus the number of accepted changes", ->
-				@rangesResponse.changes.length
-					.should.equal @ranges.changes.length - 3
+			it("should return the original number of changes minus the number of accepted changes", function() {
+				return this.rangesResponse.changes.length
+					.should.equal(this.ranges.changes.length - 3);
+			});
 
-			it "should not touch other changes", ->
-				for i in [ 0, 2 ]
-					expect(@rangesResponse.changes
-						.find((change) => change.id ==  @ranges.changes[i].id))
-						.to.deep.equal @ranges.changes[i]
+			return it("should not touch other changes", function() {
+				return [ 0, 2 ].map((i) =>
+					expect(this.rangesResponse.changes
+						.find(change => change.id ===  this.ranges.changes[i].id))
+						.to.deep.equal(this.ranges.changes[i]));
+		});
+	});
+});
+});
 					

+ 116 - 72
services/document-updater/test/unit/coffee/RateLimitManager/RateLimitManager.js

@@ -1,88 +1,132 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-expect = chai.expect
-modulePath = "../../../../app/js/RateLimitManager.js"
-SandboxedModule = require('sandboxed-module')
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS206: Consider reworking classes to avoid initClass
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require('sinon');
+const chai = require('chai');
+const should = chai.should();
+const {
+    expect
+} = chai;
+const modulePath = "../../../../app/js/RateLimitManager.js";
+const SandboxedModule = require('sandboxed-module');
 
-describe "RateLimitManager", ->
-	beforeEach ->
-		@RateLimitManager = SandboxedModule.require modulePath, requires:
-			"logger-sharelatex": @logger = { log: sinon.stub() }
-			"settings-sharelatex": @settings = {}
-			"./Metrics": @Metrics =
-				Timer: class Timer
-					done: sinon.stub()
+describe("RateLimitManager", function() {
+	beforeEach(function() {
+		let Timer;
+		this.RateLimitManager = SandboxedModule.require(modulePath, { requires: {
+			"logger-sharelatex": (this.logger = { log: sinon.stub() }),
+			"settings-sharelatex": (this.settings = {}),
+			"./Metrics": (this.Metrics = {
+				Timer: (Timer = (function() {
+					Timer = class Timer {
+						static initClass() {
+							this.prototype.done = sinon.stub();
+						}
+					};
+					Timer.initClass();
+					return Timer;
+				})()),
 				gauge: sinon.stub()
-		@callback = sinon.stub()
-		@RateLimiter = new @RateLimitManager(1)
+			})
+		}
+	}
+		);
+		this.callback = sinon.stub();
+		return this.RateLimiter = new this.RateLimitManager(1);
+	});
 
-	describe "for a single task", ->
-		beforeEach ->
-			@task = sinon.stub()
-			@RateLimiter.run @task, @callback
+	describe("for a single task", function() {
+		beforeEach(function() {
+			this.task = sinon.stub();
+			return this.RateLimiter.run(this.task, this.callback);
+		});
 
-		it "should execute the task in the background", ->
-			@task.called.should.equal true
+		it("should execute the task in the background", function() {
+			return this.task.called.should.equal(true);
+		});
 
-		it "should call the callback", ->
-			@callback.called.should.equal true
+		it("should call the callback", function() {
+			return this.callback.called.should.equal(true);
+		});
 
-		it "should finish with a worker count of one", ->
-			# because it's in the background
-			expect(@RateLimiter.ActiveWorkerCount).to.equal 1
+		return it("should finish with a worker count of one", function() {
+			// because it's in the background
+			return expect(this.RateLimiter.ActiveWorkerCount).to.equal(1);
+		});
+	});
 
-	describe "for multiple tasks", ->
-		beforeEach (done) ->
-			@task = sinon.stub()
-			@finalTask = sinon.stub()
-			task = (cb) =>
-				@task()
-				setTimeout cb, 100
-			finalTask = (cb) =>
-				@finalTask()
-				setTimeout cb, 100
-			@RateLimiter.run task, @callback
-			@RateLimiter.run task, @callback
-			@RateLimiter.run task, @callback
-			@RateLimiter.run finalTask, (err) =>
-				@callback(err)
-				done()
+	describe("for multiple tasks", function() {
+		beforeEach(function(done) {
+			this.task = sinon.stub();
+			this.finalTask = sinon.stub();
+			const task = cb => {
+				this.task();
+				return setTimeout(cb, 100);
+			};
+			const finalTask = cb => {
+				this.finalTask();
+				return setTimeout(cb, 100);
+			};
+			this.RateLimiter.run(task, this.callback);
+			this.RateLimiter.run(task, this.callback);
+			this.RateLimiter.run(task, this.callback);
+			return this.RateLimiter.run(finalTask, err => {
+				this.callback(err);
+				return done();
+			});
+		});
 
-		it "should execute the first three tasks", ->
-			@task.calledThrice.should.equal true
+		it("should execute the first three tasks", function() {
+			return this.task.calledThrice.should.equal(true);
+		});
 
-		it "should execute the final task", ->
-			@finalTask.called.should.equal true
+		it("should execute the final task", function() {
+			return this.finalTask.called.should.equal(true);
+		});
 
-		it "should call the callback", ->
-			@callback.called.should.equal true
+		it("should call the callback", function() {
+			return this.callback.called.should.equal(true);
+		});
 
-		it "should finish with worker count of zero", ->
-			expect(@RateLimiter.ActiveWorkerCount).to.equal 0
+		return it("should finish with worker count of zero", function() {
+			return expect(this.RateLimiter.ActiveWorkerCount).to.equal(0);
+		});
+	});
 
-	describe "for a mixture of long-running tasks", ->
-		beforeEach (done) ->
-			@task = sinon.stub()
-			@finalTask = sinon.stub()
-			finalTask = (cb) =>
-				@finalTask()
-				setTimeout cb, 100
-			@RateLimiter.run @task, @callback
-			@RateLimiter.run @task, @callback
-			@RateLimiter.run @task, @callback
-			@RateLimiter.run finalTask, (err) =>
-				@callback(err)
-				done()
+	return describe("for a mixture of long-running tasks", function() {
+		beforeEach(function(done) {
+			this.task = sinon.stub();
+			this.finalTask = sinon.stub();
+			const finalTask = cb => {
+				this.finalTask();
+				return setTimeout(cb, 100);
+			};
+			this.RateLimiter.run(this.task, this.callback);
+			this.RateLimiter.run(this.task, this.callback);
+			this.RateLimiter.run(this.task, this.callback);
+			return this.RateLimiter.run(finalTask, err => {
+				this.callback(err);
+				return done();
+			});
+		});
 
-		it "should execute the first three tasks", ->
-			@task.calledThrice.should.equal true
+		it("should execute the first three tasks", function() {
+			return this.task.calledThrice.should.equal(true);
+		});
 
-		it "should execute the final task", ->
-			@finalTask.called.should.equal true
+		it("should execute the final task", function() {
+			return this.finalTask.called.should.equal(true);
+		});
 
-		it "should call the callback", ->
-			@callback.called.should.equal true
+		it("should call the callback", function() {
+			return this.callback.called.should.equal(true);
+		});
 
-		it "should finish with worker count of three", ->
-			expect(@RateLimiter.ActiveWorkerCount).to.equal 3
+		return it("should finish with worker count of three", function() {
+			return expect(this.RateLimiter.ActiveWorkerCount).to.equal(3);
+		});
+	});
+});

+ 125 - 91
services/document-updater/test/unit/coffee/RealTimeRedisManager/RealTimeRedisManagerTests.js

@@ -1,95 +1,129 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-modulePath = "../../../../app/js/RealTimeRedisManager.js"
-SandboxedModule = require('sandboxed-module')
-Errors = require "../../../../app/js/Errors"
-
-describe "RealTimeRedisManager", ->
-	beforeEach ->
-		@rclient =
-			auth: () ->
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require('sinon');
+const chai = require('chai');
+const should = chai.should();
+const modulePath = "../../../../app/js/RealTimeRedisManager.js";
+const SandboxedModule = require('sandboxed-module');
+const Errors = require("../../../../app/js/Errors");
+
+describe("RealTimeRedisManager", function() {
+	beforeEach(function() {
+		this.rclient = {
+			auth() {},
 			exec: sinon.stub()
-		@rclient.multi = () => @rclient
-		@pubsubClient =
-			publish: sinon.stub()
-		@RealTimeRedisManager = SandboxedModule.require modulePath, requires:
-			"redis-sharelatex": createClient: (config) =>  if (config.name is 'pubsub') then @pubsubClient else @rclient
-			"settings-sharelatex":
-				redis:
-					documentupdater: @settings =
-						key_schema:
-							pendingUpdates: ({doc_id}) -> "PendingUpdates:#{doc_id}"
-					pubsub:
+		};
+		this.rclient.multi = () => this.rclient;
+		this.pubsubClient =
+			{publish: sinon.stub()};
+		this.RealTimeRedisManager = SandboxedModule.require(modulePath, { requires: {
+			"redis-sharelatex": { createClient: config =>  (config.name === 'pubsub') ? this.pubsubClient : this.rclient
+		},
+			"settings-sharelatex": {
+				redis: {
+					documentupdater: (this.settings = {
+						key_schema: {
+							pendingUpdates({doc_id}) { return `PendingUpdates:${doc_id}`; }
+						}
+					}),
+					pubsub: {
 						name: "pubsub"
-			"logger-sharelatex": { log: () -> }
-			"crypto": @crypto = { randomBytes: sinon.stub().withArgs(4).returns(Buffer.from([0x1, 0x2, 0x3, 0x4])) }
-			"os": @os = {hostname: sinon.stub().returns("somehost")}
-			"./Metrics": @metrics = { summary: sinon.stub()}
-
-		@doc_id = "doc-id-123"
-		@project_id = "project-id-123"
-		@callback = sinon.stub()
-
-	describe "getPendingUpdatesForDoc", ->
-		beforeEach ->
-			@rclient.lrange = sinon.stub()
-			@rclient.ltrim = sinon.stub()
-
-		describe "successfully", ->
-			beforeEach ->
-				@updates = [
+					}
+				}
+			},
+			"logger-sharelatex": { log() {} },
+			"crypto": (this.crypto = { randomBytes: sinon.stub().withArgs(4).returns(Buffer.from([0x1, 0x2, 0x3, 0x4])) }),
+			"os": (this.os = {hostname: sinon.stub().returns("somehost")}),
+			"./Metrics": (this.metrics = { summary: sinon.stub()})
+		}
+	});
+
+		this.doc_id = "doc-id-123";
+		this.project_id = "project-id-123";
+		return this.callback = sinon.stub();
+	});
+
+	describe("getPendingUpdatesForDoc", function() {
+		beforeEach(function() {
+			this.rclient.lrange = sinon.stub();
+			return this.rclient.ltrim = sinon.stub();
+		});
+
+		describe("successfully", function() {
+			beforeEach(function() {
+				this.updates = [
+					{ op: [{ i: "foo", p: 4 }] },
 					{ op: [{ i: "foo", p: 4 }] }
-					{ op: [{ i: "foo", p: 4 }] }
-				]
-				@jsonUpdates = @updates.map (update) -> JSON.stringify update
-				@rclient.exec = sinon.stub().callsArgWith(0, null, [@jsonUpdates])
-				@RealTimeRedisManager.getPendingUpdatesForDoc @doc_id, @callback
-
-			it "should get the pending updates", ->
-				@rclient.lrange
-					.calledWith("PendingUpdates:#{@doc_id}", 0, 7)
-					.should.equal true
-
-			it "should delete the pending updates", ->
-				@rclient.ltrim
-					.calledWith("PendingUpdates:#{@doc_id}", 8, -1)
-					.should.equal true
-
-			it "should call the callback with the updates", ->
-				@callback.calledWith(null, @updates).should.equal true
-
-		describe "when the JSON doesn't parse", ->
-			beforeEach ->
-				@jsonUpdates = [
-					JSON.stringify { op: [{ i: "foo", p: 4 }] }
+				];
+				this.jsonUpdates = this.updates.map(update => JSON.stringify(update));
+				this.rclient.exec = sinon.stub().callsArgWith(0, null, [this.jsonUpdates]);
+				return this.RealTimeRedisManager.getPendingUpdatesForDoc(this.doc_id, this.callback);
+			});
+
+			it("should get the pending updates", function() {
+				return this.rclient.lrange
+					.calledWith(`PendingUpdates:${this.doc_id}`, 0, 7)
+					.should.equal(true);
+			});
+
+			it("should delete the pending updates", function() {
+				return this.rclient.ltrim
+					.calledWith(`PendingUpdates:${this.doc_id}`, 8, -1)
+					.should.equal(true);
+			});
+
+			return it("should call the callback with the updates", function() {
+				return this.callback.calledWith(null, this.updates).should.equal(true);
+			});
+		});
+
+		return describe("when the JSON doesn't parse", function() {
+			beforeEach(function() {
+				this.jsonUpdates = [
+					JSON.stringify({ op: [{ i: "foo", p: 4 }] }),
 					"broken json"
-				]
-				@rclient.exec = sinon.stub().callsArgWith(0, null, [@jsonUpdates])
-				@RealTimeRedisManager.getPendingUpdatesForDoc @doc_id, @callback
-
-			it "should return an error to the callback", ->
-				@callback.calledWith(new Error("JSON parse error")).should.equal true
-
-
-	describe "getUpdatesLength", ->
-		beforeEach ->
-			@rclient.llen = sinon.stub().yields(null, @length = 3)
-			@RealTimeRedisManager.getUpdatesLength @doc_id, @callback
-
-		it "should look up the length", ->
-			@rclient.llen.calledWith("PendingUpdates:#{@doc_id}").should.equal true
-
-		it "should return the length", ->
-			@callback.calledWith(null, @length).should.equal true
-
-	describe "sendData", ->
-		beforeEach ->
-			@message_id = "doc:somehost:01020304-0"
-			@RealTimeRedisManager.sendData({op: "thisop"})
-
-		it "should send the op with a message id", ->
-			@pubsubClient.publish.calledWith("applied-ops", JSON.stringify({op:"thisop",_id:@message_id})).should.equal true
-
-		it "should track the payload size", ->
-			@metrics.summary.calledWith("redis.publish.applied-ops", JSON.stringify({op:"thisop",_id:@message_id}).length).should.equal true
+				];
+				this.rclient.exec = sinon.stub().callsArgWith(0, null, [this.jsonUpdates]);
+				return this.RealTimeRedisManager.getPendingUpdatesForDoc(this.doc_id, this.callback);
+			});
+
+			return it("should return an error to the callback", function() {
+				return this.callback.calledWith(new Error("JSON parse error")).should.equal(true);
+			});
+		});
+	});
+
+
+	describe("getUpdatesLength", function() {
+		beforeEach(function() {
+			this.rclient.llen = sinon.stub().yields(null, (this.length = 3));
+			return this.RealTimeRedisManager.getUpdatesLength(this.doc_id, this.callback);
+		});
+
+		it("should look up the length", function() {
+			return this.rclient.llen.calledWith(`PendingUpdates:${this.doc_id}`).should.equal(true);
+		});
+
+		return it("should return the length", function() {
+			return this.callback.calledWith(null, this.length).should.equal(true);
+		});
+	});
+
+	return describe("sendData", function() {
+		beforeEach(function() {
+			this.message_id = "doc:somehost:01020304-0";
+			return this.RealTimeRedisManager.sendData({op: "thisop"});
+		});
+
+		it("should send the op with a message id", function() {
+			return this.pubsubClient.publish.calledWith("applied-ops", JSON.stringify({op:"thisop",_id:this.message_id})).should.equal(true);
+		});
+
+		return it("should track the payload size", function() {
+			return this.metrics.summary.calledWith("redis.publish.applied-ops", JSON.stringify({op:"thisop",_id:this.message_id}).length).should.equal(true);
+		});
+	});
+});

+ 981 - 786
services/document-updater/test/unit/coffee/RedisManager/RedisManagerTests.js

@@ -1,803 +1,998 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-modulePath = "../../../../app/js/RedisManager.js"
-SandboxedModule = require('sandboxed-module')
-Errors = require "../../../../app/js/Errors"
-crypto = require "crypto"
-tk = require "timekeeper"
-
-describe "RedisManager", ->
-	beforeEach ->
-		@multi = exec: sinon.stub()
-		@rclient = multi: () => @multi
-		tk.freeze(new Date())
-		@RedisManager = SandboxedModule.require modulePath,
-			requires:
-				"logger-sharelatex": @logger = { error: sinon.stub(), log: sinon.stub(), warn: sinon.stub() }
-				"./ProjectHistoryRedisManager": @ProjectHistoryRedisManager = {}
-				"settings-sharelatex": @settings = {
-					documentupdater: {logHashErrors: {write:true, read:true}}
-					apis:
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require('sinon');
+const chai = require('chai');
+const should = chai.should();
+const modulePath = "../../../../app/js/RedisManager.js";
+const SandboxedModule = require('sandboxed-module');
+const Errors = require("../../../../app/js/Errors");
+const crypto = require("crypto");
+const tk = require("timekeeper");
+
+describe("RedisManager", function() {
+	beforeEach(function() {
+		let Timer;
+		this.multi = {exec: sinon.stub()};
+		this.rclient = {multi: () => this.multi};
+		tk.freeze(new Date());
+		this.RedisManager = SandboxedModule.require(modulePath, {
+			requires: {
+				"logger-sharelatex": (this.logger = { error: sinon.stub(), log: sinon.stub(), warn: sinon.stub() }),
+				"./ProjectHistoryRedisManager": (this.ProjectHistoryRedisManager = {}),
+				"settings-sharelatex": (this.settings = {
+					documentupdater: {logHashErrors: {write:true, read:true}},
+					apis: {
 						project_history: {enabled: true}
-					redis:
-						documentupdater:
-							key_schema:
-								blockingKey: ({doc_id}) -> "Blocking:#{doc_id}"
-								docLines: ({doc_id}) -> "doclines:#{doc_id}"
-								docOps: ({doc_id}) -> "DocOps:#{doc_id}"
-								docVersion: ({doc_id}) -> "DocVersion:#{doc_id}"
-								docHash: ({doc_id}) -> "DocHash:#{doc_id}"
-								projectKey: ({doc_id}) -> "ProjectId:#{doc_id}"
-								pendingUpdates: ({doc_id}) -> "PendingUpdates:#{doc_id}"
-								docsInProject: ({project_id}) -> "DocsIn:#{project_id}"
-								ranges: ({doc_id}) -> "Ranges:#{doc_id}"
-								pathname: ({doc_id}) -> "Pathname:#{doc_id}"
-								projectHistoryId: ({doc_id}) -> "ProjectHistoryId:#{doc_id}"
-								projectHistoryType: ({doc_id}) -> "ProjectHistoryType:#{doc_id}"
-								projectState: ({project_id}) -> "ProjectState:#{project_id}"
-								unflushedTime: ({doc_id}) -> "UnflushedTime:#{doc_id}"
-								lastUpdatedBy: ({doc_id}) -> "lastUpdatedBy:#{doc_id}"
-								lastUpdatedAt: ({doc_id}) -> "lastUpdatedAt:#{doc_id}"
-						history:
-							key_schema:
-								uncompressedHistoryOps: ({doc_id}) -> "UncompressedHistoryOps:#{doc_id}"
-								docsWithHistoryOps: ({project_id}) -> "DocsWithHistoryOps:#{project_id}"
-				}
-				"redis-sharelatex":
-					createClient: () => @rclient
-				"./Metrics": @metrics =
-					inc: sinon.stub()
-					summary: sinon.stub()
-					Timer: class Timer
-						constructor: () ->
-							this.start = new Date()
-						done: () ->
-							timeSpan = new Date - this.start
-							return timeSpan
+					},
+					redis: {
+						documentupdater: {
+							key_schema: {
+								blockingKey({doc_id}) { return `Blocking:${doc_id}`; },
+								docLines({doc_id}) { return `doclines:${doc_id}`; },
+								docOps({doc_id}) { return `DocOps:${doc_id}`; },
+								docVersion({doc_id}) { return `DocVersion:${doc_id}`; },
+								docHash({doc_id}) { return `DocHash:${doc_id}`; },
+								projectKey({doc_id}) { return `ProjectId:${doc_id}`; },
+								pendingUpdates({doc_id}) { return `PendingUpdates:${doc_id}`; },
+								docsInProject({project_id}) { return `DocsIn:${project_id}`; },
+								ranges({doc_id}) { return `Ranges:${doc_id}`; },
+								pathname({doc_id}) { return `Pathname:${doc_id}`; },
+								projectHistoryId({doc_id}) { return `ProjectHistoryId:${doc_id}`; },
+								projectHistoryType({doc_id}) { return `ProjectHistoryType:${doc_id}`; },
+								projectState({project_id}) { return `ProjectState:${project_id}`; },
+								unflushedTime({doc_id}) { return `UnflushedTime:${doc_id}`; },
+								lastUpdatedBy({doc_id}) { return `lastUpdatedBy:${doc_id}`; },
+								lastUpdatedAt({doc_id}) { return `lastUpdatedAt:${doc_id}`; }
+							}
+						},
+						history: {
+							key_schema: {
+								uncompressedHistoryOps({doc_id}) { return `UncompressedHistoryOps:${doc_id}`; },
+								docsWithHistoryOps({project_id}) { return `DocsWithHistoryOps:${project_id}`; }
+							}
+						}
+					}
+				}),
+				"redis-sharelatex": {
+					createClient: () => this.rclient
+				},
+				"./Metrics": (this.metrics = {
+					inc: sinon.stub(),
+					summary: sinon.stub(),
+					Timer: (Timer = class Timer {
+						constructor() {
+							this.start = new Date();
+						}
+						done() {
+							const timeSpan = new Date - this.start;
+							return timeSpan;
+						}
+					})
+				}),
 				"./Errors": Errors
-			globals:
-				JSON: @JSON = JSON
-
-		@doc_id = "doc-id-123"
-		@project_id = "project-id-123"
-		@projectHistoryId = 123
-		@callback = sinon.stub()
-
-	afterEach ->
-		tk.reset()
-
-	describe "getDoc", ->
-		beforeEach ->
-			@lines = ["one", "two", "three", "これは"] # include some utf8
-			@jsonlines = JSON.stringify @lines
-			@version = 42
-			@hash = crypto.createHash('sha1').update(@jsonlines,'utf8').digest('hex')
-			@ranges = { comments: "mock", entries: "mock" }
-			@json_ranges = JSON.stringify @ranges
-			@unflushed_time = 12345
-			@pathname = '/a/b/c.tex'
-			@multi.get = sinon.stub()
-			@multi.exec = sinon.stub().callsArgWith(0, null, [@jsonlines, @version, @hash, @project_id, @json_ranges, @pathname, @projectHistoryId.toString(), @unflushed_time])
-			@rclient.sadd = sinon.stub().yields(null, 0)
-
-		describe "successfully", ->
-			beforeEach ->
-				@RedisManager.getDoc @project_id, @doc_id, @callback
-
-			it "should get the lines from redis", ->
-				@multi.get
-					.calledWith("doclines:#{@doc_id}")
-					.should.equal true
-
-			it "should get the version from", ->
-				@multi.get
-					.calledWith("DocVersion:#{@doc_id}")
-					.should.equal true
-
-			it 'should get the hash', ->
-				@multi.get
-					.calledWith("DocHash:#{@doc_id}")
-					.should.equal true
-
-			it "should get the ranges", ->
-				@multi.get
-					.calledWith("Ranges:#{@doc_id}")
-					.should.equal true
-
-			it "should get the unflushed time", ->
-				@multi.get
-					.calledWith("UnflushedTime:#{@doc_id}")
-					.should.equal true
-
-			it "should get the pathname", ->
-				@multi.get
-					.calledWith("Pathname:#{@doc_id}")
-					.should.equal true
-
-			it "should get the projectHistoryId as an integer", ->
-				@multi.get
-					.calledWith("ProjectHistoryId:#{@doc_id}")
-					.should.equal true
-
-			it "should get lastUpdatedAt", ->
-				@multi.get
-					.calledWith("lastUpdatedAt:#{@doc_id}")
-					.should.equal true
-
-			it "should get lastUpdatedBy", ->
-				@multi.get
-					.calledWith("lastUpdatedBy:#{@doc_id}")
-					.should.equal true
-
-			it "should check if the document is in the DocsIn set", ->
-				@rclient.sadd
-					.calledWith("DocsIn:#{@project_id}")
-					.should.equal true
-
-			it 'should return the document', ->
-				@callback
-					.calledWithExactly(null, @lines, @version, @ranges, @pathname, @projectHistoryId, @unflushed_time, @lastUpdatedAt, @lastUpdatedBy)
-					.should.equal true
-
-			it 'should not log any errors', ->
-				@logger.error.calledWith()
-					.should.equal false
-
-		describe "when the document is not present", ->
-			beforeEach ->
-				@multi.exec = sinon.stub().callsArgWith(0, null, [null, null, null, null, null, null, null, null, null, null])
-				@rclient.sadd = sinon.stub().yields()
-				@RedisManager.getDoc @project_id, @doc_id, @callback
-
-			it "should not check if the document is in the DocsIn set", ->
-				@rclient.sadd
-					.calledWith("DocsIn:#{@project_id}")
-					.should.equal false
-
-			it 'should return an empty result', ->
-				@callback
+			},
+			globals: {
+				JSON: (this.JSON = JSON)
+			}
+		}
+		);
+
+		this.doc_id = "doc-id-123";
+		this.project_id = "project-id-123";
+		this.projectHistoryId = 123;
+		return this.callback = sinon.stub();
+	});
+
+	afterEach(() => tk.reset());
+
+	describe("getDoc", function() {
+		beforeEach(function() {
+			this.lines = ["one", "two", "three", "これは"]; // include some utf8
+			this.jsonlines = JSON.stringify(this.lines);
+			this.version = 42;
+			this.hash = crypto.createHash('sha1').update(this.jsonlines,'utf8').digest('hex');
+			this.ranges = { comments: "mock", entries: "mock" };
+			this.json_ranges = JSON.stringify(this.ranges);
+			this.unflushed_time = 12345;
+			this.pathname = '/a/b/c.tex';
+			this.multi.get = sinon.stub();
+			this.multi.exec = sinon.stub().callsArgWith(0, null, [this.jsonlines, this.version, this.hash, this.project_id, this.json_ranges, this.pathname, this.projectHistoryId.toString(), this.unflushed_time]);
+			return this.rclient.sadd = sinon.stub().yields(null, 0);
+		});
+
+		describe("successfully", function() {
+			beforeEach(function() {
+				return this.RedisManager.getDoc(this.project_id, this.doc_id, this.callback);
+			});
+
+			it("should get the lines from redis", function() {
+				return this.multi.get
+					.calledWith(`doclines:${this.doc_id}`)
+					.should.equal(true);
+			});
+
+			it("should get the version from", function() {
+				return this.multi.get
+					.calledWith(`DocVersion:${this.doc_id}`)
+					.should.equal(true);
+			});
+
+			it('should get the hash', function() {
+				return this.multi.get
+					.calledWith(`DocHash:${this.doc_id}`)
+					.should.equal(true);
+			});
+
+			it("should get the ranges", function() {
+				return this.multi.get
+					.calledWith(`Ranges:${this.doc_id}`)
+					.should.equal(true);
+			});
+
+			it("should get the unflushed time", function() {
+				return this.multi.get
+					.calledWith(`UnflushedTime:${this.doc_id}`)
+					.should.equal(true);
+			});
+
+			it("should get the pathname", function() {
+				return this.multi.get
+					.calledWith(`Pathname:${this.doc_id}`)
+					.should.equal(true);
+			});
+
+			it("should get the projectHistoryId as an integer", function() {
+				return this.multi.get
+					.calledWith(`ProjectHistoryId:${this.doc_id}`)
+					.should.equal(true);
+			});
+
+			it("should get lastUpdatedAt", function() {
+				return this.multi.get
+					.calledWith(`lastUpdatedAt:${this.doc_id}`)
+					.should.equal(true);
+			});
+
+			it("should get lastUpdatedBy", function() {
+				return this.multi.get
+					.calledWith(`lastUpdatedBy:${this.doc_id}`)
+					.should.equal(true);
+			});
+
+			it("should check if the document is in the DocsIn set", function() {
+				return this.rclient.sadd
+					.calledWith(`DocsIn:${this.project_id}`)
+					.should.equal(true);
+			});
+
+			it('should return the document', function() {
+				return this.callback
+					.calledWithExactly(null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId, this.unflushed_time, this.lastUpdatedAt, this.lastUpdatedBy)
+					.should.equal(true);
+			});
+
+			return it('should not log any errors', function() {
+				return this.logger.error.calledWith()
+					.should.equal(false);
+			});
+		});
+
+		describe("when the document is not present", function() {
+			beforeEach(function() {
+				this.multi.exec = sinon.stub().callsArgWith(0, null, [null, null, null, null, null, null, null, null, null, null]);
+				this.rclient.sadd = sinon.stub().yields();
+				return this.RedisManager.getDoc(this.project_id, this.doc_id, this.callback);
+			});
+
+			it("should not check if the document is in the DocsIn set", function() {
+				return this.rclient.sadd
+					.calledWith(`DocsIn:${this.project_id}`)
+					.should.equal(false);
+			});
+
+			it('should return an empty result', function() {
+				return this.callback
 					.calledWithExactly(null, null, 0, {}, null, null, null, null, null)
-					.should.equal true
-
-			it 'should not log any errors', ->
-				@logger.error.calledWith()
-					.should.equal false
-
-		describe "when the document is missing from the DocsIn set", ->
-			beforeEach ->
-				@rclient.sadd = sinon.stub().yields(null, 1)
-				@RedisManager.getDoc @project_id, @doc_id, @callback
-
-			it 'should log an error', ->
-				@logger.error.calledWith()
-					.should.equal true
-
-			it 'should return the document', ->
-				@callback
-					.calledWithExactly(null, @lines, @version, @ranges, @pathname, @projectHistoryId, @unflushed_time, @lastUpdatedAt, @lastUpdatedBy)
-					.should.equal true
-
-		describe "with a corrupted document", ->
-			beforeEach ->
-				@badHash = "INVALID-HASH-VALUE"
-				@multi.exec = sinon.stub().callsArgWith(0, null, [@jsonlines, @version, @badHash, @project_id, @json_ranges])
-				@RedisManager.getDoc @project_id, @doc_id, @callback
-
-			it 'should log a hash error', ->
-				@logger.error.calledWith()
-					.should.equal true
-
-			it 'should return the document', ->
-				@callback
-					.calledWith(null, @lines, @version, @ranges)
-					.should.equal true
-
-
-		describe "with a slow request to redis", ->
-			beforeEach ->
-				@multi.exec = sinon.stub().callsArgWith(0, null, [@jsonlines, @version, @badHash, @project_id, @json_ranges, @pathname, @unflushed_time])
-				@clock = sinon.useFakeTimers();
-				@multi.exec = (cb) =>
-					@clock.tick(6000);
-					cb(null, [@jsonlines, @version, @another_project_id, @json_ranges, @pathname, @unflushed_time])
-
-				@RedisManager.getDoc @project_id, @doc_id, @callback
-
-			afterEach ->
-				@clock.restore()
-
-			it 'should return an error', ->
-				@callback
+					.should.equal(true);
+			});
+
+			return it('should not log any errors', function() {
+				return this.logger.error.calledWith()
+					.should.equal(false);
+			});
+		});
+
+		describe("when the document is missing from the DocsIn set", function() {
+			beforeEach(function() {
+				this.rclient.sadd = sinon.stub().yields(null, 1);
+				return this.RedisManager.getDoc(this.project_id, this.doc_id, this.callback);
+			});
+
+			it('should log an error', function() {
+				return this.logger.error.calledWith()
+					.should.equal(true);
+			});
+
+			return it('should return the document', function() {
+				return this.callback
+					.calledWithExactly(null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId, this.unflushed_time, this.lastUpdatedAt, this.lastUpdatedBy)
+					.should.equal(true);
+			});
+		});
+
+		describe("with a corrupted document", function() {
+			beforeEach(function() {
+				this.badHash = "INVALID-HASH-VALUE";
+				this.multi.exec = sinon.stub().callsArgWith(0, null, [this.jsonlines, this.version, this.badHash, this.project_id, this.json_ranges]);
+				return this.RedisManager.getDoc(this.project_id, this.doc_id, this.callback);
+			});
+
+			it('should log a hash error', function() {
+				return this.logger.error.calledWith()
+					.should.equal(true);
+			});
+
+			return it('should return the document', function() {
+				return this.callback
+					.calledWith(null, this.lines, this.version, this.ranges)
+					.should.equal(true);
+			});
+		});
+
+
+		describe("with a slow request to redis", function() {
+			beforeEach(function() {
+				this.multi.exec = sinon.stub().callsArgWith(0, null, [this.jsonlines, this.version, this.badHash, this.project_id, this.json_ranges, this.pathname, this.unflushed_time]);
+				this.clock = sinon.useFakeTimers();
+				this.multi.exec = cb => {
+					this.clock.tick(6000);
+					return cb(null, [this.jsonlines, this.version, this.another_project_id, this.json_ranges, this.pathname, this.unflushed_time]);
+				};
+
+				return this.RedisManager.getDoc(this.project_id, this.doc_id, this.callback);
+			});
+
+			afterEach(function() {
+				return this.clock.restore();
+			});
+
+			return it('should return an error', function() {
+				return this.callback
 					.calledWith(new Error("redis getDoc exceeded timeout"))
-					.should.equal true
-
-		describe "getDoc with an invalid project id", ->
-			beforeEach ->
-				@another_project_id = "project-id-456"
-				@multi.exec = sinon.stub().callsArgWith(0, null, [@jsonlines, @version, @hash, @another_project_id, @json_ranges, @pathname, @unflushed_time])
-				@RedisManager.getDoc @project_id, @doc_id, @callback
-
-			it 'should return an error', ->
-				@callback
+					.should.equal(true);
+			});
+		});
+
+		return describe("getDoc with an invalid project id", function() {
+			beforeEach(function() {
+				this.another_project_id = "project-id-456";
+				this.multi.exec = sinon.stub().callsArgWith(0, null, [this.jsonlines, this.version, this.hash, this.another_project_id, this.json_ranges, this.pathname, this.unflushed_time]);
+				return this.RedisManager.getDoc(this.project_id, this.doc_id, this.callback);
+			});
+
+			return it('should return an error', function() {
+				return this.callback
 					.calledWith(new Errors.NotFoundError("not found"))
-					.should.equal true
-
-	describe "getPreviousDocOpsTests", ->
-		describe "with a start and an end value", ->
-			beforeEach ->
-				@first_version_in_redis = 30
-				@version = 70
-				@length = @version - @first_version_in_redis
-				@start  = 50
-				@end    = 60
-				@ops = [
+					.should.equal(true);
+			});
+		});
+	});
+
+	describe("getPreviousDocOpsTests", function() {
+		describe("with a start and an end value", function() {
+			beforeEach(function() {
+				this.first_version_in_redis = 30;
+				this.version = 70;
+				this.length = this.version - this.first_version_in_redis;
+				this.start  = 50;
+				this.end    = 60;
+				this.ops = [
 					{ "mock": "op-1" },
 					{ "mock": "op-2" }
-				]
-				@jsonOps = @ops.map (op) -> JSON.stringify op
-				@rclient.llen = sinon.stub().callsArgWith(1, null, @length)
-				@rclient.get = sinon.stub().callsArgWith(1, null, @version.toString())
-				@rclient.lrange = sinon.stub().callsArgWith(3, null, @jsonOps)
-				@RedisManager.getPreviousDocOps(@doc_id, @start, @end, @callback)
-
-			it "should get the length of the existing doc ops", ->
-				@rclient.llen
-					.calledWith("DocOps:#{@doc_id}")
-					.should.equal true
-
-			it "should get the current version of the doc", ->
-				@rclient.get
-					.calledWith("DocVersion:#{@doc_id}")
-					.should.equal true
-
-			it "should get the appropriate docs ops", ->
-				@rclient.lrange
-					.calledWith("DocOps:#{@doc_id}", @start - @first_version_in_redis, @end - @first_version_in_redis)
-					.should.equal true
-
-			it "should return the docs with the doc ops deserialized", ->
-				@callback.calledWith(null, @ops).should.equal true
-
-		describe "with an end value of -1", ->
-			beforeEach ->
-				@first_version_in_redis = 30
-				@version = 70
-				@length = @version - @first_version_in_redis
-				@start  = 50
-				@end    = -1
-				@ops = [
+				];
+				this.jsonOps = this.ops.map(op => JSON.stringify(op));
+				this.rclient.llen = sinon.stub().callsArgWith(1, null, this.length);
+				this.rclient.get = sinon.stub().callsArgWith(1, null, this.version.toString());
+				this.rclient.lrange = sinon.stub().callsArgWith(3, null, this.jsonOps);
+				return this.RedisManager.getPreviousDocOps(this.doc_id, this.start, this.end, this.callback);
+			});
+
+			it("should get the length of the existing doc ops", function() {
+				return this.rclient.llen
+					.calledWith(`DocOps:${this.doc_id}`)
+					.should.equal(true);
+			});
+
+			it("should get the current version of the doc", function() {
+				return this.rclient.get
+					.calledWith(`DocVersion:${this.doc_id}`)
+					.should.equal(true);
+			});
+
+			it("should get the appropriate docs ops", function() {
+				return this.rclient.lrange
+					.calledWith(`DocOps:${this.doc_id}`, this.start - this.first_version_in_redis, this.end - this.first_version_in_redis)
+					.should.equal(true);
+			});
+
+			return it("should return the docs with the doc ops deserialized", function() {
+				return this.callback.calledWith(null, this.ops).should.equal(true);
+			});
+		});
+
+		describe("with an end value of -1", function() {
+			beforeEach(function() {
+				this.first_version_in_redis = 30;
+				this.version = 70;
+				this.length = this.version - this.first_version_in_redis;
+				this.start  = 50;
+				this.end    = -1;
+				this.ops = [
 					{ "mock": "op-1" },
 					{ "mock": "op-2" }
-				]
-				@jsonOps = @ops.map (op) -> JSON.stringify op
-				@rclient.llen = sinon.stub().callsArgWith(1, null, @length)
-				@rclient.get = sinon.stub().callsArgWith(1, null, @version.toString())
-				@rclient.lrange = sinon.stub().callsArgWith(3, null, @jsonOps)
-				@RedisManager.getPreviousDocOps(@doc_id, @start, @end, @callback)
-
-			it "should get the appropriate docs ops to the end of list", ->
-				@rclient.lrange
-					.calledWith("DocOps:#{@doc_id}", @start - @first_version_in_redis, -1)
-					.should.equal true
-
-			it "should return the docs with the doc ops deserialized", ->
-				@callback.calledWith(null, @ops).should.equal true
-
-		describe "when the requested range is not in Redis", ->
-			beforeEach ->
-				@first_version_in_redis = 30
-				@version = 70
-				@length = @version - @first_version_in_redis
-				@start  = 20
-				@end    = -1
-				@ops = [
+				];
+				this.jsonOps = this.ops.map(op => JSON.stringify(op));
+				this.rclient.llen = sinon.stub().callsArgWith(1, null, this.length);
+				this.rclient.get = sinon.stub().callsArgWith(1, null, this.version.toString());
+				this.rclient.lrange = sinon.stub().callsArgWith(3, null, this.jsonOps);
+				return this.RedisManager.getPreviousDocOps(this.doc_id, this.start, this.end, this.callback);
+			});
+
+			it("should get the appropriate docs ops to the end of list", function() {
+				return this.rclient.lrange
+					.calledWith(`DocOps:${this.doc_id}`, this.start - this.first_version_in_redis, -1)
+					.should.equal(true);
+			});
+
+			return it("should return the docs with the doc ops deserialized", function() {
+				return this.callback.calledWith(null, this.ops).should.equal(true);
+			});
+		});
+
+		describe("when the requested range is not in Redis", function() {
+			beforeEach(function() {
+				this.first_version_in_redis = 30;
+				this.version = 70;
+				this.length = this.version - this.first_version_in_redis;
+				this.start  = 20;
+				this.end    = -1;
+				this.ops = [
 					{ "mock": "op-1" },
 					{ "mock": "op-2" }
-				]
-				@jsonOps = @ops.map (op) -> JSON.stringify op
-				@rclient.llen = sinon.stub().callsArgWith(1, null, @length)
-				@rclient.get = sinon.stub().callsArgWith(1, null, @version.toString())
-				@rclient.lrange = sinon.stub().callsArgWith(3, null, @jsonOps)
-				@RedisManager.getPreviousDocOps(@doc_id, @start, @end, @callback)
-
-			it "should return an error", ->
-				@callback.calledWith(new Errors.OpRangeNotAvailableError("doc ops range is not loaded in redis")).should.equal true
-
-			it "should log out the problem", ->
-				@logger.warn.called.should.equal true
-
-		describe "with a slow request to redis", ->
-			beforeEach ->
-				@first_version_in_redis = 30
-				@version = 70
-				@length = @version - @first_version_in_redis
-				@start  = 50
-				@end    = 60
-				@ops = [
+				];
+				this.jsonOps = this.ops.map(op => JSON.stringify(op));
+				this.rclient.llen = sinon.stub().callsArgWith(1, null, this.length);
+				this.rclient.get = sinon.stub().callsArgWith(1, null, this.version.toString());
+				this.rclient.lrange = sinon.stub().callsArgWith(3, null, this.jsonOps);
+				return this.RedisManager.getPreviousDocOps(this.doc_id, this.start, this.end, this.callback);
+			});
+
+			it("should return an error", function() {
+				return this.callback.calledWith(new Errors.OpRangeNotAvailableError("doc ops range is not loaded in redis")).should.equal(true);
+			});
+
+			return it("should log out the problem", function() {
+				return this.logger.warn.called.should.equal(true);
+			});
+		});
+
+		return describe("with a slow request to redis", function() {
+			beforeEach(function() {
+				this.first_version_in_redis = 30;
+				this.version = 70;
+				this.length = this.version - this.first_version_in_redis;
+				this.start  = 50;
+				this.end    = 60;
+				this.ops = [
 					{ "mock": "op-1" },
 					{ "mock": "op-2" }
-				]
-				@jsonOps = @ops.map (op) -> JSON.stringify op
-				@rclient.llen = sinon.stub().callsArgWith(1, null, @length)
-				@rclient.get = sinon.stub().callsArgWith(1, null, @version.toString())
-				@clock = sinon.useFakeTimers();
-				@rclient.lrange = (key, start, end, cb) =>
-					@clock.tick(6000);
-					cb(null, @jsonOps)
-				@RedisManager.getPreviousDocOps(@doc_id, @start, @end, @callback)
-
-			afterEach ->
-				@clock.restore()
-
-			it 'should return an error', ->
-				@callback
+				];
+				this.jsonOps = this.ops.map(op => JSON.stringify(op));
+				this.rclient.llen = sinon.stub().callsArgWith(1, null, this.length);
+				this.rclient.get = sinon.stub().callsArgWith(1, null, this.version.toString());
+				this.clock = sinon.useFakeTimers();
+				this.rclient.lrange = (key, start, end, cb) => {
+					this.clock.tick(6000);
+					return cb(null, this.jsonOps);
+				};
+				return this.RedisManager.getPreviousDocOps(this.doc_id, this.start, this.end, this.callback);
+			});
+
+			afterEach(function() {
+				return this.clock.restore();
+			});
+
+			return it('should return an error', function() {
+				return this.callback
 					.calledWith(new Error("redis getPreviousDocOps exceeded timeout"))
-					.should.equal true
-
-
-	describe "updateDocument", ->
-		beforeEach ->
-			@lines = ["one", "two", "three", "これは"]
-			@ops = [{ op: [{ i: "foo", p: 4 }] },{ op: [{ i: "bar", p: 8 }] }]
-			@version = 42
-			@hash = crypto.createHash('sha1').update(JSON.stringify(@lines),'utf8').digest('hex')
-			@ranges = { comments: "mock", entries: "mock" }
-			@updateMeta = { user_id: 'last-author-fake-id' }
-			@doc_update_list_length = sinon.stub()
-			@project_update_list_length = sinon.stub()
-
-			@RedisManager.getDocVersion = sinon.stub()
-			@multi.set = sinon.stub()
-			@multi.rpush = sinon.stub()
-			@multi.expire = sinon.stub()
-			@multi.ltrim = sinon.stub()
-			@multi.del = sinon.stub()
-			@multi.exec = sinon.stub().callsArgWith(0, null,
-				[@hash, null, null, null, null, null, null, @doc_update_list_length, null, null]
-			)
-			@ProjectHistoryRedisManager.queueOps = sinon.stub().callsArgWith(
-				@ops.length + 1, null, @project_update_list_length
-			)
-
-		describe "with a consistent version", ->
-			beforeEach ->
-
-
-			describe "with project history enabled", ->
-				beforeEach ->
-					@settings.apis.project_history.enabled = true
-					@RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version - @ops.length)
-					@RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, @ranges, @updateMeta, @callback
-
-				it "should get the current doc version to check for consistency", ->
-					@RedisManager.getDocVersion
-						.calledWith(@doc_id)
-						.should.equal true
-
-				it "should set the doclines", ->
-					@multi.set
-						.calledWith("doclines:#{@doc_id}", JSON.stringify(@lines))
-						.should.equal true
-
-				it "should set the version", ->
-					@multi.set
-						.calledWith("DocVersion:#{@doc_id}", @version)
-						.should.equal true
-
-				it "should set the hash", ->
-					@multi.set
-						.calledWith("DocHash:#{@doc_id}", @hash)
-						.should.equal true
-
-				it "should set the ranges", ->
-					@multi.set
-						.calledWith("Ranges:#{@doc_id}", JSON.stringify(@ranges))
-						.should.equal true
-
-				it "should set the unflushed time", ->
-					@multi.set
-						.calledWith("UnflushedTime:#{@doc_id}", Date.now(), "NX")
-						.should.equal true
-
-				it "should set the last updated time", ->
-					@multi.set
-						.calledWith("lastUpdatedAt:#{@doc_id}", Date.now())
-						.should.equal true
-
-				it "should set the last updater", ->
-					@multi.set
-						.calledWith("lastUpdatedBy:#{@doc_id}", 'last-author-fake-id')
-						.should.equal true
-
-				it "should push the doc op into the doc ops list", ->
-					@multi.rpush
-						.calledWith("DocOps:#{@doc_id}", JSON.stringify(@ops[0]), JSON.stringify(@ops[1]))
-						.should.equal true
-
-				it "should renew the expiry ttl on the doc ops array", ->
-					@multi.expire
-						.calledWith("DocOps:#{@doc_id}", @RedisManager.DOC_OPS_TTL)
-						.should.equal true
-
-				it "should truncate the list to 100 members", ->
-					@multi.ltrim
-						.calledWith("DocOps:#{@doc_id}", -@RedisManager.DOC_OPS_MAX_LENGTH, -1)
-						.should.equal true
-
-				it "should push the updates into the history ops list", ->
-					@multi.rpush
-						.calledWith("UncompressedHistoryOps:#{@doc_id}", JSON.stringify(@ops[0]), JSON.stringify(@ops[1]))
-						.should.equal true
-
-				it "should push the updates into the project history ops list", ->
-					@ProjectHistoryRedisManager.queueOps
-						.calledWith(@project_id, JSON.stringify(@ops[0]))
-						.should.equal true
-
-				it "should call the callback", ->
-					@callback
-						.calledWith(null, @doc_update_list_length, @project_update_list_length)
-						.should.equal true
-
-				it 'should not log any errors', ->
-					@logger.error.calledWith()
-						.should.equal false
-
-			describe "with project history disabled", ->
-				beforeEach ->
-					@settings.apis.project_history.enabled = false
-					@RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version - @ops.length)
-					@RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, @ranges, @updateMeta, @callback
-
-				it "should not push the updates into the project history ops list", ->
-					@ProjectHistoryRedisManager.queueOps.called.should.equal false
-
-				it "should call the callback", ->
-					@callback
-						.calledWith(null, @doc_update_list_length)
-						.should.equal true
-
-			describe "with a doc using project history only", ->
-				beforeEach ->
-					@RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version - @ops.length, 'project-history')
-					@RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, @ranges, @updateMeta, @callback
-
-				it "should not push the updates to the track-changes ops list", ->
-					@multi.rpush
-						.calledWith("UncompressedHistoryOps:#{@doc_id}")
-						.should.equal false
-
-				it "should push the updates into the project history ops list", ->
-					@ProjectHistoryRedisManager.queueOps
-						.calledWith(@project_id, JSON.stringify(@ops[0]))
-						.should.equal true
-
-				it "should call the callback with the project update count only", ->
-					@callback
-						.calledWith(null, undefined, @project_update_list_length)
-						.should.equal true
-
-		describe "with an inconsistent version", ->
-			beforeEach ->
-				@RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version - @ops.length - 1)
-				@RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, @ranges, @updateMeta, @callback
-
-			it "should not call multi.exec", ->
-				@multi.exec.called.should.equal false
-
-			it "should call the callback with an error", ->
-				@callback
-					.calledWith(new Error("Version mismatch. '#{@doc_id}' is corrupted."))
-					.should.equal true
-
-		describe "with no updates", ->
-			beforeEach ->
-				@RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version)
-				@RedisManager.updateDocument @project_id, @doc_id, @lines, @version, [], @ranges, @updateMeta, @callback
-
-			it "should not try to enqueue doc updates", ->
-				@multi.rpush
+					.should.equal(true);
+			});
+		});
+	});
+
+
+	describe("updateDocument", function() {
+		beforeEach(function() {
+			this.lines = ["one", "two", "three", "これは"];
+			this.ops = [{ op: [{ i: "foo", p: 4 }] },{ op: [{ i: "bar", p: 8 }] }];
+			this.version = 42;
+			this.hash = crypto.createHash('sha1').update(JSON.stringify(this.lines),'utf8').digest('hex');
+			this.ranges = { comments: "mock", entries: "mock" };
+			this.updateMeta = { user_id: 'last-author-fake-id' };
+			this.doc_update_list_length = sinon.stub();
+			this.project_update_list_length = sinon.stub();
+
+			this.RedisManager.getDocVersion = sinon.stub();
+			this.multi.set = sinon.stub();
+			this.multi.rpush = sinon.stub();
+			this.multi.expire = sinon.stub();
+			this.multi.ltrim = sinon.stub();
+			this.multi.del = sinon.stub();
+			this.multi.exec = sinon.stub().callsArgWith(0, null,
+				[this.hash, null, null, null, null, null, null, this.doc_update_list_length, null, null]
+			);
+			return this.ProjectHistoryRedisManager.queueOps = sinon.stub().callsArgWith(
+				this.ops.length + 1, null, this.project_update_list_length
+			);
+		});
+
+		describe("with a consistent version", function() {
+			beforeEach(function() {});
+
+
+			describe("with project history enabled", function() {
+				beforeEach(function() {
+					this.settings.apis.project_history.enabled = true;
+					this.RedisManager.getDocVersion.withArgs(this.doc_id).yields(null, this.version - this.ops.length);
+					return this.RedisManager.updateDocument(this.project_id, this.doc_id, this.lines, this.version, this.ops, this.ranges, this.updateMeta, this.callback);
+				});
+
+				it("should get the current doc version to check for consistency", function() {
+					return this.RedisManager.getDocVersion
+						.calledWith(this.doc_id)
+						.should.equal(true);
+				});
+
+				it("should set the doclines", function() {
+					return this.multi.set
+						.calledWith(`doclines:${this.doc_id}`, JSON.stringify(this.lines))
+						.should.equal(true);
+				});
+
+				it("should set the version", function() {
+					return this.multi.set
+						.calledWith(`DocVersion:${this.doc_id}`, this.version)
+						.should.equal(true);
+				});
+
+				it("should set the hash", function() {
+					return this.multi.set
+						.calledWith(`DocHash:${this.doc_id}`, this.hash)
+						.should.equal(true);
+				});
+
+				it("should set the ranges", function() {
+					return this.multi.set
+						.calledWith(`Ranges:${this.doc_id}`, JSON.stringify(this.ranges))
+						.should.equal(true);
+				});
+
+				it("should set the unflushed time", function() {
+					return this.multi.set
+						.calledWith(`UnflushedTime:${this.doc_id}`, Date.now(), "NX")
+						.should.equal(true);
+				});
+
+				it("should set the last updated time", function() {
+					return this.multi.set
+						.calledWith(`lastUpdatedAt:${this.doc_id}`, Date.now())
+						.should.equal(true);
+				});
+
+				it("should set the last updater", function() {
+					return this.multi.set
+						.calledWith(`lastUpdatedBy:${this.doc_id}`, 'last-author-fake-id')
+						.should.equal(true);
+				});
+
+				it("should push the doc op into the doc ops list", function() {
+					return this.multi.rpush
+						.calledWith(`DocOps:${this.doc_id}`, JSON.stringify(this.ops[0]), JSON.stringify(this.ops[1]))
+						.should.equal(true);
+				});
+
+				it("should renew the expiry ttl on the doc ops array", function() {
+					return this.multi.expire
+						.calledWith(`DocOps:${this.doc_id}`, this.RedisManager.DOC_OPS_TTL)
+						.should.equal(true);
+				});
+
+				it("should truncate the list to 100 members", function() {
+					return this.multi.ltrim
+						.calledWith(`DocOps:${this.doc_id}`, -this.RedisManager.DOC_OPS_MAX_LENGTH, -1)
+						.should.equal(true);
+				});
+
+				it("should push the updates into the history ops list", function() {
+					return this.multi.rpush
+						.calledWith(`UncompressedHistoryOps:${this.doc_id}`, JSON.stringify(this.ops[0]), JSON.stringify(this.ops[1]))
+						.should.equal(true);
+				});
+
+				it("should push the updates into the project history ops list", function() {
+					return this.ProjectHistoryRedisManager.queueOps
+						.calledWith(this.project_id, JSON.stringify(this.ops[0]))
+						.should.equal(true);
+				});
+
+				it("should call the callback", function() {
+					return this.callback
+						.calledWith(null, this.doc_update_list_length, this.project_update_list_length)
+						.should.equal(true);
+				});
+
+				return it('should not log any errors', function() {
+					return this.logger.error.calledWith()
+						.should.equal(false);
+				});
+			});
+
+			describe("with project history disabled", function() {
+				beforeEach(function() {
+					this.settings.apis.project_history.enabled = false;
+					this.RedisManager.getDocVersion.withArgs(this.doc_id).yields(null, this.version - this.ops.length);
+					return this.RedisManager.updateDocument(this.project_id, this.doc_id, this.lines, this.version, this.ops, this.ranges, this.updateMeta, this.callback);
+				});
+
+				it("should not push the updates into the project history ops list", function() {
+					return this.ProjectHistoryRedisManager.queueOps.called.should.equal(false);
+				});
+
+				return it("should call the callback", function() {
+					return this.callback
+						.calledWith(null, this.doc_update_list_length)
+						.should.equal(true);
+				});
+			});
+
+			return describe("with a doc using project history only", function() {
+				beforeEach(function() {
+					this.RedisManager.getDocVersion.withArgs(this.doc_id).yields(null, this.version - this.ops.length, 'project-history');
+					return this.RedisManager.updateDocument(this.project_id, this.doc_id, this.lines, this.version, this.ops, this.ranges, this.updateMeta, this.callback);
+				});
+
+				it("should not push the updates to the track-changes ops list", function() {
+					return this.multi.rpush
+						.calledWith(`UncompressedHistoryOps:${this.doc_id}`)
+						.should.equal(false);
+				});
+
+				it("should push the updates into the project history ops list", function() {
+					return this.ProjectHistoryRedisManager.queueOps
+						.calledWith(this.project_id, JSON.stringify(this.ops[0]))
+						.should.equal(true);
+				});
+
+				return it("should call the callback with the project update count only", function() {
+					return this.callback
+						.calledWith(null, undefined, this.project_update_list_length)
+						.should.equal(true);
+				});
+			});
+		});
+
+		describe("with an inconsistent version", function() {
+			beforeEach(function() {
+				this.RedisManager.getDocVersion.withArgs(this.doc_id).yields(null, this.version - this.ops.length - 1);
+				return this.RedisManager.updateDocument(this.project_id, this.doc_id, this.lines, this.version, this.ops, this.ranges, this.updateMeta, this.callback);
+			});
+
+			it("should not call multi.exec", function() {
+				return this.multi.exec.called.should.equal(false);
+			});
+
+			return it("should call the callback with an error", function() {
+				return this.callback
+					.calledWith(new Error(`Version mismatch. '${this.doc_id}' is corrupted.`))
+					.should.equal(true);
+			});
+		});
+
+		describe("with no updates", function() {
+			beforeEach(function() {
+				this.RedisManager.getDocVersion.withArgs(this.doc_id).yields(null, this.version);
+				return this.RedisManager.updateDocument(this.project_id, this.doc_id, this.lines, this.version, [], this.ranges, this.updateMeta, this.callback);
+			});
+
+			it("should not try to enqueue doc updates", function() {
+				return this.multi.rpush
 					.called
-					.should.equal false
+					.should.equal(false);
+			});
 
-			it "should not try to enqueue project updates", ->
-				@ProjectHistoryRedisManager.queueOps
+			it("should not try to enqueue project updates", function() {
+				return this.ProjectHistoryRedisManager.queueOps
 					.called
-					.should.equal false
-
-			it "should still set the doclines", ->
-				@multi.set
-					.calledWith("doclines:#{@doc_id}", JSON.stringify(@lines))
-					.should.equal true
-
-		describe "with empty ranges", ->
-			beforeEach ->
-				@RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version - @ops.length)
-				@RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, {}, @updateMeta, @callback
-
-			it "should not set the ranges", ->
-				@multi.set
-					.calledWith("Ranges:#{@doc_id}", JSON.stringify(@ranges))
-					.should.equal false
-
-			it "should delete the ranges key", ->
-				@multi.del
-					.calledWith("Ranges:#{@doc_id}")
-					.should.equal true
-
-		describe "with null bytes in the serialized doc lines", ->
-			beforeEach ->
-				@RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version - @ops.length)
-				@_stringify = JSON.stringify
-				@JSON.stringify = () -> return '["bad bytes! \u0000 <- here"]'
-				@RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, @ranges, @updateMeta, @callback
-
-			afterEach ->
-				@JSON.stringify = @_stringify
-
-			it "should log an error", ->
-				@logger.error.called.should.equal true
-
-			it "should call the callback with an error", ->
-				@callback.calledWith(new Error("null bytes found in doc lines")).should.equal true
-
-		describe "with ranges that are too big", ->
-			beforeEach ->
-				@RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version - @ops.length)
-				@RedisManager._serializeRanges = sinon.stub().yields(new Error("ranges are too large"))
-				@RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, @ranges, @updateMeta, @callback
-
-			it 'should log an error', ->
-				@logger.error.called.should.equal true
-
-			it "should call the callback with the error", ->
-				@callback.calledWith(new Error("ranges are too large")).should.equal true
-
-		describe "without user id from meta", ->
-			beforeEach ->
-				@RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version - @ops.length)
-				@RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, @ranges, {}, @callback
-
-			it "should set the last updater to null", ->
-				@multi.del
-					.calledWith("lastUpdatedBy:#{@doc_id}")
-					.should.equal true
-
-			it "should still set the last updated time", ->
-				@multi.set
-					.calledWith("lastUpdatedAt:#{@doc_id}", Date.now())
-					.should.equal true
-
-	describe "putDocInMemory", ->
-		beforeEach ->
-			@multi.set = sinon.stub()
-			@rclient.sadd = sinon.stub().yields()
-			@multi.del = sinon.stub()
-			@lines = ["one", "two", "three", "これは"]
-			@version = 42
-			@hash = crypto.createHash('sha1').update(JSON.stringify(@lines),'utf8').digest('hex')
-			@multi.exec = sinon.stub().callsArgWith(0, null, [@hash])
-			@ranges = { comments: "mock", entries: "mock" }
-			@pathname = '/a/b/c.tex'
-
-		describe "with non-empty ranges", ->
-			beforeEach (done) ->
-				@RedisManager.putDocInMemory @project_id, @doc_id, @lines, @version, @ranges, @pathname, @projectHistoryId, done
-
-			it "should set the lines", ->
-				@multi.set
-					.calledWith("doclines:#{@doc_id}", JSON.stringify(@lines))
-					.should.equal true
-
-			it "should set the version", ->
-				@multi.set
-					.calledWith("DocVersion:#{@doc_id}", @version)
-					.should.equal true
-
-			it "should set the hash", ->
-				@multi.set
-					.calledWith("DocHash:#{@doc_id}", @hash)
-					.should.equal true
-
-			it "should set the ranges", ->
-				@multi.set
-					.calledWith("Ranges:#{@doc_id}", JSON.stringify(@ranges))
-					.should.equal true
-
-			it "should set the project_id for the doc", ->
-				@multi.set
-					.calledWith("ProjectId:#{@doc_id}", @project_id)
-					.should.equal true
-
-			it "should set the pathname for the doc", ->
-				@multi.set
-					.calledWith("Pathname:#{@doc_id}", @pathname)
-					.should.equal true
-
-			it "should set the projectHistoryId for the doc", ->
-				@multi.set
-					.calledWith("ProjectHistoryId:#{@doc_id}", @projectHistoryId)
-					.should.equal true
-
-			it "should add the doc_id to the project set", ->
-				@rclient.sadd
-					.calledWith("DocsIn:#{@project_id}", @doc_id)
-					.should.equal true
-
-			it 'should not log any errors', ->
-				@logger.error.calledWith()
-					.should.equal false
-
-		describe "with empty ranges", ->
-			beforeEach (done) ->
-				@RedisManager.putDocInMemory @project_id, @doc_id, @lines, @version, {}, @pathname, @projectHistoryId, done
-
-			it "should delete the ranges key", ->
-				@multi.del
-					.calledWith("Ranges:#{@doc_id}")
-					.should.equal true
-
-			it "should not set the ranges", ->
-				@multi.set
-					.calledWith("Ranges:#{@doc_id}", JSON.stringify(@ranges))
-					.should.equal false
-
-		describe "with null bytes in the serialized doc lines", ->
-			beforeEach ->
-				@_stringify = JSON.stringify
-				@JSON.stringify = () -> return '["bad bytes! \u0000 <- here"]'
-				@RedisManager.putDocInMemory @project_id, @doc_id, @lines, @version, @ranges, @pathname, @projectHistoryId, @callback
-
-			afterEach ->
-				@JSON.stringify = @_stringify
-
-			it "should log an error", ->
-				@logger.error.called.should.equal true
-
-			it "should call the callback with an error", ->
-				@callback.calledWith(new Error("null bytes found in doc lines")).should.equal true
-
-		describe "with ranges that are too big", ->
-			beforeEach ->
-				@RedisManager._serializeRanges = sinon.stub().yields(new Error("ranges are too large"))
-				@RedisManager.putDocInMemory @project_id, @doc_id, @lines, @version, @ranges, @pathname, @projectHistoryId, @callback
-
-			it 'should log an error', ->
-				@logger.error.called.should.equal true
-
-			it "should call the callback with the error", ->
-				@callback.calledWith(new Error("ranges are too large")).should.equal true
-
-	describe "removeDocFromMemory", ->
-		beforeEach (done) ->
-			@multi.strlen = sinon.stub()
-			@multi.del = sinon.stub()
-			@multi.srem = sinon.stub()
-			@multi.exec.yields()
-			@RedisManager.removeDocFromMemory @project_id, @doc_id, done
-
-		it "should check the length of the current doclines", ->
-			@multi.strlen
-				.calledWith("doclines:#{@doc_id}")
-				.should.equal true
-
-		it "should delete the lines", ->
-			@multi.del
-				.calledWith("doclines:#{@doc_id}")
-				.should.equal true
-
-		it "should delete the version", ->
-			@multi.del
-				.calledWith("DocVersion:#{@doc_id}")
-				.should.equal true
-
-		it "should delete the hash", ->
-			@multi.del
-				.calledWith("DocHash:#{@doc_id}")
-				.should.equal true
-
-		it "should delete the unflushed time", ->
-			@multi.del
-				.calledWith("UnflushedTime:#{@doc_id}")
-				.should.equal true
-
-		it "should delete the project_id for the doc", ->
-			@multi.del
-				.calledWith("ProjectId:#{@doc_id}")
-				.should.equal true
-
-		it "should remove the doc_id from the project set", ->
-			@multi.srem
-				.calledWith("DocsIn:#{@project_id}", @doc_id)
-				.should.equal true
-
-		it "should delete the pathname for the doc", ->
-			@multi.del
-				.calledWith("Pathname:#{@doc_id}")
-				.should.equal true
-
-		it "should delete the pathname for the doc", ->
-			@multi.del
-				.calledWith("ProjectHistoryId:#{@doc_id}")
-				.should.equal true
-
-		it "should delete lastUpdatedAt", ->
-			@multi.del
-				.calledWith("lastUpdatedAt:#{@doc_id}")
-				.should.equal true
-
-		it "should delete lastUpdatedBy", ->
-			@multi.del
-				.calledWith("lastUpdatedBy:#{@doc_id}")
-				.should.equal true
-
-
-	describe "clearProjectState", ->
-		beforeEach (done) ->
-			@rclient.del = sinon.stub().callsArg(1)
-			@RedisManager.clearProjectState @project_id, done
-
-		it "should delete the project state", ->
-			@rclient.del
-				.calledWith("ProjectState:#{@project_id}")
-				.should.equal true
-
-	describe "renameDoc", ->
-		beforeEach () ->
-			@rclient.rpush = sinon.stub().yields()
-			@rclient.set = sinon.stub().yields()
-			@update =
-				id: @doc_id
-				pathname: @pathname = 'pathname'
-				newPathname: @newPathname = 'new-pathname'
-
-		describe "the document is cached in redis", ->
-			beforeEach ->
-				@RedisManager.getDoc = sinon.stub().callsArgWith(2, null, 'lines', 'version')
-				@ProjectHistoryRedisManager.queueRenameEntity = sinon.stub().yields()
-				@RedisManager.renameDoc @project_id, @doc_id, @userId, @update, @projectHistoryId, @callback
-
-			it "update the cached pathname", ->
-				@rclient.set
-					.calledWith("Pathname:#{@doc_id}", @newPathname)
-					.should.equal true
-
-			it "should queue an update", ->
-				@ProjectHistoryRedisManager.queueRenameEntity
-					.calledWithExactly(@project_id, @projectHistoryId, 'doc', @doc_id, @userId, @update, @callback)
-					.should.equal true
-
-		describe "the document is not cached in redis", ->
-			beforeEach ->
-				@RedisManager.getDoc = sinon.stub().callsArgWith(2, null, null, null)
-				@ProjectHistoryRedisManager.queueRenameEntity = sinon.stub().yields()
-				@RedisManager.renameDoc @project_id, @doc_id, @userId, @update, @projectHistoryId, @callback
-
-			it "does not update the cached pathname", ->
-				@rclient.set.called.should.equal false
-
-			it "should queue an update", ->
-				@ProjectHistoryRedisManager.queueRenameEntity
-					.calledWithExactly(@project_id, @projectHistoryId, 'doc', @doc_id, @userId, @update, @callback)
-					.should.equal true
-
-		describe "getDocVersion", ->
-			beforeEach ->
-				@version = 12345
-
-			describe "when the document does not have a project history type set", ->
-				beforeEach ->
-					@rclient.mget = sinon.stub().withArgs("DocVersion:#{@doc_id}", "ProjectHistoryType:#{@doc_id}").callsArgWith(2, null, ["#{@version}"])
-					@RedisManager.getDocVersion @doc_id, @callback
-
-				it "should return the document version and an undefined history type", ->
-					@callback.calledWithExactly(null, @version, undefined).should.equal true
-
-			describe "when the document has a project history type set", ->
-				beforeEach ->
-					@rclient.mget = sinon.stub().withArgs("DocVersion:#{@doc_id}", "ProjectHistoryType:#{@doc_id}").callsArgWith(2, null, ["#{@version}", 'project-history'])
-					@RedisManager.getDocVersion @doc_id, @callback
-
-				it "should return the document version and history type", ->
-					@callback.calledWithExactly(null, @version, 'project-history').should.equal true
+					.should.equal(false);
+			});
+
+			return it("should still set the doclines", function() {
+				return this.multi.set
+					.calledWith(`doclines:${this.doc_id}`, JSON.stringify(this.lines))
+					.should.equal(true);
+			});
+		});
+
+		describe("with empty ranges", function() {
+			beforeEach(function() {
+				this.RedisManager.getDocVersion.withArgs(this.doc_id).yields(null, this.version - this.ops.length);
+				return this.RedisManager.updateDocument(this.project_id, this.doc_id, this.lines, this.version, this.ops, {}, this.updateMeta, this.callback);
+			});
+
+			it("should not set the ranges", function() {
+				return this.multi.set
+					.calledWith(`Ranges:${this.doc_id}`, JSON.stringify(this.ranges))
+					.should.equal(false);
+			});
+
+			return it("should delete the ranges key", function() {
+				return this.multi.del
+					.calledWith(`Ranges:${this.doc_id}`)
+					.should.equal(true);
+			});
+		});
+
+		describe("with null bytes in the serialized doc lines", function() {
+			beforeEach(function() {
+				this.RedisManager.getDocVersion.withArgs(this.doc_id).yields(null, this.version - this.ops.length);
+				this._stringify = JSON.stringify;
+				this.JSON.stringify = () => '["bad bytes! \u0000 <- here"]';
+				return this.RedisManager.updateDocument(this.project_id, this.doc_id, this.lines, this.version, this.ops, this.ranges, this.updateMeta, this.callback);
+			});
+
+			afterEach(function() {
+				return this.JSON.stringify = this._stringify;
+			});
+
+			it("should log an error", function() {
+				return this.logger.error.called.should.equal(true);
+			});
+
+			return it("should call the callback with an error", function() {
+				return this.callback.calledWith(new Error("null bytes found in doc lines")).should.equal(true);
+			});
+		});
+
+		describe("with ranges that are too big", function() {
+			beforeEach(function() {
+				this.RedisManager.getDocVersion.withArgs(this.doc_id).yields(null, this.version - this.ops.length);
+				this.RedisManager._serializeRanges = sinon.stub().yields(new Error("ranges are too large"));
+				return this.RedisManager.updateDocument(this.project_id, this.doc_id, this.lines, this.version, this.ops, this.ranges, this.updateMeta, this.callback);
+			});
+
+			it('should log an error', function() {
+				return this.logger.error.called.should.equal(true);
+			});
+
+			return it("should call the callback with the error", function() {
+				return this.callback.calledWith(new Error("ranges are too large")).should.equal(true);
+			});
+		});
+
+		return describe("without user id from meta", function() {
+			beforeEach(function() {
+				this.RedisManager.getDocVersion.withArgs(this.doc_id).yields(null, this.version - this.ops.length);
+				return this.RedisManager.updateDocument(this.project_id, this.doc_id, this.lines, this.version, this.ops, this.ranges, {}, this.callback);
+			});
+
+			it("should set the last updater to null", function() {
+				return this.multi.del
+					.calledWith(`lastUpdatedBy:${this.doc_id}`)
+					.should.equal(true);
+			});
+
+			return it("should still set the last updated time", function() {
+				return this.multi.set
+					.calledWith(`lastUpdatedAt:${this.doc_id}`, Date.now())
+					.should.equal(true);
+			});
+		});
+	});
+
+	describe("putDocInMemory", function() {
+		beforeEach(function() {
+			this.multi.set = sinon.stub();
+			this.rclient.sadd = sinon.stub().yields();
+			this.multi.del = sinon.stub();
+			this.lines = ["one", "two", "three", "これは"];
+			this.version = 42;
+			this.hash = crypto.createHash('sha1').update(JSON.stringify(this.lines),'utf8').digest('hex');
+			this.multi.exec = sinon.stub().callsArgWith(0, null, [this.hash]);
+			this.ranges = { comments: "mock", entries: "mock" };
+			return this.pathname = '/a/b/c.tex';
+		});
+
+		describe("with non-empty ranges", function() {
+			beforeEach(function(done) {
+				return this.RedisManager.putDocInMemory(this.project_id, this.doc_id, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId, done);
+			});
+
+			it("should set the lines", function() {
+				return this.multi.set
+					.calledWith(`doclines:${this.doc_id}`, JSON.stringify(this.lines))
+					.should.equal(true);
+			});
+
+			it("should set the version", function() {
+				return this.multi.set
+					.calledWith(`DocVersion:${this.doc_id}`, this.version)
+					.should.equal(true);
+			});
+
+			it("should set the hash", function() {
+				return this.multi.set
+					.calledWith(`DocHash:${this.doc_id}`, this.hash)
+					.should.equal(true);
+			});
+
+			it("should set the ranges", function() {
+				return this.multi.set
+					.calledWith(`Ranges:${this.doc_id}`, JSON.stringify(this.ranges))
+					.should.equal(true);
+			});
+
+			it("should set the project_id for the doc", function() {
+				return this.multi.set
+					.calledWith(`ProjectId:${this.doc_id}`, this.project_id)
+					.should.equal(true);
+			});
+
+			it("should set the pathname for the doc", function() {
+				return this.multi.set
+					.calledWith(`Pathname:${this.doc_id}`, this.pathname)
+					.should.equal(true);
+			});
+
+			it("should set the projectHistoryId for the doc", function() {
+				return this.multi.set
+					.calledWith(`ProjectHistoryId:${this.doc_id}`, this.projectHistoryId)
+					.should.equal(true);
+			});
+
+			it("should add the doc_id to the project set", function() {
+				return this.rclient.sadd
+					.calledWith(`DocsIn:${this.project_id}`, this.doc_id)
+					.should.equal(true);
+			});
+
+			return it('should not log any errors', function() {
+				return this.logger.error.calledWith()
+					.should.equal(false);
+			});
+		});
+
+		describe("with empty ranges", function() {
+			beforeEach(function(done) {
+				return this.RedisManager.putDocInMemory(this.project_id, this.doc_id, this.lines, this.version, {}, this.pathname, this.projectHistoryId, done);
+			});
+
+			it("should delete the ranges key", function() {
+				return this.multi.del
+					.calledWith(`Ranges:${this.doc_id}`)
+					.should.equal(true);
+			});
+
+			return it("should not set the ranges", function() {
+				return this.multi.set
+					.calledWith(`Ranges:${this.doc_id}`, JSON.stringify(this.ranges))
+					.should.equal(false);
+			});
+		});
+
+		describe("with null bytes in the serialized doc lines", function() {
+			beforeEach(function() {
+				this._stringify = JSON.stringify;
+				this.JSON.stringify = () => '["bad bytes! \u0000 <- here"]';
+				return this.RedisManager.putDocInMemory(this.project_id, this.doc_id, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId, this.callback);
+			});
+
+			afterEach(function() {
+				return this.JSON.stringify = this._stringify;
+			});
+
+			it("should log an error", function() {
+				return this.logger.error.called.should.equal(true);
+			});
+
+			return it("should call the callback with an error", function() {
+				return this.callback.calledWith(new Error("null bytes found in doc lines")).should.equal(true);
+			});
+		});
+
+		return describe("with ranges that are too big", function() {
+			beforeEach(function() {
+				this.RedisManager._serializeRanges = sinon.stub().yields(new Error("ranges are too large"));
+				return this.RedisManager.putDocInMemory(this.project_id, this.doc_id, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId, this.callback);
+			});
+
+			it('should log an error', function() {
+				return this.logger.error.called.should.equal(true);
+			});
+
+			return it("should call the callback with the error", function() {
+				return this.callback.calledWith(new Error("ranges are too large")).should.equal(true);
+			});
+		});
+	});
+
+	describe("removeDocFromMemory", function() {
+		beforeEach(function(done) {
+			this.multi.strlen = sinon.stub();
+			this.multi.del = sinon.stub();
+			this.multi.srem = sinon.stub();
+			this.multi.exec.yields();
+			return this.RedisManager.removeDocFromMemory(this.project_id, this.doc_id, done);
+		});
+
+		it("should check the length of the current doclines", function() {
+			return this.multi.strlen
+				.calledWith(`doclines:${this.doc_id}`)
+				.should.equal(true);
+		});
+
+		it("should delete the lines", function() {
+			return this.multi.del
+				.calledWith(`doclines:${this.doc_id}`)
+				.should.equal(true);
+		});
+
+		it("should delete the version", function() {
+			return this.multi.del
+				.calledWith(`DocVersion:${this.doc_id}`)
+				.should.equal(true);
+		});
+
+		it("should delete the hash", function() {
+			return this.multi.del
+				.calledWith(`DocHash:${this.doc_id}`)
+				.should.equal(true);
+		});
+
+		it("should delete the unflushed time", function() {
+			return this.multi.del
+				.calledWith(`UnflushedTime:${this.doc_id}`)
+				.should.equal(true);
+		});
+
+		it("should delete the project_id for the doc", function() {
+			return this.multi.del
+				.calledWith(`ProjectId:${this.doc_id}`)
+				.should.equal(true);
+		});
+
+		it("should remove the doc_id from the project set", function() {
+			return this.multi.srem
+				.calledWith(`DocsIn:${this.project_id}`, this.doc_id)
+				.should.equal(true);
+		});
+
+		it("should delete the pathname for the doc", function() {
+			return this.multi.del
+				.calledWith(`Pathname:${this.doc_id}`)
+				.should.equal(true);
+		});
+
+		it("should delete the pathname for the doc", function() {
+			return this.multi.del
+				.calledWith(`ProjectHistoryId:${this.doc_id}`)
+				.should.equal(true);
+		});
+
+		it("should delete lastUpdatedAt", function() {
+			return this.multi.del
+				.calledWith(`lastUpdatedAt:${this.doc_id}`)
+				.should.equal(true);
+		});
+
+		return it("should delete lastUpdatedBy", function() {
+			return this.multi.del
+				.calledWith(`lastUpdatedBy:${this.doc_id}`)
+				.should.equal(true);
+		});
+	});
+
+
+	describe("clearProjectState", function() {
+		beforeEach(function(done) {
+			this.rclient.del = sinon.stub().callsArg(1);
+			return this.RedisManager.clearProjectState(this.project_id, done);
+		});
+
+		return it("should delete the project state", function() {
+			return this.rclient.del
+				.calledWith(`ProjectState:${this.project_id}`)
+				.should.equal(true);
+		});
+	});
+
+	return describe("renameDoc", function() {
+		beforeEach(function() {
+			this.rclient.rpush = sinon.stub().yields();
+			this.rclient.set = sinon.stub().yields();
+			return this.update = {
+				id: this.doc_id,
+				pathname: (this.pathname = 'pathname'),
+				newPathname: (this.newPathname = 'new-pathname')
+			};
+		});
+
+		describe("the document is cached in redis", function() {
+			beforeEach(function() {
+				this.RedisManager.getDoc = sinon.stub().callsArgWith(2, null, 'lines', 'version');
+				this.ProjectHistoryRedisManager.queueRenameEntity = sinon.stub().yields();
+				return this.RedisManager.renameDoc(this.project_id, this.doc_id, this.userId, this.update, this.projectHistoryId, this.callback);
+			});
+
+			it("update the cached pathname", function() {
+				return this.rclient.set
+					.calledWith(`Pathname:${this.doc_id}`, this.newPathname)
+					.should.equal(true);
+			});
+
+			return it("should queue an update", function() {
+				return this.ProjectHistoryRedisManager.queueRenameEntity
+					.calledWithExactly(this.project_id, this.projectHistoryId, 'doc', this.doc_id, this.userId, this.update, this.callback)
+					.should.equal(true);
+			});
+		});
+
+		describe("the document is not cached in redis", function() {
+			beforeEach(function() {
+				this.RedisManager.getDoc = sinon.stub().callsArgWith(2, null, null, null);
+				this.ProjectHistoryRedisManager.queueRenameEntity = sinon.stub().yields();
+				return this.RedisManager.renameDoc(this.project_id, this.doc_id, this.userId, this.update, this.projectHistoryId, this.callback);
+			});
+
+			it("does not update the cached pathname", function() {
+				return this.rclient.set.called.should.equal(false);
+			});
+
+			return it("should queue an update", function() {
+				return this.ProjectHistoryRedisManager.queueRenameEntity
+					.calledWithExactly(this.project_id, this.projectHistoryId, 'doc', this.doc_id, this.userId, this.update, this.callback)
+					.should.equal(true);
+			});
+		});
+
+		return describe("getDocVersion", function() {
+			beforeEach(function() {
+				return this.version = 12345;
+			});
+
+			describe("when the document does not have a project history type set", function() {
+				beforeEach(function() {
+					this.rclient.mget = sinon.stub().withArgs(`DocVersion:${this.doc_id}`, `ProjectHistoryType:${this.doc_id}`).callsArgWith(2, null, [`${this.version}`]);
+					return this.RedisManager.getDocVersion(this.doc_id, this.callback);
+				});
+
+				return it("should return the document version and an undefined history type", function() {
+					return this.callback.calledWithExactly(null, this.version, undefined).should.equal(true);
+				});
+			});
+
+			return describe("when the document has a project history type set", function() {
+				beforeEach(function() {
+					this.rclient.mget = sinon.stub().withArgs(`DocVersion:${this.doc_id}`, `ProjectHistoryType:${this.doc_id}`).callsArgWith(2, null, [`${this.version}`, 'project-history']);
+					return this.RedisManager.getDocVersion(this.doc_id, this.callback);
+				});
+
+				return it("should return the document version and history type", function() {
+					return this.callback.calledWithExactly(null, this.version, 'project-history').should.equal(true);
+				});
+			});
+		});
+	});
+});

+ 342 - 267
services/document-updater/test/unit/coffee/ShareJS/TextTransformTests.js

@@ -1,283 +1,358 @@
-text = require "../../../../app/js/sharejs/types/text"
-require("chai").should()
-RangesTracker = require "../../../../app/js/RangesTracker"
-
-describe "ShareJS text type", ->
-	beforeEach ->
-		@t = "mock-thread-id"
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS202: Simplify dynamic range loops
+ * DS205: Consider reworking code to avoid use of IIFEs
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const text = require("../../../../app/js/sharejs/types/text");
+require("chai").should();
+const RangesTracker = require("../../../../app/js/RangesTracker");
+
+describe("ShareJS text type", function() {
+	beforeEach(function() {
+		return this.t = "mock-thread-id";
+	});
 		
-	describe "transform", ->
-		describe "insert / insert", ->
-			it "with an insert before", ->
-				dest = []
-				text._tc(dest, { i: "foo", p: 9 }, { i: "bar", p: 3 })
-				dest.should.deep.equal [{ i: "foo", p: 12 }]
-
-			it "with an insert after", ->
-				dest = []
-				text._tc(dest, { i: "foo", p: 3 }, { i: "bar", p: 9 })
-				dest.should.deep.equal [{ i: "foo", p: 3 }]
-
-			it "with an insert at the same place with side == 'right'", ->
-				dest = []
-				text._tc(dest, { i: "foo", p: 3 }, { i: "bar", p: 3 }, 'right')
-				dest.should.deep.equal [{ i: "foo", p: 6 }]
-
-			it "with an insert at the same place with side == 'left'", ->
-				dest = []
-				text._tc(dest, { i: "foo", p: 3 }, { i: "bar", p: 3 }, 'left')
-				dest.should.deep.equal [{ i: "foo", p: 3 }]
-
-		describe "insert / delete", ->
-			it "with a delete before", ->
-				dest = []
-				text._tc(dest, { i: "foo", p: 9 }, { d: "bar", p: 3 })
-				dest.should.deep.equal [{ i: "foo", p: 6 }]
-
-			it "with a delete after", ->
-				dest = []
-				text._tc(dest, { i: "foo", p: 3 }, { d: "bar", p: 9 })
-				dest.should.deep.equal [{ i: "foo", p: 3 }]
-
-			it "with a delete at the same place with side == 'right'", ->
-				dest = []
-				text._tc(dest, { i: "foo", p: 3 }, { d: "bar", p: 3 }, 'right')
-				dest.should.deep.equal [{ i: "foo", p: 3 }]
-
-			it "with a delete at the same place with side == 'left'", ->
-				dest = []
+	describe("transform", function() {
+		describe("insert / insert", function() {
+			it("with an insert before", function() {
+				const dest = [];
+				text._tc(dest, { i: "foo", p: 9 }, { i: "bar", p: 3 });
+				return dest.should.deep.equal([{ i: "foo", p: 12 }]);
+		});
+
+			it("with an insert after", function() {
+				const dest = [];
+				text._tc(dest, { i: "foo", p: 3 }, { i: "bar", p: 9 });
+				return dest.should.deep.equal([{ i: "foo", p: 3 }]);
+		});
+
+			it("with an insert at the same place with side == 'right'", function() {
+				const dest = [];
+				text._tc(dest, { i: "foo", p: 3 }, { i: "bar", p: 3 }, 'right');
+				return dest.should.deep.equal([{ i: "foo", p: 6 }]);
+		});
+
+			return it("with an insert at the same place with side == 'left'", function() {
+				const dest = [];
+				text._tc(dest, { i: "foo", p: 3 }, { i: "bar", p: 3 }, 'left');
+				return dest.should.deep.equal([{ i: "foo", p: 3 }]);
+		});
+	});
+
+		describe("insert / delete", function() {
+			it("with a delete before", function() {
+				const dest = [];
+				text._tc(dest, { i: "foo", p: 9 }, { d: "bar", p: 3 });
+				return dest.should.deep.equal([{ i: "foo", p: 6 }]);
+		});
+
+			it("with a delete after", function() {
+				const dest = [];
+				text._tc(dest, { i: "foo", p: 3 }, { d: "bar", p: 9 });
+				return dest.should.deep.equal([{ i: "foo", p: 3 }]);
+		});
+
+			it("with a delete at the same place with side == 'right'", function() {
+				const dest = [];
+				text._tc(dest, { i: "foo", p: 3 }, { d: "bar", p: 3 }, 'right');
+				return dest.should.deep.equal([{ i: "foo", p: 3 }]);
+		});
+
+			return it("with a delete at the same place with side == 'left'", function() {
+				const dest = [];
 				
-				text._tc(dest, { i: "foo", p: 3 }, { d: "bar", p: 3 }, 'left')
-				dest.should.deep.equal [{ i: "foo", p: 3 }]
-
-		describe "delete / insert", ->
-			it "with an insert before", ->
-				dest = []
-				text._tc(dest, { d: "foo", p: 9 }, { i: "bar", p: 3 })
-				dest.should.deep.equal [{ d: "foo", p: 12 }]
-
-			it "with an insert after", ->
-				dest = []
-				text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 9 })
-				dest.should.deep.equal [{ d: "foo", p: 3 }]
-
-			it "with an insert at the same place with side == 'right'", ->
-				dest = []
-				text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 3 }, 'right')
-				dest.should.deep.equal [{ d: "foo", p: 6 }]
-
-			it "with an insert at the same place with side == 'left'", ->
-				dest = []
-				text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 3 }, 'left')
-				dest.should.deep.equal [{ d: "foo", p: 6 }]
+				text._tc(dest, { i: "foo", p: 3 }, { d: "bar", p: 3 }, 'left');
+				return dest.should.deep.equal([{ i: "foo", p: 3 }]);
+		});
+	});
+
+		describe("delete / insert", function() {
+			it("with an insert before", function() {
+				const dest = [];
+				text._tc(dest, { d: "foo", p: 9 }, { i: "bar", p: 3 });
+				return dest.should.deep.equal([{ d: "foo", p: 12 }]);
+		});
+
+			it("with an insert after", function() {
+				const dest = [];
+				text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 9 });
+				return dest.should.deep.equal([{ d: "foo", p: 3 }]);
+		});
+
+			it("with an insert at the same place with side == 'right'", function() {
+				const dest = [];
+				text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 3 }, 'right');
+				return dest.should.deep.equal([{ d: "foo", p: 6 }]);
+		});
+
+			it("with an insert at the same place with side == 'left'", function() {
+				const dest = [];
+				text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 3 }, 'left');
+				return dest.should.deep.equal([{ d: "foo", p: 6 }]);
+		});
 			
-			it "with a delete that overlaps the insert location", ->
-				dest = []
-				text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 4 })
-				dest.should.deep.equal [{ d: "f", p: 3 }, { d: "oo", p: 6 }]
+			return it("with a delete that overlaps the insert location", function() {
+				const dest = [];
+				text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 4 });
+				return dest.should.deep.equal([{ d: "f", p: 3 }, { d: "oo", p: 6 }]);
+		});
+	});
 				
 
-		describe "delete / delete", ->
-			it "with a delete before", ->
-				dest = []
-				text._tc(dest, { d: "foo", p: 9 }, { d: "bar", p: 3 })
-				dest.should.deep.equal [{ d: "foo", p: 6 }]
-
-			it "with a delete after", ->
-				dest = []
-				text._tc(dest, { d: "foo", p: 3 }, { d: "bar", p: 9 })
-				dest.should.deep.equal [{ d: "foo", p: 3 }]
-
-			it "with deleting the same content", ->
-				dest = []
-				text._tc(dest, { d: "foo", p: 3 }, { d: "foo", p: 3 }, 'right')
-				dest.should.deep.equal []
-
-			it "with the delete overlapping before", ->
-				dest = []
-				text._tc(dest, { d: "foobar", p: 3 }, { d: "abcfoo", p: 0 }, 'right')
-				dest.should.deep.equal [{ d: "bar", p: 0 }]
-
-			it "with the delete overlapping after", ->
-				dest = []
-				text._tc(dest, { d: "abcfoo", p: 3 }, { d: "foobar", p: 6 })
-				dest.should.deep.equal [{ d: "abc", p: 3 }]
-
-			it "with the delete overlapping the whole delete", ->
-				dest = []
-				text._tc(dest, { d: "abcfoo123", p: 3 }, { d: "foo", p: 6 })
-				dest.should.deep.equal [{ d: "abc123", p: 3 }]
-
-			it "with the delete inside the whole delete", ->
-				dest = []
-				text._tc(dest, { d: "foo", p: 6 }, { d: "abcfoo123", p: 3 })
-				dest.should.deep.equal []
+		describe("delete / delete", function() {
+			it("with a delete before", function() {
+				const dest = [];
+				text._tc(dest, { d: "foo", p: 9 }, { d: "bar", p: 3 });
+				return dest.should.deep.equal([{ d: "foo", p: 6 }]);
+		});
+
+			it("with a delete after", function() {
+				const dest = [];
+				text._tc(dest, { d: "foo", p: 3 }, { d: "bar", p: 9 });
+				return dest.should.deep.equal([{ d: "foo", p: 3 }]);
+		});
+
+			it("with deleting the same content", function() {
+				const dest = [];
+				text._tc(dest, { d: "foo", p: 3 }, { d: "foo", p: 3 }, 'right');
+				return dest.should.deep.equal([]);
+		});
+
+			it("with the delete overlapping before", function() {
+				const dest = [];
+				text._tc(dest, { d: "foobar", p: 3 }, { d: "abcfoo", p: 0 }, 'right');
+				return dest.should.deep.equal([{ d: "bar", p: 0 }]);
+		});
+
+			it("with the delete overlapping after", function() {
+				const dest = [];
+				text._tc(dest, { d: "abcfoo", p: 3 }, { d: "foobar", p: 6 });
+				return dest.should.deep.equal([{ d: "abc", p: 3 }]);
+		});
+
+			it("with the delete overlapping the whole delete", function() {
+				const dest = [];
+				text._tc(dest, { d: "abcfoo123", p: 3 }, { d: "foo", p: 6 });
+				return dest.should.deep.equal([{ d: "abc123", p: 3 }]);
+		});
+
+			return it("with the delete inside the whole delete", function() {
+				const dest = [];
+				text._tc(dest, { d: "foo", p: 6 }, { d: "abcfoo123", p: 3 });
+				return dest.should.deep.equal([]);
+		});
+	});
 	
-		describe "comment / insert", ->
-			it "with an insert before", ->
-				dest = []
-				text._tc(dest, { c: "foo", p: 9, @t }, { i: "bar", p: 3 })
-				dest.should.deep.equal [{ c: "foo", p: 12, @t }]
-
-			it "with an insert after", ->
-				dest = []
-				text._tc(dest, { c: "foo", p: 3, @t }, { i: "bar", p: 9 })
-				dest.should.deep.equal [{ c: "foo", p: 3, @t }]
-
-			it "with an insert at the left edge", ->
-				dest = []
-				text._tc(dest, { c: "foo", p: 3, @t }, { i: "bar", p: 3 })
-				# RangesTracker doesn't inject inserts into comments on edges, so neither should we
-				dest.should.deep.equal [{ c: "foo", p: 6, @t }]
-
-			it "with an insert at the right edge", ->
-				dest = []
-				text._tc(dest, { c: "foo", p: 3, @t }, { i: "bar", p: 6 })
-				# RangesTracker doesn't inject inserts into comments on edges, so neither should we
-				dest.should.deep.equal [{ c: "foo", p: 3, @t }]
-
-			it "with an insert in the middle", ->
-				dest = []
-				text._tc(dest, { c: "foo", p: 3, @t }, { i: "bar", p: 5 })
-				dest.should.deep.equal [{ c: "fobaro", p: 3, @t }]
+		describe("comment / insert", function() {
+			it("with an insert before", function() {
+				const dest = [];
+				text._tc(dest, { c: "foo", p: 9, t: this.t }, { i: "bar", p: 3 });
+				return dest.should.deep.equal([{ c: "foo", p: 12, t: this.t }]);
+		});
+
+			it("with an insert after", function() {
+				const dest = [];
+				text._tc(dest, { c: "foo", p: 3, t: this.t }, { i: "bar", p: 9 });
+				return dest.should.deep.equal([{ c: "foo", p: 3, t: this.t }]);
+		});
+
+			it("with an insert at the left edge", function() {
+				const dest = [];
+				text._tc(dest, { c: "foo", p: 3, t: this.t }, { i: "bar", p: 3 });
+				// RangesTracker doesn't inject inserts into comments on edges, so neither should we
+				return dest.should.deep.equal([{ c: "foo", p: 6, t: this.t }]);
+		});
+
+			it("with an insert at the right edge", function() {
+				const dest = [];
+				text._tc(dest, { c: "foo", p: 3, t: this.t }, { i: "bar", p: 6 });
+				// RangesTracker doesn't inject inserts into comments on edges, so neither should we
+				return dest.should.deep.equal([{ c: "foo", p: 3, t: this.t }]);
+		});
+
+			return it("with an insert in the middle", function() {
+				const dest = [];
+				text._tc(dest, { c: "foo", p: 3, t: this.t }, { i: "bar", p: 5 });
+				return dest.should.deep.equal([{ c: "fobaro", p: 3, t: this.t }]);
+		});
+	});
 	
-		describe "comment / delete", ->
-			it "with a delete before", ->
-				dest = []
-				text._tc(dest, { c: "foo", p: 9, @t }, { d: "bar", p: 3 })
-				dest.should.deep.equal [{ c: "foo", p: 6, @t }]
-
-			it "with a delete after", ->
-				dest = []
-				text._tc(dest, { c: "foo", p: 3, @t }, { i: "bar", p: 9 })
-				dest.should.deep.equal [{ c: "foo", p: 3, @t }]
-
-			it "with a delete overlapping the comment content before", ->
-				dest = []
-				text._tc(dest, { c: "foobar", p: 6, @t }, { d: "123foo", p: 3 })
-				dest.should.deep.equal [{ c: "bar", p: 3, @t }]
-
-			it "with a delete overlapping the comment content after", ->
-				dest = []
-				text._tc(dest, { c: "foobar", p: 6, @t }, { d: "bar123", p: 9 })
-				dest.should.deep.equal [{ c: "foo", p: 6, @t }]
-
-			it "with a delete overlapping the comment content in the middle", ->
-				dest = []
-				text._tc(dest, { c: "foo123bar", p: 6, @t }, { d: "123", p: 9 })
-				dest.should.deep.equal [{ c: "foobar", p: 6, @t }]
-
-			it "with a delete overlapping the whole comment", ->
-				dest = []
-				text._tc(dest, { c: "foo", p: 6, @t }, { d: "123foo456", p: 3 })
-				dest.should.deep.equal [{ c: "", p: 3, @t }]
+		describe("comment / delete", function() {
+			it("with a delete before", function() {
+				const dest = [];
+				text._tc(dest, { c: "foo", p: 9, t: this.t }, { d: "bar", p: 3 });
+				return dest.should.deep.equal([{ c: "foo", p: 6, t: this.t }]);
+		});
+
+			it("with a delete after", function() {
+				const dest = [];
+				text._tc(dest, { c: "foo", p: 3, t: this.t }, { i: "bar", p: 9 });
+				return dest.should.deep.equal([{ c: "foo", p: 3, t: this.t }]);
+		});
+
+			it("with a delete overlapping the comment content before", function() {
+				const dest = [];
+				text._tc(dest, { c: "foobar", p: 6, t: this.t }, { d: "123foo", p: 3 });
+				return dest.should.deep.equal([{ c: "bar", p: 3, t: this.t }]);
+		});
+
+			it("with a delete overlapping the comment content after", function() {
+				const dest = [];
+				text._tc(dest, { c: "foobar", p: 6, t: this.t }, { d: "bar123", p: 9 });
+				return dest.should.deep.equal([{ c: "foo", p: 6, t: this.t }]);
+		});
+
+			it("with a delete overlapping the comment content in the middle", function() {
+				const dest = [];
+				text._tc(dest, { c: "foo123bar", p: 6, t: this.t }, { d: "123", p: 9 });
+				return dest.should.deep.equal([{ c: "foobar", p: 6, t: this.t }]);
+		});
+
+			return it("with a delete overlapping the whole comment", function() {
+				const dest = [];
+				text._tc(dest, { c: "foo", p: 6, t: this.t }, { d: "123foo456", p: 3 });
+				return dest.should.deep.equal([{ c: "", p: 3, t: this.t }]);
+		});
+	});
 	
-		describe "comment / insert", ->
-			it "should not do anything", ->
-				dest = []
-				text._tc(dest, { i: "foo", p: 6 }, { c: "bar", p: 3 })
-				dest.should.deep.equal [{ i: "foo", p: 6 }]
+		describe("comment / insert", () => it("should not do anything", function() {
+            const dest = [];
+            text._tc(dest, { i: "foo", p: 6 }, { c: "bar", p: 3 });
+            return dest.should.deep.equal([{ i: "foo", p: 6 }]);
+    }));
 	
-		describe "comment / delete", ->
-			it "should not do anything", ->
-				dest = []
-				text._tc(dest, { d: "foo", p: 6 }, { c: "bar", p: 3 })
-				dest.should.deep.equal [{ d: "foo", p: 6 }]
+		describe("comment / delete", () => it("should not do anything", function() {
+            const dest = [];
+            text._tc(dest, { d: "foo", p: 6 }, { c: "bar", p: 3 });
+            return dest.should.deep.equal([{ d: "foo", p: 6 }]);
+    }));
 	
-		describe "comment / comment", ->
-			it "should not do anything", ->
-				dest = []
-				text._tc(dest, { c: "foo", p: 6 }, { c: "bar", p: 3 })
-				dest.should.deep.equal [{ c: "foo", p: 6 }]
+		return describe("comment / comment", () => it("should not do anything", function() {
+            const dest = [];
+            text._tc(dest, { c: "foo", p: 6 }, { c: "bar", p: 3 });
+            return dest.should.deep.equal([{ c: "foo", p: 6 }]);
+    }));
+});
 
-	describe "apply", ->
-		it "should apply an insert", ->
-			text.apply("foo", [{ i: "bar", p: 2 }]).should.equal "fobaro"
+	describe("apply", function() {
+		it("should apply an insert", () => text.apply("foo", [{ i: "bar", p: 2 }]).should.equal("fobaro"));
 
-		it "should apply a delete", ->
-			text.apply("foo123bar", [{ d: "123", p: 3 }]).should.equal "foobar"
+		it("should apply a delete", () => text.apply("foo123bar", [{ d: "123", p: 3 }]).should.equal("foobar"));
 
-		it "should do nothing with a comment", ->
-			text.apply("foo123bar", [{ c: "123", p: 3 }]).should.equal "foo123bar"
+		it("should do nothing with a comment", () => text.apply("foo123bar", [{ c: "123", p: 3 }]).should.equal("foo123bar"));
 		
-		it "should throw an error when deleted content does not match", ->
-			(() ->
-				text.apply("foo123bar", [{ d: "456", p: 3 }])
-			).should.throw(Error)
+		it("should throw an error when deleted content does not match", () => ((() => text.apply("foo123bar", [{ d: "456", p: 3 }]))).should.throw(Error));
 		
-		it "should throw an error when comment content does not match", ->
-			(() ->
-				text.apply("foo123bar", [{ c: "456", p: 3 }])
-			).should.throw(Error)
+		return it("should throw an error when comment content does not match", () => ((() => text.apply("foo123bar", [{ c: "456", p: 3 }]))).should.throw(Error));
+	});
 	
-	describe "applying ops and comments in different orders", ->
-		it "should not matter which op or comment is applied first", ->
-			transform = (op1, op2, side) ->
-				d = []
-				text._tc(d, op1, op2, side)
-				return d
-			
-			applySnapshot = (snapshot, op) ->
-				return text.apply(snapshot, op)
-			
-			applyRanges = (rangesTracker, ops) ->
-				for op in ops
-					rangesTracker.applyOp(op, {})
-				return rangesTracker
-			
-			commentsEqual = (comments1, comments2) ->
-				return false if comments1.length != comments2.length
-				comments1.sort (a,b) ->
-					if a.offset - b.offset == 0
-						return a.length - b.length
-					else
-						return a.offset - b.offset
-				comments2.sort (a,b) ->
-					if a.offset - b.offset == 0
-						return a.length - b.length
-					else
-						return a.offset - b.offset
-				for comment1, i in comments1
-					comment2 = comments2[i]
-					if comment1.offset != comment2.offset or comment1.length != comment2.length
-						return false
-				return true
-			
-			SNAPSHOT = "123"
-			
-			OPS = []
-			# Insert ops
-			for p in [0..SNAPSHOT.length]
-				OPS.push {i: "a", p: p}
-				OPS.push {i: "bc", p: p}
-			for p in [0..(SNAPSHOT.length-1)]
-				for length in [1..(SNAPSHOT.length - p)]
-					OPS.push {d: SNAPSHOT.slice(p, p+length), p}
-			for p in [0..(SNAPSHOT.length-1)]
-				for length in [1..(SNAPSHOT.length - p)]
-					OPS.push {c: SNAPSHOT.slice(p, p+length), p, @t}
-
-			for op1 in OPS
-				for op2 in OPS
-					op1_t = transform(op1, op2, "left")
-					op2_t = transform(op2, op1, "right")
-					
-					rt12 = new RangesTracker()
-					snapshot12 = applySnapshot(applySnapshot(SNAPSHOT, [op1]), op2_t)
-					applyRanges(rt12, [op1])
-					applyRanges(rt12, op2_t)
-					
-					rt21 = new RangesTracker()
-					snapshot21 = applySnapshot(applySnapshot(SNAPSHOT, [op2]), op1_t)
-					applyRanges(rt21, [op2])
-					applyRanges(rt21, op1_t)
-					
-					if snapshot12 != snapshot21
-						console.error {op1, op2, op1_t, op2_t, snapshot12, snapshot21}, "Ops are not consistent"
-						throw new Error("OT is inconsistent")
-					
-					if !commentsEqual(rt12.comments, rt21.comments)
-						console.log rt12.comments
-						console.log rt21.comments
-						console.error {op1, op2, op1_t, op2_t, rt12_comments: rt12.comments, rt21_comments: rt21.comments}, "Comments are not consistent"
-						throw new Error("OT is inconsistent")
+	return describe("applying ops and comments in different orders", () => it("should not matter which op or comment is applied first", function() {
+        let length, p;
+        let asc, end;
+        let asc1, end1;
+        let asc3, end3;
+        const transform = function(op1, op2, side) {
+            const d = [];
+            text._tc(d, op1, op2, side);
+            return d;
+        };
+        
+        const applySnapshot = (snapshot, op) => text.apply(snapshot, op);
+        
+        const applyRanges = function(rangesTracker, ops) {
+            for (let op of Array.from(ops)) {
+                rangesTracker.applyOp(op, {});
+            }
+            return rangesTracker;
+        };
+        
+        const commentsEqual = function(comments1, comments2) {
+            if (comments1.length !== comments2.length) { return false; }
+            comments1.sort(function(a,b) {
+                if ((a.offset - b.offset) === 0) {
+                    return a.length - b.length;
+                } else {
+                    return a.offset - b.offset;
+                }
+            });
+            comments2.sort(function(a,b) {
+                if ((a.offset - b.offset) === 0) {
+                    return a.length - b.length;
+                } else {
+                    return a.offset - b.offset;
+                }
+            });
+            for (let i = 0; i < comments1.length; i++) {
+                const comment1 = comments1[i];
+                const comment2 = comments2[i];
+                if ((comment1.offset !== comment2.offset) || (comment1.length !== comment2.length)) {
+                    return false;
+                }
+            }
+            return true;
+        };
+        
+        const SNAPSHOT = "123";
+        
+        const OPS = [];
+        // Insert ops
+        for (p = 0, end = SNAPSHOT.length, asc = 0 <= end; asc ? p <= end : p >= end; asc ? p++ : p--) {
+            OPS.push({i: "a", p});
+            OPS.push({i: "bc", p});
+        }
+        for (p = 0, end1 = SNAPSHOT.length-1, asc1 = 0 <= end1; asc1 ? p <= end1 : p >= end1; asc1 ? p++ : p--) {
+            var asc2, end2;
+            for (length = 1, end2 = SNAPSHOT.length - p, asc2 = 1 <= end2; asc2 ? length <= end2 : length >= end2; asc2 ? length++ : length--) {
+                OPS.push({d: SNAPSHOT.slice(p, p+length), p});
+            }
+        }
+        for (p = 0, end3 = SNAPSHOT.length-1, asc3 = 0 <= end3; asc3 ? p <= end3 : p >= end3; asc3 ? p++ : p--) {
+            var asc4, end4;
+            for (length = 1, end4 = SNAPSHOT.length - p, asc4 = 1 <= end4; asc4 ? length <= end4 : length >= end4; asc4 ? length++ : length--) {
+                OPS.push({c: SNAPSHOT.slice(p, p+length), p, t: this.t});
+            }
+        }
+
+        return (() => {
+            const result = [];
+            for (var op1 of Array.from(OPS)) {
+                result.push((() => {
+                    const result1 = [];
+                    for (let op2 of Array.from(OPS)) {
+                        const op1_t = transform(op1, op2, "left");
+                        const op2_t = transform(op2, op1, "right");
+                
+                        const rt12 = new RangesTracker();
+                        const snapshot12 = applySnapshot(applySnapshot(SNAPSHOT, [op1]), op2_t);
+                        applyRanges(rt12, [op1]);
+                        applyRanges(rt12, op2_t);
+                
+                        const rt21 = new RangesTracker();
+                        const snapshot21 = applySnapshot(applySnapshot(SNAPSHOT, [op2]), op1_t);
+                        applyRanges(rt21, [op2]);
+                        applyRanges(rt21, op1_t);
+                
+                        if (snapshot12 !== snapshot21) {
+                            console.error({op1, op2, op1_t, op2_t, snapshot12, snapshot21}, "Ops are not consistent");
+                            throw new Error("OT is inconsistent");
+                        }
+                
+                        if (!commentsEqual(rt12.comments, rt21.comments)) {
+                            console.log(rt12.comments);
+                            console.log(rt21.comments);
+                            console.error({op1, op2, op1_t, op2_t, rt12_comments: rt12.comments, rt21_comments: rt21.comments}, "Comments are not consistent");
+                            throw new Error("OT is inconsistent");
+                        } else {
+                            result1.push(undefined);
+                        }
+                    }
+                    return result1;
+                })());
+            }
+            return result;
+        })();
+    }));
+});

+ 127 - 91
services/document-updater/test/unit/coffee/ShareJsDB/ShareJsDBTests.js

@@ -1,93 +1,129 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-expect = chai.expect
-modulePath = "../../../../app/js/ShareJsDB.js"
-SandboxedModule = require('sandboxed-module')
-Errors = require "../../../../app/js/Errors"
-
-describe "ShareJsDB", ->
-	beforeEach ->
-		@doc_id = "document-id"
-		@project_id = "project-id"
-		@doc_key = "#{@project_id}:#{@doc_id}"
-		@callback = sinon.stub()
-		@ShareJsDB = SandboxedModule.require modulePath, requires:
-			"./RedisManager": @RedisManager = {}
-
-		@version = 42
-		@lines = ["one", "two", "three"]
-		@db = new @ShareJsDB(@project_id, @doc_id, @lines, @version)
-
-	describe "getSnapshot", ->
-		describe "successfully", ->
-			beforeEach ->
-				@db.getSnapshot @doc_key, @callback
-
-			it "should return the doc lines", ->
-				@callback.args[0][1].snapshot.should.equal @lines.join("\n")
-
-			it "should return the doc version", ->
-				@callback.args[0][1].v.should.equal @version
-
-			it "should return the type as text", ->
-				@callback.args[0][1].type.should.equal "text"
-
-		describe "when the key does not match", ->
-			beforeEach ->
-				@db.getSnapshot "bad:key", @callback
-
-			it "should return the callback with a NotFoundError", ->
-				@callback.calledWith(new Errors.NotFoundError("not found")).should.equal true
-
-	describe "getOps", ->
-		describe "with start == end", ->
-			beforeEach ->
-				@start = @end = 42
-				@db.getOps @doc_key, @start, @end, @callback
-
-			it "should return an empty array", ->
-				@callback.calledWith(null, []).should.equal true
+/*
+ * 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');
+const should = chai.should();
+const {
+    expect
+} = chai;
+const modulePath = "../../../../app/js/ShareJsDB.js";
+const SandboxedModule = require('sandboxed-module');
+const Errors = require("../../../../app/js/Errors");
+
+describe("ShareJsDB", function() {
+	beforeEach(function() {
+		this.doc_id = "document-id";
+		this.project_id = "project-id";
+		this.doc_key = `${this.project_id}:${this.doc_id}`;
+		this.callback = sinon.stub();
+		this.ShareJsDB = SandboxedModule.require(modulePath, { requires: {
+			"./RedisManager": (this.RedisManager = {})
+		}
+	});
+
+		this.version = 42;
+		this.lines = ["one", "two", "three"];
+		return this.db = new this.ShareJsDB(this.project_id, this.doc_id, this.lines, this.version);
+	});
+
+	describe("getSnapshot", function() {
+		describe("successfully", function() {
+			beforeEach(function() {
+				return this.db.getSnapshot(this.doc_key, this.callback);
+			});
+
+			it("should return the doc lines", function() {
+				return this.callback.args[0][1].snapshot.should.equal(this.lines.join("\n"));
+			});
+
+			it("should return the doc version", function() {
+				return this.callback.args[0][1].v.should.equal(this.version);
+			});
+
+			return it("should return the type as text", function() {
+				return this.callback.args[0][1].type.should.equal("text");
+			});
+		});
+
+		return describe("when the key does not match", function() {
+			beforeEach(function() {
+				return this.db.getSnapshot("bad:key", this.callback);
+			});
+
+			return it("should return the callback with a NotFoundError", function() {
+				return this.callback.calledWith(new Errors.NotFoundError("not found")).should.equal(true);
+			});
+		});
+	});
+
+	describe("getOps", function() {
+		describe("with start == end", function() {
+			beforeEach(function() {
+				this.start = (this.end = 42);
+				return this.db.getOps(this.doc_key, this.start, this.end, this.callback);
+			});
+
+			return it("should return an empty array", function() {
+				return this.callback.calledWith(null, []).should.equal(true);
+			});
+		});
 		
-		describe "with a non empty range", ->
-			beforeEach ->
-				@start = 35
-				@end = 42
-				@RedisManager.getPreviousDocOps = sinon.stub().callsArgWith(3, null, @ops)
-				@db.getOps @doc_key, @start, @end, @callback
-
-			it "should get the range from redis", ->
-				@RedisManager.getPreviousDocOps
-					.calledWith(@doc_id, @start, @end-1)
-					.should.equal true
-
-			it "should return the ops", ->
-				@callback.calledWith(null, @ops).should.equal true
-
-		describe "with no specified end", ->
-			beforeEach ->
-				@start = 35
-				@end = null
-				@RedisManager.getPreviousDocOps = sinon.stub().callsArgWith(3, null, @ops)
-				@db.getOps @doc_key, @start, @end, @callback
+		describe("with a non empty range", function() {
+			beforeEach(function() {
+				this.start = 35;
+				this.end = 42;
+				this.RedisManager.getPreviousDocOps = sinon.stub().callsArgWith(3, null, this.ops);
+				return this.db.getOps(this.doc_key, this.start, this.end, this.callback);
+			});
+
+			it("should get the range from redis", function() {
+				return this.RedisManager.getPreviousDocOps
+					.calledWith(this.doc_id, this.start, this.end-1)
+					.should.equal(true);
+			});
+
+			return it("should return the ops", function() {
+				return this.callback.calledWith(null, this.ops).should.equal(true);
+			});
+		});
+
+		return describe("with no specified end", function() {
+			beforeEach(function() {
+				this.start = 35;
+				this.end = null;
+				this.RedisManager.getPreviousDocOps = sinon.stub().callsArgWith(3, null, this.ops);
+				return this.db.getOps(this.doc_key, this.start, this.end, this.callback);
+			});
 			
-			it "should get until the end of the list", ->
-				@RedisManager.getPreviousDocOps
-					.calledWith(@doc_id, @start, -1)
-					.should.equal true
-
-	describe "writeOps", ->
-		describe "writing an op", ->
-			beforeEach ->
-				@opData =
-					op: {p: 20, t: "foo"}
-					meta: {source: "bar"}
-					v: @version
-				@db.writeOp @doc_key, @opData, @callback
-
-			it "should write into appliedOps", ->
-				expect(@db.appliedOps[@doc_key]).to.deep.equal [@opData]
-
-			it "should call the callback without an error", ->
-				@callback.called.should.equal true
-				(@callback.args[0][0]?).should.equal false
+			return it("should get until the end of the list", function() {
+				return this.RedisManager.getPreviousDocOps
+					.calledWith(this.doc_id, this.start, -1)
+					.should.equal(true);
+			});
+		});
+	});
+
+	return describe("writeOps", () => describe("writing an op", function() {
+        beforeEach(function() {
+            this.opData = {
+                op: {p: 20, t: "foo"},
+                meta: {source: "bar"},
+                v: this.version
+            };
+            return this.db.writeOp(this.doc_key, this.opData, this.callback);
+        });
+
+        it("should write into appliedOps", function() {
+            return expect(this.db.appliedOps[this.doc_key]).to.deep.equal([this.opData]);
+    });
+
+        return it("should call the callback without an error", function() {
+            this.callback.called.should.equal(true);
+            return (this.callback.args[0][0] != null).should.equal(false);
+        });
+    }));
+});

+ 178 - 128
services/document-updater/test/unit/coffee/ShareJsUpdateManager/ShareJsUpdateManagerTests.js

@@ -1,131 +1,181 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-modulePath = "../../../../app/js/ShareJsUpdateManager.js"
-SandboxedModule = require('sandboxed-module')
-crypto = require('crypto')
-
-describe "ShareJsUpdateManager", ->
-	beforeEach ->
-		@project_id = "project-id-123"
-		@doc_id = "document-id-123"
-		@callback = sinon.stub()
-		@ShareJsUpdateManager = SandboxedModule.require modulePath,
-			requires:
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require('sinon');
+const chai = require('chai');
+const should = chai.should();
+const modulePath = "../../../../app/js/ShareJsUpdateManager.js";
+const SandboxedModule = require('sandboxed-module');
+const crypto = require('crypto');
+
+describe("ShareJsUpdateManager", function() {
+	beforeEach(function() {
+		let Model;
+		this.project_id = "project-id-123";
+		this.doc_id = "document-id-123";
+		this.callback = sinon.stub();
+		return this.ShareJsUpdateManager = SandboxedModule.require(modulePath, {
+			requires: {
 				"./sharejs/server/model":
-					class Model
-						constructor: (@db) ->
-				"./ShareJsDB" : @ShareJsDB = { mockDB: true }
-				"redis-sharelatex" : createClient: () => @rclient = auth:->
-				"logger-sharelatex": @logger = { log: sinon.stub() }
-				"./RealTimeRedisManager": @RealTimeRedisManager = {}
-				"./Metrics": @metrics = { inc: sinon.stub() }
-			globals:
-				clearTimeout: @clearTimeout = sinon.stub()
-
-	describe "applyUpdate", ->
-		beforeEach ->
-			@lines = ["one", "two"]
-			@version = 34
-			@updatedDocLines = ["onefoo", "two"]
-			content = @updatedDocLines.join("\n")
-			@hash = crypto.createHash('sha1').update("blob " + content.length + "\x00").update(content, 'utf8').digest('hex')
-			@update = {p: 4, t: "foo", v:@version, hash:@hash}
-			@model =
-				applyOp: sinon.stub().callsArg(2)
-				getSnapshot: sinon.stub()
-				db:
+					(Model = class Model {
+						constructor(db) {
+							this.db = db;
+						}
+					}),
+				"./ShareJsDB" : (this.ShareJsDB = { mockDB: true }),
+				"redis-sharelatex" : { createClient: () => { return this.rclient = {auth() {}}; }
+			},
+				"logger-sharelatex": (this.logger = { log: sinon.stub() }),
+				"./RealTimeRedisManager": (this.RealTimeRedisManager = {}),
+				"./Metrics": (this.metrics = { inc: sinon.stub() })
+			},
+			globals: {
+				clearTimeout: (this.clearTimeout = sinon.stub())
+			}
+		}
+		);
+	});
+
+	describe("applyUpdate", function() {
+		beforeEach(function() {
+			this.lines = ["one", "two"];
+			this.version = 34;
+			this.updatedDocLines = ["onefoo", "two"];
+			const content = this.updatedDocLines.join("\n");
+			this.hash = crypto.createHash('sha1').update("blob " + content.length + "\x00").update(content, 'utf8').digest('hex');
+			this.update = {p: 4, t: "foo", v:this.version, hash:this.hash};
+			this.model = {
+				applyOp: sinon.stub().callsArg(2),
+				getSnapshot: sinon.stub(),
+				db: {
 					appliedOps: {}
-			@ShareJsUpdateManager.getNewShareJsModel = sinon.stub().returns(@model)
-			@ShareJsUpdateManager._listenForOps = sinon.stub()
-			@ShareJsUpdateManager.removeDocFromCache = sinon.stub().callsArg(1)
-
-		describe "successfully", ->
-			beforeEach (done) ->
-				@model.getSnapshot.callsArgWith(1, null, {snapshot: @updatedDocLines.join("\n"), v: @version})
-				@model.db.appliedOps["#{@project_id}:#{@doc_id}"] = @appliedOps = ["mock-ops"]
-				@ShareJsUpdateManager.applyUpdate @project_id, @doc_id, @update, @lines, @version, (err, docLines, version, appliedOps) =>
-					@callback(err, docLines, version, appliedOps)
-					done()
-
-			it "should create a new ShareJs model", ->
-				@ShareJsUpdateManager.getNewShareJsModel
-					.calledWith(@project_id, @doc_id, @lines, @version)
-					.should.equal true
-
-			it "should listen for ops on the model", ->
-				@ShareJsUpdateManager._listenForOps
-					.calledWith(@model)
-					.should.equal true
-
-			it "should send the update to ShareJs", ->
-				@model.applyOp
-					.calledWith("#{@project_id}:#{@doc_id}", @update)
-					.should.equal true
-
-			it "should get the updated doc lines", ->
-				@model.getSnapshot
-					.calledWith("#{@project_id}:#{@doc_id}")
-					.should.equal true
-
-			it "should return the updated doc lines, version and ops", ->
-				@callback.calledWith(null, @updatedDocLines, @version, @appliedOps).should.equal true
-
-		describe "when applyOp fails", ->
-			beforeEach (done) ->
-				@error = new Error("Something went wrong")
-				@model.applyOp = sinon.stub().callsArgWith(2, @error)
-				@ShareJsUpdateManager.applyUpdate @project_id, @doc_id, @update, @lines, @version, (err, docLines, version) =>
-					@callback(err, docLines, version)
-					done()
-
-			it "should call the callback with the error", ->
-				@callback.calledWith(@error).should.equal true
-
-		describe "when getSnapshot fails", ->
-			beforeEach (done) ->
-				@error = new Error("Something went wrong")
-				@model.getSnapshot.callsArgWith(1, @error)
-				@ShareJsUpdateManager.applyUpdate @project_id, @doc_id, @update, @lines, @version, (err, docLines, version) =>
-					@callback(err, docLines, version)
-					done()
-
-			it "should call the callback with the error", ->
-				@callback.calledWith(@error).should.equal true
-
-		describe "with an invalid hash", ->
-			beforeEach (done) ->
-				@error = new Error("invalid hash")
-				@model.getSnapshot.callsArgWith(1, null, {snapshot: "unexpected content", v: @version})
-				@model.db.appliedOps["#{@project_id}:#{@doc_id}"] = @appliedOps = ["mock-ops"]
-				@ShareJsUpdateManager.applyUpdate @project_id, @doc_id, @update, @lines, @version, (err, docLines, version, appliedOps) =>
-					@callback(err, docLines, version, appliedOps)
-					done()
-
-			it "should call the callback with the error", ->
-				@callback.calledWith(@error).should.equal true
-
-	describe "_listenForOps", ->
-		beforeEach ->
-			@model = on: (event, callback) =>
-				@callback = callback
-			sinon.spy @model, "on"
-			@ShareJsUpdateManager._listenForOps(@model)
-
-		it "should listen to the model for updates", ->
-			@model.on.calledWith("applyOp")
-				.should.equal true
-
-		describe "the callback", ->
-			beforeEach ->
-				@opData =
-					op: {t: "foo", p: 1}
-					meta: source: "bar"
-				@RealTimeRedisManager.sendData = sinon.stub()
-				@callback("#{@project_id}:#{@doc_id}", @opData)
-
-			it "should publish the op to redis", ->
-				@RealTimeRedisManager.sendData
-					.calledWith({project_id: @project_id, doc_id: @doc_id, op: @opData})
-					.should.equal true
+				}
+			};
+			this.ShareJsUpdateManager.getNewShareJsModel = sinon.stub().returns(this.model);
+			this.ShareJsUpdateManager._listenForOps = sinon.stub();
+			return this.ShareJsUpdateManager.removeDocFromCache = sinon.stub().callsArg(1);
+		});
+
+		describe("successfully", function() {
+			beforeEach(function(done) {
+				this.model.getSnapshot.callsArgWith(1, null, {snapshot: this.updatedDocLines.join("\n"), v: this.version});
+				this.model.db.appliedOps[`${this.project_id}:${this.doc_id}`] = (this.appliedOps = ["mock-ops"]);
+				return this.ShareJsUpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.lines, this.version, (err, docLines, version, appliedOps) => {
+					this.callback(err, docLines, version, appliedOps);
+					return done();
+				});
+			});
+
+			it("should create a new ShareJs model", function() {
+				return this.ShareJsUpdateManager.getNewShareJsModel
+					.calledWith(this.project_id, this.doc_id, this.lines, this.version)
+					.should.equal(true);
+			});
+
+			it("should listen for ops on the model", function() {
+				return this.ShareJsUpdateManager._listenForOps
+					.calledWith(this.model)
+					.should.equal(true);
+			});
+
+			it("should send the update to ShareJs", function() {
+				return this.model.applyOp
+					.calledWith(`${this.project_id}:${this.doc_id}`, this.update)
+					.should.equal(true);
+			});
+
+			it("should get the updated doc lines", function() {
+				return this.model.getSnapshot
+					.calledWith(`${this.project_id}:${this.doc_id}`)
+					.should.equal(true);
+			});
+
+			return it("should return the updated doc lines, version and ops", function() {
+				return this.callback.calledWith(null, this.updatedDocLines, this.version, this.appliedOps).should.equal(true);
+			});
+		});
+
+		describe("when applyOp fails", function() {
+			beforeEach(function(done) {
+				this.error = new Error("Something went wrong");
+				this.model.applyOp = sinon.stub().callsArgWith(2, this.error);
+				return this.ShareJsUpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.lines, this.version, (err, docLines, version) => {
+					this.callback(err, docLines, version);
+					return done();
+				});
+			});
+
+			return it("should call the callback with the error", function() {
+				return this.callback.calledWith(this.error).should.equal(true);
+			});
+		});
+
+		describe("when getSnapshot fails", function() {
+			beforeEach(function(done) {
+				this.error = new Error("Something went wrong");
+				this.model.getSnapshot.callsArgWith(1, this.error);
+				return this.ShareJsUpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.lines, this.version, (err, docLines, version) => {
+					this.callback(err, docLines, version);
+					return done();
+				});
+			});
+
+			return it("should call the callback with the error", function() {
+				return this.callback.calledWith(this.error).should.equal(true);
+			});
+		});
+
+		return describe("with an invalid hash", function() {
+			beforeEach(function(done) {
+				this.error = new Error("invalid hash");
+				this.model.getSnapshot.callsArgWith(1, null, {snapshot: "unexpected content", v: this.version});
+				this.model.db.appliedOps[`${this.project_id}:${this.doc_id}`] = (this.appliedOps = ["mock-ops"]);
+				return this.ShareJsUpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.lines, this.version, (err, docLines, version, appliedOps) => {
+					this.callback(err, docLines, version, appliedOps);
+					return done();
+				});
+			});
+
+			return it("should call the callback with the error", function() {
+				return this.callback.calledWith(this.error).should.equal(true);
+			});
+		});
+	});
+
+	return describe("_listenForOps", function() {
+		beforeEach(function() {
+			this.model = { on: (event, callback) => {
+				return this.callback = callback;
+			}
+		};
+			sinon.spy(this.model, "on");
+			return this.ShareJsUpdateManager._listenForOps(this.model);
+		});
+
+		it("should listen to the model for updates", function() {
+			return this.model.on.calledWith("applyOp")
+				.should.equal(true);
+		});
+
+		return describe("the callback", function() {
+			beforeEach(function() {
+				this.opData = {
+					op: {t: "foo", p: 1},
+					meta: { source: "bar"
+				}
+				};
+				this.RealTimeRedisManager.sendData = sinon.stub();
+				return this.callback(`${this.project_id}:${this.doc_id}`, this.opData);
+			});
+
+			return it("should publish the op to redis", function() {
+				return this.RealTimeRedisManager.sendData
+					.calledWith({project_id: this.project_id, doc_id: this.doc_id, op: this.opData})
+					.should.equal(true);
+			});
+		});
+	});
+});
 

+ 474 - 362
services/document-updater/test/unit/coffee/UpdateManager/UpdateManagerTests.js

@@ -1,369 +1,481 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-modulePath = "../../../../app/js/UpdateManager.js"
-SandboxedModule = require('sandboxed-module')
-
-describe "UpdateManager", ->
-	beforeEach ->
-		@project_id = "project-id-123"
-		@projectHistoryId = "history-id-123"
-		@doc_id = "document-id-123"
-		@callback = sinon.stub()
-		@UpdateManager = SandboxedModule.require modulePath, requires:
-			"./LockManager" : @LockManager = {}
-			"./RedisManager" : @RedisManager = {}
-			"./RealTimeRedisManager" : @RealTimeRedisManager = {}
-			"./ShareJsUpdateManager" : @ShareJsUpdateManager = {}
-			"./HistoryManager" : @HistoryManager = {}
-			"logger-sharelatex": @logger = { log: sinon.stub() }
-			"./Metrics": @Metrics =
-				Timer: class Timer
-					done: sinon.stub()
-			"settings-sharelatex": @Settings = {}
-			"./DocumentManager": @DocumentManager = {}
-			"./RangesManager": @RangesManager = {}
-			"./SnapshotManager": @SnapshotManager = {}
-			"./Profiler": class Profiler
-				log: sinon.stub().returns { end: sinon.stub() }
-				end: sinon.stub()
-
-	describe "processOutstandingUpdates", ->
-		beforeEach ->
-			@UpdateManager.fetchAndApplyUpdates = sinon.stub().callsArg(2)
-			@UpdateManager.processOutstandingUpdates @project_id, @doc_id, @callback
-
-		it "should apply the updates", ->
-			@UpdateManager.fetchAndApplyUpdates.calledWith(@project_id, @doc_id).should.equal true
-
-		it "should call the callback", ->
-			@callback.called.should.equal true
-
-		it "should time the execution", ->
-			@Metrics.Timer::done.called.should.equal true
-
-	describe "processOutstandingUpdatesWithLock", ->
-		describe "when the lock is free", ->
-			beforeEach ->
-				@LockManager.tryLock = sinon.stub().callsArgWith(1, null, true, @lockValue = "mock-lock-value")
-				@LockManager.releaseLock = sinon.stub().callsArg(2)
-				@UpdateManager.continueProcessingUpdatesWithLock = sinon.stub().callsArg(2)
-				@UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2)
-
-			describe "successfully", ->
-				beforeEach ->
-					@UpdateManager.processOutstandingUpdatesWithLock @project_id, @doc_id, @callback
-
-				it "should acquire the lock", ->
-					@LockManager.tryLock.calledWith(@doc_id).should.equal true
-
-				it "should free the lock", ->
-					@LockManager.releaseLock.calledWith(@doc_id, @lockValue).should.equal true
-
-				it "should process the outstanding updates", ->
-					@UpdateManager.processOutstandingUpdates.calledWith(@project_id, @doc_id).should.equal true
-
-				it "should do everything with the lock acquired", ->
-					@UpdateManager.processOutstandingUpdates.calledAfter(@LockManager.tryLock).should.equal true
-					@UpdateManager.processOutstandingUpdates.calledBefore(@LockManager.releaseLock).should.equal true
-
-				it "should continue processing new updates that may have come in", ->
-					@UpdateManager.continueProcessingUpdatesWithLock.calledWith(@project_id, @doc_id).should.equal true
-
-				it "should return the callback", ->
-					@callback.called.should.equal true
-
-			describe "when processOutstandingUpdates returns an error", ->
-				beforeEach ->
-					@UpdateManager.processOutstandingUpdates = sinon.stub().callsArgWith(2, @error = new Error("Something went wrong"))
-					@UpdateManager.processOutstandingUpdatesWithLock @project_id, @doc_id, @callback
-
-				it "should free the lock", ->
-					@LockManager.releaseLock.calledWith(@doc_id, @lockValue).should.equal true
-
-				it "should return the error in the callback", ->
-					@callback.calledWith(@error).should.equal true
-
-		describe "when the lock is taken", ->
-			beforeEach ->
-				@LockManager.tryLock = sinon.stub().callsArgWith(1, null, false)
-				@UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2)
-				@UpdateManager.processOutstandingUpdatesWithLock @project_id, @doc_id, @callback
-
-			it "should return the callback", ->
-				@callback.called.should.equal true
-
-			it "should not process the updates", ->
-				@UpdateManager.processOutstandingUpdates.called.should.equal false
-
-	describe "continueProcessingUpdatesWithLock", ->
-		describe "when there are outstanding updates", ->
-			beforeEach ->
-				@RealTimeRedisManager.getUpdatesLength = sinon.stub().callsArgWith(1, null, 3)
-				@UpdateManager.processOutstandingUpdatesWithLock = sinon.stub().callsArg(2)
-				@UpdateManager.continueProcessingUpdatesWithLock @project_id, @doc_id, @callback
-
-			it "should process the outstanding updates", ->
-				@UpdateManager.processOutstandingUpdatesWithLock.calledWith(@project_id, @doc_id).should.equal true
-
-			it "should return the callback", ->
-				@callback.called.should.equal true
-
-		describe "when there are no outstanding updates", ->
-			beforeEach ->
-				@RealTimeRedisManager.getUpdatesLength = sinon.stub().callsArgWith(1, null, 0)
-				@UpdateManager.processOutstandingUpdatesWithLock = sinon.stub().callsArg(2)
-				@UpdateManager.continueProcessingUpdatesWithLock @project_id, @doc_id, @callback
-
-			it "should not try to process the outstanding updates", ->
-				@UpdateManager.processOutstandingUpdatesWithLock.called.should.equal false
-
-			it "should return the callback", ->
-				@callback.called.should.equal true
-
-	describe "fetchAndApplyUpdates", ->
-		describe "with updates", ->
-			beforeEach ->
-				@updates = [{p: 1, t: "foo"}]
-				@updatedDocLines = ["updated", "lines"]
-				@version = 34
-				@RealTimeRedisManager.getPendingUpdatesForDoc = sinon.stub().callsArgWith(1, null, @updates)
-				@UpdateManager.applyUpdate = sinon.stub().callsArgWith(3, null, @updatedDocLines, @version)
-				@UpdateManager.fetchAndApplyUpdates @project_id, @doc_id, @callback
-
-			it "should get the pending updates", ->
-				@RealTimeRedisManager.getPendingUpdatesForDoc.calledWith(@doc_id).should.equal true
-
-			it "should apply the updates", ->
-				for update in @updates
-					@UpdateManager.applyUpdate
-						.calledWith(@project_id, @doc_id, update)
-						.should.equal true
-
-			it "should call the callback", ->
-				@callback.called.should.equal true
-
-		describe "when there are no updates", ->
-			beforeEach ->
-				@updates = []
-				@RealTimeRedisManager.getPendingUpdatesForDoc = sinon.stub().callsArgWith(1, null, @updates)
-				@UpdateManager.applyUpdate = sinon.stub()
-				@RedisManager.setDocument = sinon.stub()
-				@UpdateManager.fetchAndApplyUpdates @project_id, @doc_id, @callback
-
-			it "should not call applyUpdate", ->
-				@UpdateManager.applyUpdate.called.should.equal false
-
-			it "should call the callback", ->
-				@callback.called.should.equal true
-
-	describe "applyUpdate", ->
-		beforeEach ->
-			@updateMeta = { user_id: 'last-author-fake-id' }
-			@update = {op: [{p: 42, i: "foo"}], meta: @updateMeta}
-			@updatedDocLines = ["updated", "lines"]
-			@version = 34
-			@lines = ["original", "lines"]
-			@ranges = { entries: "mock", comments: "mock" }
-			@updated_ranges = { entries: "updated", comments: "updated" }
-			@appliedOps = [ {v: 42, op: "mock-op-42"}, { v: 45, op: "mock-op-45" }]
-			@doc_ops_length = sinon.stub()
-			@project_ops_length = sinon.stub()
-			@pathname = '/a/b/c.tex'
-			@DocumentManager.getDoc = sinon.stub().yields(null, @lines, @version, @ranges, @pathname, @projectHistoryId)
-			@RangesManager.applyUpdate = sinon.stub().yields(null, @updated_ranges, false)
-			@ShareJsUpdateManager.applyUpdate = sinon.stub().yields(null, @updatedDocLines, @version, @appliedOps)
-			@RedisManager.updateDocument = sinon.stub().yields(null, @doc_ops_length, @project_ops_length)
-			@RealTimeRedisManager.sendData = sinon.stub()
-			@UpdateManager._addProjectHistoryMetadataToOps = sinon.stub()
-			@HistoryManager.recordAndFlushHistoryOps = sinon.stub().callsArg(5)
-
-		describe "normally", ->
-			beforeEach ->
-				@UpdateManager.applyUpdate @project_id, @doc_id, @update, @callback
-
-			it "should apply the updates via ShareJS", ->
-				@ShareJsUpdateManager.applyUpdate
-					.calledWith(@project_id, @doc_id, @update, @lines, @version)
-					.should.equal true
-
-			it "should update the ranges", ->
-				@RangesManager.applyUpdate
-					.calledWith(@project_id, @doc_id, @ranges, @appliedOps, @updatedDocLines)
-					.should.equal true
-
-			it "should save the document", ->
-				@RedisManager.updateDocument
-					.calledWith(@project_id, @doc_id, @updatedDocLines, @version, @appliedOps, @updated_ranges, @updateMeta)
-					.should.equal true
-
-			it "should add metadata to the ops" , ->
-				@UpdateManager._addProjectHistoryMetadataToOps
-					.calledWith(@appliedOps, @pathname, @projectHistoryId, @lines)
-					.should.equal true
-
-			it "should push the applied ops into the history queue", ->
-				@HistoryManager.recordAndFlushHistoryOps
-					.calledWith(@project_id, @doc_id, @appliedOps, @doc_ops_length, @project_ops_length)
-					.should.equal true
-
-			it "should call the callback", ->
-				@callback.called.should.equal true
-
-		describe "with UTF-16 surrogate pairs in the update", ->
-			beforeEach ->
-				@update = {op: [{p: 42, i: "\uD835\uDC00"}]}
-				@UpdateManager.applyUpdate @project_id, @doc_id, @update, @callback
-
-			it "should apply the update but with surrogate pairs removed", ->
-				@ShareJsUpdateManager.applyUpdate
-					.calledWith(@project_id, @doc_id, @update)
-					.should.equal true
-
-				# \uFFFD is 'replacement character'
-				@update.op[0].i.should.equal "\uFFFD\uFFFD"
-
-		describe "with an error", ->
-			beforeEach ->
-				@error = new Error("something went wrong")
-				@ShareJsUpdateManager.applyUpdate = sinon.stub().yields(@error)
-				@UpdateManager.applyUpdate @project_id, @doc_id, @update, @callback
-
-			it "should call RealTimeRedisManager.sendData with the error", ->
-				@RealTimeRedisManager.sendData
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS206: Consider reworking classes to avoid initClass
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require('sinon');
+const chai = require('chai');
+const should = chai.should();
+const modulePath = "../../../../app/js/UpdateManager.js";
+const SandboxedModule = require('sandboxed-module');
+
+describe("UpdateManager", function() {
+	beforeEach(function() {
+		let Profiler, Timer;
+		this.project_id = "project-id-123";
+		this.projectHistoryId = "history-id-123";
+		this.doc_id = "document-id-123";
+		this.callback = sinon.stub();
+		return this.UpdateManager = SandboxedModule.require(modulePath, { requires: {
+			"./LockManager" : (this.LockManager = {}),
+			"./RedisManager" : (this.RedisManager = {}),
+			"./RealTimeRedisManager" : (this.RealTimeRedisManager = {}),
+			"./ShareJsUpdateManager" : (this.ShareJsUpdateManager = {}),
+			"./HistoryManager" : (this.HistoryManager = {}),
+			"logger-sharelatex": (this.logger = { log: sinon.stub() }),
+			"./Metrics": (this.Metrics = {
+				Timer: (Timer = (function() {
+					Timer = class Timer {
+						static initClass() {
+							this.prototype.done = sinon.stub();
+						}
+					};
+					Timer.initClass();
+					return Timer;
+				})())
+			}),
+			"settings-sharelatex": (this.Settings = {}),
+			"./DocumentManager": (this.DocumentManager = {}),
+			"./RangesManager": (this.RangesManager = {}),
+			"./SnapshotManager": (this.SnapshotManager = {}),
+			"./Profiler": (Profiler = (function() {
+				Profiler = class Profiler {
+					static initClass() {
+						this.prototype.log = sinon.stub().returns({ end: sinon.stub() });
+						this.prototype.end = sinon.stub();
+					}
+				};
+				Profiler.initClass();
+				return Profiler;
+			})())
+		}
+	}
+		);
+	});
+
+	describe("processOutstandingUpdates", function() {
+		beforeEach(function() {
+			this.UpdateManager.fetchAndApplyUpdates = sinon.stub().callsArg(2);
+			return this.UpdateManager.processOutstandingUpdates(this.project_id, this.doc_id, this.callback);
+		});
+
+		it("should apply the updates", function() {
+			return this.UpdateManager.fetchAndApplyUpdates.calledWith(this.project_id, this.doc_id).should.equal(true);
+		});
+
+		it("should call the callback", function() {
+			return this.callback.called.should.equal(true);
+		});
+
+		return it("should time the execution", function() {
+			return this.Metrics.Timer.prototype.done.called.should.equal(true);
+		});
+	});
+
+	describe("processOutstandingUpdatesWithLock", function() {
+		describe("when the lock is free", function() {
+			beforeEach(function() {
+				this.LockManager.tryLock = sinon.stub().callsArgWith(1, null, true, (this.lockValue = "mock-lock-value"));
+				this.LockManager.releaseLock = sinon.stub().callsArg(2);
+				this.UpdateManager.continueProcessingUpdatesWithLock = sinon.stub().callsArg(2);
+				return this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2);
+			});
+
+			describe("successfully", function() {
+				beforeEach(function() {
+					return this.UpdateManager.processOutstandingUpdatesWithLock(this.project_id, this.doc_id, this.callback);
+				});
+
+				it("should acquire the lock", function() {
+					return this.LockManager.tryLock.calledWith(this.doc_id).should.equal(true);
+				});
+
+				it("should free the lock", function() {
+					return this.LockManager.releaseLock.calledWith(this.doc_id, this.lockValue).should.equal(true);
+				});
+
+				it("should process the outstanding updates", function() {
+					return this.UpdateManager.processOutstandingUpdates.calledWith(this.project_id, this.doc_id).should.equal(true);
+				});
+
+				it("should do everything with the lock acquired", function() {
+					this.UpdateManager.processOutstandingUpdates.calledAfter(this.LockManager.tryLock).should.equal(true);
+					return this.UpdateManager.processOutstandingUpdates.calledBefore(this.LockManager.releaseLock).should.equal(true);
+				});
+
+				it("should continue processing new updates that may have come in", function() {
+					return this.UpdateManager.continueProcessingUpdatesWithLock.calledWith(this.project_id, this.doc_id).should.equal(true);
+				});
+
+				return it("should return the callback", function() {
+					return this.callback.called.should.equal(true);
+				});
+			});
+
+			return describe("when processOutstandingUpdates returns an error", function() {
+				beforeEach(function() {
+					this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArgWith(2, (this.error = new Error("Something went wrong")));
+					return this.UpdateManager.processOutstandingUpdatesWithLock(this.project_id, this.doc_id, this.callback);
+				});
+
+				it("should free the lock", function() {
+					return this.LockManager.releaseLock.calledWith(this.doc_id, this.lockValue).should.equal(true);
+				});
+
+				return it("should return the error in the callback", function() {
+					return this.callback.calledWith(this.error).should.equal(true);
+				});
+			});
+		});
+
+		return describe("when the lock is taken", function() {
+			beforeEach(function() {
+				this.LockManager.tryLock = sinon.stub().callsArgWith(1, null, false);
+				this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2);
+				return this.UpdateManager.processOutstandingUpdatesWithLock(this.project_id, this.doc_id, this.callback);
+			});
+
+			it("should return the callback", function() {
+				return this.callback.called.should.equal(true);
+			});
+
+			return it("should not process the updates", function() {
+				return this.UpdateManager.processOutstandingUpdates.called.should.equal(false);
+			});
+		});
+	});
+
+	describe("continueProcessingUpdatesWithLock", function() {
+		describe("when there are outstanding updates", function() {
+			beforeEach(function() {
+				this.RealTimeRedisManager.getUpdatesLength = sinon.stub().callsArgWith(1, null, 3);
+				this.UpdateManager.processOutstandingUpdatesWithLock = sinon.stub().callsArg(2);
+				return this.UpdateManager.continueProcessingUpdatesWithLock(this.project_id, this.doc_id, this.callback);
+			});
+
+			it("should process the outstanding updates", function() {
+				return this.UpdateManager.processOutstandingUpdatesWithLock.calledWith(this.project_id, this.doc_id).should.equal(true);
+			});
+
+			return it("should return the callback", function() {
+				return this.callback.called.should.equal(true);
+			});
+		});
+
+		return describe("when there are no outstanding updates", function() {
+			beforeEach(function() {
+				this.RealTimeRedisManager.getUpdatesLength = sinon.stub().callsArgWith(1, null, 0);
+				this.UpdateManager.processOutstandingUpdatesWithLock = sinon.stub().callsArg(2);
+				return this.UpdateManager.continueProcessingUpdatesWithLock(this.project_id, this.doc_id, this.callback);
+			});
+
+			it("should not try to process the outstanding updates", function() {
+				return this.UpdateManager.processOutstandingUpdatesWithLock.called.should.equal(false);
+			});
+
+			return it("should return the callback", function() {
+				return this.callback.called.should.equal(true);
+			});
+		});
+	});
+
+	describe("fetchAndApplyUpdates", function() {
+		describe("with updates", function() {
+			beforeEach(function() {
+				this.updates = [{p: 1, t: "foo"}];
+				this.updatedDocLines = ["updated", "lines"];
+				this.version = 34;
+				this.RealTimeRedisManager.getPendingUpdatesForDoc = sinon.stub().callsArgWith(1, null, this.updates);
+				this.UpdateManager.applyUpdate = sinon.stub().callsArgWith(3, null, this.updatedDocLines, this.version);
+				return this.UpdateManager.fetchAndApplyUpdates(this.project_id, this.doc_id, this.callback);
+			});
+
+			it("should get the pending updates", function() {
+				return this.RealTimeRedisManager.getPendingUpdatesForDoc.calledWith(this.doc_id).should.equal(true);
+			});
+
+			it("should apply the updates", function() {
+				return Array.from(this.updates).map((update) =>
+					this.UpdateManager.applyUpdate
+						.calledWith(this.project_id, this.doc_id, update)
+						.should.equal(true));
+			});
+
+			return it("should call the callback", function() {
+				return this.callback.called.should.equal(true);
+			});
+		});
+
+		return describe("when there are no updates", function() {
+			beforeEach(function() {
+				this.updates = [];
+				this.RealTimeRedisManager.getPendingUpdatesForDoc = sinon.stub().callsArgWith(1, null, this.updates);
+				this.UpdateManager.applyUpdate = sinon.stub();
+				this.RedisManager.setDocument = sinon.stub();
+				return this.UpdateManager.fetchAndApplyUpdates(this.project_id, this.doc_id, this.callback);
+			});
+
+			it("should not call applyUpdate", function() {
+				return this.UpdateManager.applyUpdate.called.should.equal(false);
+			});
+
+			return it("should call the callback", function() {
+				return this.callback.called.should.equal(true);
+			});
+		});
+	});
+
+	describe("applyUpdate", function() {
+		beforeEach(function() {
+			this.updateMeta = { user_id: 'last-author-fake-id' };
+			this.update = {op: [{p: 42, i: "foo"}], meta: this.updateMeta};
+			this.updatedDocLines = ["updated", "lines"];
+			this.version = 34;
+			this.lines = ["original", "lines"];
+			this.ranges = { entries: "mock", comments: "mock" };
+			this.updated_ranges = { entries: "updated", comments: "updated" };
+			this.appliedOps = [ {v: 42, op: "mock-op-42"}, { v: 45, op: "mock-op-45" }];
+			this.doc_ops_length = sinon.stub();
+			this.project_ops_length = sinon.stub();
+			this.pathname = '/a/b/c.tex';
+			this.DocumentManager.getDoc = sinon.stub().yields(null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId);
+			this.RangesManager.applyUpdate = sinon.stub().yields(null, this.updated_ranges, false);
+			this.ShareJsUpdateManager.applyUpdate = sinon.stub().yields(null, this.updatedDocLines, this.version, this.appliedOps);
+			this.RedisManager.updateDocument = sinon.stub().yields(null, this.doc_ops_length, this.project_ops_length);
+			this.RealTimeRedisManager.sendData = sinon.stub();
+			this.UpdateManager._addProjectHistoryMetadataToOps = sinon.stub();
+			return this.HistoryManager.recordAndFlushHistoryOps = sinon.stub().callsArg(5);
+		});
+
+		describe("normally", function() {
+			beforeEach(function() {
+				return this.UpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.callback);
+			});
+
+			it("should apply the updates via ShareJS", function() {
+				return this.ShareJsUpdateManager.applyUpdate
+					.calledWith(this.project_id, this.doc_id, this.update, this.lines, this.version)
+					.should.equal(true);
+			});
+
+			it("should update the ranges", function() {
+				return this.RangesManager.applyUpdate
+					.calledWith(this.project_id, this.doc_id, this.ranges, this.appliedOps, this.updatedDocLines)
+					.should.equal(true);
+			});
+
+			it("should save the document", function() {
+				return this.RedisManager.updateDocument
+					.calledWith(this.project_id, this.doc_id, this.updatedDocLines, this.version, this.appliedOps, this.updated_ranges, this.updateMeta)
+					.should.equal(true);
+			});
+
+			it("should add metadata to the ops" , function() {
+				return this.UpdateManager._addProjectHistoryMetadataToOps
+					.calledWith(this.appliedOps, this.pathname, this.projectHistoryId, this.lines)
+					.should.equal(true);
+			});
+
+			it("should push the applied ops into the history queue", function() {
+				return this.HistoryManager.recordAndFlushHistoryOps
+					.calledWith(this.project_id, this.doc_id, this.appliedOps, this.doc_ops_length, this.project_ops_length)
+					.should.equal(true);
+			});
+
+			return it("should call the callback", function() {
+				return this.callback.called.should.equal(true);
+			});
+		});
+
+		describe("with UTF-16 surrogate pairs in the update", function() {
+			beforeEach(function() {
+				this.update = {op: [{p: 42, i: "\uD835\uDC00"}]};
+				return this.UpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.callback);
+			});
+
+			return it("should apply the update but with surrogate pairs removed", function() {
+				this.ShareJsUpdateManager.applyUpdate
+					.calledWith(this.project_id, this.doc_id, this.update)
+					.should.equal(true);
+
+				// \uFFFD is 'replacement character'
+				return this.update.op[0].i.should.equal("\uFFFD\uFFFD");
+			});
+		});
+
+		describe("with an error", function() {
+			beforeEach(function() {
+				this.error = new Error("something went wrong");
+				this.ShareJsUpdateManager.applyUpdate = sinon.stub().yields(this.error);
+				return this.UpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.callback);
+			});
+
+			it("should call RealTimeRedisManager.sendData with the error", function() {
+				return this.RealTimeRedisManager.sendData
 					.calledWith({
-						project_id: @project_id,
-						doc_id: @doc_id,
-						error: @error.message
+						project_id: this.project_id,
+						doc_id: this.doc_id,
+						error: this.error.message
 					})
-					.should.equal true
-
-			it "should call the callback with the error", ->
-				@callback.calledWith(@error).should.equal true
-
-		describe "when ranges get collapsed", ->
-			beforeEach ->
-				@RangesManager.applyUpdate = sinon.stub().yields(null, @updated_ranges, true)
-				@SnapshotManager.recordSnapshot = sinon.stub().yields()
-				@UpdateManager.applyUpdate @project_id, @doc_id, @update, @callback
-
-			it "should call SnapshotManager.recordSnapshot", ->
-				@SnapshotManager.recordSnapshot
+					.should.equal(true);
+			});
+
+			return it("should call the callback with the error", function() {
+				return this.callback.calledWith(this.error).should.equal(true);
+			});
+		});
+
+		return describe("when ranges get collapsed", function() {
+			beforeEach(function() {
+				this.RangesManager.applyUpdate = sinon.stub().yields(null, this.updated_ranges, true);
+				this.SnapshotManager.recordSnapshot = sinon.stub().yields();
+				return this.UpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.callback);
+			});
+
+			return it("should call SnapshotManager.recordSnapshot", function() {
+				return this.SnapshotManager.recordSnapshot
 					.calledWith(
-						@project_id,
-						@doc_id,
-						@version,
-						@pathname,
-						@lines,
-						@ranges
+						this.project_id,
+						this.doc_id,
+						this.version,
+						this.pathname,
+						this.lines,
+						this.ranges
 					)
-					.should.equal true
-
-
-	describe "_addProjectHistoryMetadataToOps", ->
-		it "should add projectHistoryId, pathname and doc_length metadata to the ops", ->
-			lines = [
-				'some'
-				'test'
-				'data'
-			]
-			appliedOps = [
-				{ v: 42, op: [{i: "foo", p: 4}, { i: "bar", p: 6 }] },
-				{ v: 45, op: [{d: "qux", p: 4}, { i: "bazbaz", p: 14 }] },
-				{ v: 49, op: [{i: "penguin", p: 18}] }
-			]
-			@UpdateManager._addProjectHistoryMetadataToOps(appliedOps, @pathname, @projectHistoryId, lines)
-			appliedOps.should.deep.equal [{
-				projectHistoryId: @projectHistoryId
-				v: 42
-				op: [{i: "foo", p: 4}, { i: "bar", p: 6 }]
-				meta:
-					pathname: @pathname
-					doc_length: 14
-			}, {
-				projectHistoryId: @projectHistoryId
-				v: 45
-				op: [{d: "qux", p: 4}, { i: "bazbaz", p: 14 }]
-				meta:
-					pathname: @pathname
-					doc_length: 20 # 14 + 'foo' + 'bar'
-			}, {
-				projectHistoryId: @projectHistoryId
-				v: 49
-				op: [{i: "penguin", p: 18}]
-				meta:
-					pathname: @pathname
-					doc_length: 23 # 14 - 'qux' + 'bazbaz'
-			}]
-
-	describe "lockUpdatesAndDo", ->
-		beforeEach ->
-			@method = sinon.stub().callsArgWith(3, null, @response_arg1)
-			@callback = sinon.stub()
-			@arg1 = "argument 1"
-			@response_arg1 = "response argument 1"
-			@lockValue = "mock-lock-value"
-			@LockManager.getLock = sinon.stub().callsArgWith(1, null, @lockValue)
-			@LockManager.releaseLock = sinon.stub().callsArg(2)
-
-		describe "successfully", ->
-			beforeEach ->
-				@UpdateManager.continueProcessingUpdatesWithLock = sinon.stub()
-				@UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2)
-				@UpdateManager.lockUpdatesAndDo @method, @project_id, @doc_id, @arg1, @callback
-
-			it "should lock the doc", ->
-				@LockManager.getLock
-					.calledWith(@doc_id)
-					.should.equal true
-
-			it "should process any outstanding updates", ->
-				@UpdateManager.processOutstandingUpdates
-					.calledWith(@project_id, @doc_id)
-					.should.equal true
-
-			it "should call the method", ->
-				@method
-					.calledWith(@project_id, @doc_id, @arg1)
-					.should.equal true
-
-			it "should return the method response to the callback", ->
-				@callback
-					.calledWith(null, @response_arg1)
-					.should.equal true
-
-			it "should release the lock", ->
-				@LockManager.releaseLock
-					.calledWith(@doc_id, @lockValue)
-					.should.equal true
-
-			it "should continue processing updates", ->
-				@UpdateManager.continueProcessingUpdatesWithLock
-					.calledWith(@project_id, @doc_id)
-					.should.equal true
-
-		describe "when processOutstandingUpdates returns an error", ->
-			beforeEach ->
-				@UpdateManager.processOutstandingUpdates = sinon.stub().callsArgWith(2, @error = new Error("Something went wrong"))
-				@UpdateManager.lockUpdatesAndDo @method, @project_id, @doc_id, @arg1, @callback
-
-			it "should free the lock", ->
-				@LockManager.releaseLock.calledWith(@doc_id, @lockValue).should.equal true
-
-			it "should return the error in the callback", ->
-				@callback.calledWith(@error).should.equal true
-
-		describe "when the method returns an error", ->
-			beforeEach ->
-				@UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2)
-				@method = sinon.stub().callsArgWith(3, @error = new Error("something went wrong"), @response_arg1)
-				@UpdateManager.lockUpdatesAndDo @method, @project_id, @doc_id, @arg1, @callback
-
-			it "should free the lock", ->
-				@LockManager.releaseLock.calledWith(@doc_id, @lockValue).should.equal true
-
-			it "should return the error in the callback", ->
-				@callback.calledWith(@error).should.equal true
+					.should.equal(true);
+			});
+		});
+	});
+
+
+	describe("_addProjectHistoryMetadataToOps", () => it("should add projectHistoryId, pathname and doc_length metadata to the ops", function() {
+        const lines = [
+            'some',
+            'test',
+            'data'
+        ];
+        const appliedOps = [
+            { v: 42, op: [{i: "foo", p: 4}, { i: "bar", p: 6 }] },
+            { v: 45, op: [{d: "qux", p: 4}, { i: "bazbaz", p: 14 }] },
+            { v: 49, op: [{i: "penguin", p: 18}] }
+        ];
+        this.UpdateManager._addProjectHistoryMetadataToOps(appliedOps, this.pathname, this.projectHistoryId, lines);
+        return appliedOps.should.deep.equal([{
+            projectHistoryId: this.projectHistoryId,
+            v: 42,
+            op: [{i: "foo", p: 4}, { i: "bar", p: 6 }],
+            meta: {
+                pathname: this.pathname,
+                doc_length: 14
+            }
+        }, {
+            projectHistoryId: this.projectHistoryId,
+            v: 45,
+            op: [{d: "qux", p: 4}, { i: "bazbaz", p: 14 }],
+            meta: {
+                pathname: this.pathname,
+                doc_length: 20
+            } // 14 + 'foo' + 'bar'
+        }, {
+            projectHistoryId: this.projectHistoryId,
+            v: 49,
+            op: [{i: "penguin", p: 18}],
+            meta: {
+                pathname: this.pathname,
+                doc_length: 23
+            } // 14 - 'qux' + 'bazbaz'
+        }]);
+}));
+
+	return describe("lockUpdatesAndDo", function() {
+		beforeEach(function() {
+			this.method = sinon.stub().callsArgWith(3, null, this.response_arg1);
+			this.callback = sinon.stub();
+			this.arg1 = "argument 1";
+			this.response_arg1 = "response argument 1";
+			this.lockValue = "mock-lock-value";
+			this.LockManager.getLock = sinon.stub().callsArgWith(1, null, this.lockValue);
+			return this.LockManager.releaseLock = sinon.stub().callsArg(2);
+		});
+
+		describe("successfully", function() {
+			beforeEach(function() {
+				this.UpdateManager.continueProcessingUpdatesWithLock = sinon.stub();
+				this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2);
+				return this.UpdateManager.lockUpdatesAndDo(this.method, this.project_id, this.doc_id, this.arg1, this.callback);
+			});
+
+			it("should lock the doc", function() {
+				return this.LockManager.getLock
+					.calledWith(this.doc_id)
+					.should.equal(true);
+			});
+
+			it("should process any outstanding updates", function() {
+				return this.UpdateManager.processOutstandingUpdates
+					.calledWith(this.project_id, this.doc_id)
+					.should.equal(true);
+			});
+
+			it("should call the method", function() {
+				return this.method
+					.calledWith(this.project_id, this.doc_id, this.arg1)
+					.should.equal(true);
+			});
+
+			it("should return the method response to the callback", function() {
+				return this.callback
+					.calledWith(null, this.response_arg1)
+					.should.equal(true);
+			});
+
+			it("should release the lock", function() {
+				return this.LockManager.releaseLock
+					.calledWith(this.doc_id, this.lockValue)
+					.should.equal(true);
+			});
+
+			return it("should continue processing updates", function() {
+				return this.UpdateManager.continueProcessingUpdatesWithLock
+					.calledWith(this.project_id, this.doc_id)
+					.should.equal(true);
+			});
+		});
+
+		describe("when processOutstandingUpdates returns an error", function() {
+			beforeEach(function() {
+				this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArgWith(2, (this.error = new Error("Something went wrong")));
+				return this.UpdateManager.lockUpdatesAndDo(this.method, this.project_id, this.doc_id, this.arg1, this.callback);
+			});
+
+			it("should free the lock", function() {
+				return this.LockManager.releaseLock.calledWith(this.doc_id, this.lockValue).should.equal(true);
+			});
+
+			return it("should return the error in the callback", function() {
+				return this.callback.calledWith(this.error).should.equal(true);
+			});
+		});
+
+		return describe("when the method returns an error", function() {
+			beforeEach(function() {
+				this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2);
+				this.method = sinon.stub().callsArgWith(3, (this.error = new Error("something went wrong")), this.response_arg1);
+				return this.UpdateManager.lockUpdatesAndDo(this.method, this.project_id, this.doc_id, this.arg1, this.callback);
+			});
+
+			it("should free the lock", function() {
+				return this.LockManager.releaseLock.calledWith(this.doc_id, this.lockValue).should.equal(true);
+			});
+
+			return it("should return the error in the callback", function() {
+				return this.callback.calledWith(this.error).should.equal(true);
+			});
+		});
+	});
+});