Procházet zdrojové kódy

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

decaffeinate před 6 roky
rodič
revize
86a9593ec0

+ 369 - 304
services/track-changes/test/unit/coffee/DiffGenerator/DiffGeneratorTests.js

@@ -1,344 +1,409 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-expect = chai.expect
-modulePath = "../../../../app/js/DiffGenerator.js"
-SandboxedModule = require('sandboxed-module')
-
-describe "DiffGenerator", ->
-	beforeEach ->
-		@DiffGenerator = SandboxedModule.require modulePath, requires:
+/*
+ * 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/DiffGenerator.js";
+const SandboxedModule = require('sandboxed-module');
+
+describe("DiffGenerator", function() {
+	beforeEach(function() {
+		this.DiffGenerator = SandboxedModule.require(modulePath, { requires: {
 			"logger-sharelatex": { warn: sinon.stub() }
-		@ts = Date.now()
-		@user_id = "mock-user-id"
-		@user_id_2 = "mock-user-id-2"
-		@meta = {
-			start_ts: @ts, end_ts: @ts, user_id: @user_id
 		}
-
-	describe "rewindOp", ->
-		describe "rewinding an insert", ->
-			it "should undo the insert", ->
-				content = "hello world"
-				rewoundContent = @DiffGenerator.rewindOp content, { p: 6, i: "wo" }
-				rewoundContent.should.equal "hello rld"
-
-		describe "rewinding a delete", ->
-			it "should undo the delete", ->
-				content = "hello rld"
-				rewoundContent = @DiffGenerator.rewindOp content, { p: 6, d: "wo" }
-				rewoundContent.should.equal "hello world"
-
-		describe "with an inconsistent update", ->
-			it "should throw an error", ->
-				content = "hello world"
-				expect( () =>
-					@DiffGenerator.rewindOp content, { p: 6, i: "foo" }
-				).to.throw(@DiffGenerator.ConsistencyError)
+	});
+		this.ts = Date.now();
+		this.user_id = "mock-user-id";
+		this.user_id_2 = "mock-user-id-2";
+		return this.meta = {
+			start_ts: this.ts, end_ts: this.ts, user_id: this.user_id
+		};});
+
+	describe("rewindOp", function() {
+		describe("rewinding an insert", () =>
+			it("should undo the insert", function() {
+				const content = "hello world";
+				const rewoundContent = this.DiffGenerator.rewindOp(content, { p: 6, i: "wo" });
+				return rewoundContent.should.equal("hello rld");
+			})
+		);
+
+		describe("rewinding a delete", () =>
+			it("should undo the delete", function() {
+				const content = "hello rld";
+				const rewoundContent = this.DiffGenerator.rewindOp(content, { p: 6, d: "wo" });
+				return rewoundContent.should.equal("hello world");
+			})
+		);
+
+		describe("with an inconsistent update", () =>
+			it("should throw an error", function() {
+				const content = "hello world";
+				return expect( () => {
+					return this.DiffGenerator.rewindOp(content, { p: 6, i: "foo" });
+				}).to.throw(this.DiffGenerator.ConsistencyError);
+			})
+		);
 		
-		describe "with an update which is beyond the length of the content", ->
-			it "should undo the insert as if it were at the end of the content", ->
-				content = "foobar"
-				rewoundContent = @DiffGenerator.rewindOp content, { p: 4, i: "bar" }
-				rewoundContent.should.equal "foo"
-
-	describe "rewindUpdate", ->
-		it "should rewind ops in reverse", ->
-			content = "aaabbbccc"
-			update =
-				op: [{ p: 3, i: "bbb" }, { p: 6, i: "ccc" }]
-			rewoundContent = @DiffGenerator.rewindUpdate content, update
-			rewoundContent.should.equal "aaa"
-
-	describe "rewindUpdates", ->
-		it "should rewind updates in reverse", ->
-			content = "aaabbbccc"
-			updates = [
+		return describe("with an update which is beyond the length of the content", () =>
+			it("should undo the insert as if it were at the end of the content", function() {
+				const content = "foobar";
+				const rewoundContent = this.DiffGenerator.rewindOp(content, { p: 4, i: "bar" });
+				return rewoundContent.should.equal("foo");
+			})
+		);
+	});
+
+	describe("rewindUpdate", () =>
+		it("should rewind ops in reverse", function() {
+			const content = "aaabbbccc";
+			const update =
+				{op: [{ p: 3, i: "bbb" }, { p: 6, i: "ccc" }]};
+			const rewoundContent = this.DiffGenerator.rewindUpdate(content, update);
+			return rewoundContent.should.equal("aaa");
+		})
+	);
+
+	describe("rewindUpdates", () =>
+		it("should rewind updates in reverse", function() {
+			const content = "aaabbbccc";
+			const updates = [
 				{ op: [{ p: 3, i: "bbb" }] },
 				{ op: [{ p: 6, i: "ccc" }] }
-			]
-			rewoundContent = @DiffGenerator.rewindUpdates content, updates
-			rewoundContent.should.equal "aaa"
-
-	describe "buildDiff", ->
-		beforeEach ->
-			@diff = [ u: "mock-diff" ]
-			@content = "Hello world"
-			@updates = [
-				{ i: "mock-update-1" }
-				{ i: "mock-update-2" }
+			];
+			const rewoundContent = this.DiffGenerator.rewindUpdates(content, updates);
+			return rewoundContent.should.equal("aaa");
+		})
+	);
+
+	describe("buildDiff", function() {
+		beforeEach(function() {
+			this.diff = [ {u: "mock-diff"} ];
+			this.content = "Hello world";
+			this.updates = [
+				{ i: "mock-update-1" },
+				{ i: "mock-update-2" },
 				{ i: "mock-update-3" }
-			]
-			@DiffGenerator.applyUpdateToDiff = sinon.stub().returns(@diff)
-			@DiffGenerator.compressDiff = sinon.stub().returns(@diff)
-			@result = @DiffGenerator.buildDiff(@content, @updates)
-
-		it "should return the diff", ->
-			@result.should.deep.equal @diff
-
-		it "should build the content into an initial diff", ->
-			@DiffGenerator.applyUpdateToDiff
+			];
+			this.DiffGenerator.applyUpdateToDiff = sinon.stub().returns(this.diff);
+			this.DiffGenerator.compressDiff = sinon.stub().returns(this.diff);
+			return this.result = this.DiffGenerator.buildDiff(this.content, this.updates);
+		});
+
+		it("should return the diff", function() {
+			return this.result.should.deep.equal(this.diff);
+		});
+
+		it("should build the content into an initial diff", function() {
+			return this.DiffGenerator.applyUpdateToDiff
 				.calledWith([{
-					u: @content
-				}], @updates[0])
-				.should.equal true
-
-		it "should apply each update", ->
-			for update in @updates
-				@DiffGenerator.applyUpdateToDiff
+					u: this.content
+				}], this.updates[0])
+				.should.equal(true);
+		});
+
+		it("should apply each update", function() {
+			return Array.from(this.updates).map((update) =>
+				this.DiffGenerator.applyUpdateToDiff
 					.calledWith(sinon.match.any, update)
-					.should.equal true
-
-		it "should compress the diff", ->
-			@DiffGenerator.compressDiff
-				.calledWith(@diff)
-				.should.equal true
-
-	describe "compressDiff", ->
-		describe "with adjacent inserts with the same user_id", ->
-			it "should create one update with combined meta data and min/max timestamps", ->
-				diff = @DiffGenerator.compressDiff([
-					{ i: "foo", meta: { start_ts: 10, end_ts: 20, user: { id: @user_id } }}
-					{ i: "bar", meta: { start_ts: 5,  end_ts: 15, user: { id: @user_id } }}
-				])
-				expect(diff).to.deep.equal([
-					{ i: "foobar", meta: { start_ts: 5, end_ts: 20, user: { id: @user_id } }}
-				])
-
-		describe "with adjacent inserts with different user_ids", ->
-			it "should leave the inserts unchanged", ->
-				input = [
-					{ i: "foo", meta: { start_ts: 10, end_ts: 20, user: { id: @user_id } }}
-					{ i: "bar", meta: { start_ts: 5,  end_ts: 15, user: { id: @user_id_2 } }}
-				]
-				output = @DiffGenerator.compressDiff(input)
-				expect(output).to.deep.equal(input)
-
-		describe "with adjacent deletes with the same user_id", ->
-			it "should create one update with combined meta data and min/max timestamps", ->
-				diff = @DiffGenerator.compressDiff([
-					{ d: "foo", meta: { start_ts: 10, end_ts: 20, user: { id: @user_id } }}
-					{ d: "bar", meta: { start_ts: 5,  end_ts: 15, user: { id: @user_id } }}
-				])
-				expect(diff).to.deep.equal([
-					{ d: "foobar", meta: { start_ts: 5, end_ts: 20, user: { id: @user_id } }}
-				])
-
-		describe "with adjacent deletes with different user_ids", ->
-			it "should leave the deletes unchanged", ->
-				input = [
-					{ d: "foo", meta: { start_ts: 10, end_ts: 20, user: { id: @user_id } }}
-					{ d: "bar", meta: { start_ts: 5,  end_ts: 15, user: { id: @user_id_2 } }}
-				]
-				output = @DiffGenerator.compressDiff(input)
-				expect(output).to.deep.equal(input)
-
-	describe "applyUpdateToDiff", ->
-		describe "an insert", ->
-			it "should insert into the middle of (u)nchanged text", ->
-				diff = @DiffGenerator.applyUpdateToDiff(
+					.should.equal(true));
+		});
+
+		return it("should compress the diff", function() {
+			return this.DiffGenerator.compressDiff
+				.calledWith(this.diff)
+				.should.equal(true);
+		});
+	});
+
+	describe("compressDiff", function() {
+		describe("with adjacent inserts with the same user_id", () =>
+			it("should create one update with combined meta data and min/max timestamps", function() {
+				const diff = this.DiffGenerator.compressDiff([
+					{ i: "foo", meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } }},
+					{ i: "bar", meta: { start_ts: 5,  end_ts: 15, user: { id: this.user_id } }}
+				]);
+				return expect(diff).to.deep.equal([
+					{ i: "foobar", meta: { start_ts: 5, end_ts: 20, user: { id: this.user_id } }}
+				]);
+			})
+		);
+
+		describe("with adjacent inserts with different user_ids", () =>
+			it("should leave the inserts unchanged", function() {
+				const input = [
+					{ i: "foo", meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } }},
+					{ i: "bar", meta: { start_ts: 5,  end_ts: 15, user: { id: this.user_id_2 } }}
+				];
+				const output = this.DiffGenerator.compressDiff(input);
+				return expect(output).to.deep.equal(input);
+			})
+		);
+
+		describe("with adjacent deletes with the same user_id", () =>
+			it("should create one update with combined meta data and min/max timestamps", function() {
+				const diff = this.DiffGenerator.compressDiff([
+					{ d: "foo", meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } }},
+					{ d: "bar", meta: { start_ts: 5,  end_ts: 15, user: { id: this.user_id } }}
+				]);
+				return expect(diff).to.deep.equal([
+					{ d: "foobar", meta: { start_ts: 5, end_ts: 20, user: { id: this.user_id } }}
+				]);
+			})
+		);
+
+		return describe("with adjacent deletes with different user_ids", () =>
+			it("should leave the deletes unchanged", function() {
+				const input = [
+					{ d: "foo", meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } }},
+					{ d: "bar", meta: { start_ts: 5,  end_ts: 15, user: { id: this.user_id_2 } }}
+				];
+				const output = this.DiffGenerator.compressDiff(input);
+				return expect(output).to.deep.equal(input);
+			})
+		);
+	});
+
+	return describe("applyUpdateToDiff", function() {
+		describe("an insert", function() {
+			it("should insert into the middle of (u)nchanged text", function() {
+				const diff = this.DiffGenerator.applyUpdateToDiff(
 					[ { u: "foobar" } ],
-					{ op: [{ p: 3, i: "baz" }], meta: @meta }
-				)
-				expect(diff).to.deep.equal([
-					{ u: "foo" }
-					{ i: "baz", meta: @meta }
+					{ op: [{ p: 3, i: "baz" }], meta: this.meta }
+				);
+				return expect(diff).to.deep.equal([
+					{ u: "foo" },
+					{ i: "baz", meta: this.meta },
 					{ u: "bar" }
-				])
+				]);
+			});
 
-			it "should insert into the start of (u)changed text", ->
-				diff = @DiffGenerator.applyUpdateToDiff(
+			it("should insert into the start of (u)changed text", function() {
+				const diff = this.DiffGenerator.applyUpdateToDiff(
 					[ { u: "foobar" } ],
-					{ op: [{ p: 0, i: "baz" }], meta: @meta }
-				)
-				expect(diff).to.deep.equal([
-					{ i: "baz", meta: @meta }
+					{ op: [{ p: 0, i: "baz" }], meta: this.meta }
+				);
+				return expect(diff).to.deep.equal([
+					{ i: "baz", meta: this.meta },
 					{ u: "foobar" }
-				])
+				]);
+			});
 
-			it "should insert into the end of (u)changed text", ->
-				diff = @DiffGenerator.applyUpdateToDiff(
+			it("should insert into the end of (u)changed text", function() {
+				const diff = this.DiffGenerator.applyUpdateToDiff(
 					[ { u: "foobar" } ],
-					{ op: [{ p: 6, i: "baz" }], meta: @meta }
-				)
-				expect(diff).to.deep.equal([
-					{ u: "foobar" }
-					{ i: "baz", meta: @meta }
-				])
-
-			it "should insert into the middle of (i)inserted text", ->
-				diff = @DiffGenerator.applyUpdateToDiff(
-					[ { i: "foobar", meta: @meta } ],
-					{ op: [{ p: 3, i: "baz" }], meta: @meta }
-				)
-				expect(diff).to.deep.equal([
-					{ i: "foo", meta: @meta }
-					{ i: "baz", meta: @meta }
-					{ i: "bar", meta: @meta }
-				])
-
-			it "should not count deletes in the running length total", ->
-				diff = @DiffGenerator.applyUpdateToDiff(
+					{ op: [{ p: 6, i: "baz" }], meta: this.meta }
+				);
+				return expect(diff).to.deep.equal([
+					{ u: "foobar" },
+					{ i: "baz", meta: this.meta }
+				]);
+			});
+
+			it("should insert into the middle of (i)inserted text", function() {
+				const diff = this.DiffGenerator.applyUpdateToDiff(
+					[ { i: "foobar", meta: this.meta } ],
+					{ op: [{ p: 3, i: "baz" }], meta: this.meta }
+				);
+				return expect(diff).to.deep.equal([
+					{ i: "foo", meta: this.meta },
+					{ i: "baz", meta: this.meta },
+					{ i: "bar", meta: this.meta }
+				]);
+			});
+
+			return it("should not count deletes in the running length total", function() {
+				const diff = this.DiffGenerator.applyUpdateToDiff(
 					[
-						{ d: "deleted", meta: @meta }
+						{ d: "deleted", meta: this.meta },
 						{ u: "foobar" }
 					],
-					{ op: [{ p: 3, i: "baz" }], meta: @meta }
-				)
-				expect(diff).to.deep.equal([
-					{ d: "deleted", meta: @meta }
-					{ u: "foo" }
-					{ i: "baz", meta: @meta }
+					{ op: [{ p: 3, i: "baz" }], meta: this.meta }
+				);
+				return expect(diff).to.deep.equal([
+					{ d: "deleted", meta: this.meta },
+					{ u: "foo" },
+					{ i: "baz", meta: this.meta },
 					{ u: "bar" }
-				])
-
-		describe "a delete", ->
-			describe "deleting unchanged text", ->
-				it "should delete from the middle of (u)nchanged text", ->
-					diff = @DiffGenerator.applyUpdateToDiff(
+				]);
+			});
+		});
+
+		return describe("a delete", function() {
+			describe("deleting unchanged text", function() {
+				it("should delete from the middle of (u)nchanged text", function() {
+					const diff = this.DiffGenerator.applyUpdateToDiff(
 						[ { u: "foobazbar" } ],
-						{ op: [{ p: 3, d: "baz" }], meta: @meta }
-					)
-					expect(diff).to.deep.equal([
-						{ u: "foo" }
-						{ d: "baz", meta: @meta }
+						{ op: [{ p: 3, d: "baz" }], meta: this.meta }
+					);
+					return expect(diff).to.deep.equal([
+						{ u: "foo" },
+						{ d: "baz", meta: this.meta },
 						{ u: "bar" }
-					])
+					]);
+				});
 
-				it "should delete from the start of (u)nchanged text", ->
-					diff = @DiffGenerator.applyUpdateToDiff(
+				it("should delete from the start of (u)nchanged text", function() {
+					const diff = this.DiffGenerator.applyUpdateToDiff(
 						[ { u: "foobazbar" } ],
-						{ op: [{ p: 0, d: "foo" }], meta: @meta }
-					)
-					expect(diff).to.deep.equal([
-						{ d: "foo", meta: @meta }
+						{ op: [{ p: 0, d: "foo" }], meta: this.meta }
+					);
+					return expect(diff).to.deep.equal([
+						{ d: "foo", meta: this.meta },
 						{ u: "bazbar" }
-					])
+					]);
+				});
 
-				it "should delete from the end of (u)nchanged text", ->
-					diff = @DiffGenerator.applyUpdateToDiff(
+				it("should delete from the end of (u)nchanged text", function() {
+					const diff = this.DiffGenerator.applyUpdateToDiff(
 						[ { u: "foobazbar" } ],
-						{ op: [{ p: 6, d: "bar" }], meta: @meta }
-					)
-					expect(diff).to.deep.equal([
-						{ u: "foobaz" }
-						{ d: "bar", meta: @meta }
-					])
-
-				it "should delete across multiple (u)changed text parts", ->
-					diff = @DiffGenerator.applyUpdateToDiff(
+						{ op: [{ p: 6, d: "bar" }], meta: this.meta }
+					);
+					return expect(diff).to.deep.equal([
+						{ u: "foobaz" },
+						{ d: "bar", meta: this.meta }
+					]);
+				});
+
+				return it("should delete across multiple (u)changed text parts", function() {
+					const diff = this.DiffGenerator.applyUpdateToDiff(
 						[ { u: "foo" }, { u: "baz" }, { u: "bar" } ],
-						{ op: [{ p: 2, d: "obazb" }], meta: @meta }
-					)
-					expect(diff).to.deep.equal([
-						{ u: "fo" }
-						{ d: "o", meta: @meta }
-						{ d: "baz", meta: @meta }
-						{ d: "b", meta: @meta }
+						{ op: [{ p: 2, d: "obazb" }], meta: this.meta }
+					);
+					return expect(diff).to.deep.equal([
+						{ u: "fo" },
+						{ d: "o", meta: this.meta },
+						{ d: "baz", meta: this.meta },
+						{ d: "b", meta: this.meta },
 						{ u: "ar" }
-					])
-
-			describe "deleting inserts", ->
-				it "should delete from the middle of (i)nserted text", ->
-					diff = @DiffGenerator.applyUpdateToDiff(
-						[ { i: "foobazbar", meta: @meta } ],
-						{ op: [{ p: 3, d: "baz" }], meta: @meta }
-					)
-					expect(diff).to.deep.equal([
-						{ i: "foo", meta: @meta }
-						{ i: "bar", meta: @meta }
-					])
-
-				it "should delete from the start of (u)nchanged text", ->
-					diff = @DiffGenerator.applyUpdateToDiff(
-						[ { i: "foobazbar", meta: @meta } ],
-						{ op: [{ p: 0, d: "foo" }], meta: @meta }
-					)
-					expect(diff).to.deep.equal([
-						{ i: "bazbar", meta: @meta }
-					])
-
-				it "should delete from the end of (u)nchanged text", ->
-					diff = @DiffGenerator.applyUpdateToDiff(
-						[ { i: "foobazbar", meta: @meta } ],
-						{ op: [{ p: 6, d: "bar" }], meta: @meta }
-					)
-					expect(diff).to.deep.equal([
-						{ i: "foobaz", meta: @meta }
-					])
-
-				it "should delete across multiple (u)changed and (i)nserted text parts", ->
-					diff = @DiffGenerator.applyUpdateToDiff(
-						[ { u: "foo" }, { i: "baz", meta: @meta }, { u: "bar" } ],
-						{ op: [{ p: 2, d: "obazb" }], meta: @meta }
-					)
-					expect(diff).to.deep.equal([
-						{ u: "fo" }
-						{ d: "o", meta: @meta }
-						{ d: "b", meta: @meta }
+					]);
+				});
+			});
+
+			describe("deleting inserts", function() {
+				it("should delete from the middle of (i)nserted text", function() {
+					const diff = this.DiffGenerator.applyUpdateToDiff(
+						[ { i: "foobazbar", meta: this.meta } ],
+						{ op: [{ p: 3, d: "baz" }], meta: this.meta }
+					);
+					return expect(diff).to.deep.equal([
+						{ i: "foo", meta: this.meta },
+						{ i: "bar", meta: this.meta }
+					]);
+				});
+
+				it("should delete from the start of (u)nchanged text", function() {
+					const diff = this.DiffGenerator.applyUpdateToDiff(
+						[ { i: "foobazbar", meta: this.meta } ],
+						{ op: [{ p: 0, d: "foo" }], meta: this.meta }
+					);
+					return expect(diff).to.deep.equal([
+						{ i: "bazbar", meta: this.meta }
+					]);
+				});
+
+				it("should delete from the end of (u)nchanged text", function() {
+					const diff = this.DiffGenerator.applyUpdateToDiff(
+						[ { i: "foobazbar", meta: this.meta } ],
+						{ op: [{ p: 6, d: "bar" }], meta: this.meta }
+					);
+					return expect(diff).to.deep.equal([
+						{ i: "foobaz", meta: this.meta }
+					]);
+				});
+
+				return it("should delete across multiple (u)changed and (i)nserted text parts", function() {
+					const diff = this.DiffGenerator.applyUpdateToDiff(
+						[ { u: "foo" }, { i: "baz", meta: this.meta }, { u: "bar" } ],
+						{ op: [{ p: 2, d: "obazb" }], meta: this.meta }
+					);
+					return expect(diff).to.deep.equal([
+						{ u: "fo" },
+						{ d: "o", meta: this.meta },
+						{ d: "b", meta: this.meta },
 						{ u: "ar" }
-					])
-
-			describe "deleting over existing deletes", ->
-				it "should delete across multiple (u)changed and (d)deleted text parts", ->
-					diff = @DiffGenerator.applyUpdateToDiff(
-						[ { u: "foo" }, { d: "baz", meta: @meta }, { u: "bar" } ],
-						{ op: [{ p: 2, d: "ob" }], meta: @meta }
-					)
-					expect(diff).to.deep.equal([
-						{ u: "fo" }
-						{ d: "o", meta: @meta }
-						{ d: "baz", meta: @meta }
-						{ d: "b", meta: @meta }
+					]);
+				});
+			});
+
+			describe("deleting over existing deletes", () =>
+				it("should delete across multiple (u)changed and (d)deleted text parts", function() {
+					const diff = this.DiffGenerator.applyUpdateToDiff(
+						[ { u: "foo" }, { d: "baz", meta: this.meta }, { u: "bar" } ],
+						{ op: [{ p: 2, d: "ob" }], meta: this.meta }
+					);
+					return expect(diff).to.deep.equal([
+						{ u: "fo" },
+						{ d: "o", meta: this.meta },
+						{ d: "baz", meta: this.meta },
+						{ d: "b", meta: this.meta },
 						{ u: "ar" }
-					])
-
-			describe "deleting when the text doesn't match", ->
-				it "should throw an error when deleting from the middle of (u)nchanged text", ->
-					expect(
-						() => @DiffGenerator.applyUpdateToDiff(
+					]);
+				})
+			);
+
+			describe("deleting when the text doesn't match", function() {
+				it("should throw an error when deleting from the middle of (u)nchanged text", function() {
+					return expect(
+						() => this.DiffGenerator.applyUpdateToDiff(
 							[ { u: "foobazbar" } ],
-							{ op: [{ p: 3, d: "xxx" }], meta: @meta }
+							{ op: [{ p: 3, d: "xxx" }], meta: this.meta }
 						)
-					).to.throw(@DiffGenerator.ConsistencyError)
+					).to.throw(this.DiffGenerator.ConsistencyError);
+				});
 
-				it "should throw an error when deleting from the start of (u)nchanged text", ->
-					expect(
-						() => @DiffGenerator.applyUpdateToDiff(
+				it("should throw an error when deleting from the start of (u)nchanged text", function() {
+					return expect(
+						() => this.DiffGenerator.applyUpdateToDiff(
 							[ { u: "foobazbar" } ],
-							{ op: [{ p: 0, d: "xxx" }], meta: @meta }
+							{ op: [{ p: 0, d: "xxx" }], meta: this.meta }
 						)
-					).to.throw(@DiffGenerator.ConsistencyError)
+					).to.throw(this.DiffGenerator.ConsistencyError);
+				});
 
-				it "should throw an error when deleting from the end of (u)nchanged text", ->
-					expect(
-						() => @DiffGenerator.applyUpdateToDiff(
+				return it("should throw an error when deleting from the end of (u)nchanged text", function() {
+					return expect(
+						() => this.DiffGenerator.applyUpdateToDiff(
 							[ { u: "foobazbar" } ],
-							{ op: [{ p: 6, d: "xxx" }] , meta: @meta }
+							{ op: [{ p: 6, d: "xxx" }] , meta: this.meta }
 						)
-					).to.throw(@DiffGenerator.ConsistencyError)
-
-			describe "when the last update in the existing diff is a delete", ->
-				it "should insert the new update before the delete", ->
-					diff = @DiffGenerator.applyUpdateToDiff(
-						[ { u: "foo" }, { d: "bar", meta: @meta } ],
-						{ op: [{ p: 3, i: "baz" }], meta: @meta }
-					)
-					expect(diff).to.deep.equal([
-						{ u: "foo" }
-						{ i: "baz", meta: @meta }
-						{ d: "bar", meta: @meta }
-					])
-
-			describe "when the only update in the existing diff is a delete", ->
-				it "should insert the new update after the delete", ->
-					diff = @DiffGenerator.applyUpdateToDiff(
-						[ { d: "bar", meta: @meta } ],
-						{ op: [{ p: 0, i: "baz" }], meta: @meta }
-					)
-					expect(diff).to.deep.equal([
-						{ d: "bar", meta: @meta }
-						{ i: "baz", meta: @meta }
-					])
+					).to.throw(this.DiffGenerator.ConsistencyError);
+				});
+			});
+
+			describe("when the last update in the existing diff is a delete", () =>
+				it("should insert the new update before the delete", function() {
+					const diff = this.DiffGenerator.applyUpdateToDiff(
+						[ { u: "foo" }, { d: "bar", meta: this.meta } ],
+						{ op: [{ p: 3, i: "baz" }], meta: this.meta }
+					);
+					return expect(diff).to.deep.equal([
+						{ u: "foo" },
+						{ i: "baz", meta: this.meta },
+						{ d: "bar", meta: this.meta }
+					]);
+				})
+			);
+
+			return describe("when the only update in the existing diff is a delete", () =>
+				it("should insert the new update after the delete", function() {
+					const diff = this.DiffGenerator.applyUpdateToDiff(
+						[ { d: "bar", meta: this.meta } ],
+						{ op: [{ p: 0, i: "baz" }], meta: this.meta }
+					);
+					return expect(diff).to.deep.equal([
+						{ d: "bar", meta: this.meta },
+						{ i: "baz", meta: this.meta }
+					]);
+				})
+			);
+		});
+	});
+});
 
 

+ 285 - 222
services/track-changes/test/unit/coffee/DiffManager/DiffManagerTests.js

@@ -1,240 +1,303 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-expect = chai.expect
-modulePath = "../../../../app/js/DiffManager.js"
-SandboxedModule = require('sandboxed-module')
-
-describe "DiffManager", ->
-	beforeEach ->
-		@DiffManager = SandboxedModule.require modulePath, requires:
-			"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() }
-			"./UpdatesManager": @UpdatesManager = {}
-			"./DocumentUpdaterManager": @DocumentUpdaterManager = {}
-			"./DiffGenerator": @DiffGenerator = {}
-		@callback = sinon.stub()
-		@from = new Date()
-		@to = new Date(Date.now() + 10000)
-		@project_id = "mock-project-id"
-		@doc_id = "mock-doc-id"
-
-	describe "getLatestDocAndUpdates", ->
-		beforeEach ->
-			@content = "hello world"
-			@version = 42
-			@updates = [ "mock-update-1", "mock-update-2" ]
-
-			@DocumentUpdaterManager.getDocument = sinon.stub().callsArgWith(2, null, @content, @version)
-			@UpdatesManager.getDocUpdatesWithUserInfo = sinon.stub().callsArgWith(3, null, @updates)
-
-		describe "with a fromVersion", ->
-			beforeEach ->
-				@DiffManager.getLatestDocAndUpdates @project_id, @doc_id, @from, @callback
-
-			it "should get the latest version of the doc", ->
-				@DocumentUpdaterManager.getDocument
-					.calledWith(@project_id, @doc_id)
-					.should.equal true
-
-			it "should get the latest updates", ->
-				@UpdatesManager.getDocUpdatesWithUserInfo
-					.calledWith(@project_id, @doc_id, from: @from)
-					.should.equal true
-
-			it "should call the callback with the content, version and updates", ->
-				@callback.calledWith(null, @content, @version, @updates).should.equal true
-
-		describe "with no fromVersion", ->
-			beforeEach ->
-				@DiffManager.getLatestDocAndUpdates @project_id, @doc_id, null, @callback
-
-			it "should get the latest version of the doc", ->
-				@DocumentUpdaterManager.getDocument
-					.calledWith(@project_id, @doc_id)
-					.should.equal true
-
-			it "should not get the latest updates", ->
-				@UpdatesManager.getDocUpdatesWithUserInfo
-					.called.should.equal false
-
-			it "should call the callback with the content, version and blank updates", ->
-				@callback.calledWith(null, @content, @version, []).should.equal 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 sinon = require('sinon');
+const chai = require('chai');
+const should = chai.should();
+const { expect } = chai;
+const modulePath = "../../../../app/js/DiffManager.js";
+const SandboxedModule = require('sandboxed-module');
+
+describe("DiffManager", function() {
+	beforeEach(function() {
+		this.DiffManager = SandboxedModule.require(modulePath, { requires: {
+			"logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() }),
+			"./UpdatesManager": (this.UpdatesManager = {}),
+			"./DocumentUpdaterManager": (this.DocumentUpdaterManager = {}),
+			"./DiffGenerator": (this.DiffGenerator = {})
+		}
+	});
+		this.callback = sinon.stub();
+		this.from = new Date();
+		this.to = new Date(Date.now() + 10000);
+		this.project_id = "mock-project-id";
+		return this.doc_id = "mock-doc-id";
+	});
+
+	describe("getLatestDocAndUpdates", function() {
+		beforeEach(function() {
+			this.content = "hello world";
+			this.version = 42;
+			this.updates = [ "mock-update-1", "mock-update-2" ];
+
+			this.DocumentUpdaterManager.getDocument = sinon.stub().callsArgWith(2, null, this.content, this.version);
+			return this.UpdatesManager.getDocUpdatesWithUserInfo = sinon.stub().callsArgWith(3, null, this.updates);
+		});
+
+		describe("with a fromVersion", function() {
+			beforeEach(function() {
+				return this.DiffManager.getLatestDocAndUpdates(this.project_id, this.doc_id, this.from, this.callback);
+			});
+
+			it("should get the latest version of the doc", function() {
+				return this.DocumentUpdaterManager.getDocument
+					.calledWith(this.project_id, this.doc_id)
+					.should.equal(true);
+			});
+
+			it("should get the latest updates", function() {
+				return this.UpdatesManager.getDocUpdatesWithUserInfo
+					.calledWith(this.project_id, this.doc_id, {from: this.from})
+					.should.equal(true);
+			});
+
+			return it("should call the callback with the content, version and updates", function() {
+				return this.callback.calledWith(null, this.content, this.version, this.updates).should.equal(true);
+			});
+		});
+
+		return describe("with no fromVersion", function() {
+			beforeEach(function() {
+				return this.DiffManager.getLatestDocAndUpdates(this.project_id, this.doc_id, null, this.callback);
+			});
+
+			it("should get the latest version of the doc", function() {
+				return this.DocumentUpdaterManager.getDocument
+					.calledWith(this.project_id, this.doc_id)
+					.should.equal(true);
+			});
+
+			it("should not get the latest updates", function() {
+				return this.UpdatesManager.getDocUpdatesWithUserInfo
+					.called.should.equal(false);
+			});
+
+			return it("should call the callback with the content, version and blank updates", function() {
+				return this.callback.calledWith(null, this.content, this.version, []).should.equal(true);
+			});
+		});
+	});
 			
 
-	describe "getDiff", ->
-		beforeEach ->
-			@content = "hello world"
-			# Op versions are the version they were applied to, so doc is always one version
-			# ahead.s
-			@version = 43
-			@updates = [
-				{ op: "mock-4", v: 42, meta: { start_ts: new Date(@to.getTime() + 20)} }
-				{ op: "mock-3", v: 41, meta: { start_ts: new Date(@to.getTime() + 10)} }
-				{ op: "mock-2", v: 40, meta: { start_ts: new Date(@to.getTime() - 10)} }
-				{ op: "mock-1", v: 39, meta: { start_ts: new Date(@to.getTime() - 20)} }
-			]
-			@fromVersion = 39
-			@toVersion = 40
-			@diffed_updates = @updates.slice(2)
-			@rewound_content = "rewound-content"
-			@diff = [ u: "mock-diff" ]
+	describe("getDiff", function() {
+		beforeEach(function() {
+			this.content = "hello world";
+			// Op versions are the version they were applied to, so doc is always one version
+			// ahead.s
+			this.version = 43;
+			this.updates = [
+				{ op: "mock-4", v: 42, meta: { start_ts: new Date(this.to.getTime() + 20)} },
+				{ op: "mock-3", v: 41, meta: { start_ts: new Date(this.to.getTime() + 10)} },
+				{ op: "mock-2", v: 40, meta: { start_ts: new Date(this.to.getTime() - 10)} },
+				{ op: "mock-1", v: 39, meta: { start_ts: new Date(this.to.getTime() - 20)} }
+			];
+			this.fromVersion = 39;
+			this.toVersion = 40;
+			this.diffed_updates = this.updates.slice(2);
+			this.rewound_content = "rewound-content";
+			return this.diff = [ {u: "mock-diff"} ];});
 			
-		describe "with matching versions", ->
-			beforeEach ->
-				@DiffManager.getDocumentBeforeVersion = sinon.stub().callsArgWith(3, null, @rewound_content, @updates)
-				@DiffGenerator.buildDiff = sinon.stub().returns(@diff)
-				@DiffManager.getDiff @project_id, @doc_id, @fromVersion, @toVersion, @callback
-
-			it "should get the latest doc and version with all recent updates", ->
-				@DiffManager.getDocumentBeforeVersion
-					.calledWith(@project_id, @doc_id, @fromVersion)
-					.should.equal true
-
-			it "should generate the diff", ->
-				@DiffGenerator.buildDiff
-					.calledWith(@rewound_content, @diffed_updates.slice().reverse())
-					.should.equal true
-
-			it "should call the callback with the diff", ->
-				@callback.calledWith(null, @diff).should.equal true
-
-		describe "when the updates are inconsistent", ->
-			beforeEach ->
-				@DiffManager.getLatestDocAndUpdates = sinon.stub().callsArgWith(3, null, @content, @version, @updates)
-				@DiffGenerator.buildDiff = sinon.stub().throws(@error = new Error("inconsistent!"))
-				@DiffManager.getDiff @project_id, @doc_id, @fromVersion, @toVersion, @callback
-
-			it "should call the callback with an error", ->
-				@callback
-					.calledWith(@error)
-					.should.equal true
-
-	describe "getDocumentBeforeVersion", ->
-		beforeEach ->
-			@DiffManager._tryGetDocumentBeforeVersion = sinon.stub()
-			@document = "mock-documents"
-			@rewound_updates = "mock-rewound-updates"
-
-		describe "succesfully", ->
-			beforeEach ->
-				@DiffManager._tryGetDocumentBeforeVersion.yields(null, @document, @rewound_updates)
-				@DiffManager.getDocumentBeforeVersion @project_id, @doc_id, @version, @callback
+		describe("with matching versions", function() {
+			beforeEach(function() {
+				this.DiffManager.getDocumentBeforeVersion = sinon.stub().callsArgWith(3, null, this.rewound_content, this.updates);
+				this.DiffGenerator.buildDiff = sinon.stub().returns(this.diff);
+				return this.DiffManager.getDiff(this.project_id, this.doc_id, this.fromVersion, this.toVersion, this.callback);
+			});
+
+			it("should get the latest doc and version with all recent updates", function() {
+				return this.DiffManager.getDocumentBeforeVersion
+					.calledWith(this.project_id, this.doc_id, this.fromVersion)
+					.should.equal(true);
+			});
+
+			it("should generate the diff", function() {
+				return this.DiffGenerator.buildDiff
+					.calledWith(this.rewound_content, this.diffed_updates.slice().reverse())
+					.should.equal(true);
+			});
+
+			return it("should call the callback with the diff", function() {
+				return this.callback.calledWith(null, this.diff).should.equal(true);
+			});
+		});
+
+		return describe("when the updates are inconsistent", function() {
+			beforeEach(function() {
+				this.DiffManager.getLatestDocAndUpdates = sinon.stub().callsArgWith(3, null, this.content, this.version, this.updates);
+				this.DiffGenerator.buildDiff = sinon.stub().throws(this.error = new Error("inconsistent!"));
+				return this.DiffManager.getDiff(this.project_id, this.doc_id, this.fromVersion, this.toVersion, this.callback);
+			});
+
+			return it("should call the callback with an error", function() {
+				return this.callback
+					.calledWith(this.error)
+					.should.equal(true);
+			});
+		});
+	});
+
+	describe("getDocumentBeforeVersion", function() {
+		beforeEach(function() {
+			this.DiffManager._tryGetDocumentBeforeVersion = sinon.stub();
+			this.document = "mock-documents";
+			return this.rewound_updates = "mock-rewound-updates";
+		});
+
+		describe("succesfully", function() {
+			beforeEach(function() {
+				this.DiffManager._tryGetDocumentBeforeVersion.yields(null, this.document, this.rewound_updates);
+				return this.DiffManager.getDocumentBeforeVersion(this.project_id, this.doc_id, this.version, this.callback);
+			});
 			
-			it "should call _tryGetDocumentBeforeVersion", ->
-				@DiffManager._tryGetDocumentBeforeVersion
-					.calledWith(@project_id, @doc_id, @version)
-					.should.equal true
+			it("should call _tryGetDocumentBeforeVersion", function() {
+				return this.DiffManager._tryGetDocumentBeforeVersion
+					.calledWith(this.project_id, this.doc_id, this.version)
+					.should.equal(true);
+			});
 			
-			it "should call the callback with the response", ->
-				@callback.calledWith(null, @document, @rewound_updates).should.equal true
+			return it("should call the callback with the response", function() {
+				return this.callback.calledWith(null, this.document, this.rewound_updates).should.equal(true);
+			});
+		});
 		
-		describe "with a retry needed", ->
-			beforeEach ->
-				retried = false
-				@DiffManager._tryGetDocumentBeforeVersion = (project_id, doc_id, version, callback) =>
-					if !retried
-						retried = true
-						error = new Error()
-						error.retry = true
-						callback error
-					else
-						callback(null, @document, @rewound_updates)
-				sinon.spy @DiffManager, "_tryGetDocumentBeforeVersion"
-				@DiffManager.getDocumentBeforeVersion @project_id, @doc_id, @version, @callback
+		describe("with a retry needed", function() {
+			beforeEach(function() {
+				let retried = false;
+				this.DiffManager._tryGetDocumentBeforeVersion = (project_id, doc_id, version, callback) => {
+					if (!retried) {
+						retried = true;
+						const error = new Error();
+						error.retry = true;
+						return callback(error);
+					} else {
+						return callback(null, this.document, this.rewound_updates);
+					}
+				};
+				sinon.spy(this.DiffManager, "_tryGetDocumentBeforeVersion");
+				return this.DiffManager.getDocumentBeforeVersion(this.project_id, this.doc_id, this.version, this.callback);
+			});
 			
-			it "should call _tryGetDocumentBeforeVersion twice", ->
-				@DiffManager._tryGetDocumentBeforeVersion
+			it("should call _tryGetDocumentBeforeVersion twice", function() {
+				return this.DiffManager._tryGetDocumentBeforeVersion
 					.calledTwice
-					.should.equal true
+					.should.equal(true);
+			});
 			
-			it "should call the callback with the response", ->
-				@callback.calledWith(null, @document, @rewound_updates).should.equal true
+			return it("should call the callback with the response", function() {
+				return this.callback.calledWith(null, this.document, this.rewound_updates).should.equal(true);
+			});
+		});
 		
-		describe "with a non-retriable error", ->
-			beforeEach ->
-				@error = new Error("oops")
-				@DiffManager._tryGetDocumentBeforeVersion.yields(@error)
-				@DiffManager.getDocumentBeforeVersion @project_id, @doc_id, @version, @callback
+		describe("with a non-retriable error", function() {
+			beforeEach(function() {
+				this.error = new Error("oops");
+				this.DiffManager._tryGetDocumentBeforeVersion.yields(this.error);
+				return this.DiffManager.getDocumentBeforeVersion(this.project_id, this.doc_id, this.version, this.callback);
+			});
 			
-			it "should call _tryGetDocumentBeforeVersion once", ->
-				@DiffManager._tryGetDocumentBeforeVersion
+			it("should call _tryGetDocumentBeforeVersion once", function() {
+				return this.DiffManager._tryGetDocumentBeforeVersion
 					.calledOnce
-					.should.equal true
+					.should.equal(true);
+			});
 			
-			it "should call the callback with the error", ->
-				@callback.calledWith(@error).should.equal true
+			return it("should call the callback with the error", function() {
+				return this.callback.calledWith(this.error).should.equal(true);
+			});
+		});
 		
-		describe "when retry limit is matched", ->
-			beforeEach ->
-				@error = new Error("oops")
-				@error.retry = true
-				@DiffManager._tryGetDocumentBeforeVersion.yields(@error)
-				@DiffManager.getDocumentBeforeVersion @project_id, @doc_id, @version, @callback
+		return describe("when retry limit is matched", function() {
+			beforeEach(function() {
+				this.error = new Error("oops");
+				this.error.retry = true;
+				this.DiffManager._tryGetDocumentBeforeVersion.yields(this.error);
+				return this.DiffManager.getDocumentBeforeVersion(this.project_id, this.doc_id, this.version, this.callback);
+			});
 			
-			it "should call _tryGetDocumentBeforeVersion three times (max retries)", ->
-				@DiffManager._tryGetDocumentBeforeVersion
+			it("should call _tryGetDocumentBeforeVersion three times (max retries)", function() {
+				return this.DiffManager._tryGetDocumentBeforeVersion
 					.calledThrice
-					.should.equal true
+					.should.equal(true);
+			});
 			
-			it "should call the callback with the error", ->
-				@callback.calledWith(@error).should.equal true
-
-	describe "_tryGetDocumentBeforeVersion", ->
-		beforeEach ->
-			@content = "hello world"
-			# Op versions are the version they were applied to, so doc is always one version
-			# ahead.s
-			@version = 43
-			@updates = [
-				{ op: "mock-4", v: 42, meta: { start_ts: new Date(@to.getTime() + 20)} }
-				{ op: "mock-3", v: 41, meta: { start_ts: new Date(@to.getTime() + 10)} }
-				{ op: "mock-2", v: 40, meta: { start_ts: new Date(@to.getTime() - 10)} }
-				{ op: "mock-1", v: 39, meta: { start_ts: new Date(@to.getTime() - 20)} }
-			]
-			@fromVersion = 39
-			@rewound_content = "rewound-content"
-			@diff = [ u: "mock-diff" ]
+			return it("should call the callback with the error", function() {
+				return this.callback.calledWith(this.error).should.equal(true);
+			});
+		});
+	});
+
+	return describe("_tryGetDocumentBeforeVersion", function() {
+		beforeEach(function() {
+			this.content = "hello world";
+			// Op versions are the version they were applied to, so doc is always one version
+			// ahead.s
+			this.version = 43;
+			this.updates = [
+				{ op: "mock-4", v: 42, meta: { start_ts: new Date(this.to.getTime() + 20)} },
+				{ op: "mock-3", v: 41, meta: { start_ts: new Date(this.to.getTime() + 10)} },
+				{ op: "mock-2", v: 40, meta: { start_ts: new Date(this.to.getTime() - 10)} },
+				{ op: "mock-1", v: 39, meta: { start_ts: new Date(this.to.getTime() - 20)} }
+			];
+			this.fromVersion = 39;
+			this.rewound_content = "rewound-content";
+			return this.diff = [ {u: "mock-diff"} ];});
 			
-		describe "with matching versions", ->
-			beforeEach ->
-				@DiffManager.getLatestDocAndUpdates = sinon.stub().callsArgWith(3, null, @content, @version, @updates)
-				@DiffGenerator.rewindUpdates = sinon.spy (content, updates) =>
-					# the rewindUpdates method reverses the 'updates' array
-					updates.reverse()
-					return @rewound_content
-				@rewindUpdatesWithArgs = @DiffGenerator.rewindUpdates.withArgs(@content, @updates.slice().reverse())
-				@DiffManager._tryGetDocumentBeforeVersion @project_id, @doc_id, @fromVersion, @callback
-
-			it "should get the latest doc and version with all recent updates", ->
-				@DiffManager.getLatestDocAndUpdates
-					.calledWith(@project_id, @doc_id, @fromVersion)
-					.should.equal true
-
-			it "should rewind the diff", ->
-				sinon.assert.calledOnce(@rewindUpdatesWithArgs)
-
-			it "should call the callback with the rewound document and updates", ->
-				@callback.calledWith(null, @rewound_content, @updates).should.equal true
-
-		describe "with mismatching versions", ->
-			beforeEach ->
-				@version = 50
-				@updates = [ { op: "mock-1", v: 40 }, { op: "mock-1", v: 39 } ]
-				@DiffManager.getLatestDocAndUpdates = sinon.stub().callsArgWith(3, null, @content, @version, @updates)
-				@DiffManager._tryGetDocumentBeforeVersion @project_id, @doc_id, @fromVersion, @callback
-
-			it "should call the callback with an error with retry = true set", ->
-				@callback.calledOnce.should.equal true
-				error = @callback.args[0][0]
-				expect(error.retry).to.equal true
-
-		describe "when the updates are inconsistent", ->
-			beforeEach ->
-				@DiffManager.getLatestDocAndUpdates = sinon.stub().callsArgWith(3, null, @content, @version, @updates)
-				@DiffGenerator.rewindUpdates = sinon.stub().throws(@error = new Error("inconsistent!"))
-				@DiffManager.getDocumentBeforeVersion @project_id, @doc_id, @fromVersion, @callback
-
-			it "should call the callback with an error", ->
-				@callback
-					.calledWith(@error)
-					.should.equal true
+		describe("with matching versions", function() {
+			beforeEach(function() {
+				this.DiffManager.getLatestDocAndUpdates = sinon.stub().callsArgWith(3, null, this.content, this.version, this.updates);
+				this.DiffGenerator.rewindUpdates = sinon.spy((content, updates) => {
+					// the rewindUpdates method reverses the 'updates' array
+					updates.reverse();
+					return this.rewound_content;
+				});
+				this.rewindUpdatesWithArgs = this.DiffGenerator.rewindUpdates.withArgs(this.content, this.updates.slice().reverse());
+				return this.DiffManager._tryGetDocumentBeforeVersion(this.project_id, this.doc_id, this.fromVersion, this.callback);
+			});
+
+			it("should get the latest doc and version with all recent updates", function() {
+				return this.DiffManager.getLatestDocAndUpdates
+					.calledWith(this.project_id, this.doc_id, this.fromVersion)
+					.should.equal(true);
+			});
+
+			it("should rewind the diff", function() {
+				return sinon.assert.calledOnce(this.rewindUpdatesWithArgs);
+			});
+
+			return it("should call the callback with the rewound document and updates", function() {
+				return this.callback.calledWith(null, this.rewound_content, this.updates).should.equal(true);
+			});
+		});
+
+		describe("with mismatching versions", function() {
+			beforeEach(function() {
+				this.version = 50;
+				this.updates = [ { op: "mock-1", v: 40 }, { op: "mock-1", v: 39 } ];
+				this.DiffManager.getLatestDocAndUpdates = sinon.stub().callsArgWith(3, null, this.content, this.version, this.updates);
+				return this.DiffManager._tryGetDocumentBeforeVersion(this.project_id, this.doc_id, this.fromVersion, this.callback);
+			});
+
+			return it("should call the callback with an error with retry = true set", function() {
+				this.callback.calledOnce.should.equal(true);
+				const error = this.callback.args[0][0];
+				return expect(error.retry).to.equal(true);
+			});
+		});
+
+		return describe("when the updates are inconsistent", function() {
+			beforeEach(function() {
+				this.DiffManager.getLatestDocAndUpdates = sinon.stub().callsArgWith(3, null, this.content, this.version, this.updates);
+				this.DiffGenerator.rewindUpdates = sinon.stub().throws(this.error = new Error("inconsistent!"));
+				return this.DiffManager.getDocumentBeforeVersion(this.project_id, this.doc_id, this.fromVersion, this.callback);
+			});
+
+			return it("should call the callback with an error", function() {
+				return this.callback
+					.calledWith(this.error)
+					.should.equal(true);
+			});
+		});
+	});
+});

+ 78 - 58
services/track-changes/test/unit/coffee/DocArchive/MongoAWS.js

@@ -1,70 +1,90 @@
-chai = require('chai')
-chai.should()
-sinon = require("sinon")
-modulePath = "../../../../app/js/MongoAWS.js"
-SandboxedModule = require('sandboxed-module')
-{ObjectId} = require("mongojs")
-MemoryStream = require('memorystream')
-zlib = require "zlib"
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const chai = require('chai');
+chai.should();
+const sinon = require("sinon");
+const modulePath = "../../../../app/js/MongoAWS.js";
+const SandboxedModule = require('sandboxed-module');
+const {ObjectId} = require("mongojs");
+const MemoryStream = require('memorystream');
+const zlib = require("zlib");
 
-describe "MongoAWS", ->
-	beforeEach ->
-		@MongoAWS = SandboxedModule.require modulePath, requires:
-			"settings-sharelatex": @settings =
-				trackchanges:
-					s3:
-						secret: "s3-secret"
+describe("MongoAWS", function() {
+	beforeEach(function() {
+		this.MongoAWS = SandboxedModule.require(modulePath, { requires: {
+			"settings-sharelatex": (this.settings = {
+				trackchanges: {
+					s3: {
+						secret: "s3-secret",
 						key: "s3-key"
-					stores:
+					},
+					stores: {
 						doc_history: "s3-bucket"
-			"child_process": @child_process = {}
-			"mongo-uri": @mongouri = {}
-			"logger-sharelatex": @logger = {log: sinon.stub(), error: sinon.stub(), err:->}
-			"aws-sdk": @awssdk = {}
-			"fs": @fs = {}
-			"s3-streams": @S3S = {}
-			"./mongojs" : { db: @db = {}, ObjectId: ObjectId }
-			"JSONStream": @JSONStream = {}
-			"readline-stream": @readline = sinon.stub()
-			'metrics-sharelatex': {inc: ()->}
+					}
+				}
+			}),
+			"child_process": (this.child_process = {}),
+			"mongo-uri": (this.mongouri = {}),
+			"logger-sharelatex": (this.logger = {log: sinon.stub(), error: sinon.stub(), err() {}}),
+			"aws-sdk": (this.awssdk = {}),
+			"fs": (this.fs = {}),
+			"s3-streams": (this.S3S = {}),
+			"./mongojs" : { db: (this.db = {}), ObjectId },
+			"JSONStream": (this.JSONStream = {}),
+			"readline-stream": (this.readline = sinon.stub()),
+			'metrics-sharelatex': {inc(){}}
+		}
+	});
 
-		@project_id = ObjectId().toString()
-		@doc_id = ObjectId().toString()
-		@pack_id = ObjectId()
-		@update = { v:123 }
-		@callback = sinon.stub()
+		this.project_id = ObjectId().toString();
+		this.doc_id = ObjectId().toString();
+		this.pack_id = ObjectId();
+		this.update = { v:123 };
+		return this.callback = sinon.stub();
+	});
 
-	describe "archivePack", ->
+	describe("archivePack", function() {
 
-		beforeEach (done) ->
-			@awssdk.config = { update: sinon.stub() }
-			@awssdk.S3 = sinon.stub()
-			@S3S.WriteStream = () ->
-				MemoryStream.createWriteStream()
-			@db.docHistory = {}
-			@db.docHistory.findOne = sinon.stub().callsArgWith(1, null, {"pack":"hello"})
+		beforeEach(function(done) {
+			this.awssdk.config = { update: sinon.stub() };
+			this.awssdk.S3 = sinon.stub();
+			this.S3S.WriteStream = () => MemoryStream.createWriteStream();
+			this.db.docHistory = {};
+			this.db.docHistory.findOne = sinon.stub().callsArgWith(1, null, {"pack":"hello"});
 
-			@MongoAWS.archivePack @project_id, @doc_id, @pack_id, (err, result) =>
-				@callback()
-				done()
+			return this.MongoAWS.archivePack(this.project_id, this.doc_id, this.pack_id, (err, result) => {
+				this.callback();
+				return done();
+			});
+		});
 
-		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 "unArchivePack", ->
+	return describe("unArchivePack", function() {
 
-		beforeEach (done) ->
-			zlib.gzip '{"pack":"123"}', (err, zbuf) =>
-				@awssdk.config = { update: sinon.stub() }
-				@awssdk.S3 = sinon.stub()
-				@S3S.ReadStream = () ->
-					MemoryStream.createReadStream(zbuf, {readable:true})
-				@db.docHistory = {}
-				@db.docHistory.insert = sinon.stub().callsArgWith(1, null, "pack")
+		beforeEach(function(done) {
+			return zlib.gzip('{"pack":"123"}', (err, zbuf) => {
+				this.awssdk.config = { update: sinon.stub() };
+				this.awssdk.S3 = sinon.stub();
+				this.S3S.ReadStream = () => MemoryStream.createReadStream(zbuf, {readable:true});
+				this.db.docHistory = {};
+				this.db.docHistory.insert = sinon.stub().callsArgWith(1, null, "pack");
 
-				@MongoAWS.unArchivePack @project_id, @doc_id, @pack_id, (err, result) =>
-					@callback()
-					done()
+				return this.MongoAWS.unArchivePack(this.project_id, this.doc_id, this.pack_id, (err, result) => {
+					this.callback();
+					return done();
+				});
+			});
+		});
 
-		it "should call db.docHistory.insert", ->
-			@db.docHistory.insert.called.should.equal true
+		return it("should call db.docHistory.insert", function() {
+			return this.db.docHistory.insert.called.should.equal(true);
+		});
+	});
+});

+ 125 - 91
services/track-changes/test/unit/coffee/DocumentUpdaterManager/DocumentUpdaterManagerTests.js

@@ -1,95 +1,129 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-expect = chai.expect
-modulePath = "../../../../app/js/DocumentUpdaterManager.js"
-SandboxedModule = require('sandboxed-module')
-
-describe "DocumentUpdaterManager", ->
-	beforeEach ->
-		@DocumentUpdaterManager = SandboxedModule.require modulePath, requires:
-			"request": @request = {}
-			"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
-			'settings-sharelatex': @settings =
-				apis : documentupdater: url : "http://example.com"
-		@callback = sinon.stub()
-		@lines = ["one", "two", "three"]
-		@version = 42
-
-	describe "getDocument", ->
-		describe "successfully", ->
-			beforeEach ->
-				@body = JSON.stringify
-					lines: @lines
-					version: @version
-					ops: []
-				@request.get = sinon.stub().callsArgWith(1, null, {statusCode: 200}, @body)
-				@DocumentUpdaterManager.getDocument @project_id, @doc_id, @callback
-
-			it 'should get the document from the document updater', ->
-				url = "#{@settings.apis.documentupdater.url}/project/#{@project_id}/doc/#{@doc_id}"
-				@request.get.calledWith(url).should.equal true
-
-			it "should call the callback with the content and version", ->
-				@callback.calledWith(null, @lines.join("\n"), @version).should.equal true
-
-		describe "when the document updater API returns an error", ->
-			beforeEach ->
-				@request.get = sinon.stub().callsArgWith(1, @error = new Error("something went wrong"), null, null)
-				@DocumentUpdaterManager.getDocument @project_id, @doc_id, @callback
-
-			it "should return an error to the callback", ->
-				@callback.calledWith(@error).should.equal true
-
-		describe "when the document updater returns a failure error code", ->
-			beforeEach ->
-				@request.get = sinon.stub().callsArgWith(1, null, { statusCode: 500 }, "")
-				@DocumentUpdaterManager.getDocument @project_id, @doc_id, @callback
-
-			it "should return the callback with an error", ->
-				@callback
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require('sinon');
+const chai = require('chai');
+const should = chai.should();
+const { expect } = chai;
+const modulePath = "../../../../app/js/DocumentUpdaterManager.js";
+const SandboxedModule = require('sandboxed-module');
+
+describe("DocumentUpdaterManager", function() {
+	beforeEach(function() {
+		this.DocumentUpdaterManager = SandboxedModule.require(modulePath, { requires: {
+			"request": (this.request = {}),
+			"logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }),
+			'settings-sharelatex': (this.settings =
+				{apis : {documentupdater: {url : "http://example.com"}}})
+		}
+	}
+		);
+		this.callback = sinon.stub();
+		this.lines = ["one", "two", "three"];
+		return this.version = 42;
+	});
+
+	describe("getDocument", function() {
+		describe("successfully", function() {
+			beforeEach(function() {
+				this.body = JSON.stringify({
+					lines: this.lines,
+					version: this.version,
+					ops: []});
+				this.request.get = sinon.stub().callsArgWith(1, null, {statusCode: 200}, this.body);
+				return this.DocumentUpdaterManager.getDocument(this.project_id, this.doc_id, this.callback);
+			});
+
+			it('should get the document from the document updater', function() {
+				const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}`;
+				return this.request.get.calledWith(url).should.equal(true);
+			});
+
+			return it("should call the callback with the content and version", function() {
+				return this.callback.calledWith(null, this.lines.join("\n"), this.version).should.equal(true);
+			});
+		});
+
+		describe("when the document updater API returns an error", function() {
+			beforeEach(function() {
+				this.request.get = sinon.stub().callsArgWith(1, (this.error = new Error("something went wrong")), null, null);
+				return this.DocumentUpdaterManager.getDocument(this.project_id, this.doc_id, this.callback);
+			});
+
+			return it("should return an error to the callback", function() {
+				return this.callback.calledWith(this.error).should.equal(true);
+			});
+		});
+
+		return describe("when the document updater returns a failure error code", function() {
+			beforeEach(function() {
+				this.request.get = sinon.stub().callsArgWith(1, null, { statusCode: 500 }, "");
+				return this.DocumentUpdaterManager.getDocument(this.project_id, this.doc_id, this.callback);
+			});
+
+			return it("should return the callback with an error", function() {
+				return this.callback
 					.calledWith(sinon.match.has('message', "doc updater returned a non-success status code: 500"))
-					.should.equal true
-
-	describe "setDocument", ->
-		beforeEach ->
-			@content = "mock content"
-			@user_id = "user-id-123"
-
-		describe "successfully", ->
-			beforeEach ->
-				@request.post = sinon.stub().callsArgWith(1, null, {statusCode: 200})
-				@DocumentUpdaterManager.setDocument @project_id, @doc_id, @content, @user_id, @callback
-
-			it 'should set the document in the document updater', ->
-				url = "#{@settings.apis.documentupdater.url}/project/#{@project_id}/doc/#{@doc_id}"
-				@request.post
+					.should.equal(true);
+			});
+		});
+	});
+
+	return describe("setDocument", function() {
+		beforeEach(function() {
+			this.content = "mock content";
+			return this.user_id = "user-id-123";
+		});
+
+		describe("successfully", function() {
+			beforeEach(function() {
+				this.request.post = sinon.stub().callsArgWith(1, null, {statusCode: 200});
+				return this.DocumentUpdaterManager.setDocument(this.project_id, this.doc_id, this.content, this.user_id, this.callback);
+			});
+
+			it('should set the document in the document updater', function() {
+				const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}`;
+				return this.request.post
 					.calledWith({
-						url: url
-						json:
-							lines: @content.split("\n")
-							source: "restore"
-							user_id: @user_id
+						url,
+						json: {
+							lines: this.content.split("\n"),
+							source: "restore",
+							user_id: this.user_id,
 							undoing: true
-					}).should.equal true
-
-			it "should call the callback", ->
-				@callback.calledWith(null).should.equal true
-
-		describe "when the document updater API returns an error", ->
-			beforeEach ->
-				@request.post = sinon.stub().callsArgWith(1, @error = new Error("something went wrong"), null, null)
-				@DocumentUpdaterManager.setDocument @project_id, @doc_id, @content, @user_id, @callback
-
-			it "should return an error to the callback", ->
-				@callback.calledWith(@error).should.equal true
-
-		describe "when the document updater returns a failure error code", ->
-			beforeEach ->
-				@request.post = sinon.stub().callsArgWith(1, null, { statusCode: 500 }, "")
-				@DocumentUpdaterManager.setDocument @project_id, @doc_id, @content, @user_id, @callback
-
-			it "should return the callback with an error", ->
-				@callback
+						}
+					}).should.equal(true);
+			});
+
+			return it("should call the callback", function() {
+				return this.callback.calledWith(null).should.equal(true);
+			});
+		});
+
+		describe("when the document updater API returns an error", function() {
+			beforeEach(function() {
+				this.request.post = sinon.stub().callsArgWith(1, (this.error = new Error("something went wrong")), null, null);
+				return this.DocumentUpdaterManager.setDocument(this.project_id, this.doc_id, this.content, this.user_id, this.callback);
+			});
+
+			return it("should return an error to the callback", function() {
+				return this.callback.calledWith(this.error).should.equal(true);
+			});
+		});
+
+		return describe("when the document updater returns a failure error code", function() {
+			beforeEach(function() {
+				this.request.post = sinon.stub().callsArgWith(1, null, { statusCode: 500 }, "");
+				return this.DocumentUpdaterManager.setDocument(this.project_id, this.doc_id, this.content, this.user_id, this.callback);
+			});
+
+			return it("should return the callback with an error", function() {
+				return this.callback
 					.calledWith(sinon.match.has('message', "doc updater returned a non-success status code: 500"))
-					.should.equal true
+					.should.equal(true);
+			});
+		});
+	});
+});

+ 176 - 134
services/track-changes/test/unit/coffee/HttpController/HttpControllerTests.js

@@ -1,135 +1,177 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-expect = chai.expect
-modulePath = "../../../../app/js/HttpController.js"
-SandboxedModule = require('sandboxed-module')
-
-describe "HttpController", ->
-	beforeEach ->
-		@HttpController = SandboxedModule.require modulePath, requires:
-			"logger-sharelatex": { log: sinon.stub() }
-			"./UpdatesManager": @UpdatesManager = {}
-			"./DiffManager": @DiffManager = {}
-			"./RestoreManager": @RestoreManager = {}
-			"./PackManager": @PackManager = {}
-			"./DocArchiveManager": @DocArchiveManager = {}
-			"./HealthChecker": @HealthChecker = {}
-		@doc_id = "doc-id-123"
-		@project_id = "project-id-123"
-		@next = sinon.stub()
-		@user_id = "mock-user-123"
-		@now = Date.now()
-
-	describe "flushDoc", ->
-		beforeEach ->
-			@req =
-				params:
-					doc_id: @doc_id
-					project_id: @project_id
-			@res =
-				send: sinon.stub()
-			@UpdatesManager.processUncompressedUpdatesWithLock = sinon.stub().callsArg(2)
-			@HttpController.flushDoc @req, @res, @next
-
-		it "should process the updates", ->
-			@UpdatesManager.processUncompressedUpdatesWithLock
-				.calledWith(@project_id, @doc_id)
-				.should.equal true
-
-		it "should return a success code", ->
-			@res.send.calledWith(204).should.equal true
-
-	describe "flushProject", ->
-		beforeEach ->
-			@req =
-				params:
-					project_id: @project_id
-			@res =
-				send: sinon.stub()
-			@UpdatesManager.processUncompressedUpdatesForProject = sinon.stub().callsArg(1)
-			@HttpController.flushProject @req, @res, @next
-
-		it "should process the updates", ->
-			@UpdatesManager.processUncompressedUpdatesForProject
-				.calledWith(@project_id)
-				.should.equal true
-
-		it "should return a success code", ->
-			@res.send.calledWith(204).should.equal true
-
-
-	describe "getDiff", ->
-		beforeEach ->
-			@from = 42
-			@to = 45
-			@req =
-				params:
-					doc_id: @doc_id
-					project_id: @project_id
-				query:
-					from: @from.toString()
-					to: @to.toString()
-			@res =
-				json: sinon.stub()
-			@diff = [ u: "mock-diff" ]
-			@DiffManager.getDiff = sinon.stub().callsArgWith(4, null, @diff)
-			@HttpController.getDiff @req, @res, @next
-
-		it "should get the diff", ->
-			@DiffManager.getDiff
-				.calledWith(@project_id, @doc_id, parseInt(@from, 10), parseInt(@to, 10))
-				.should.equal true
-
-		it "should return the diff", ->
-			@res.json.calledWith({diff: @diff}).should.equal true
-
-	describe "getUpdates", ->
-		beforeEach ->
-			@before = Date.now()
-			@nextBeforeTimestamp = @before - 100
-			@min_count = 10
-			@req =
-				params:
-					project_id: @project_id
-				query:
-					before:    @before.toString()
-					min_count: @min_count.toString()
-			@res =
-				json: sinon.stub()
-			@updates = ["mock-summarized-updates"]
-			@UpdatesManager.getSummarizedProjectUpdates = sinon.stub().callsArgWith(2, null, @updates, @nextBeforeTimestamp)
-			@HttpController.getUpdates @req, @res, @next
-
-		it "should get the updates", ->
-			@UpdatesManager.getSummarizedProjectUpdates
-				.calledWith(@project_id, before: @before, min_count: @min_count)
-				.should.equal true
-
-		it "should return the formatted updates", ->
-			@res.json.calledWith({updates: @updates, nextBeforeTimestamp: @nextBeforeTimestamp}).should.equal true
-
-	describe "RestoreManager", ->
-		beforeEach ->
-			@version = "42"
-			@req =
-				params:
-					doc_id: @doc_id
-					project_id: @project_id
-					version: @version
-				headers:
-					"x-user-id": @user_id
-			@res =
-				send: sinon.stub()
-
-			@RestoreManager.restoreToBeforeVersion = sinon.stub().callsArg(4)
-			@HttpController.restore @req, @res, @next
-
-		it "should restore the document", ->
-			@RestoreManager.restoreToBeforeVersion
-				.calledWith(@project_id, @doc_id, parseInt(@version, 10), @user_id)
-				.should.equal true
-
-		it "should return a success code", ->
-			@res.send.calledWith(204).should.equal 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 sinon = require('sinon');
+const chai = require('chai');
+const should = chai.should();
+const { expect } = chai;
+const modulePath = "../../../../app/js/HttpController.js";
+const SandboxedModule = require('sandboxed-module');
+
+describe("HttpController", function() {
+	beforeEach(function() {
+		this.HttpController = SandboxedModule.require(modulePath, { requires: {
+			"logger-sharelatex": { log: sinon.stub() },
+			"./UpdatesManager": (this.UpdatesManager = {}),
+			"./DiffManager": (this.DiffManager = {}),
+			"./RestoreManager": (this.RestoreManager = {}),
+			"./PackManager": (this.PackManager = {}),
+			"./DocArchiveManager": (this.DocArchiveManager = {}),
+			"./HealthChecker": (this.HealthChecker = {})
+		}
+	});
+		this.doc_id = "doc-id-123";
+		this.project_id = "project-id-123";
+		this.next = sinon.stub();
+		this.user_id = "mock-user-123";
+		return this.now = Date.now();
+	});
+
+	describe("flushDoc", function() {
+		beforeEach(function() {
+			this.req = {
+				params: {
+					doc_id: this.doc_id,
+					project_id: this.project_id
+				}
+			};
+			this.res =
+				{send: sinon.stub()};
+			this.UpdatesManager.processUncompressedUpdatesWithLock = sinon.stub().callsArg(2);
+			return this.HttpController.flushDoc(this.req, this.res, this.next);
+		});
+
+		it("should process the updates", function() {
+			return this.UpdatesManager.processUncompressedUpdatesWithLock
+				.calledWith(this.project_id, this.doc_id)
+				.should.equal(true);
+		});
+
+		return it("should return a success code", function() {
+			return this.res.send.calledWith(204).should.equal(true);
+		});
+	});
+
+	describe("flushProject", function() {
+		beforeEach(function() {
+			this.req = {
+				params: {
+					project_id: this.project_id
+				}
+			};
+			this.res =
+				{send: sinon.stub()};
+			this.UpdatesManager.processUncompressedUpdatesForProject = sinon.stub().callsArg(1);
+			return this.HttpController.flushProject(this.req, this.res, this.next);
+		});
+
+		it("should process the updates", function() {
+			return this.UpdatesManager.processUncompressedUpdatesForProject
+				.calledWith(this.project_id)
+				.should.equal(true);
+		});
+
+		return it("should return a success code", function() {
+			return this.res.send.calledWith(204).should.equal(true);
+		});
+	});
+
+
+	describe("getDiff", function() {
+		beforeEach(function() {
+			this.from = 42;
+			this.to = 45;
+			this.req = {
+				params: {
+					doc_id: this.doc_id,
+					project_id: this.project_id
+				},
+				query: {
+					from: this.from.toString(),
+					to: this.to.toString()
+				}
+			};
+			this.res =
+				{json: sinon.stub()};
+			this.diff = [ {u: "mock-diff"} ];
+			this.DiffManager.getDiff = sinon.stub().callsArgWith(4, null, this.diff);
+			return this.HttpController.getDiff(this.req, this.res, this.next);
+		});
+
+		it("should get the diff", function() {
+			return this.DiffManager.getDiff
+				.calledWith(this.project_id, this.doc_id, parseInt(this.from, 10), parseInt(this.to, 10))
+				.should.equal(true);
+		});
+
+		return it("should return the diff", function() {
+			return this.res.json.calledWith({diff: this.diff}).should.equal(true);
+		});
+	});
+
+	describe("getUpdates", function() {
+		beforeEach(function() {
+			this.before = Date.now();
+			this.nextBeforeTimestamp = this.before - 100;
+			this.min_count = 10;
+			this.req = {
+				params: {
+					project_id: this.project_id
+				},
+				query: {
+					before:    this.before.toString(),
+					min_count: this.min_count.toString()
+				}
+			};
+			this.res =
+				{json: sinon.stub()};
+			this.updates = ["mock-summarized-updates"];
+			this.UpdatesManager.getSummarizedProjectUpdates = sinon.stub().callsArgWith(2, null, this.updates, this.nextBeforeTimestamp);
+			return this.HttpController.getUpdates(this.req, this.res, this.next);
+		});
+
+		it("should get the updates", function() {
+			return this.UpdatesManager.getSummarizedProjectUpdates
+				.calledWith(this.project_id, {before: this.before, min_count: this.min_count})
+				.should.equal(true);
+		});
+
+		return it("should return the formatted updates", function() {
+			return this.res.json.calledWith({updates: this.updates, nextBeforeTimestamp: this.nextBeforeTimestamp}).should.equal(true);
+		});
+	});
+
+	return describe("RestoreManager", function() {
+		beforeEach(function() {
+			this.version = "42";
+			this.req = {
+				params: {
+					doc_id: this.doc_id,
+					project_id: this.project_id,
+					version: this.version
+				},
+				headers: {
+					"x-user-id": this.user_id
+				}
+			};
+			this.res =
+				{send: sinon.stub()};
+
+			this.RestoreManager.restoreToBeforeVersion = sinon.stub().callsArg(4);
+			return this.HttpController.restore(this.req, this.res, this.next);
+		});
+
+		it("should restore the document", function() {
+			return this.RestoreManager.restoreToBeforeVersion
+				.calledWith(this.project_id, this.doc_id, parseInt(this.version, 10), this.user_id)
+				.should.equal(true);
+		});
+
+		return it("should return a success code", function() {
+			return this.res.send.calledWith(204).should.equal(true);
+		});
+	});
+});
 

+ 272 - 195
services/track-changes/test/unit/coffee/LockManager/LockManagerTests.js

@@ -1,198 +1,275 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-expect = chai.expect
-modulePath = "../../../../app/js/LockManager.js"
-SandboxedModule = require('sandboxed-module')
-
-describe "LockManager", ->
-	beforeEach ->
-		@Settings = 		
-			redis:
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require('sinon');
+const chai = require('chai');
+const should = chai.should();
+const { expect } = chai;
+const modulePath = "../../../../app/js/LockManager.js";
+const SandboxedModule = require('sandboxed-module');
+
+describe("LockManager", function() {
+	beforeEach(function() {
+		this.Settings = { 		
+			redis: {
 				lock:{}
-		@LockManager = SandboxedModule.require modulePath, requires:
-			"redis-sharelatex":
-				createClient: () => @rclient =
-					auth: sinon.stub()
-			"settings-sharelatex": @Settings
-			"logger-sharelatex": {error: ->}
-
-		@key = "lock-key"
-		@callback = sinon.stub()
-
-	describe "checkLock", ->
-		describe "when the lock is taken", ->
-			beforeEach ->
-				@rclient.exists = sinon.stub().callsArgWith(1, null, "1")
-				@LockManager.checkLock @key, @callback
-
-			it "should check the lock in redis", ->
-				@rclient.exists
-					.calledWith(@key)
-					.should.equal true
-
-			it "should return the callback with false", ->
-				@callback.calledWith(null, false).should.equal true
-
-		describe "when the lock is free", ->
-			beforeEach ->
-				@rclient.exists = sinon.stub().callsArgWith(1, null, "0")
-				@LockManager.checkLock @key, @callback
-
-			it "should return the callback with true", ->
-				@callback.calledWith(null, true).should.equal true
-
-
-	describe "tryLock", ->
-		describe "when the lock is taken", ->
-			beforeEach ->
-				@rclient.set = sinon.stub().callsArgWith(5, null, null)
-				@LockManager.randomLock = sinon.stub().returns "locked-random-value"
-				@LockManager.tryLock @key, @callback
-
-			it "should check the lock in redis", ->
-				@rclient.set
-					.calledWith(@key, "locked-random-value", "EX", @LockManager.LOCK_TTL, "NX")
-					.should.equal true
-
-			it "should return the callback with false", ->
-				@callback.calledWith(null, false).should.equal true
-
-		describe "when the lock is free", ->
-			beforeEach ->
-				@rclient.set = sinon.stub().callsArgWith(5, null, "OK")
-				@LockManager.tryLock @key, @callback
-
-			it "should return the callback with true", ->
-				@callback.calledWith(null, true).should.equal true
-
-	describe "deleteLock", ->
-		beforeEach -> 
-			beforeEach ->
-				@rclient.del = sinon.stub().callsArg(1)
-				@LockManager.deleteLock @key, @callback
-
-			it "should delete the lock in redis", ->
-				@rclient.del
+			}
+		};
+		this.LockManager = SandboxedModule.require(modulePath, { requires: {
+			"redis-sharelatex": {
+				createClient: () => { return this.rclient =
+					{auth: sinon.stub()}; }
+			},
+			"settings-sharelatex": this.Settings,
+			"logger-sharelatex": {error() {}}
+		}
+	});
+
+		this.key = "lock-key";
+		return this.callback = sinon.stub();
+	});
+
+	describe("checkLock", function() {
+		describe("when the lock is taken", function() {
+			beforeEach(function() {
+				this.rclient.exists = sinon.stub().callsArgWith(1, null, "1");
+				return this.LockManager.checkLock(this.key, this.callback);
+			});
+
+			it("should check the lock in redis", function() {
+				return this.rclient.exists
+					.calledWith(this.key)
+					.should.equal(true);
+			});
+
+			return it("should return the callback with false", function() {
+				return this.callback.calledWith(null, false).should.equal(true);
+			});
+		});
+
+		return describe("when the lock is free", function() {
+			beforeEach(function() {
+				this.rclient.exists = sinon.stub().callsArgWith(1, null, "0");
+				return this.LockManager.checkLock(this.key, this.callback);
+			});
+
+			return it("should return the callback with true", function() {
+				return this.callback.calledWith(null, true).should.equal(true);
+			});
+		});
+	});
+
+
+	describe("tryLock", function() {
+		describe("when the lock is taken", function() {
+			beforeEach(function() {
+				this.rclient.set = sinon.stub().callsArgWith(5, null, null);
+				this.LockManager.randomLock = sinon.stub().returns("locked-random-value");
+				return this.LockManager.tryLock(this.key, this.callback);
+			});
+
+			it("should check the lock in redis", function() {
+				return this.rclient.set
+					.calledWith(this.key, "locked-random-value", "EX", this.LockManager.LOCK_TTL, "NX")
+					.should.equal(true);
+			});
+
+			return it("should return the callback with false", function() {
+				return this.callback.calledWith(null, false).should.equal(true);
+			});
+		});
+
+		return describe("when the lock is free", function() {
+			beforeEach(function() {
+				this.rclient.set = sinon.stub().callsArgWith(5, null, "OK");
+				return this.LockManager.tryLock(this.key, this.callback);
+			});
+
+			return it("should return the callback with true", function() {
+				return this.callback.calledWith(null, true).should.equal(true);
+			});
+		});
+	});
+
+	describe("deleteLock", () =>
+		beforeEach(function() { 
+			beforeEach(function() {
+				this.rclient.del = sinon.stub().callsArg(1);
+				return this.LockManager.deleteLock(this.key, this.callback);
+			});
+
+			it("should delete the lock in redis", function() {
+				return this.rclient.del
 					.calledWith(key)
-					.should.equal true
-
-			it "should call the callback", ->
-				@callback.called.should.equal true
-
-	describe "getLock", ->
-		describe "when the lock is not taken", ->
-			beforeEach (done) ->
-				@LockManager.tryLock = sinon.stub().callsArgWith(1, null, true)
-				@LockManager.getLock @key, (args...) =>
-					@callback(args...)
-					done()
-
-			it "should try to get the lock", ->
-				@LockManager.tryLock
-					.calledWith(@key)
-					.should.equal true
-
-			it "should only need to try once", ->
-				@LockManager.tryLock.callCount.should.equal 1
-
-			it "should return the callback", ->
-				@callback.calledWith(null).should.equal true
-
-		describe "when the lock is initially set", ->
-			beforeEach (done) ->
-				startTime = Date.now()
-				@LockManager.LOCK_TEST_INTERVAL = 5
-				@LockManager.tryLock = (doc_id, callback = (error, isFree) ->) ->
-					if Date.now() - startTime < 100
-						callback null, false
-					else
-						callback null, true
-				sinon.spy @LockManager, "tryLock"
-
-				@LockManager.getLock @key, (args...) =>
-					@callback(args...)
-					done()
-
-			it "should call tryLock multiple times until free", ->
-				(@LockManager.tryLock.callCount > 1).should.equal true
-
-			it "should return the callback", ->
-				@callback.calledWith(null).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 @key, (args...) =>
-					@callback(args...)
-					done()
-
-			it "should return the callback with an error", ->
-				@callback.calledWith(sinon.match.instanceOf(Error)).should.equal true
-
-	describe "runWithLock", ->
-		describe "with successful run", ->
-			beforeEach ->
-				@runner = (releaseLock = (error) ->) ->
-					releaseLock()
-				sinon.spy @, "runner"
-				@LockManager.getLock = sinon.stub().callsArg(1)
-				@LockManager.releaseLock = sinon.stub().callsArg(2)
-				@LockManager.runWithLock @key, @runner, @callback
-
-			it "should get the lock", ->
-				@LockManager.getLock
-					.calledWith(@key)
-					.should.equal true
-
-			it "should run the passed function", ->
-				@runner.called.should.equal true
-
-			it "should release the lock", ->
-				@LockManager.releaseLock
-					.calledWith(@key)
-					.should.equal true
-
-			it "should call the callback", ->
-				@callback.called.should.equal true
-
-		describe "when the runner function returns an error", ->
-			beforeEach ->
-				@error = new Error("oops")
-				@runner = (releaseLock = (error) ->) =>
-					releaseLock(@error)
-				sinon.spy @, "runner"
-				@LockManager.getLock = sinon.stub().callsArg(1)
-				@LockManager.releaseLock = sinon.stub().callsArg(2)
-				@LockManager.runWithLock @key, @runner, @callback
-
-			it "should release the lock", ->
-				@LockManager.releaseLock
-					.calledWith(@key)
-					.should.equal true
-
-			it "should call the callback with the error", ->
-				@callback.calledWith(@error).should.equal true
-
-		describe "releaseLock", ->
-			describe "when the lock is current", ->
-				beforeEach ->
-					@rclient.eval = sinon.stub().yields(null, 1)
-					@LockManager.releaseLock @key, @lockValue, @callback
-
-				it 'should clear the data from redis', ->
-					@rclient.eval.calledWith(@LockManager.unlockScript, 1, @key, @lockValue).should.equal true
-
-				it 'should call the callback', ->
-					@callback.called.should.equal true
-
-			describe "when the lock has expired", ->
-				beforeEach ->
-					@rclient.eval = sinon.stub().yields(null, 0)
-					@LockManager.releaseLock @key, @lockValue, @callback
-
-				it 'should return an error if the lock has expired', ->
-					@callback.calledWith(sinon.match.has('message', "tried to release timed out lock")).should.equal true
+					.should.equal(true);
+			});
+
+			return it("should call the callback", function() {
+				return this.callback.called.should.equal(true);
+			});
+		})
+	);
+
+	describe("getLock", function() {
+		describe("when the lock is not taken", function() {
+			beforeEach(function(done) {
+				this.LockManager.tryLock = sinon.stub().callsArgWith(1, null, true);
+				return this.LockManager.getLock(this.key, (...args) => {
+					this.callback(...Array.from(args || []));
+					return done();
+				});
+			});
+
+			it("should try to get the lock", function() {
+				return this.LockManager.tryLock
+					.calledWith(this.key)
+					.should.equal(true);
+			});
+
+			it("should only need to try once", function() {
+				return this.LockManager.tryLock.callCount.should.equal(1);
+			});
+
+			return it("should return the callback", function() {
+				return this.callback.calledWith(null).should.equal(true);
+			});
+		});
+
+		describe("when the lock is initially set", function() {
+			beforeEach(function(done) {
+				const startTime = Date.now();
+				this.LockManager.LOCK_TEST_INTERVAL = 5;
+				this.LockManager.tryLock = function(doc_id, callback) {
+					if (callback == null) { callback = function(error, isFree) {}; }
+					if ((Date.now() - startTime) < 100) {
+						return callback(null, false);
+					} else {
+						return callback(null, true);
+					}
+				};
+				sinon.spy(this.LockManager, "tryLock");
+
+				return this.LockManager.getLock(this.key, (...args) => {
+					this.callback(...Array.from(args || []));
+					return done();
+				});
+			});
+
+			it("should call tryLock multiple times until free", function() {
+				return (this.LockManager.tryLock.callCount > 1).should.equal(true);
+			});
+
+			return it("should return the callback", function() {
+				return this.callback.calledWith(null).should.equal(true);
+			});
+		});
+
+		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.key, (...args) => {
+					this.callback(...Array.from(args || []));
+					return done();
+				});
+			});
+
+			return it("should return the callback with an error", function() {
+				return this.callback.calledWith(sinon.match.instanceOf(Error)).should.equal(true);
+			});
+		});
+	});
+
+	return describe("runWithLock", function() {
+		describe("with successful run", function() {
+			beforeEach(function() {
+				this.runner = function(releaseLock) {
+					if (releaseLock == null) { releaseLock = function(error) {}; }
+					return releaseLock();
+				};
+				sinon.spy(this, "runner");
+				this.LockManager.getLock = sinon.stub().callsArg(1);
+				this.LockManager.releaseLock = sinon.stub().callsArg(2);
+				return this.LockManager.runWithLock(this.key, this.runner, this.callback);
+			});
+
+			it("should get the lock", function() {
+				return this.LockManager.getLock
+					.calledWith(this.key)
+					.should.equal(true);
+			});
+
+			it("should run the passed function", function() {
+				return this.runner.called.should.equal(true);
+			});
+
+			it("should release the lock", function() {
+				return this.LockManager.releaseLock
+					.calledWith(this.key)
+					.should.equal(true);
+			});
+
+			return it("should call the callback", function() {
+				return this.callback.called.should.equal(true);
+			});
+		});
+
+		describe("when the runner function returns an error", function() {
+			beforeEach(function() {
+				this.error = new Error("oops");
+				this.runner = releaseLock => {
+					if (releaseLock == null) { releaseLock = function(error) {}; }
+					return releaseLock(this.error);
+				};
+				sinon.spy(this, "runner");
+				this.LockManager.getLock = sinon.stub().callsArg(1);
+				this.LockManager.releaseLock = sinon.stub().callsArg(2);
+				return this.LockManager.runWithLock(this.key, this.runner, this.callback);
+			});
+
+			it("should release the lock", function() {
+				return this.LockManager.releaseLock
+					.calledWith(this.key)
+					.should.equal(true);
+			});
+
+			return it("should call the callback with the error", function() {
+				return this.callback.calledWith(this.error).should.equal(true);
+			});
+		});
+
+		return describe("releaseLock", function() {
+			describe("when the lock is current", function() {
+				beforeEach(function() {
+					this.rclient.eval = sinon.stub().yields(null, 1);
+					return this.LockManager.releaseLock(this.key, this.lockValue, this.callback);
+				});
+
+				it('should clear the data from redis', function() {
+					return this.rclient.eval.calledWith(this.LockManager.unlockScript, 1, this.key, this.lockValue).should.equal(true);
+				});
+
+				return it('should call the callback', function() {
+					return this.callback.called.should.equal(true);
+				});
+			});
+
+			return describe("when the lock has expired", function() {
+				beforeEach(function() {
+					this.rclient.eval = sinon.stub().yields(null, 0);
+					return this.LockManager.releaseLock(this.key, this.lockValue, this.callback);
+				});
+
+				return it('should return an error if the lock has expired', function() {
+					return this.callback.calledWith(sinon.match.has('message', "tried to release timed out lock")).should.equal(true);
+				});
+			});
+		});
+	});
+});
 

+ 184 - 145
services/track-changes/test/unit/coffee/MongoManager/MongoManagerTests.js

@@ -1,158 +1,197 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-expect = chai.expect
-modulePath = "../../../../app/js/MongoManager.js"
-packModulePath = "../../../../app/js/PackManager.js"
-SandboxedModule = require('sandboxed-module')
-{ObjectId} = require("mongojs")
-tk = require "timekeeper"
-
-describe "MongoManager", ->
-	beforeEach ->
-		tk.freeze(new Date())
-		@MongoManager = SandboxedModule.require modulePath, requires:
-			"./mongojs" : { db: @db = {}, ObjectId: ObjectId }
-			"./PackManager" : @PackManager = {}
-			'metrics-sharelatex': {timeAsyncMethod: ()->}
-			'logger-sharelatex': {log: ()->}
-		@callback = sinon.stub()
-		@doc_id = ObjectId().toString()
-		@project_id = ObjectId().toString()
-
-	afterEach ->
-		tk.reset()
-
-	describe "getLastCompressedUpdate", ->
-		beforeEach ->
-			@update = "mock-update"
-			@db.docHistory = {}
-			@db.docHistory.find = sinon.stub().returns @db.docHistory
-			@db.docHistory.findOne = sinon.stub().returns @db.docHistory
-			@db.docHistory.sort = sinon.stub().returns @db.docHistory
-			@db.docHistory.limit = sinon.stub().returns @db.docHistory
-			@db.docHistory.toArray = sinon.stub().callsArgWith(0, null, [@update])
-
-			@MongoManager.getLastCompressedUpdate @doc_id, @callback
-
-		it "should find the updates for the doc", ->
-			@db.docHistory.find
-				.calledWith(doc_id: ObjectId(@doc_id))
-				.should.equal true
-
-		it "should limit to one result", ->
-			@db.docHistory.limit
+/*
+ * 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/MongoManager.js";
+const packModulePath = "../../../../app/js/PackManager.js";
+const SandboxedModule = require('sandboxed-module');
+const {ObjectId} = require("mongojs");
+const tk = require("timekeeper");
+
+describe("MongoManager", function() {
+	beforeEach(function() {
+		tk.freeze(new Date());
+		this.MongoManager = SandboxedModule.require(modulePath, { requires: {
+			"./mongojs" : { db: (this.db = {}), ObjectId },
+			"./PackManager" : (this.PackManager = {}),
+			'metrics-sharelatex': {timeAsyncMethod(){}},
+			'logger-sharelatex': {log(){}}
+		}
+	});
+		this.callback = sinon.stub();
+		this.doc_id = ObjectId().toString();
+		return this.project_id = ObjectId().toString();
+	});
+
+	afterEach(() => tk.reset());
+
+	describe("getLastCompressedUpdate", function() {
+		beforeEach(function() {
+			this.update = "mock-update";
+			this.db.docHistory = {};
+			this.db.docHistory.find = sinon.stub().returns(this.db.docHistory);
+			this.db.docHistory.findOne = sinon.stub().returns(this.db.docHistory);
+			this.db.docHistory.sort = sinon.stub().returns(this.db.docHistory);
+			this.db.docHistory.limit = sinon.stub().returns(this.db.docHistory);
+			this.db.docHistory.toArray = sinon.stub().callsArgWith(0, null, [this.update]);
+
+			return this.MongoManager.getLastCompressedUpdate(this.doc_id, this.callback);
+		});
+
+		it("should find the updates for the doc", function() {
+			return this.db.docHistory.find
+				.calledWith({doc_id: ObjectId(this.doc_id)})
+				.should.equal(true);
+		});
+
+		it("should limit to one result", function() {
+			return this.db.docHistory.limit
 				.calledWith(1)
-				.should.equal true
-
-		it "should sort in descending version order", ->
-			@db.docHistory.sort
-				.calledWith(v: -1)
-				.should.equal true
-
-		it "should call the call back with the update", ->
-			@callback.calledWith(null, @update).should.equal true
-
-
-	describe "peekLastCompressedUpdate", ->
-		describe "when there is no last update", ->
-			beforeEach ->
-				@PackManager.getLastPackFromIndex =  sinon.stub().callsArgWith(1, null, null)
-				@MongoManager.getLastCompressedUpdate = sinon.stub().callsArgWith(1, null, null)
-				@MongoManager.peekLastCompressedUpdate @doc_id, @callback
-
-			it "should get the last update", ->
-				@MongoManager.getLastCompressedUpdate
-					.calledWith(@doc_id)
-					.should.equal true
-
-			it "should call the callback with no update", ->
-				@callback.calledWith(null, null).should.equal true
-
-		describe "when there is an update", ->
-			beforeEach ->
-				@update = { _id: Object() }
-				@MongoManager.getLastCompressedUpdate = sinon.stub().callsArgWith(1, null, @update)
-				@MongoManager.peekLastCompressedUpdate @doc_id, @callback
-
-			it "should get the last update", ->
-				@MongoManager.getLastCompressedUpdate
-					.calledWith(@doc_id)
-					.should.equal true
-
-			it "should call the callback with the update", ->
-				@callback.calledWith(null, @update).should.equal true
-
-		describe "when there is a last update in S3", ->
-			beforeEach ->
-				@update = { _id: Object(), v: 12345, v_end: 12345, inS3:true}
-				@PackManager.getLastPackFromIndex =  sinon.stub().callsArgWith(1, null, @update)
-				@MongoManager.getLastCompressedUpdate = sinon.stub().callsArgWith(1, null)
-				@MongoManager.peekLastCompressedUpdate @doc_id, @callback
-
-			it "should get the last update", ->
-				@MongoManager.getLastCompressedUpdate
-					.calledWith(@doc_id)
-					.should.equal true
-
-			it "should call the callback with a null update and the correct version", ->
-				@callback.calledWith(null, null, @update.v_end).should.equal true
-
-
-	describe "backportProjectId", ->
-		beforeEach ->
-			@db.docHistory =
-				update: sinon.stub().callsArg(3)
-			@MongoManager.backportProjectId @project_id, @doc_id, @callback
-
-		it "should insert the project_id into all entries for the doc_id which don't have it set", ->
-			@db.docHistory.update
+				.should.equal(true);
+		});
+
+		it("should sort in descending version order", function() {
+			return this.db.docHistory.sort
+				.calledWith({v: -1})
+				.should.equal(true);
+		});
+
+		return it("should call the call back with the update", function() {
+			return this.callback.calledWith(null, this.update).should.equal(true);
+		});
+	});
+
+
+	describe("peekLastCompressedUpdate", function() {
+		describe("when there is no last update", function() {
+			beforeEach(function() {
+				this.PackManager.getLastPackFromIndex =  sinon.stub().callsArgWith(1, null, null);
+				this.MongoManager.getLastCompressedUpdate = sinon.stub().callsArgWith(1, null, null);
+				return this.MongoManager.peekLastCompressedUpdate(this.doc_id, this.callback);
+			});
+
+			it("should get the last update", function() {
+				return this.MongoManager.getLastCompressedUpdate
+					.calledWith(this.doc_id)
+					.should.equal(true);
+			});
+
+			return it("should call the callback with no update", function() {
+				return this.callback.calledWith(null, null).should.equal(true);
+			});
+		});
+
+		describe("when there is an update", function() {
+			beforeEach(function() {
+				this.update = { _id: Object() };
+				this.MongoManager.getLastCompressedUpdate = sinon.stub().callsArgWith(1, null, this.update);
+				return this.MongoManager.peekLastCompressedUpdate(this.doc_id, this.callback);
+			});
+
+			it("should get the last update", function() {
+				return this.MongoManager.getLastCompressedUpdate
+					.calledWith(this.doc_id)
+					.should.equal(true);
+			});
+
+			return it("should call the callback with the update", function() {
+				return this.callback.calledWith(null, this.update).should.equal(true);
+			});
+		});
+
+		return describe("when there is a last update in S3", function() {
+			beforeEach(function() {
+				this.update = { _id: Object(), v: 12345, v_end: 12345, inS3:true};
+				this.PackManager.getLastPackFromIndex =  sinon.stub().callsArgWith(1, null, this.update);
+				this.MongoManager.getLastCompressedUpdate = sinon.stub().callsArgWith(1, null);
+				return this.MongoManager.peekLastCompressedUpdate(this.doc_id, this.callback);
+			});
+
+			it("should get the last update", function() {
+				return this.MongoManager.getLastCompressedUpdate
+					.calledWith(this.doc_id)
+					.should.equal(true);
+			});
+
+			return it("should call the callback with a null update and the correct version", function() {
+				return this.callback.calledWith(null, null, this.update.v_end).should.equal(true);
+			});
+		});
+	});
+
+
+	describe("backportProjectId", function() {
+		beforeEach(function() {
+			this.db.docHistory =
+				{update: sinon.stub().callsArg(3)};
+			return this.MongoManager.backportProjectId(this.project_id, this.doc_id, this.callback);
+		});
+
+		it("should insert the project_id into all entries for the doc_id which don't have it set", function() {
+			return this.db.docHistory.update
 				.calledWith({
-					doc_id: ObjectId(@doc_id)
+					doc_id: ObjectId(this.doc_id),
 					project_id: { $exists: false }
 				}, {
-					$set: { project_id: ObjectId(@project_id) }
+					$set: { project_id: ObjectId(this.project_id) }
 				}, {
 					multi: true
 				})
-				.should.equal true
-
-		it "should call the callback", ->
-			@callback.called.should.equal true
-
-	describe "getProjectMetaData", ->
-		beforeEach ->
-			@metadata = { "mock": "metadata" }
-			@db.projectHistoryMetaData =
-				find: sinon.stub().callsArgWith(1, null, [@metadata])
-			@MongoManager.getProjectMetaData @project_id, @callback
-
-		it "should look up the meta data in the db", ->
-			@db.projectHistoryMetaData.find
-				.calledWith({ project_id: ObjectId(@project_id) })
-				.should.equal true
-
-		it "should return the metadata", ->
-			@callback.calledWith(null, @metadata).should.equal true
-
-	describe "setProjectMetaData", ->
-		beforeEach ->
-			@metadata = { "mock": "metadata" }
-			@db.projectHistoryMetaData =
-				update: sinon.stub().callsArgWith(3, null, [@metadata])
-			@MongoManager.setProjectMetaData @project_id, @metadata, @callback
-
-		it "should upsert the metadata into the DB", ->
-			@db.projectHistoryMetaData.update
+				.should.equal(true);
+		});
+
+		return it("should call the callback", function() {
+			return this.callback.called.should.equal(true);
+		});
+	});
+
+	describe("getProjectMetaData", function() {
+		beforeEach(function() {
+			this.metadata = { "mock": "metadata" };
+			this.db.projectHistoryMetaData =
+				{find: sinon.stub().callsArgWith(1, null, [this.metadata])};
+			return this.MongoManager.getProjectMetaData(this.project_id, this.callback);
+		});
+
+		it("should look up the meta data in the db", function() {
+			return this.db.projectHistoryMetaData.find
+				.calledWith({ project_id: ObjectId(this.project_id) })
+				.should.equal(true);
+		});
+
+		return it("should return the metadata", function() {
+			return this.callback.calledWith(null, this.metadata).should.equal(true);
+		});
+	});
+
+	return describe("setProjectMetaData", function() {
+		beforeEach(function() {
+			this.metadata = { "mock": "metadata" };
+			this.db.projectHistoryMetaData =
+				{update: sinon.stub().callsArgWith(3, null, [this.metadata])};
+			return this.MongoManager.setProjectMetaData(this.project_id, this.metadata, this.callback);
+		});
+
+		it("should upsert the metadata into the DB", function() {
+			return this.db.projectHistoryMetaData.update
 				.calledWith({
-					project_id: ObjectId(@project_id)
+					project_id: ObjectId(this.project_id)
 				}, {
-					$set: @metadata
+					$set: this.metadata
 				}, {
 					upsert: true
 				})
-				.should.equal true
-
-		it "should call the callback", ->
-			@callback.called.should.equal true
+				.should.equal(true);
+		});
+
+		return it("should call the callback", function() {
+			return this.callback.called.should.equal(true);
+		});
+	});
+});
 

+ 446 - 351
services/track-changes/test/unit/coffee/PackManager/PackManagerTests.js

@@ -1,369 +1,464 @@
-sinon = require('sinon')
-chai = require('chai')
-assert = require('chai').assert
-should = chai.should()
-expect = chai.expect
-modulePath = "../../../../app/js/PackManager.js"
-SandboxedModule = require('sandboxed-module')
-{ObjectId} = require("mongojs")
-bson = require("bson")
-BSON = new bson.BSONPure()
-_ = require("underscore")
-
-tk = require "timekeeper"
-
-describe "PackManager", ->
-	beforeEach ->
-		tk.freeze(new Date())
-		@PackManager = SandboxedModule.require modulePath, requires:
-			"./mongojs" : { db: @db = {}, ObjectId: ObjectId, BSON: BSON }
-			"./LockManager" : {}
-			"./MongoAWS": {}
-			"logger-sharelatex": { log: sinon.stub(), error: sinon.stub() }
-			'metrics-sharelatex': {inc: ()->}
-			"./ProjectIterator": require("../../../../app/js/ProjectIterator.js") # Cache for speed
-			"settings-sharelatex":
-				redis: lock: key_schema: {}
-		@callback = sinon.stub()
-		@doc_id = ObjectId().toString()
-		@project_id = ObjectId().toString()
-		@PackManager.MAX_COUNT = 512
-
-	afterEach ->
-		tk.reset()
-
-	describe "insertCompressedUpdates", ->
-		beforeEach ->
-			@lastUpdate = {
-				_id: "12345"
+/*
+ * 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 { assert } = require('chai');
+const should = chai.should();
+const { expect } = chai;
+const modulePath = "../../../../app/js/PackManager.js";
+const SandboxedModule = require('sandboxed-module');
+const {ObjectId} = require("mongojs");
+const bson = require("bson");
+const BSON = new bson.BSONPure();
+const _ = require("underscore");
+
+const tk = require("timekeeper");
+
+describe("PackManager", function() {
+	beforeEach(function() {
+		tk.freeze(new Date());
+		this.PackManager = SandboxedModule.require(modulePath, { requires: {
+			"./mongojs" : { db: (this.db = {}), ObjectId, BSON },
+			"./LockManager" : {},
+			"./MongoAWS": {},
+			"logger-sharelatex": { log: sinon.stub(), error: sinon.stub() },
+			'metrics-sharelatex': {inc(){}},
+			"./ProjectIterator": require("../../../../app/js/ProjectIterator.js"), // Cache for speed
+			"settings-sharelatex": {
+				redis: {lock: {key_schema: {}}}
+			}
+		}
+	});
+		this.callback = sinon.stub();
+		this.doc_id = ObjectId().toString();
+		this.project_id = ObjectId().toString();
+		return this.PackManager.MAX_COUNT = 512;
+	});
+
+	afterEach(() => tk.reset());
+
+	describe("insertCompressedUpdates", function() {
+		beforeEach(function() {
+			this.lastUpdate = {
+				_id: "12345",
 				pack: [
 					{ op: "op-1", meta: "meta-1", v: 1},
 					{ op: "op-2", meta: "meta-2", v: 2}
-				]
-				n : 2
+				],
+				n : 2,
 				sz : 100
-			}
-			@newUpdates = [
+			};
+			this.newUpdates = [
 				{ op: "op-3", meta: "meta-3", v: 3},
 				{ op: "op-4", meta: "meta-4", v: 4}
-			]
-			@db.docHistory =
-				save: sinon.stub().callsArg(1)
-				insert: sinon.stub().callsArg(1)
+			];
+			return this.db.docHistory = {
+				save: sinon.stub().callsArg(1),
+				insert: sinon.stub().callsArg(1),
 				findAndModify: sinon.stub().callsArg(1)
-
-		describe "with no last update", ->
-			beforeEach ->
-				@PackManager.insertUpdatesIntoNewPack = sinon.stub().callsArg(4)
-				@PackManager.insertCompressedUpdates @project_id, @doc_id, null, @newUpdates, true, @callback
-
-			describe "for a small update", ->
-				it "should insert the update into a new pack", ->
-					@PackManager.insertUpdatesIntoNewPack.calledWith(@project_id, @doc_id, @newUpdates, true).should.equal true
-
-				it "should call the callback", ->
-					@callback.called.should.equal true
-
-			describe "for many small updates", ->
-				beforeEach ->
-					@newUpdates = ({ op: "op-#{i}", meta: "meta-#{i}", v: i} for i in [0..2048])
-					@PackManager.insertCompressedUpdates @project_id, @doc_id, null, @newUpdates, false, @callback
-
-				it "should append the initial updates to the existing pack", ->
-					@PackManager.insertUpdatesIntoNewPack.calledWith(@project_id, @doc_id, @newUpdates[0...512], false).should.equal true
-
-				it "should insert the first set remaining updates as a new pack", ->
-					@PackManager.insertUpdatesIntoNewPack.calledWith(@project_id, @doc_id, @newUpdates[512...1024], false).should.equal true
-
-				it "should insert the second set of remaining updates as a new pack", ->
-					@PackManager.insertUpdatesIntoNewPack.calledWith(@project_id, @doc_id, @newUpdates[1024...1536], false).should.equal true
-
-				it "should insert the third set of remaining updates as a new pack", ->
-					@PackManager.insertUpdatesIntoNewPack.calledWith(@project_id, @doc_id, @newUpdates[1536...2048], false).should.equal true
-
-				it "should insert the final set of remaining updates as a new pack", ->
-					@PackManager.insertUpdatesIntoNewPack.calledWith(@project_id, @doc_id, @newUpdates[2048..2048], false).should.equal true
-
-				it "should call the callback", ->
-					@callback.called.should.equal true
-
-
-
-		describe "with an existing pack as the last update", ->
-			beforeEach ->
-				@PackManager.appendUpdatesToExistingPack = sinon.stub().callsArg(5)
-				@PackManager.insertUpdatesIntoNewPack = sinon.stub().callsArg(4)
-				@PackManager.insertCompressedUpdates @project_id, @doc_id, @lastUpdate, @newUpdates, false, @callback
-
-			describe "for a small update", ->
-				it "should append the update to the existing pack", ->
-					@PackManager.appendUpdatesToExistingPack.calledWith(@project_id, @doc_id, @lastUpdate, @newUpdates, false).should.equal true
-				it "should not insert any new packs", ->
-					@PackManager.insertUpdatesIntoNewPack.called.should.equal false
-				it "should call the callback", ->
-					@callback.called.should.equal true
-
-			describe "for many small updates", ->
-				beforeEach ->
-					@newUpdates = ({ op: "op-#{i}", meta: "meta-#{i}", v: i} for i in [0..2048])
-					@PackManager.insertCompressedUpdates @project_id, @doc_id, @lastUpdate, @newUpdates, false, @callback
-
-				it "should append the initial updates to the existing pack", ->
-					@PackManager.appendUpdatesToExistingPack.calledWith(@project_id, @doc_id, @lastUpdate, @newUpdates[0...510], false).should.equal true
-
-				it "should insert the first set remaining updates as a new pack", ->
-					@PackManager.insertUpdatesIntoNewPack.calledWith(@project_id, @doc_id, @newUpdates[510...1022], false).should.equal true
-
-				it "should insert the second set of remaining updates as a new pack", ->
-					@PackManager.insertUpdatesIntoNewPack.calledWith(@project_id, @doc_id, @newUpdates[1022...1534], false).should.equal true
-
-				it "should insert the third set of remaining updates as a new pack", ->
-					@PackManager.insertUpdatesIntoNewPack.calledWith(@project_id, @doc_id, @newUpdates[1534...2046], false).should.equal true
-
-				it "should insert the final set of remaining updates as a new pack", ->
-					@PackManager.insertUpdatesIntoNewPack.calledWith(@project_id, @doc_id, @newUpdates[2046..2048], false).should.equal true
-
-				it "should call the callback", ->
-					@callback.called.should.equal true
-
-			describe "for many big updates", ->
-				beforeEach ->
-					longString = ("a" for [0 .. (0.75*@PackManager.MAX_SIZE)]).join("")
-					@newUpdates = ({ op: "op-#{i}-#{longString}", meta: "meta-#{i}", v: i} for i in [0..4])
-					@PackManager.insertCompressedUpdates @project_id, @doc_id, @lastUpdate, @newUpdates, false, @callback
-
-				it "should append the initial updates to the existing pack", ->
-					@PackManager.appendUpdatesToExistingPack.calledWith(@project_id, @doc_id, @lastUpdate, @newUpdates[0..0], false).should.equal true
-
-				it "should insert the first set remaining updates as a new pack", ->
-					@PackManager.insertUpdatesIntoNewPack.calledWith(@project_id, @doc_id, @newUpdates[1..1], false).should.equal true
-
-				it "should insert the second set of remaining updates as a new pack", ->
-					@PackManager.insertUpdatesIntoNewPack.calledWith(@project_id, @doc_id, @newUpdates[2..2], false).should.equal true
-
-				it "should insert the third set of remaining updates as a new pack", ->
-					@PackManager.insertUpdatesIntoNewPack.calledWith(@project_id, @doc_id, @newUpdates[3..3], false).should.equal true
-
-				it "should insert the final set of remaining updates as a new pack", ->
-					@PackManager.insertUpdatesIntoNewPack.calledWith(@project_id, @doc_id, @newUpdates[4..4], false).should.equal true
-
-				it "should call the callback", ->
-					@callback.called.should.equal true
-
-		describe "flushCompressedUpdates", ->
-			describe "when there is no previous update",  ->
-				beforeEach ->
-					@PackManager.flushCompressedUpdates @project_id, @doc_id, null, @newUpdates, true, @callback
-
-				describe "for a small update  that will expire", ->
-					it "should insert the update into mongo", ->
-						@db.docHistory.save.calledWithMatch({
-							pack: @newUpdates,
-							project_id: ObjectId(@project_id),
-							doc_id: ObjectId(@doc_id)
-							n: @newUpdates.length
-							v: @newUpdates[0].v
-							v_end: @newUpdates[@newUpdates.length-1].v
-						}).should.equal true
-
-					it "should set an expiry time in the future", ->
-						@db.docHistory.save.calledWithMatch({
-							expiresAt: new Date(Date.now() + 7 * 24 * 3600 * 1000)
-						}).should.equal true
-
-					it "should call the callback", ->
-						@callback.called.should.equal true
-
-		describe "when there is a recent previous update in mongo that expires", ->
-			beforeEach ->
-				@lastUpdate = {
-					_id: "12345"
+			};
+		});
+
+		describe("with no last update", function() {
+			beforeEach(function() {
+				this.PackManager.insertUpdatesIntoNewPack = sinon.stub().callsArg(4);
+				return this.PackManager.insertCompressedUpdates(this.project_id, this.doc_id, null, this.newUpdates, true, this.callback);
+			});
+
+			describe("for a small update", function() {
+				it("should insert the update into a new pack", function() {
+					return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates, true).should.equal(true);
+				});
+
+				return it("should call the callback", function() {
+					return this.callback.called.should.equal(true);
+				});
+			});
+
+			return describe("for many small updates", function() {
+				beforeEach(function() {
+					this.newUpdates = (__range__(0, 2048, true).map((i) => ({ op: `op-${i}`, meta: `meta-${i}`, v: i})));
+					return this.PackManager.insertCompressedUpdates(this.project_id, this.doc_id, null, this.newUpdates, false, this.callback);
+				});
+
+				it("should append the initial updates to the existing pack", function() {
+					return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates.slice(0, 512), false).should.equal(true);
+				});
+
+				it("should insert the first set remaining updates as a new pack", function() {
+					return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates.slice(512, 1024), false).should.equal(true);
+				});
+
+				it("should insert the second set of remaining updates as a new pack", function() {
+					return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates.slice(1024, 1536), false).should.equal(true);
+				});
+
+				it("should insert the third set of remaining updates as a new pack", function() {
+					return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates.slice(1536, 2048), false).should.equal(true);
+				});
+
+				it("should insert the final set of remaining updates as a new pack", function() {
+					return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates.slice(2048, 2049), false).should.equal(true);
+				});
+
+				return it("should call the callback", function() {
+					return this.callback.called.should.equal(true);
+				});
+			});
+		});
+
+
+
+		describe("with an existing pack as the last update", function() {
+			beforeEach(function() {
+				this.PackManager.appendUpdatesToExistingPack = sinon.stub().callsArg(5);
+				this.PackManager.insertUpdatesIntoNewPack = sinon.stub().callsArg(4);
+				return this.PackManager.insertCompressedUpdates(this.project_id, this.doc_id, this.lastUpdate, this.newUpdates, false, this.callback);
+			});
+
+			describe("for a small update", function() {
+				it("should append the update to the existing pack", function() {
+					return this.PackManager.appendUpdatesToExistingPack.calledWith(this.project_id, this.doc_id, this.lastUpdate, this.newUpdates, false).should.equal(true);
+				});
+				it("should not insert any new packs", function() {
+					return this.PackManager.insertUpdatesIntoNewPack.called.should.equal(false);
+				});
+				return it("should call the callback", function() {
+					return this.callback.called.should.equal(true);
+				});
+			});
+
+			describe("for many small updates", function() {
+				beforeEach(function() {
+					this.newUpdates = (__range__(0, 2048, true).map((i) => ({ op: `op-${i}`, meta: `meta-${i}`, v: i})));
+					return this.PackManager.insertCompressedUpdates(this.project_id, this.doc_id, this.lastUpdate, this.newUpdates, false, this.callback);
+				});
+
+				it("should append the initial updates to the existing pack", function() {
+					return this.PackManager.appendUpdatesToExistingPack.calledWith(this.project_id, this.doc_id, this.lastUpdate, this.newUpdates.slice(0, 510), false).should.equal(true);
+				});
+
+				it("should insert the first set remaining updates as a new pack", function() {
+					return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates.slice(510, 1022), false).should.equal(true);
+				});
+
+				it("should insert the second set of remaining updates as a new pack", function() {
+					return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates.slice(1022, 1534), false).should.equal(true);
+				});
+
+				it("should insert the third set of remaining updates as a new pack", function() {
+					return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates.slice(1534, 2046), false).should.equal(true);
+				});
+
+				it("should insert the final set of remaining updates as a new pack", function() {
+					return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates.slice(2046, 2049), false).should.equal(true);
+				});
+
+				return it("should call the callback", function() {
+					return this.callback.called.should.equal(true);
+				});
+			});
+
+			return describe("for many big updates", function() {
+				beforeEach(function() {
+					const longString = (__range__(0, (0.75*this.PackManager.MAX_SIZE), true).map((j) => "a")).join("");
+					this.newUpdates = ([0, 1, 2, 3, 4].map((i) => ({ op: `op-${i}-${longString}`, meta: `meta-${i}`, v: i})));
+					return this.PackManager.insertCompressedUpdates(this.project_id, this.doc_id, this.lastUpdate, this.newUpdates, false, this.callback);
+				});
+
+				it("should append the initial updates to the existing pack", function() {
+					return this.PackManager.appendUpdatesToExistingPack.calledWith(this.project_id, this.doc_id, this.lastUpdate, this.newUpdates.slice(0, 1), false).should.equal(true);
+				});
+
+				it("should insert the first set remaining updates as a new pack", function() {
+					return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates.slice(1, 2), false).should.equal(true);
+				});
+
+				it("should insert the second set of remaining updates as a new pack", function() {
+					return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates.slice(2, 3), false).should.equal(true);
+				});
+
+				it("should insert the third set of remaining updates as a new pack", function() {
+					return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates.slice(3, 4), false).should.equal(true);
+				});
+
+				it("should insert the final set of remaining updates as a new pack", function() {
+					return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates.slice(4, 5), false).should.equal(true);
+				});
+
+				return it("should call the callback", function() {
+					return this.callback.called.should.equal(true);
+				});
+			});
+		});
+
+		describe("flushCompressedUpdates", () =>
+			describe("when there is no previous update",  function() {
+				beforeEach(function() {
+					return this.PackManager.flushCompressedUpdates(this.project_id, this.doc_id, null, this.newUpdates, true, this.callback);
+				});
+
+				return describe("for a small update  that will expire", function() {
+					it("should insert the update into mongo", function() {
+						return this.db.docHistory.save.calledWithMatch({
+							pack: this.newUpdates,
+							project_id: ObjectId(this.project_id),
+							doc_id: ObjectId(this.doc_id),
+							n: this.newUpdates.length,
+							v: this.newUpdates[0].v,
+							v_end: this.newUpdates[this.newUpdates.length-1].v
+						}).should.equal(true);
+					});
+
+					it("should set an expiry time in the future", function() {
+						return this.db.docHistory.save.calledWithMatch({
+							expiresAt: new Date(Date.now() + (7 * 24 * 3600 * 1000))
+						}).should.equal(true);
+					});
+
+					return it("should call the callback", function() {
+						return this.callback.called.should.equal(true);
+					});
+				});
+			})
+		);
+
+		describe("when there is a recent previous update in mongo that expires", function() {
+			beforeEach(function() {
+				this.lastUpdate = {
+					_id: "12345",
 					pack: [
 						{ op: "op-1", meta: "meta-1", v: 1},
 						{ op: "op-2", meta: "meta-2", v: 2}
-					]
-					n : 2
-					sz : 100
-					meta: {start_ts: Date.now() - 6 * 3600 * 1000}
+					],
+					n : 2,
+					sz : 100,
+					meta: {start_ts: Date.now() - (6 * 3600 * 1000)},
 					expiresAt: new Date(Date.now())
-				}
-
-				@PackManager.flushCompressedUpdates @project_id, @doc_id, @lastUpdate, @newUpdates, true, @callback
-
-			describe "for a small update that will expire", ->
-				it "should append the update in mongo", ->
-					@db.docHistory.findAndModify.calledWithMatch({
-						query: {_id: @lastUpdate._id}
-						update: { $push: {"pack" : {$each: @newUpdates}}, $set: {v_end: @newUpdates[@newUpdates.length-1].v}}
-					}).should.equal true
-
-				it "should set an expiry time in the future", ->
-					@db.docHistory.findAndModify.calledWithMatch({
-						update: {$set: {expiresAt: new Date(Date.now() + 7 * 24 * 3600 * 1000)}}
-					}).should.equal true
-
-				it "should call the callback", ->
-					@callback.called.should.equal true
-
-
-		describe "when there is a recent previous update in mongo that expires", ->
-			beforeEach ->
-				@PackManager.updateIndex = sinon.stub().callsArg(2)
-
-				@lastUpdate = {
-					_id: "12345"
+				};
+
+				return this.PackManager.flushCompressedUpdates(this.project_id, this.doc_id, this.lastUpdate, this.newUpdates, true, this.callback);
+			});
+
+			return describe("for a small update that will expire", function() {
+				it("should append the update in mongo", function() {
+					return this.db.docHistory.findAndModify.calledWithMatch({
+						query: {_id: this.lastUpdate._id},
+						update: { $push: {"pack" : {$each: this.newUpdates}}, $set: {v_end: this.newUpdates[this.newUpdates.length-1].v}}
+					}).should.equal(true);
+				});
+
+				it("should set an expiry time in the future", function() {
+					return this.db.docHistory.findAndModify.calledWithMatch({
+						update: {$set: {expiresAt: new Date(Date.now() + (7 * 24 * 3600 * 1000))}}
+					}).should.equal(true);
+				});
+
+				return it("should call the callback", function() {
+					return this.callback.called.should.equal(true);
+				});
+			});
+		});
+
+
+		describe("when there is a recent previous update in mongo that expires", function() {
+			beforeEach(function() {
+				this.PackManager.updateIndex = sinon.stub().callsArg(2);
+
+				this.lastUpdate = {
+					_id: "12345",
 					pack: [
 						{ op: "op-1", meta: "meta-1", v: 1},
 						{ op: "op-2", meta: "meta-2", v: 2}
-					]
-					n : 2
-					sz : 100
-					meta: {start_ts: Date.now() - 6 * 3600 * 1000}
+					],
+					n : 2,
+					sz : 100,
+					meta: {start_ts: Date.now() - (6 * 3600 * 1000)},
 					expiresAt: new Date(Date.now())
-				}
-
-				@PackManager.flushCompressedUpdates @project_id, @doc_id, @lastUpdate, @newUpdates, false, @callback
-
-			describe "for a small update that will not expire", ->
-				it "should insert the update into mongo", ->
-					@db.docHistory.save.calledWithMatch({
-						pack: @newUpdates,
-						project_id: ObjectId(@project_id),
-						doc_id: ObjectId(@doc_id)
-						n: @newUpdates.length
-						v: @newUpdates[0].v
-						v_end: @newUpdates[@newUpdates.length-1].v
-					}).should.equal true
-
-				it "should not set any expiry time", ->
-					@db.docHistory.save.neverCalledWithMatch(sinon.match.has("expiresAt")).should.equal true
-
-				it "should call the callback", ->
-					@callback.called.should.equal true
-
-		describe "when there is an old previous update in mongo", ->
-			beforeEach ->
-				@lastUpdate = {
-					_id: "12345"
+				};
+
+				return this.PackManager.flushCompressedUpdates(this.project_id, this.doc_id, this.lastUpdate, this.newUpdates, false, this.callback);
+			});
+
+			return describe("for a small update that will not expire", function() {
+				it("should insert the update into mongo", function() {
+					return this.db.docHistory.save.calledWithMatch({
+						pack: this.newUpdates,
+						project_id: ObjectId(this.project_id),
+						doc_id: ObjectId(this.doc_id),
+						n: this.newUpdates.length,
+						v: this.newUpdates[0].v,
+						v_end: this.newUpdates[this.newUpdates.length-1].v
+					}).should.equal(true);
+				});
+
+				it("should not set any expiry time", function() {
+					return this.db.docHistory.save.neverCalledWithMatch(sinon.match.has("expiresAt")).should.equal(true);
+				});
+
+				return it("should call the callback", function() {
+					return this.callback.called.should.equal(true);
+				});
+			});
+		});
+
+		return describe("when there is an old previous update in mongo", function() {
+			beforeEach(function() {
+				this.lastUpdate = {
+					_id: "12345",
 					pack: [
 						{ op: "op-1", meta: "meta-1", v: 1},
 						{ op: "op-2", meta: "meta-2", v: 2}
-					]
-					n : 2
-					sz : 100
-					meta: {start_ts: Date.now() - 30 * 24 * 3600 * 1000}
-					expiresAt: new Date(Date.now() - 30 * 24 * 3600 * 1000)
-				}
-
-				@PackManager.flushCompressedUpdates @project_id, @doc_id, @lastUpdate, @newUpdates, true, @callback
-
-			describe "for a small update that will expire", ->
-				it "should insert the update into mongo", ->
-					@db.docHistory.save.calledWithMatch({
-						pack: @newUpdates,
-						project_id: ObjectId(@project_id),
-						doc_id: ObjectId(@doc_id)
-						n: @newUpdates.length
-						v: @newUpdates[0].v
-						v_end: @newUpdates[@newUpdates.length-1].v
-					}).should.equal true
-
-				it "should set an expiry time in the future", ->
-					@db.docHistory.save.calledWithMatch({
-						expiresAt: new Date(Date.now() + 7 * 24 * 3600 * 1000)
-					}).should.equal true
-
-				it "should call the callback", ->
-					@callback.called.should.equal true
-
-
-	describe "getOpsByVersionRange", ->
-
-	describe "loadPacksByVersionRange", ->
-
-	describe "fetchPacksIfNeeded", ->
-
-	describe "makeProjectIterator", ->
-
-	describe "getPackById", ->
-
-	describe "increaseTTL", ->
-
-	describe "getIndex", ->
-
-	describe "getPackFromIndex", ->
-# getLastPackFromIndex:
-# getIndexWithKeys
-# initialiseIndex
-# updateIndex
-# findCompletedPacks
-# findUnindexedPacks
-# insertPacksIntoIndexWithLock
-# _insertPacksIntoIndex
-# archivePack
-# checkArchivedPack
-# processOldPack
-# 	updateIndexIfNeeded
-# 	findUnarchivedPacks
-
-	describe "checkArchiveNotInProgress", ->
-
-		describe "when an archive is in progress", ->
-			beforeEach ->
-				@db.docHistoryIndex =
-					findOne: sinon.stub().callsArgWith(2, null, {inS3:false})
-				@PackManager.checkArchiveNotInProgress @project_id, @doc_id, @pack_id, @callback
-			it "should call the callback", ->
-				@callback.called.should.equal true
-			it "should return an error", ->
-				@callback.calledWith(sinon.match.has('message')).should.equal true
-
-		describe "when an archive is completed", ->
-			beforeEach ->
-				@db.docHistoryIndex =
-					findOne: sinon.stub().callsArgWith(2, null, {inS3:true})
-				@PackManager.checkArchiveNotInProgress @project_id, @doc_id, @pack_id, @callback
-			it "should call the callback", ->
-				@callback.called.should.equal true
-			it "should return an error", ->
-				@callback.calledWith(sinon.match.has('message')).should.equal true
-
-		describe "when the archive has not started or completed", ->
-			beforeEach ->
-				@db.docHistoryIndex =
-					findOne: sinon.stub().callsArgWith(2, null, {})
-				@PackManager.checkArchiveNotInProgress @project_id, @doc_id, @pack_id, @callback
-			it "should call the callback with no error", ->
-				@callback.called.should.equal true
-			it "should return with no error", ->
-				(typeof @callback.lastCall.args[0]).should.equal 'undefined'
-
-	# describe "setTTLOnArchivedPack", ->
-	# 	beforeEach ->
-	# 		@pack_id = "somepackid"
-	# 		@onedayinms = 86400000
-	# 		@db.docHistory =
-	# 			findAndModify : sinon.stub().callsArgWith(1)
-
-	# 	it "should set expires to 1 day", (done)->
-	# 		#@PackManager._getOneDayInFutureWithRandomDelay = sinon.stub().returns(@onedayinms)
-	# 		@PackManager.setTTLOnArchivedPack @project_id, @doc_id, @pack_id, =>
-	# 			args = @db.docHistory.findAndModify.args[0][0]
-	# 			args.query._id.should.equal @pack_id
-	# 			args.update['$set'].expiresAt.should.equal @onedayinms
-	# 			done()
-
-
-	# describe "_getOneDayInFutureWithRandomDelay", ->
-	# 	beforeEach ->
-	# 		@onedayinms = 86400000
-	# 		@thirtyMins = 1000 * 60 * 30
-
-	# 	it "should give 1 day + 30 mins random time", (done)->
-	# 		loops = 10000
-	# 		while --loops > 0
-	# 			randomDelay = @PackManager._getOneDayInFutureWithRandomDelay() - new Date(Date.now() + @onedayinms)
-	# 			randomDelay.should.be.above(0)
-	# 			randomDelay.should.be.below(@thirtyMins + 1)
-	# 		done()
-
+					],
+					n : 2,
+					sz : 100,
+					meta: {start_ts: Date.now() - (30 * 24 * 3600 * 1000)},
+					expiresAt: new Date(Date.now() - (30 * 24 * 3600 * 1000))
+				};
+
+				return this.PackManager.flushCompressedUpdates(this.project_id, this.doc_id, this.lastUpdate, this.newUpdates, true, this.callback);
+			});
+
+			return describe("for a small update that will expire", function() {
+				it("should insert the update into mongo", function() {
+					return this.db.docHistory.save.calledWithMatch({
+						pack: this.newUpdates,
+						project_id: ObjectId(this.project_id),
+						doc_id: ObjectId(this.doc_id),
+						n: this.newUpdates.length,
+						v: this.newUpdates[0].v,
+						v_end: this.newUpdates[this.newUpdates.length-1].v
+					}).should.equal(true);
+				});
+
+				it("should set an expiry time in the future", function() {
+					return this.db.docHistory.save.calledWithMatch({
+						expiresAt: new Date(Date.now() + (7 * 24 * 3600 * 1000))
+					}).should.equal(true);
+				});
+
+				return it("should call the callback", function() {
+					return this.callback.called.should.equal(true);
+				});
+			});
+		});
+	});
+
+
+	describe("getOpsByVersionRange", function() {});
+
+	describe("loadPacksByVersionRange", function() {});
+
+	describe("fetchPacksIfNeeded", function() {});
+
+	describe("makeProjectIterator", function() {});
+
+	describe("getPackById", function() {});
+
+	describe("increaseTTL", function() {});
+
+	describe("getIndex", function() {});
+
+	describe("getPackFromIndex", function() {});
+// getLastPackFromIndex:
+// getIndexWithKeys
+// initialiseIndex
+// updateIndex
+// findCompletedPacks
+// findUnindexedPacks
+// insertPacksIntoIndexWithLock
+// _insertPacksIntoIndex
+// archivePack
+// checkArchivedPack
+// processOldPack
+// 	updateIndexIfNeeded
+// 	findUnarchivedPacks
+
+	return describe("checkArchiveNotInProgress", function() {
+
+		describe("when an archive is in progress", function() {
+			beforeEach(function() {
+				this.db.docHistoryIndex =
+					{findOne: sinon.stub().callsArgWith(2, null, {inS3:false})};
+				return this.PackManager.checkArchiveNotInProgress(this.project_id, this.doc_id, this.pack_id, this.callback);
+			});
+			it("should call the callback", function() {
+				return this.callback.called.should.equal(true);
+			});
+			return it("should return an error", function() {
+				return this.callback.calledWith(sinon.match.has('message')).should.equal(true);
+			});
+		});
+
+		describe("when an archive is completed", function() {
+			beforeEach(function() {
+				this.db.docHistoryIndex =
+					{findOne: sinon.stub().callsArgWith(2, null, {inS3:true})};
+				return this.PackManager.checkArchiveNotInProgress(this.project_id, this.doc_id, this.pack_id, this.callback);
+			});
+			it("should call the callback", function() {
+				return this.callback.called.should.equal(true);
+			});
+			return it("should return an error", function() {
+				return this.callback.calledWith(sinon.match.has('message')).should.equal(true);
+			});
+		});
+
+		return describe("when the archive has not started or completed", function() {
+			beforeEach(function() {
+				this.db.docHistoryIndex =
+					{findOne: sinon.stub().callsArgWith(2, null, {})};
+				return this.PackManager.checkArchiveNotInProgress(this.project_id, this.doc_id, this.pack_id, this.callback);
+			});
+			it("should call the callback with no error", function() {
+				return this.callback.called.should.equal(true);
+			});
+			return it("should return with no error", function() {
+				return (typeof this.callback.lastCall.args[0]).should.equal('undefined');
+			});
+		});
+	});
+});
+
+	// describe "setTTLOnArchivedPack", ->
+	// 	beforeEach ->
+	// 		@pack_id = "somepackid"
+	// 		@onedayinms = 86400000
+	// 		@db.docHistory =
+	// 			findAndModify : sinon.stub().callsArgWith(1)
+
+	// 	it "should set expires to 1 day", (done)->
+	// 		#@PackManager._getOneDayInFutureWithRandomDelay = sinon.stub().returns(@onedayinms)
+	// 		@PackManager.setTTLOnArchivedPack @project_id, @doc_id, @pack_id, =>
+	// 			args = @db.docHistory.findAndModify.args[0][0]
+	// 			args.query._id.should.equal @pack_id
+	// 			args.update['$set'].expiresAt.should.equal @onedayinms
+	// 			done()
+
+
+	// describe "_getOneDayInFutureWithRandomDelay", ->
+	// 	beforeEach ->
+	// 		@onedayinms = 86400000
+	// 		@thirtyMins = 1000 * 60 * 30
+
+	// 	it "should give 1 day + 30 mins random time", (done)->
+	// 		loops = 10000
+	// 		while --loops > 0
+	// 			randomDelay = @PackManager._getOneDayInFutureWithRandomDelay() - new Date(Date.now() + @onedayinms)
+	// 			randomDelay.should.be.above(0)
+	// 			randomDelay.should.be.below(@thirtyMins + 1)
+	// 		done()
+
+
+function __range__(left, right, inclusive) {
+  let range = [];
+  let ascending = left < right;
+  let end = !inclusive ? right : ascending ? right + 1 : right - 1;
+  for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) {
+    range.push(i);
+  }
+  return range;
+}

+ 121 - 87
services/track-changes/test/unit/coffee/RedisManager/RedisManagerTests.js

@@ -1,87 +1,121 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-expect = chai.expect
-modulePath = "../../../../app/js/RedisManager.js"
-SandboxedModule = require('sandboxed-module')
-
-describe "RedisManager", ->
-	beforeEach ->
-		@RedisManager = SandboxedModule.require modulePath, requires:
-			"redis-sharelatex" : 
-				createClient: () => @rclient =
-					auth: sinon.stub()
-					multi: () => @rclient
-			"settings-sharelatex":
-				redis:
-					history:
-						key_schema:
-							uncompressedHistoryOps: ({doc_id}) -> "UncompressedHistoryOps:#{doc_id}"
-							docsWithHistoryOps: ({project_id}) -> "DocsWithHistoryOps:#{project_id}"
-		@doc_id = "doc-id-123"
-		@project_id = "project-id-123"
-		@batchSize = 100
-		@callback = sinon.stub()
-
-	describe "getOldestDocUpdates", ->
-		beforeEach ->
-			@rawUpdates = [ {v: 42, op: "mock-op-42"}, { v: 45, op: "mock-op-45" }]
-			@jsonUpdates = (JSON.stringify(update) for update in @rawUpdates)
-			@rclient.lrange = sinon.stub().callsArgWith(3, null, @jsonUpdates)
-			@RedisManager.getOldestDocUpdates @doc_id, @batchSize, @callback
-
-		it "should read the updates from redis", ->
-			@rclient.lrange
-				.calledWith("UncompressedHistoryOps:#{@doc_id}", 0, @batchSize - 1)
-				.should.equal true
-
-		it "should call the callback with the unparsed ops", ->
-			@callback.calledWith(null, @jsonUpdates).should.equal true
-
-
-		describe "expandDocUpdates", ->
-			beforeEach ->
-				@RedisManager.expandDocUpdates @jsonUpdates, @callback
-
-			it "should call the callback with the parsed ops", ->
-				@callback.calledWith(null, @rawUpdates).should.equal true
-
-
-		describe "deleteAppliedDocUpdates", ->
-			beforeEach ->
-				@rclient.lrem = sinon.stub()
-				@rclient.srem = sinon.stub()
-				@rclient.exec = sinon.stub().callsArgWith(0)
-				@RedisManager.deleteAppliedDocUpdates @project_id, @doc_id, @jsonUpdates, @callback
-
-			it "should delete the first update from redis", ->
-				@rclient.lrem
-					.calledWith("UncompressedHistoryOps:#{@doc_id}", 1, @jsonUpdates[0])
-					.should.equal true
-
-			it "should delete the second update from redis", ->
-				@rclient.lrem
-					.calledWith("UncompressedHistoryOps:#{@doc_id}", 1, @jsonUpdates[1])
-					.should.equal true
-
-			it "should delete the doc from the set of docs with history ops", ->
-				@rclient.srem
-					.calledWith("DocsWithHistoryOps:#{@project_id}", @doc_id)
-					.should.equal true
-
-			it "should call the callback ", ->
-				@callback.called.should.equal true
-
-	describe "getDocIdsWithHistoryOps", ->
-		beforeEach ->
-			@doc_ids = ["mock-id-1", "mock-id-2"]
-			@rclient.smembers = sinon.stub().callsArgWith(1, null, @doc_ids)
-			@RedisManager.getDocIdsWithHistoryOps @project_id, @callback
-
-		it "should read the doc_ids from redis", ->
-			@rclient.smembers
-				.calledWith("DocsWithHistoryOps:#{@project_id}")
-				.should.equal true
-
-		it "should call the callback with the doc_ids", ->
-			@callback.calledWith(null, @doc_ids).should.equal true
+/*
+ * 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/RedisManager.js";
+const SandboxedModule = require('sandboxed-module');
+
+describe("RedisManager", function() {
+	beforeEach(function() {
+		this.RedisManager = SandboxedModule.require(modulePath, { requires: {
+			"redis-sharelatex" : { 
+				createClient: () => { return this.rclient = {
+					auth: sinon.stub(),
+					multi: () => this.rclient
+				}; }
+			},
+			"settings-sharelatex": {
+				redis: {
+					history: {
+						key_schema: {
+							uncompressedHistoryOps({doc_id}) { return `UncompressedHistoryOps:${doc_id}`; },
+							docsWithHistoryOps({project_id}) { return `DocsWithHistoryOps:${project_id}`; }
+						}
+					}
+				}
+			}
+		}
+	}
+		);
+		this.doc_id = "doc-id-123";
+		this.project_id = "project-id-123";
+		this.batchSize = 100;
+		return this.callback = sinon.stub();
+	});
+
+	describe("getOldestDocUpdates", function() {
+		beforeEach(function() {
+			this.rawUpdates = [ {v: 42, op: "mock-op-42"}, { v: 45, op: "mock-op-45" }];
+			this.jsonUpdates = (Array.from(this.rawUpdates).map((update) => JSON.stringify(update)));
+			this.rclient.lrange = sinon.stub().callsArgWith(3, null, this.jsonUpdates);
+			return this.RedisManager.getOldestDocUpdates(this.doc_id, this.batchSize, this.callback);
+		});
+
+		it("should read the updates from redis", function() {
+			return this.rclient.lrange
+				.calledWith(`UncompressedHistoryOps:${this.doc_id}`, 0, this.batchSize - 1)
+				.should.equal(true);
+		});
+
+		it("should call the callback with the unparsed ops", function() {
+			return this.callback.calledWith(null, this.jsonUpdates).should.equal(true);
+		});
+
+
+		describe("expandDocUpdates", function() {
+			beforeEach(function() {
+				return this.RedisManager.expandDocUpdates(this.jsonUpdates, this.callback);
+			});
+
+			return it("should call the callback with the parsed ops", function() {
+				return this.callback.calledWith(null, this.rawUpdates).should.equal(true);
+			});
+		});
+
+
+		return describe("deleteAppliedDocUpdates", function() {
+			beforeEach(function() {
+				this.rclient.lrem = sinon.stub();
+				this.rclient.srem = sinon.stub();
+				this.rclient.exec = sinon.stub().callsArgWith(0);
+				return this.RedisManager.deleteAppliedDocUpdates(this.project_id, this.doc_id, this.jsonUpdates, this.callback);
+			});
+
+			it("should delete the first update from redis", function() {
+				return this.rclient.lrem
+					.calledWith(`UncompressedHistoryOps:${this.doc_id}`, 1, this.jsonUpdates[0])
+					.should.equal(true);
+			});
+
+			it("should delete the second update from redis", function() {
+				return this.rclient.lrem
+					.calledWith(`UncompressedHistoryOps:${this.doc_id}`, 1, this.jsonUpdates[1])
+					.should.equal(true);
+			});
+
+			it("should delete the doc from the set of docs with history ops", function() {
+				return this.rclient.srem
+					.calledWith(`DocsWithHistoryOps:${this.project_id}`, this.doc_id)
+					.should.equal(true);
+			});
+
+			return it("should call the callback ", function() {
+				return this.callback.called.should.equal(true);
+			});
+		});
+	});
+
+	return describe("getDocIdsWithHistoryOps", function() {
+		beforeEach(function() {
+			this.doc_ids = ["mock-id-1", "mock-id-2"];
+			this.rclient.smembers = sinon.stub().callsArgWith(1, null, this.doc_ids);
+			return this.RedisManager.getDocIdsWithHistoryOps(this.project_id, this.callback);
+		});
+
+		it("should read the doc_ids from redis", function() {
+			return this.rclient.smembers
+				.calledWith(`DocsWithHistoryOps:${this.project_id}`)
+				.should.equal(true);
+		});
+
+		return it("should call the callback with the doc_ids", function() {
+			return this.callback.calledWith(null, this.doc_ids).should.equal(true);
+		});
+	});
+});

+ 47 - 33
services/track-changes/test/unit/coffee/RestoreManager/RestoreManagerTests.js

@@ -1,39 +1,53 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-expect = chai.expect
-modulePath = "../../../../app/js/RestoreManager.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/RestoreManager.js";
+const SandboxedModule = require('sandboxed-module');
 
-describe "RestoreManager", ->
-	beforeEach ->
-		@RestoreManager = SandboxedModule.require modulePath, requires:
-			"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
-			"./DocumentUpdaterManager": @DocumentUpdaterManager = {}
-			"./DiffManager": @DiffManager = {}
-		@callback = sinon.stub()
-		@project_id = "mock-project-id"
-		@doc_id = "mock-doc-id"
-		@user_id = "mock-user-id"
-		@version = 42
+describe("RestoreManager", function() {
+	beforeEach(function() {
+		this.RestoreManager = SandboxedModule.require(modulePath, { requires: {
+			"logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }),
+			"./DocumentUpdaterManager": (this.DocumentUpdaterManager = {}),
+			"./DiffManager": (this.DiffManager = {})
+		}
+	});
+		this.callback = sinon.stub();
+		this.project_id = "mock-project-id";
+		this.doc_id = "mock-doc-id";
+		this.user_id = "mock-user-id";
+		return this.version = 42;
+	});
 
-	describe "restoreToBeforeVersion", ->
-		beforeEach ->
-			@content = "mock content"
-			@DocumentUpdaterManager.setDocument = sinon.stub().callsArg(4)
-			@DiffManager.getDocumentBeforeVersion = sinon.stub().callsArgWith(3, null, @content)
-			@RestoreManager.restoreToBeforeVersion @project_id, @doc_id, @version, @user_id, @callback
+	return describe("restoreToBeforeVersion", function() {
+		beforeEach(function() {
+			this.content = "mock content";
+			this.DocumentUpdaterManager.setDocument = sinon.stub().callsArg(4);
+			this.DiffManager.getDocumentBeforeVersion = sinon.stub().callsArgWith(3, null, this.content);
+			return this.RestoreManager.restoreToBeforeVersion(this.project_id, this.doc_id, this.version, this.user_id, this.callback);
+		});
 
-		it "should get the content before the requested version", ->
-			@DiffManager.getDocumentBeforeVersion
-				.calledWith(@project_id, @doc_id, @version)
-				.should.equal true
+		it("should get the content before the requested version", function() {
+			return this.DiffManager.getDocumentBeforeVersion
+				.calledWith(this.project_id, this.doc_id, this.version)
+				.should.equal(true);
+		});
 
-		it "should set the document in the document updater", ->
-			@DocumentUpdaterManager.setDocument
-				.calledWith(@project_id, @doc_id, @content, @user_id)
-				.should.equal true
+		it("should set the document in the document updater", function() {
+			return this.DocumentUpdaterManager.setDocument
+				.calledWith(this.project_id, this.doc_id, this.content, this.user_id)
+				.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);
+		});
+	});
+});
 			

+ 468 - 344
services/track-changes/test/unit/coffee/UpdateCompressor/UpdateCompressorTests.js

@@ -1,466 +1,590 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-expect = chai.expect
-modulePath = "../../../../app/js/UpdateCompressor.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/UpdateCompressor.js";
+const SandboxedModule = require('sandboxed-module');
 
-bigstring = ("a" for [0 .. 2*1024*1024]).join("")
-mediumstring = ("a" for [0 .. 1024*1024]).join("")
+const bigstring = (__range__(0, 2*1024*1024, true).map((i) => "a")).join("");
+const mediumstring = (__range__(0, 1024*1024, true).map((j) => "a")).join("");
 
-describe "UpdateCompressor", ->
-	beforeEach ->
-		@UpdateCompressor = SandboxedModule.require modulePath, requires:
+describe("UpdateCompressor", function() {
+	beforeEach(function() {
+		this.UpdateCompressor = SandboxedModule.require(modulePath, { requires: {
 			"../lib/diff_match_patch": require("../../../../app/lib/diff_match_patch")
-		@user_id = "user-id-1"
-		@other_user_id = "user-id-2"
-		@ts1 = Date.now()
-		@ts2 = Date.now() + 1000
+		}
+	}
+		);
+		this.user_id = "user-id-1";
+		this.other_user_id = "user-id-2";
+		this.ts1 = Date.now();
+		return this.ts2 = Date.now() + 1000;
+	});
 
-	describe "convertToSingleOpUpdates", ->
-		it "should split grouped updates into individual updates", ->
-			expect(@UpdateCompressor.convertToSingleOpUpdates [{
-				op:   [ @op1 = { p: 0, i: "Foo" }, @op2 = { p: 6, i: "bar"} ]
-				meta: { ts: @ts1, user_id: @user_id }
+	describe("convertToSingleOpUpdates", function() {
+		it("should split grouped updates into individual updates", function() {
+			return expect(this.UpdateCompressor.convertToSingleOpUpdates([{
+				op:   [ (this.op1 = { p: 0, i: "Foo" }), (this.op2 = { p: 6, i: "bar"}) ],
+				meta: { ts: this.ts1, user_id: this.user_id },
 				v: 42
 			}, {
-				op:   [ @op3 = { p: 10, i: "baz" } ]
-				meta: { ts: @ts2, user_id: @other_user_id }
+				op:   [ (this.op3 = { p: 10, i: "baz" }) ],
+				meta: { ts: this.ts2, user_id: this.other_user_id },
 				v: 43
-			}])
-			.to.deep.equal [{
-				op: @op1,
-				meta: { start_ts: @ts1, end_ts: @ts1, user_id: @user_id },
+			}]))
+			.to.deep.equal([{
+				op: this.op1,
+				meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id },
 				v: 42
 			}, {
-				op: @op2,
-				meta: { start_ts: @ts1, end_ts: @ts1, user_id: @user_id },
+				op: this.op2,
+				meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id },
 				v: 42
 			}, {
-				op: @op3,
-				meta: { start_ts: @ts2, end_ts: @ts2, user_id: @other_user_id },
+				op: this.op3,
+				meta: { start_ts: this.ts2, end_ts: this.ts2, user_id: this.other_user_id },
 				v: 43
-			}]
+			}]);
+	});
 
-		it "should return no-op updates when the op list is empty", ->
-			expect(@UpdateCompressor.convertToSingleOpUpdates [{
-				op:   []
-				meta: { ts: @ts1, user_id: @user_id }
+		it("should return no-op updates when the op list is empty", function() {
+			return expect(this.UpdateCompressor.convertToSingleOpUpdates([{
+				op:   [],
+				meta: { ts: this.ts1, user_id: this.user_id },
 				v: 42
-			}])
-			.to.deep.equal [{
-				op: @UpdateCompressor.NOOP
-				meta: { start_ts: @ts1, end_ts: @ts1, user_id: @user_id },
+			}]))
+			.to.deep.equal([{
+				op: this.UpdateCompressor.NOOP,
+				meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id },
 				v: 42
-			}]
+			}]);
+	});
 
-		it "should ignore comment ops", ->
-			expect(@UpdateCompressor.convertToSingleOpUpdates [{
-				op:   [ @op1 = { p: 0, i: "Foo" }, @op2 = { p: 9, c: "baz"}, @op3 = { p: 6, i: "bar"} ]
-				meta: { ts: @ts1, user_id: @user_id }
+		return it("should ignore comment ops", function() {
+			return expect(this.UpdateCompressor.convertToSingleOpUpdates([{
+				op:   [ (this.op1 = { p: 0, i: "Foo" }), (this.op2 = { p: 9, c: "baz"}), (this.op3 = { p: 6, i: "bar"}) ],
+				meta: { ts: this.ts1, user_id: this.user_id },
 				v: 42
-			}])
-			.to.deep.equal [{
-				op: @op1,
-				meta: { start_ts: @ts1, end_ts: @ts1, user_id: @user_id },
+			}]))
+			.to.deep.equal([{
+				op: this.op1,
+				meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id },
 				v: 42
 			}, {
-				op: @op3,
-				meta: { start_ts: @ts1, end_ts: @ts1, user_id: @user_id },
+				op: this.op3,
+				meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id },
 				v: 42
-			}]
+			}]);
+	});
+});
 
-	describe "concatUpdatesWithSameVersion", ->
-		it "should concat updates with the same version", ->
-			expect(@UpdateCompressor.concatUpdatesWithSameVersion [{
-				op:   @op1 = { p: 0, i: "Foo" }
-				meta: { start_ts: @ts1, end_ts: @ts1, user_id: @user_id }
+	describe("concatUpdatesWithSameVersion", function() {
+		it("should concat updates with the same version", function() {
+			return expect(this.UpdateCompressor.concatUpdatesWithSameVersion([{
+				op:   (this.op1 = { p: 0, i: "Foo" }),
+				meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id },
 				v: 42
 			}, {
-				op:   @op2 = { p: 6, i: "bar" }
-				meta: { start_ts: @ts1, end_ts: @ts1, user_id: @user_id }
+				op:   (this.op2 = { p: 6, i: "bar" }),
+				meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id },
 				v: 42
 			}, {
-				op:   @op3 = { p: 10, i: "baz" }
-				meta: { start_ts: @ts2, end_ts: @ts2, user_id: @other_user_id }
+				op:   (this.op3 = { p: 10, i: "baz" }),
+				meta: { start_ts: this.ts2, end_ts: this.ts2, user_id: this.other_user_id },
 				v: 43
-			}])
-			.to.deep.equal [{
-				op:   [ @op1, @op2 ]
-				meta: { start_ts: @ts1, end_ts: @ts1, user_id: @user_id }
+			}]))
+			.to.deep.equal([{
+				op:   [ this.op1, this.op2 ],
+				meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id },
 				v: 42
 			}, {
-				op:   [ @op3 ]
-				meta: {  start_ts: @ts2, end_ts: @ts2, user_id: @other_user_id }
+				op:   [ this.op3 ],
+				meta: {  start_ts: this.ts2, end_ts: this.ts2, user_id: this.other_user_id },
 				v: 43
-			}]
+			}]);
+	});
 
-		it "should turn a noop into an empty op", ->
-			expect(@UpdateCompressor.concatUpdatesWithSameVersion [{
-				op:   @UpdateCompressor.NOOP
-				meta: { start_ts: @ts1, end_ts: @ts1, user_id: @user_id }
+		return it("should turn a noop into an empty op", function() {
+			return expect(this.UpdateCompressor.concatUpdatesWithSameVersion([{
+				op:   this.UpdateCompressor.NOOP,
+				meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id },
 				v: 42
-			}])
-			.to.deep.equal [{
-				op:   []
-				meta: { start_ts: @ts1, end_ts: @ts1, user_id: @user_id }
+			}]))
+			.to.deep.equal([{
+				op:   [],
+				meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id },
 				v: 42
-			}]
+			}]);
+	});
+});
 
-	describe "compress", ->
-		describe "insert - insert", ->
-			it "should append one insert to the other", ->
-				expect(@UpdateCompressor.compressUpdates [{
-					op: { p: 3, i: "foo" }
-					meta: ts: @ts1, user_id: @user_id
+	describe("compress", function() {
+		describe("insert - insert", function() {
+			it("should append one insert to the other", function() {
+				return expect(this.UpdateCompressor.compressUpdates([{
+					op: { p: 3, i: "foo" },
+					meta: { ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, {
-					op: { p: 6, i: "bar" }
-					meta: ts: @ts2, user_id: @user_id
-					v: 43
-				}])
-				.to.deep.equal [{
-					op: { p: 3, i: "foobar" }
-					meta: start_ts: @ts1, end_ts: @ts2, user_id: @user_id
-					v: 43
-				}]
+					op: { p: 6, i: "bar" },
+					meta: { ts: this.ts2, user_id: this.user_id
+				},
+					v: 43
+				}]))
+				.to.deep.equal([{
+					op: { p: 3, i: "foobar" },
+					meta: { start_ts: this.ts1, end_ts: this.ts2, user_id: this.user_id
+				},
+					v: 43
+				}]);
+		});
 
-			it "should insert one insert inside the other", ->
-				expect(@UpdateCompressor.compressUpdates [{
-					op: { p: 3, i: "foo" }
-					meta: ts: @ts1, user_id: @user_id
+			it("should insert one insert inside the other", function() {
+				return expect(this.UpdateCompressor.compressUpdates([{
+					op: { p: 3, i: "foo" },
+					meta: { ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, {
-					op: { p: 5, i: "bar" }
-					meta: ts: @ts2, user_id: @user_id
-					v: 43
-				}])
-				.to.deep.equal [{
-					op: { p: 3, i: "fobaro" }
-					meta: start_ts: @ts1, end_ts: @ts2, user_id: @user_id
-					v: 43
-				}]
+					op: { p: 5, i: "bar" },
+					meta: { ts: this.ts2, user_id: this.user_id
+				},
+					v: 43
+				}]))
+				.to.deep.equal([{
+					op: { p: 3, i: "fobaro" },
+					meta: { start_ts: this.ts1, end_ts: this.ts2, user_id: this.user_id
+				},
+					v: 43
+				}]);
+		});
 
-			it "should not append separated inserts", ->
-				expect(@UpdateCompressor.compressUpdates [{
-					op: { p: 3, i: "foo" }
-					meta: ts: @ts1, user_id: @user_id
+			it("should not append separated inserts", function() {
+				return expect(this.UpdateCompressor.compressUpdates([{
+					op: { p: 3, i: "foo" },
+					meta: { ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, {
-					op: { p: 9, i: "bar" }
-					meta: ts: @ts2, user_id: @user_id
-					v: 43
-				}])
-				.to.deep.equal [{
-					op: { p: 3, i: "foo" }
-					meta: start_ts: @ts1, end_ts: @ts1, user_id: @user_id
+					op: { p: 9, i: "bar" },
+					meta: { ts: this.ts2, user_id: this.user_id
+				},
+					v: 43
+				}]))
+				.to.deep.equal([{
+					op: { p: 3, i: "foo" },
+					meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, {
-					op: { p: 9, i: "bar" }
-					meta: start_ts: @ts2, end_ts: @ts2, user_id: @user_id
+					op: { p: 9, i: "bar" },
+					meta: { start_ts: this.ts2, end_ts: this.ts2, user_id: this.user_id
+				},
 					v: 43
-				}]
+				}]);
+		});
 
-			it "should not append inserts that are too big (second op)", ->
-				expect(@UpdateCompressor.compressUpdates [{
-					op: { p: 3, i: "foo" }
-					meta: ts: @ts1, user_id: @user_id
+			it("should not append inserts that are too big (second op)", function() {
+				return expect(this.UpdateCompressor.compressUpdates([{
+					op: { p: 3, i: "foo" },
+					meta: { ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, {
-					op: { p: 6, i: bigstring }
-					meta: ts: @ts2, user_id: @user_id
-					v: 43
-				}])
-				.to.deep.equal [{
-					op: { p: 3, i: "foo" }
-					meta: start_ts: @ts1, end_ts: @ts1, user_id: @user_id
+					op: { p: 6, i: bigstring },
+					meta: { ts: this.ts2, user_id: this.user_id
+				},
+					v: 43
+				}]))
+				.to.deep.equal([{
+					op: { p: 3, i: "foo" },
+					meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, {
-					op: { p: 6, i: bigstring }
-					meta: start_ts: @ts2, end_ts: @ts2, user_id: @user_id
+					op: { p: 6, i: bigstring },
+					meta: { start_ts: this.ts2, end_ts: this.ts2, user_id: this.user_id
+				},
 					v: 43
-				}]
+				}]);
+		});
 
-			it "should not append inserts that are too big (first op)", ->
-				expect(@UpdateCompressor.compressUpdates [{
-					op: { p: 3, i: bigstring }
-					meta: ts: @ts1, user_id: @user_id
+			it("should not append inserts that are too big (first op)", function() {
+				return expect(this.UpdateCompressor.compressUpdates([{
+					op: { p: 3, i: bigstring },
+					meta: { ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, {
-					op: { p: 3 + bigstring.length, i: "bar" }
-					meta: ts: @ts2, user_id: @user_id
-					v: 43
-				}])
-				.to.deep.equal [{
-					op: { p: 3, i: bigstring }
-					meta: start_ts: @ts1, end_ts: @ts1, user_id: @user_id
+					op: { p: 3 + bigstring.length, i: "bar" },
+					meta: { ts: this.ts2, user_id: this.user_id
+				},
+					v: 43
+				}]))
+				.to.deep.equal([{
+					op: { p: 3, i: bigstring },
+					meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, {
-					op: { p: 3 + bigstring.length, i: "bar" }
-					meta: start_ts: @ts2, end_ts: @ts2, user_id: @user_id
+					op: { p: 3 + bigstring.length, i: "bar" },
+					meta: { start_ts: this.ts2, end_ts: this.ts2, user_id: this.user_id
+				},
 					v: 43
-				}]
+				}]);
+		});
 
-			it "should not append inserts that are too big (first and second op)", ->
-				expect(@UpdateCompressor.compressUpdates [{
-					op: { p: 3, i: mediumstring }
-					meta: ts: @ts1, user_id: @user_id
+			return it("should not append inserts that are too big (first and second op)", function() {
+				return expect(this.UpdateCompressor.compressUpdates([{
+					op: { p: 3, i: mediumstring },
+					meta: { ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, {
-					op: { p: 3 + mediumstring.length, i: mediumstring }
-					meta: ts: @ts2, user_id: @user_id
-					v: 43
-				}])
-				.to.deep.equal [{
-					op: { p: 3, i: mediumstring }
-					meta: start_ts: @ts1, end_ts: @ts1, user_id: @user_id
+					op: { p: 3 + mediumstring.length, i: mediumstring },
+					meta: { ts: this.ts2, user_id: this.user_id
+				},
+					v: 43
+				}]))
+				.to.deep.equal([{
+					op: { p: 3, i: mediumstring },
+					meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, {
-					op: { p: 3 + mediumstring.length, i: mediumstring }
-					meta: start_ts: @ts2, end_ts: @ts2, user_id: @user_id
+					op: { p: 3 + mediumstring.length, i: mediumstring },
+					meta: { start_ts: this.ts2, end_ts: this.ts2, user_id: this.user_id
+				},
 					v: 43
-				}]
+				}]);
+		});
+	});
 
 
-		describe "delete - delete", ->
-			it "should append one delete to the other", ->
-				expect(@UpdateCompressor.compressUpdates [{
-					op: { p: 3, d: "foo" }
-					meta: ts: @ts1, user_id: @user_id
+		describe("delete - delete", function() {
+			it("should append one delete to the other", function() {
+				return expect(this.UpdateCompressor.compressUpdates([{
+					op: { p: 3, d: "foo" },
+					meta: { ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, {
-					op: { p: 3, d: "bar" }
-					meta: ts: @ts2, user_id: @user_id
-					v: 43
-				}])
-				.to.deep.equal [{
-					op: { p: 3, d: "foobar" }
-					meta: start_ts: @ts1, end_ts: @ts2, user_id: @user_id
-					v: 43
-				}]
+					op: { p: 3, d: "bar" },
+					meta: { ts: this.ts2, user_id: this.user_id
+				},
+					v: 43
+				}]))
+				.to.deep.equal([{
+					op: { p: 3, d: "foobar" },
+					meta: { start_ts: this.ts1, end_ts: this.ts2, user_id: this.user_id
+				},
+					v: 43
+				}]);
+		});
 				
-			it "should insert one delete inside the other", ->
-				expect(@UpdateCompressor.compressUpdates [{
-					op: { p: 3, d: "foo" }
-					meta: ts: @ts1, user_id: @user_id
+			it("should insert one delete inside the other", function() {
+				return expect(this.UpdateCompressor.compressUpdates([{
+					op: { p: 3, d: "foo" },
+					meta: { ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, {
-					op: { p: 1, d: "bar" }
-					meta: ts: @ts2, user_id: @user_id
-					v: 43
-				}])
-				.to.deep.equal [{
-					op: { p: 1, d: "bafoor" }
-					meta: start_ts: @ts1, end_ts: @ts2, user_id: @user_id
-					v: 43
-				}]
+					op: { p: 1, d: "bar" },
+					meta: { ts: this.ts2, user_id: this.user_id
+				},
+					v: 43
+				}]))
+				.to.deep.equal([{
+					op: { p: 1, d: "bafoor" },
+					meta: { start_ts: this.ts1, end_ts: this.ts2, user_id: this.user_id
+				},
+					v: 43
+				}]);
+		});
 				
-			it "should not append separated deletes", ->
-				expect(@UpdateCompressor.compressUpdates [{
-					op: { p: 3, d: "foo" }
-					meta: ts: @ts1, user_id: @user_id
+			return it("should not append separated deletes", function() {
+				return expect(this.UpdateCompressor.compressUpdates([{
+					op: { p: 3, d: "foo" },
+					meta: { ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, {
-					op: { p: 9, d: "bar" }
-					meta: ts: @ts2, user_id: @user_id
-					v: 43
-				}])
-				.to.deep.equal [{
-					op: { p: 3, d: "foo" }
-					meta: start_ts: @ts1, end_ts: @ts1, user_id: @user_id
+					op: { p: 9, d: "bar" },
+					meta: { ts: this.ts2, user_id: this.user_id
+				},
+					v: 43
+				}]))
+				.to.deep.equal([{
+					op: { p: 3, d: "foo" },
+					meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, {
-					op: { p: 9, d: "bar" }
-					meta: start_ts: @ts2, end_ts: @ts2, user_id: @user_id
+					op: { p: 9, d: "bar" },
+					meta: { start_ts: this.ts2, end_ts: this.ts2, user_id: this.user_id
+				},
 					v: 43
-				}]
+				}]);
+		});
+	});
 
-		describe "insert - delete", ->
-			it "should undo a previous insert", ->
-				expect(@UpdateCompressor.compressUpdates [{
-					op: { p: 3, i: "foo" }
-					meta: ts: @ts1, user_id: @user_id
+		describe("insert - delete", function() {
+			it("should undo a previous insert", function() {
+				return expect(this.UpdateCompressor.compressUpdates([{
+					op: { p: 3, i: "foo" },
+					meta: { ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, {
-					op: { p: 5, d: "o" }
-					meta: ts: @ts2, user_id: @user_id
-					v: 43
-				}])
-				.to.deep.equal [{
-					op: { p: 3, i: "fo" }
-					meta: start_ts: @ts1, end_ts: @ts2, user_id: @user_id
-					v: 43
-				}]
+					op: { p: 5, d: "o" },
+					meta: { ts: this.ts2, user_id: this.user_id
+				},
+					v: 43
+				}]))
+				.to.deep.equal([{
+					op: { p: 3, i: "fo" },
+					meta: { start_ts: this.ts1, end_ts: this.ts2, user_id: this.user_id
+				},
+					v: 43
+				}]);
+		});
 				
-			it "should remove part of an insert from the middle", ->
-				expect(@UpdateCompressor.compressUpdates [{
-					op: { p: 3, i: "fobaro" }
-					meta: ts: @ts1, user_id: @user_id
+			it("should remove part of an insert from the middle", function() {
+				return expect(this.UpdateCompressor.compressUpdates([{
+					op: { p: 3, i: "fobaro" },
+					meta: { ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, {
-					op: { p: 5, d: "bar" }
-					meta: ts: @ts2, user_id: @user_id
-					v: 43
-				}])
-				.to.deep.equal [{
-					op: { p: 3, i: "foo" }
-					meta: start_ts: @ts1, end_ts: @ts2, user_id: @user_id
-					v: 43
-				}]
+					op: { p: 5, d: "bar" },
+					meta: { ts: this.ts2, user_id: this.user_id
+				},
+					v: 43
+				}]))
+				.to.deep.equal([{
+					op: { p: 3, i: "foo" },
+					meta: { start_ts: this.ts1, end_ts: this.ts2, user_id: this.user_id
+				},
+					v: 43
+				}]);
+		});
 
-			it "should cancel out two opposite updates", ->
-				expect(@UpdateCompressor.compressUpdates [{
-					op: { p: 3, i: "foo" }
-					meta: ts: @ts1, user_id: @user_id
+			it("should cancel out two opposite updates", function() {
+				return expect(this.UpdateCompressor.compressUpdates([{
+					op: { p: 3, i: "foo" },
+					meta: { ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, {
-					op: { p: 3, d: "foo" }
-					meta: ts: @ts2, user_id: @user_id
-					v: 43
-				}])
-				.to.deep.equal [
-					op: { p: 3, i: "" }
-					meta: start_ts: @ts1, end_ts: @ts2, user_id: @user_id
-					v: 43
-				]
+					op: { p: 3, d: "foo" },
+					meta: { ts: this.ts2, user_id: this.user_id
+				},
+					v: 43
+				}]))
+				.to.deep.equal([{
+					op: { p: 3, i: "" },
+					meta: { start_ts: this.ts1, end_ts: this.ts2, user_id: this.user_id
+				},
+					v: 43
+				}
+				]);
+		});
 				
-			it "should not combine separated updates", ->
-				expect(@UpdateCompressor.compressUpdates [{
-					op: { p: 3, i: "foo" }
-					meta: ts: @ts1, user_id: @user_id
+			it("should not combine separated updates", function() {
+				return expect(this.UpdateCompressor.compressUpdates([{
+					op: { p: 3, i: "foo" },
+					meta: { ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, {
-					op: { p: 9, d: "bar" }
-					meta: ts: @ts2, user_id: @user_id
-					v: 43
-				}])
-				.to.deep.equal [{
-					op: { p: 3, i: "foo" }
-					meta: start_ts: @ts1, end_ts: @ts1, user_id: @user_id
+					op: { p: 9, d: "bar" },
+					meta: { ts: this.ts2, user_id: this.user_id
+				},
+					v: 43
+				}]))
+				.to.deep.equal([{
+					op: { p: 3, i: "foo" },
+					meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, {
-					op: { p: 9, d: "bar" }
-					meta: start_ts: @ts2, end_ts: @ts2, user_id: @user_id
+					op: { p: 9, d: "bar" },
+					meta: { start_ts: this.ts2, end_ts: this.ts2, user_id: this.user_id
+				},
 					v: 43
-				}]
+				}]);
+		});
 
-			it "should not combine updates with overlap beyond the end", ->
-				expect(@UpdateCompressor.compressUpdates [{
-					op: { p: 3, i: "foobar" }
-					meta: ts: @ts1, user_id: @user_id
+			return it("should not combine updates with overlap beyond the end", function() {
+				return expect(this.UpdateCompressor.compressUpdates([{
+					op: { p: 3, i: "foobar" },
+					meta: { ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, {
-					op: { p: 6, d: "bardle" }
-					meta: ts: @ts2, user_id: @user_id
-					v: 43
-				}])
-				.to.deep.equal [{
-					op: { p: 3, i: "foobar" }
-					meta: start_ts: @ts1, end_ts: @ts1, user_id: @user_id
+					op: { p: 6, d: "bardle" },
+					meta: { ts: this.ts2, user_id: this.user_id
+				},
+					v: 43
+				}]))
+				.to.deep.equal([{
+					op: { p: 3, i: "foobar" },
+					meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, {
-					op: { p: 6, d: "bardle" }
-					meta: start_ts: @ts2, end_ts: @ts2, user_id: @user_id
+					op: { p: 6, d: "bardle" },
+					meta: { start_ts: this.ts2, end_ts: this.ts2, user_id: this.user_id
+				},
 					v: 43
-				}]
+				}]);
+		});
+	});
 		
-		describe "delete - insert", ->
-			it "should do a diff of the content", ->
-				expect(@UpdateCompressor.compressUpdates [{
-					op: { p: 3, d: "one two three four five six seven eight" }
-					meta: ts: @ts1, user_id: @user_id
+		describe("delete - insert", function() {
+			it("should do a diff of the content", function() {
+				return expect(this.UpdateCompressor.compressUpdates([{
+					op: { p: 3, d: "one two three four five six seven eight" },
+					meta: { ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, {
-					op: { p: 3, i: "one 2 three four five six seven eight" }
-					meta: ts: @ts2, user_id: @user_id
+					op: { p: 3, i: "one 2 three four five six seven eight" },
+					meta: { ts: this.ts2, user_id: this.user_id
+				},
 					v: 43
-				}])
-				.to.deep.equal [{
-					op: { p: 7, d: "two" }
-					meta: start_ts: @ts1, end_ts: @ts2, user_id: @user_id
+				}]))
+				.to.deep.equal([{
+					op: { p: 7, d: "two" },
+					meta: { start_ts: this.ts1, end_ts: this.ts2, user_id: this.user_id
+				},
 					v: 43
 				}, {
-					op: { p: 7, i: "2" }
-					meta: start_ts: @ts1, end_ts: @ts2, user_id: @user_id
+					op: { p: 7, i: "2" },
+					meta: { start_ts: this.ts1, end_ts: this.ts2, user_id: this.user_id
+				},
 					v: 43
-				}]
+				}]);
+		});
 			
-			it "should return a no-op if the delete and insert are the same", ->
-				expect(@UpdateCompressor.compressUpdates [{
-					op: { p: 3, d: "one two three four five six seven eight" }
-					meta: ts: @ts1, user_id: @user_id
+			return it("should return a no-op if the delete and insert are the same", function() {
+				return expect(this.UpdateCompressor.compressUpdates([{
+					op: { p: 3, d: "one two three four five six seven eight" },
+					meta: { ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, {
-					op: { p: 3, i: "one two three four five six seven eight" }
-					meta: ts: @ts2, user_id: @user_id
-					v: 43
-				}])
-				.to.deep.equal [{
-					op: { p: 3, i: "" }
-					meta: start_ts: @ts1, end_ts: @ts2, user_id: @user_id
-					v: 43
-				}]
+					op: { p: 3, i: "one two three four five six seven eight" },
+					meta: { ts: this.ts2, user_id: this.user_id
+				},
+					v: 43
+				}]))
+				.to.deep.equal([{
+					op: { p: 3, i: "" },
+					meta: { start_ts: this.ts1, end_ts: this.ts2, user_id: this.user_id
+				},
+					v: 43
+				}]);
+		});
+	});
 
-		describe "noop - insert", ->
-			it "should leave them untouched", ->
-				expect(@UpdateCompressor.compressUpdates [{
-					op: @UpdateCompressor.NOOP
-					meta: ts: @ts1, user_id: @user_id
+		describe("noop - insert", () =>
+			it("should leave them untouched", function() {
+				return expect(this.UpdateCompressor.compressUpdates([{
+					op: this.UpdateCompressor.NOOP,
+					meta: { ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, {
-					op: { p: 6, i: "bar" }
-					meta: ts: @ts1, user_id: @user_id
-					v: 43
-				}])
-				.to.deep.equal [{
-					op: @UpdateCompressor.NOOP
-					meta: start_ts: @ts1, end_ts: @ts1, user_id: @user_id
+					op: { p: 6, i: "bar" },
+					meta: { ts: this.ts1, user_id: this.user_id
+				},
+					v: 43
+				}]))
+				.to.deep.equal([{
+					op: this.UpdateCompressor.NOOP,
+					meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, {
-					op: { p: 6, i: "bar" }
-					meta: start_ts: @ts1, end_ts: @ts1, user_id: @user_id
+					op: { p: 6, i: "bar" },
+					meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id
+				},
 					v: 43
-				}]
+				}]);
+		})
+	);
 
-		describe "noop - delete", ->
-			it "should leave them untouched", ->
-				expect(@UpdateCompressor.compressUpdates [{
-					op: @UpdateCompressor.NOOP
-					meta: ts: @ts1, user_id: @user_id
+		return describe("noop - delete", () =>
+			it("should leave them untouched", function() {
+				return expect(this.UpdateCompressor.compressUpdates([{
+					op: this.UpdateCompressor.NOOP,
+					meta: { ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, {
-					op: { p: 6, d: "bar" }
-					meta: ts: @ts1, user_id: @user_id
-					v: 43
-				}])
-				.to.deep.equal [{
-					op: @UpdateCompressor.NOOP
-					meta: start_ts: @ts1, end_ts: @ts1, user_id: @user_id
+					op: { p: 6, d: "bar" },
+					meta: { ts: this.ts1, user_id: this.user_id
+				},
+					v: 43
+				}]))
+				.to.deep.equal([{
+					op: this.UpdateCompressor.NOOP,
+					meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, {
-					op: { p: 6, d: "bar" }
-					meta: start_ts: @ts1, end_ts: @ts1, user_id: @user_id
-					v: 43
-				}]
+					op: { p: 6, d: "bar" },
+					meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id
+				},
+					v: 43
+				}]);
+		})
+	);
+});
 
-	describe "compressRawUpdates", ->
-		describe "merging in-place with an array op", ->
-			it "should not change the existing last updates", ->
-				expect(@UpdateCompressor.compressRawUpdates {
-					op: [ {"p":1000,"d":"hello"}, {"p":1000,"i":"HELLO()"} ]
-					meta: start_ts: @ts1, end_ts: @ts1, user_id: @user_id
+	return describe("compressRawUpdates", () =>
+		describe("merging in-place with an array op", () =>
+			it("should not change the existing last updates", function() {
+				return expect(this.UpdateCompressor.compressRawUpdates({
+					op: [ {"p":1000,"d":"hello"}, {"p":1000,"i":"HELLO()"} ],
+					meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				}, [{
-					op: [{ p: 1006, i: "WORLD" }]
-					meta: ts: @ts2, user_id: @user_id
-					v: 43
-				}])
-				.to.deep.equal [{
-					op: [{"p":1000,"d":"hello"}, {"p":1000,"i":"HELLO()"} ]
-					meta: start_ts: @ts1, end_ts: @ts1, user_id: @user_id
+					op: [{ p: 1006, i: "WORLD" }],
+					meta: { ts: this.ts2, user_id: this.user_id
+				},
+					v: 43
+				}]))
+				.to.deep.equal([{
+					op: [{"p":1000,"d":"hello"}, {"p":1000,"i":"HELLO()"} ],
+					meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id
+				},
 					v: 42
 				},{
-					op: [{"p":1006,"i":"WORLD"}]
-					meta: start_ts: @ts2, end_ts: @ts2, user_id: @user_id
-					v: 43
-				}]
+					op: [{"p":1006,"i":"WORLD"}],
+					meta: { start_ts: this.ts2, end_ts: this.ts2, user_id: this.user_id
+				},
+					v: 43
+				}]);
+		})
+	)
+);
+});
+
+function __range__(left, right, inclusive) {
+  let range = [];
+  let ascending = left < right;
+  let end = !inclusive ? right : ascending ? right + 1 : right - 1;
+  for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) {
+    range.push(i);
+  }
+  return range;
+}

+ 157 - 120
services/track-changes/test/unit/coffee/UpdateTrimmer/UpdateTrimmerTests.js

@@ -1,122 +1,159 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-expect = chai.expect
-modulePath = "../../../../app/js/UpdateTrimmer.js"
-SandboxedModule = require('sandboxed-module')
-tk = require "timekeeper"
-
-describe "UpdateTrimmer", ->
-	beforeEach ->
-		@now = new Date()
-		tk.freeze(@now)
-
-		@UpdateTrimmer = SandboxedModule.require modulePath, requires:
-			"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
-			"./WebApiManager": @WebApiManager = {}
-			"./MongoManager": @MongoManager = {}
-
-		@callback = sinon.stub()
-		@project_id = "mock-project-id"
-
-	afterEach ->
-		tk.reset()
-
-	describe "shouldTrimUpdates", ->
-		beforeEach ->
-			@metadata = {}
-			@details =
-				features: {}
-			@MongoManager.getProjectMetaData = sinon.stub().callsArgWith(1, null, @metadata)
-			@MongoManager.setProjectMetaData = sinon.stub().callsArgWith(2)
-			@MongoManager.upgradeHistory = sinon.stub().callsArgWith(1)
-			@WebApiManager.getProjectDetails = sinon.stub().callsArgWith(1, null, @details)
-
-		describe "with preserveHistory set in the project meta data", ->
-			beforeEach ->
-				@metadata.preserveHistory = true
-				@UpdateTrimmer.shouldTrimUpdates @project_id, @callback
-
-			it "should look up the meta data", ->
-				@MongoManager.getProjectMetaData
-					.calledWith(@project_id)
-					.should.equal true
-
-			it "should not look up the project details", ->
-				@WebApiManager.getProjectDetails
+/*
+ * 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/UpdateTrimmer.js";
+const SandboxedModule = require('sandboxed-module');
+const tk = require("timekeeper");
+
+describe("UpdateTrimmer", function() {
+	beforeEach(function() {
+		this.now = new Date();
+		tk.freeze(this.now);
+
+		this.UpdateTrimmer = SandboxedModule.require(modulePath, { requires: {
+			"logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }),
+			"./WebApiManager": (this.WebApiManager = {}),
+			"./MongoManager": (this.MongoManager = {})
+		}
+	});
+
+		this.callback = sinon.stub();
+		return this.project_id = "mock-project-id";
+	});
+
+	afterEach(() => tk.reset());
+
+	return describe("shouldTrimUpdates", function() {
+		beforeEach(function() {
+			this.metadata = {};
+			this.details =
+				{features: {}};
+			this.MongoManager.getProjectMetaData = sinon.stub().callsArgWith(1, null, this.metadata);
+			this.MongoManager.setProjectMetaData = sinon.stub().callsArgWith(2);
+			this.MongoManager.upgradeHistory = sinon.stub().callsArgWith(1);
+			return this.WebApiManager.getProjectDetails = sinon.stub().callsArgWith(1, null, this.details);
+		});
+
+		describe("with preserveHistory set in the project meta data", function() {
+			beforeEach(function() {
+				this.metadata.preserveHistory = true;
+				return this.UpdateTrimmer.shouldTrimUpdates(this.project_id, this.callback);
+			});
+
+			it("should look up the meta data", function() {
+				return this.MongoManager.getProjectMetaData
+					.calledWith(this.project_id)
+					.should.equal(true);
+			});
+
+			it("should not look up the project details", function() {
+				return this.WebApiManager.getProjectDetails
 					.called
-					.should.equal false
-
-			it "should return false", ->
-				@callback.calledWith(null, false).should.equal true
-
-		describe "without preserveHistory set in the project meta data", ->
-			beforeEach ->
-				@metadata.preserveHistory = false
-
-			describe "when the project has the versioning feature", ->
-				beforeEach ->
-					@details.features.versioning = true
-					@UpdateTrimmer.shouldTrimUpdates @project_id, @callback
-
-				it "should look up the meta data", ->
-					@MongoManager.getProjectMetaData
-						.calledWith(@project_id)
-						.should.equal true
-
-				it "should look up the project details", ->
-					@WebApiManager.getProjectDetails
-						.calledWith(@project_id)
-						.should.equal true
-
-				it "should insert preserveHistory into the metadata", ->
-					@MongoManager.setProjectMetaData
-						.calledWith(@project_id, {preserveHistory: true})
-						.should.equal true
-
-				it "should upgrade any existing history", ->
-					@MongoManager.upgradeHistory
-						.calledWith(@project_id)
-						.should.equal true
-
-				it "should return false", ->
-					@callback.calledWith(null, false).should.equal true
-
-			describe "when the project does not have the versioning feature", ->
-				beforeEach ->
-					@details.features.versioning = false
-					@UpdateTrimmer.shouldTrimUpdates @project_id, @callback
-
-				it "should return true", ->
-					@callback.calledWith(null, true).should.equal true
-
-		describe "without any meta data", ->
-			beforeEach ->
-				@MongoManager.getProjectMetaData = sinon.stub().callsArgWith(1, null, null)
-
-			describe "when the project has the versioning feature", ->
-				beforeEach ->
-					@details.features.versioning = true
-					@UpdateTrimmer.shouldTrimUpdates @project_id, @callback
-
-				it "should insert preserveHistory into the metadata", ->
-					@MongoManager.setProjectMetaData
-						.calledWith(@project_id, {preserveHistory: true})
-						.should.equal true
-
-				it "should upgrade any existing history", ->
-					@MongoManager.upgradeHistory
-						.calledWith(@project_id)
-						.should.equal true
-
-				it "should return false", ->
-					@callback.calledWith(null, false).should.equal true
-
-			describe "when the project does not have the versioning feature", ->
-				beforeEach ->
-					@details.features.versioning = false
-					@UpdateTrimmer.shouldTrimUpdates @project_id, @callback
-
-				it "should return true", ->
-					@callback.calledWith(null, true).should.equal true
+					.should.equal(false);
+			});
+
+			return it("should return false", function() {
+				return this.callback.calledWith(null, false).should.equal(true);
+			});
+		});
+
+		describe("without preserveHistory set in the project meta data", function() {
+			beforeEach(function() {
+				return this.metadata.preserveHistory = false;
+			});
+
+			describe("when the project has the versioning feature", function() {
+				beforeEach(function() {
+					this.details.features.versioning = true;
+					return this.UpdateTrimmer.shouldTrimUpdates(this.project_id, this.callback);
+				});
+
+				it("should look up the meta data", function() {
+					return this.MongoManager.getProjectMetaData
+						.calledWith(this.project_id)
+						.should.equal(true);
+				});
+
+				it("should look up the project details", function() {
+					return this.WebApiManager.getProjectDetails
+						.calledWith(this.project_id)
+						.should.equal(true);
+				});
+
+				it("should insert preserveHistory into the metadata", function() {
+					return this.MongoManager.setProjectMetaData
+						.calledWith(this.project_id, {preserveHistory: true})
+						.should.equal(true);
+				});
+
+				it("should upgrade any existing history", function() {
+					return this.MongoManager.upgradeHistory
+						.calledWith(this.project_id)
+						.should.equal(true);
+				});
+
+				return it("should return false", function() {
+					return this.callback.calledWith(null, false).should.equal(true);
+				});
+			});
+
+			return describe("when the project does not have the versioning feature", function() {
+				beforeEach(function() {
+					this.details.features.versioning = false;
+					return this.UpdateTrimmer.shouldTrimUpdates(this.project_id, this.callback);
+				});
+
+				return it("should return true", function() {
+					return this.callback.calledWith(null, true).should.equal(true);
+				});
+			});
+		});
+
+		return describe("without any meta data", function() {
+			beforeEach(function() {
+				return this.MongoManager.getProjectMetaData = sinon.stub().callsArgWith(1, null, null);
+			});
+
+			describe("when the project has the versioning feature", function() {
+				beforeEach(function() {
+					this.details.features.versioning = true;
+					return this.UpdateTrimmer.shouldTrimUpdates(this.project_id, this.callback);
+				});
+
+				it("should insert preserveHistory into the metadata", function() {
+					return this.MongoManager.setProjectMetaData
+						.calledWith(this.project_id, {preserveHistory: true})
+						.should.equal(true);
+				});
+
+				it("should upgrade any existing history", function() {
+					return this.MongoManager.upgradeHistory
+						.calledWith(this.project_id)
+						.should.equal(true);
+				});
+
+				return it("should return false", function() {
+					return this.callback.calledWith(null, false).should.equal(true);
+				});
+			});
+
+			return describe("when the project does not have the versioning feature", function() {
+				beforeEach(function() {
+					this.details.features.versioning = false;
+					return this.UpdateTrimmer.shouldTrimUpdates(this.project_id, this.callback);
+				});
+
+				return it("should return true", function() {
+					return this.callback.calledWith(null, true).should.equal(true);
+				});
+			});
+		});
+	});
+});
 

+ 941 - 751
services/track-changes/test/unit/coffee/UpdatesManager/UpdatesManagerTests.js

@@ -1,823 +1,1013 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-expect = chai.expect
-modulePath = "../../../../app/js/UpdatesManager.js"
-SandboxedModule = require('sandboxed-module')
-
-describe "UpdatesManager", ->
-	beforeEach ->
-		@UpdatesManager = SandboxedModule.require modulePath, requires:
-			"./UpdateCompressor": @UpdateCompressor = {}
-			"./MongoManager" : @MongoManager = {}
-			"./PackManager" : @PackManager = {}
-			"./RedisManager" : @RedisManager = {}
-			"./LockManager"  : @LockManager = {}
-			"./WebApiManager": @WebApiManager = {}
-			"./UpdateTrimmer": @UpdateTrimmer = {}
-			"./DocArchiveManager": @DocArchiveManager = {}
-			"logger-sharelatex": { log: sinon.stub(), error: sinon.stub() }
-			"settings-sharelatex":
-				redis: lock: key_schema:
-					historyLock: ({doc_id}) -> "HistoryLock:#{doc_id}"
-		@doc_id = "doc-id-123"
-		@project_id = "project-id-123"
-		@callback = sinon.stub()
-		@temporary = "temp-mock"
-
-	describe "compressAndSaveRawUpdates", ->
-		describe "when there are no raw ops", ->
-			beforeEach ->
-				@MongoManager.peekLastCompressedUpdate = sinon.stub()
-				@UpdatesManager.compressAndSaveRawUpdates @project_id, @doc_id, [], @temporary, @callback
-
-			it "should not need to access the database", ->
-				@MongoManager.peekLastCompressedUpdate.called.should.equal false
-
-			it "should call the callback", ->
-				@callback.called.should.equal true
-
-		describe "when there is no compressed history to begin with", ->
-			beforeEach ->
-				@rawUpdates = [{ v: 12, op: "mock-op-12" }, { v: 13, op: "mock-op-13" }]
-				@compressedUpdates = [ { v: 13, op: "compressed-op-12" } ]
-
-				@MongoManager.peekLastCompressedUpdate = sinon.stub().callsArgWith(1, null, null)
-				@PackManager.insertCompressedUpdates = sinon.stub().callsArg(5)
-				@UpdateCompressor.compressRawUpdates = sinon.stub().returns(@compressedUpdates)
-				@UpdatesManager.compressAndSaveRawUpdates @project_id, @doc_id, @rawUpdates, @temporary, @callback
-
-			it "should look at the last compressed op", ->
-				@MongoManager.peekLastCompressedUpdate
-					.calledWith(@doc_id)
-					.should.equal true
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require('sinon');
+const chai = require('chai');
+const should = chai.should();
+const { expect } = chai;
+const modulePath = "../../../../app/js/UpdatesManager.js";
+const SandboxedModule = require('sandboxed-module');
+
+describe("UpdatesManager", function() {
+	beforeEach(function() {
+		this.UpdatesManager = SandboxedModule.require(modulePath, { requires: {
+			"./UpdateCompressor": (this.UpdateCompressor = {}),
+			"./MongoManager" : (this.MongoManager = {}),
+			"./PackManager" : (this.PackManager = {}),
+			"./RedisManager" : (this.RedisManager = {}),
+			"./LockManager"  : (this.LockManager = {}),
+			"./WebApiManager": (this.WebApiManager = {}),
+			"./UpdateTrimmer": (this.UpdateTrimmer = {}),
+			"./DocArchiveManager": (this.DocArchiveManager = {}),
+			"logger-sharelatex": { log: sinon.stub(), error: sinon.stub() },
+			"settings-sharelatex": {
+				redis: { lock: { key_schema: {
+					historyLock({doc_id}) { return `HistoryLock:${doc_id}`; }
+				}
+			}
+			}
+			}
+		}
+	}
+		);
+		this.doc_id = "doc-id-123";
+		this.project_id = "project-id-123";
+		this.callback = sinon.stub();
+		return this.temporary = "temp-mock";
+	});
+
+	describe("compressAndSaveRawUpdates", function() {
+		describe("when there are no raw ops", function() {
+			beforeEach(function() {
+				this.MongoManager.peekLastCompressedUpdate = sinon.stub();
+				return this.UpdatesManager.compressAndSaveRawUpdates(this.project_id, this.doc_id, [], this.temporary, this.callback);
+			});
+
+			it("should not need to access the database", function() {
+				return this.MongoManager.peekLastCompressedUpdate.called.should.equal(false);
+			});
+
+			return it("should call the callback", function() {
+				return this.callback.called.should.equal(true);
+			});
+		});
+
+		describe("when there is no compressed history to begin with", function() {
+			beforeEach(function() {
+				this.rawUpdates = [{ v: 12, op: "mock-op-12" }, { v: 13, op: "mock-op-13" }];
+				this.compressedUpdates = [ { v: 13, op: "compressed-op-12" } ];
+
+				this.MongoManager.peekLastCompressedUpdate = sinon.stub().callsArgWith(1, null, null);
+				this.PackManager.insertCompressedUpdates = sinon.stub().callsArg(5);
+				this.UpdateCompressor.compressRawUpdates = sinon.stub().returns(this.compressedUpdates);
+				return this.UpdatesManager.compressAndSaveRawUpdates(this.project_id, this.doc_id, this.rawUpdates, this.temporary, this.callback);
+			});
+
+			it("should look at the last compressed op", function() {
+				return this.MongoManager.peekLastCompressedUpdate
+					.calledWith(this.doc_id)
+					.should.equal(true);
+			});
 			
-			it "should save the compressed ops as a pack", ->
-				@PackManager.insertCompressedUpdates
-					.calledWith(@project_id, @doc_id, null, @compressedUpdates, @temporary)
-					.should.equal true
-
-			it "should call the callback", ->
-				@callback.called.should.equal true
-
-		describe "when the raw ops need appending to existing history", ->
-			beforeEach ->
-				@lastCompressedUpdate = { v: 11, op: "compressed-op-11" }
-				@compressedUpdates = [ { v: 12, op: "compressed-op-11+12" }, { v: 13, op: "compressed-op-12" } ]
-
-				@MongoManager.peekLastCompressedUpdate = sinon.stub().callsArgWith(1, null, @lastCompressedUpdate, @lastCompressedUpdate.v)
-				@PackManager.insertCompressedUpdates = sinon.stub().callsArg(5)
-				@UpdateCompressor.compressRawUpdates = sinon.stub().returns(@compressedUpdates)
-
-			describe "when the raw ops start where the existing history ends", ->
-				beforeEach ->
-					@rawUpdates = [{ v: 12, op: "mock-op-12" }, { v: 13, op: "mock-op-13" }]
-					@UpdatesManager.compressAndSaveRawUpdates @project_id, @doc_id, @rawUpdates, @temporary, @callback
-
-				it "should look at the last compressed op", ->
-					@MongoManager.peekLastCompressedUpdate
-						.calledWith(@doc_id)
-						.should.equal true
+			it("should save the compressed ops as a pack", function() {
+				return this.PackManager.insertCompressedUpdates
+					.calledWith(this.project_id, this.doc_id, null, this.compressedUpdates, this.temporary)
+					.should.equal(true);
+			});
+
+			return it("should call the callback", function() {
+				return this.callback.called.should.equal(true);
+			});
+		});
+
+		describe("when the raw ops need appending to existing history", function() {
+			beforeEach(function() {
+				this.lastCompressedUpdate = { v: 11, op: "compressed-op-11" };
+				this.compressedUpdates = [ { v: 12, op: "compressed-op-11+12" }, { v: 13, op: "compressed-op-12" } ];
+
+				this.MongoManager.peekLastCompressedUpdate = sinon.stub().callsArgWith(1, null, this.lastCompressedUpdate, this.lastCompressedUpdate.v);
+				this.PackManager.insertCompressedUpdates = sinon.stub().callsArg(5);
+				return this.UpdateCompressor.compressRawUpdates = sinon.stub().returns(this.compressedUpdates);
+			});
+
+			describe("when the raw ops start where the existing history ends", function() {
+				beforeEach(function() {
+					this.rawUpdates = [{ v: 12, op: "mock-op-12" }, { v: 13, op: "mock-op-13" }];
+					return this.UpdatesManager.compressAndSaveRawUpdates(this.project_id, this.doc_id, this.rawUpdates, this.temporary, this.callback);
+				});
+
+				it("should look at the last compressed op", function() {
+					return this.MongoManager.peekLastCompressedUpdate
+						.calledWith(this.doc_id)
+						.should.equal(true);
+				});
 				
-				it "should compress the raw ops", ->
-					@UpdateCompressor.compressRawUpdates
-						.calledWith(null, @rawUpdates)
-						.should.equal true
-
-				it "should save the new compressed ops into a pack", ->
-					@PackManager.insertCompressedUpdates
-						.calledWith(@project_id, @doc_id, @lastCompressedUpdate, @compressedUpdates, @temporary)
-						.should.equal true
-
-				it "should call the callback", ->
-					@callback.called.should.equal true
-
-			describe "when the raw ops start where the existing history ends and the history is in a pack", ->
-				beforeEach ->
-					@lastCompressedUpdate = {pack: [{ v: 11, op: "compressed-op-11" }], v:11}
-					@rawUpdates = [{ v: 12, op: "mock-op-12" }, { v: 13, op: "mock-op-13" }]
-					@MongoManager.peekLastCompressedUpdate = sinon.stub().callsArgWith(1, null, @lastCompressedUpdate, @lastCompressedUpdate.v)
-					@UpdatesManager.compressAndSaveRawUpdates @project_id, @doc_id, @rawUpdates, @temporary, @callback
-
-				it "should look at the last compressed op", ->
-					@MongoManager.peekLastCompressedUpdate
-						.calledWith(@doc_id)
-						.should.equal true
-
-				it "should compress the raw ops", ->
-					@UpdateCompressor.compressRawUpdates
-						.calledWith(null, @rawUpdates)
-						.should.equal true
-
-				it "should save the new compressed ops into a pack", ->
-					@PackManager.insertCompressedUpdates
-						.calledWith(@project_id, @doc_id, @lastCompressedUpdate, @compressedUpdates, @temporary)
-						.should.equal true
-
-				it "should call the callback", ->
-					@callback.called.should.equal true
-
-			describe "when some raw ops are passed that have already been compressed", ->
-				beforeEach ->
-					@rawUpdates = [{ v: 10, op: "mock-op-10" }, { v: 11, op: "mock-op-11"}, { v: 12, op: "mock-op-12" }, { v: 13, op: "mock-op-13" }]
-
-					@UpdatesManager.compressAndSaveRawUpdates @project_id, @doc_id, @rawUpdates, @temporary, @callback
-
-				it "should only compress the more recent raw ops", ->
-					@UpdateCompressor.compressRawUpdates
-						.calledWith(null, @rawUpdates.slice(-2))
-						.should.equal true
-
-			describe "when the raw ops do not follow from the last compressed op version", ->
-				beforeEach ->
-					@rawUpdates = [{ v: 13, op: "mock-op-13" }]
-					@UpdatesManager.compressAndSaveRawUpdates @project_id, @doc_id, @rawUpdates, @temporary, @callback
-
-				it "should call the callback with an error", ->
-					@callback
+				it("should compress the raw ops", function() {
+					return this.UpdateCompressor.compressRawUpdates
+						.calledWith(null, this.rawUpdates)
+						.should.equal(true);
+				});
+
+				it("should save the new compressed ops into a pack", function() {
+					return this.PackManager.insertCompressedUpdates
+						.calledWith(this.project_id, this.doc_id, this.lastCompressedUpdate, this.compressedUpdates, this.temporary)
+						.should.equal(true);
+				});
+
+				return it("should call the callback", function() {
+					return this.callback.called.should.equal(true);
+				});
+			});
+
+			describe("when the raw ops start where the existing history ends and the history is in a pack", function() {
+				beforeEach(function() {
+					this.lastCompressedUpdate = {pack: [{ v: 11, op: "compressed-op-11" }], v:11};
+					this.rawUpdates = [{ v: 12, op: "mock-op-12" }, { v: 13, op: "mock-op-13" }];
+					this.MongoManager.peekLastCompressedUpdate = sinon.stub().callsArgWith(1, null, this.lastCompressedUpdate, this.lastCompressedUpdate.v);
+					return this.UpdatesManager.compressAndSaveRawUpdates(this.project_id, this.doc_id, this.rawUpdates, this.temporary, this.callback);
+				});
+
+				it("should look at the last compressed op", function() {
+					return this.MongoManager.peekLastCompressedUpdate
+						.calledWith(this.doc_id)
+						.should.equal(true);
+				});
+
+				it("should compress the raw ops", function() {
+					return this.UpdateCompressor.compressRawUpdates
+						.calledWith(null, this.rawUpdates)
+						.should.equal(true);
+				});
+
+				it("should save the new compressed ops into a pack", function() {
+					return this.PackManager.insertCompressedUpdates
+						.calledWith(this.project_id, this.doc_id, this.lastCompressedUpdate, this.compressedUpdates, this.temporary)
+						.should.equal(true);
+				});
+
+				return it("should call the callback", function() {
+					return this.callback.called.should.equal(true);
+				});
+			});
+
+			describe("when some raw ops are passed that have already been compressed", function() {
+				beforeEach(function() {
+					this.rawUpdates = [{ v: 10, op: "mock-op-10" }, { v: 11, op: "mock-op-11"}, { v: 12, op: "mock-op-12" }, { v: 13, op: "mock-op-13" }];
+
+					return this.UpdatesManager.compressAndSaveRawUpdates(this.project_id, this.doc_id, this.rawUpdates, this.temporary, this.callback);
+				});
+
+				return it("should only compress the more recent raw ops", function() {
+					return this.UpdateCompressor.compressRawUpdates
+						.calledWith(null, this.rawUpdates.slice(-2))
+						.should.equal(true);
+				});
+			});
+
+			describe("when the raw ops do not follow from the last compressed op version", function() {
+				beforeEach(function() {
+					this.rawUpdates = [{ v: 13, op: "mock-op-13" }];
+					return this.UpdatesManager.compressAndSaveRawUpdates(this.project_id, this.doc_id, this.rawUpdates, this.temporary, this.callback);
+				});
+
+				it("should call the callback with an error", function() {
+					return this.callback
 						.calledWith(sinon.match.has('message', "Tried to apply raw op at version 13 to last compressed update with version 11 from unknown time"))
-						.should.equal true
-
-				it "should not insert any update into mongo", ->
-					@PackManager.insertCompressedUpdates.called.should.equal false
-
-			describe "when the raw ops are out of order", ->
-				beforeEach ->
-					@rawUpdates = [{ v: 13, op: "mock-op-13" }, { v: 12, op: "mock-op-12" }]
-					@UpdatesManager.compressAndSaveRawUpdates @project_id, @doc_id, @rawUpdates, @temporary, @callback
-
-				it "should call the callback with an error", ->
-					@callback
+						.should.equal(true);
+				});
+
+				return it("should not insert any update into mongo", function() {
+					return this.PackManager.insertCompressedUpdates.called.should.equal(false);
+				});
+			});
+
+			return describe("when the raw ops are out of order", function() {
+				beforeEach(function() {
+					this.rawUpdates = [{ v: 13, op: "mock-op-13" }, { v: 12, op: "mock-op-12" }];
+					return this.UpdatesManager.compressAndSaveRawUpdates(this.project_id, this.doc_id, this.rawUpdates, this.temporary, this.callback);
+				});
+
+				it("should call the callback with an error", function() {
+					return this.callback
 						.calledWith(sinon.match.has('message'))
-						.should.equal true
-
-				it "should not insert any update into mongo", ->
-					@PackManager.insertCompressedUpdates.called.should.equal false
-
-
-		describe "when the raw ops need appending to existing history which is in S3", ->
-			beforeEach ->
-				@lastCompressedUpdate = null
-				@lastVersion = 11
-				@compressedUpdates = [ { v: 13, op: "compressed-op-12" } ]
-
-				@MongoManager.peekLastCompressedUpdate = sinon.stub().callsArgWith(1, null, null, @lastVersion)
-				@PackManager.insertCompressedUpdates = sinon.stub().callsArg(5)
-				@UpdateCompressor.compressRawUpdates = sinon.stub().returns(@compressedUpdates)
-
-			describe "when the raw ops start where the existing history ends", ->
-				beforeEach ->
-					@rawUpdates = [{ v: 12, op: "mock-op-12" }, { v: 13, op: "mock-op-13" }]
-					@UpdatesManager.compressAndSaveRawUpdates @project_id, @doc_id, @rawUpdates, @temporary, @callback
-
-				it "should try to look at the last compressed op", ->
-					@MongoManager.peekLastCompressedUpdate
-						.calledWith(@doc_id)
-						.should.equal true
+						.should.equal(true);
+				});
+
+				return it("should not insert any update into mongo", function() {
+					return this.PackManager.insertCompressedUpdates.called.should.equal(false);
+				});
+			});
+		});
+
+
+		return describe("when the raw ops need appending to existing history which is in S3", function() {
+			beforeEach(function() {
+				this.lastCompressedUpdate = null;
+				this.lastVersion = 11;
+				this.compressedUpdates = [ { v: 13, op: "compressed-op-12" } ];
+
+				this.MongoManager.peekLastCompressedUpdate = sinon.stub().callsArgWith(1, null, null, this.lastVersion);
+				this.PackManager.insertCompressedUpdates = sinon.stub().callsArg(5);
+				return this.UpdateCompressor.compressRawUpdates = sinon.stub().returns(this.compressedUpdates);
+			});
+
+			return describe("when the raw ops start where the existing history ends", function() {
+				beforeEach(function() {
+					this.rawUpdates = [{ v: 12, op: "mock-op-12" }, { v: 13, op: "mock-op-13" }];
+					return this.UpdatesManager.compressAndSaveRawUpdates(this.project_id, this.doc_id, this.rawUpdates, this.temporary, this.callback);
+				});
+
+				it("should try to look at the last compressed op", function() {
+					return this.MongoManager.peekLastCompressedUpdate
+						.calledWith(this.doc_id)
+						.should.equal(true);
+				});
 				
-				it "should compress the last compressed op and the raw ops", ->
-					@UpdateCompressor.compressRawUpdates
-						.calledWith(@lastCompressedUpdate, @rawUpdates)
-						.should.equal true
+				it("should compress the last compressed op and the raw ops", function() {
+					return this.UpdateCompressor.compressRawUpdates
+						.calledWith(this.lastCompressedUpdate, this.rawUpdates)
+						.should.equal(true);
+				});
 				
-				it "should save the compressed ops", ->
-					@PackManager.insertCompressedUpdates
-						.calledWith(@project_id, @doc_id, null, @compressedUpdates, @temporary)
-						.should.equal true
-
-				it "should call the callback", ->
-					@callback.called.should.equal true
-
-	describe "processUncompressedUpdates", ->
-		beforeEach ->
-			@UpdatesManager.compressAndSaveRawUpdates = sinon.stub().callsArgWith(4)
-			@RedisManager.deleteAppliedDocUpdates = sinon.stub().callsArg(3)
-			@MongoManager.backportProjectId = sinon.stub().callsArg(2)
-			@UpdateTrimmer.shouldTrimUpdates = sinon.stub().callsArgWith(1, null, @temporary = "temp mock")
-
-		describe "when there is fewer than one batch to send", ->
-			beforeEach ->
-				@updates = ["mock-update"]
-				@RedisManager.getOldestDocUpdates = sinon.stub().callsArgWith(2, null, @updates)
-				@RedisManager.expandDocUpdates = sinon.stub().callsArgWith(1, null, @updates)
-				@UpdatesManager.processUncompressedUpdates @project_id, @doc_id, @temporary, @callback
-
-			it "should get the oldest updates", ->
-				@RedisManager.getOldestDocUpdates
-					.calledWith(@doc_id, @UpdatesManager.REDIS_READ_BATCH_SIZE)
-					.should.equal true
-
-			it "should compress and save the updates", ->
-				@UpdatesManager.compressAndSaveRawUpdates
-					.calledWith(@project_id, @doc_id, @updates, @temporary)
-					.should.equal true
-
-			it "should delete the batch of uncompressed updates that was just processed", ->
-				@RedisManager.deleteAppliedDocUpdates
-					.calledWith(@project_id, @doc_id, @updates)
-					.should.equal true
-
-			it "should call the callback", ->
-				@callback.called.should.equal true
-
-		describe "when there are multiple batches to send", ->
-			beforeEach (done) ->
-				@UpdatesManager.REDIS_READ_BATCH_SIZE = 2
-				@updates = ["mock-update-0", "mock-update-1", "mock-update-2", "mock-update-3", "mock-update-4"]
-				@redisArray = @updates.slice()
-				@RedisManager.getOldestDocUpdates = (doc_id, batchSize, callback = (error, updates) ->) =>
-					updates = @redisArray.slice(0, batchSize)
-					@redisArray = @redisArray.slice(batchSize)
-					callback null, updates
-				sinon.spy @RedisManager, "getOldestDocUpdates"
-				@RedisManager.expandDocUpdates = (jsonUpdates, callback) =>
-					callback null, jsonUpdates
-				sinon.spy @RedisManager, "expandDocUpdates"
-				@UpdatesManager.processUncompressedUpdates @project_id, @doc_id, @temporary, (args...) =>
-					@callback(args...)
-					done()
-
-			it "should get the oldest updates in three batches ", ->
-				@RedisManager.getOldestDocUpdates.callCount.should.equal 3
-
-			it "should compress and save the updates in batches", ->
-				@UpdatesManager.compressAndSaveRawUpdates
-					.calledWith(@project_id, @doc_id, @updates.slice(0,2), @temporary)
-					.should.equal true
-				@UpdatesManager.compressAndSaveRawUpdates
-					.calledWith(@project_id, @doc_id, @updates.slice(2,4), @temporary)
-					.should.equal true
-				@UpdatesManager.compressAndSaveRawUpdates
-					.calledWith(@project_id, @doc_id, @updates.slice(4,5), @temporary)
-					.should.equal true
-
-			it "should delete the batches of uncompressed updates", ->
-				@RedisManager.deleteAppliedDocUpdates.callCount.should.equal 3
-
-			it "should call the callback", ->
-				@callback.called.should.equal true
-
-	describe "processCompressedUpdatesWithLock", ->
-		beforeEach ->
-			@UpdateTrimmer.shouldTrimUpdates = sinon.stub().callsArgWith(1, null, @temporary = "temp mock")
-			@MongoManager.backportProjectId = sinon.stub().callsArg(2)
-			@UpdatesManager._processUncompressedUpdates = sinon.stub().callsArg(3)
-			@LockManager.runWithLock = sinon.stub().callsArg(2)
-			@UpdatesManager.processUncompressedUpdatesWithLock @project_id, @doc_id, @callback
-
-		it "should check if the updates are temporary", ->
-			@UpdateTrimmer.shouldTrimUpdates
-				.calledWith(@project_id)
-				.should.equal true
-
-		it "should backport the project id", ->
-			@MongoManager.backportProjectId
-				.calledWith(@project_id, @doc_id)
-				.should.equal true
-
-		it "should run processUncompressedUpdates with the lock", ->
-			@LockManager.runWithLock
+				it("should save the compressed ops", function() {
+					return this.PackManager.insertCompressedUpdates
+						.calledWith(this.project_id, this.doc_id, null, this.compressedUpdates, this.temporary)
+						.should.equal(true);
+				});
+
+				return it("should call the callback", function() {
+					return this.callback.called.should.equal(true);
+				});
+			});
+		});
+	});
+
+	describe("processUncompressedUpdates", function() {
+		beforeEach(function() {
+			this.UpdatesManager.compressAndSaveRawUpdates = sinon.stub().callsArgWith(4);
+			this.RedisManager.deleteAppliedDocUpdates = sinon.stub().callsArg(3);
+			this.MongoManager.backportProjectId = sinon.stub().callsArg(2);
+			return this.UpdateTrimmer.shouldTrimUpdates = sinon.stub().callsArgWith(1, null, (this.temporary = "temp mock"));
+		});
+
+		describe("when there is fewer than one batch to send", function() {
+			beforeEach(function() {
+				this.updates = ["mock-update"];
+				this.RedisManager.getOldestDocUpdates = sinon.stub().callsArgWith(2, null, this.updates);
+				this.RedisManager.expandDocUpdates = sinon.stub().callsArgWith(1, null, this.updates);
+				return this.UpdatesManager.processUncompressedUpdates(this.project_id, this.doc_id, this.temporary, this.callback);
+			});
+
+			it("should get the oldest updates", function() {
+				return this.RedisManager.getOldestDocUpdates
+					.calledWith(this.doc_id, this.UpdatesManager.REDIS_READ_BATCH_SIZE)
+					.should.equal(true);
+			});
+
+			it("should compress and save the updates", function() {
+				return this.UpdatesManager.compressAndSaveRawUpdates
+					.calledWith(this.project_id, this.doc_id, this.updates, this.temporary)
+					.should.equal(true);
+			});
+
+			it("should delete the batch of uncompressed updates that was just processed", function() {
+				return this.RedisManager.deleteAppliedDocUpdates
+					.calledWith(this.project_id, this.doc_id, this.updates)
+					.should.equal(true);
+			});
+
+			return it("should call the callback", function() {
+				return this.callback.called.should.equal(true);
+			});
+		});
+
+		return describe("when there are multiple batches to send", function() {
+			beforeEach(function(done) {
+				this.UpdatesManager.REDIS_READ_BATCH_SIZE = 2;
+				this.updates = ["mock-update-0", "mock-update-1", "mock-update-2", "mock-update-3", "mock-update-4"];
+				this.redisArray = this.updates.slice();
+				this.RedisManager.getOldestDocUpdates = (doc_id, batchSize, callback) => {
+					if (callback == null) { callback = function(error, updates) {}; }
+					const updates = this.redisArray.slice(0, batchSize);
+					this.redisArray = this.redisArray.slice(batchSize);
+					return callback(null, updates);
+				};
+				sinon.spy(this.RedisManager, "getOldestDocUpdates");
+				this.RedisManager.expandDocUpdates = (jsonUpdates, callback) => {
+					return callback(null, jsonUpdates);
+				};
+				sinon.spy(this.RedisManager, "expandDocUpdates");
+				return this.UpdatesManager.processUncompressedUpdates(this.project_id, this.doc_id, this.temporary, (...args) => {
+					this.callback(...Array.from(args || []));
+					return done();
+				});
+			});
+
+			it("should get the oldest updates in three batches ", function() {
+				return this.RedisManager.getOldestDocUpdates.callCount.should.equal(3);
+			});
+
+			it("should compress and save the updates in batches", function() {
+				this.UpdatesManager.compressAndSaveRawUpdates
+					.calledWith(this.project_id, this.doc_id, this.updates.slice(0,2), this.temporary)
+					.should.equal(true);
+				this.UpdatesManager.compressAndSaveRawUpdates
+					.calledWith(this.project_id, this.doc_id, this.updates.slice(2,4), this.temporary)
+					.should.equal(true);
+				return this.UpdatesManager.compressAndSaveRawUpdates
+					.calledWith(this.project_id, this.doc_id, this.updates.slice(4,5), this.temporary)
+					.should.equal(true);
+			});
+
+			it("should delete the batches of uncompressed updates", function() {
+				return this.RedisManager.deleteAppliedDocUpdates.callCount.should.equal(3);
+			});
+
+			return it("should call the callback", function() {
+				return this.callback.called.should.equal(true);
+			});
+		});
+	});
+
+	describe("processCompressedUpdatesWithLock", function() {
+		beforeEach(function() {
+			this.UpdateTrimmer.shouldTrimUpdates = sinon.stub().callsArgWith(1, null, (this.temporary = "temp mock"));
+			this.MongoManager.backportProjectId = sinon.stub().callsArg(2);
+			this.UpdatesManager._processUncompressedUpdates = sinon.stub().callsArg(3);
+			this.LockManager.runWithLock = sinon.stub().callsArg(2);
+			return this.UpdatesManager.processUncompressedUpdatesWithLock(this.project_id, this.doc_id, this.callback);
+		});
+
+		it("should check if the updates are temporary", function() {
+			return this.UpdateTrimmer.shouldTrimUpdates
+				.calledWith(this.project_id)
+				.should.equal(true);
+		});
+
+		it("should backport the project id", function() {
+			return this.MongoManager.backportProjectId
+				.calledWith(this.project_id, this.doc_id)
+				.should.equal(true);
+		});
+
+		it("should run processUncompressedUpdates with the lock", function() {
+			return this.LockManager.runWithLock
 				.calledWith(
-					"HistoryLock:#{@doc_id}"
+					`HistoryLock:${this.doc_id}`
 				)
-				.should.equal true
-
-		it "should call the callback", ->
-			@callback.called.should.equal true
-
-	describe "getDocUpdates", ->
-		beforeEach ->
-			@updates = ["mock-updates"]
-			@options = { to: "mock-to", limit: "mock-limit" }
-			@PackManager.getOpsByVersionRange = sinon.stub().callsArgWith(4, null, @updates)
-			@UpdatesManager.processUncompressedUpdatesWithLock = sinon.stub().callsArg(2)
-			@UpdatesManager.getDocUpdates @project_id, @doc_id, @options, @callback
-
-		it "should process outstanding updates", ->
-			@UpdatesManager.processUncompressedUpdatesWithLock
-				.calledWith(@project_id, @doc_id)
-				.should.equal true
-
-		it "should get the updates from the database", ->
-			@PackManager.getOpsByVersionRange
-				.calledWith(@project_id, @doc_id, @options.from, @options.to)
-				.should.equal true
-
-		it "should return the updates", ->
-			@callback
-				.calledWith(null, @updates)
-				.should.equal true
-
-	describe "getDocUpdatesWithUserInfo", ->
-		beforeEach ->
-			@updates = ["mock-updates"]
-			@options = { to: "mock-to", limit: "mock-limit" }
-			@updatesWithUserInfo = ["updates-with-user-info"]
-			@UpdatesManager.getDocUpdates = sinon.stub().callsArgWith(3, null, @updates)
-			@UpdatesManager.fillUserInfo = sinon.stub().callsArgWith(1, null, @updatesWithUserInfo)
-			@UpdatesManager.getDocUpdatesWithUserInfo @project_id, @doc_id, @options, @callback
-
-		it "should get the updates", ->
-			@UpdatesManager.getDocUpdates
-				.calledWith(@project_id, @doc_id, @options)
-				.should.equal true
-
-		it "should file the updates with the user info", ->
-			@UpdatesManager.fillUserInfo
-				.calledWith(@updates)
-				.should.equal true
-
-		it "should return the updates with the filled details", ->
-			@callback.calledWith(null, @updatesWithUserInfo).should.equal true
-
-	describe "processUncompressedUpdatesForProject", ->
-		beforeEach (done) ->
-			@doc_ids = ["mock-id-1", "mock-id-2"]
-			@UpdateTrimmer.shouldTrimUpdates = sinon.stub().callsArgWith(1, null, @temporary = "temp mock")
-			@MongoManager.backportProjectId = sinon.stub().callsArg(2)
-			@UpdatesManager._processUncompressedUpdatesForDocWithLock = sinon.stub().callsArg(3)
-			@RedisManager.getDocIdsWithHistoryOps = sinon.stub().callsArgWith(1, null, @doc_ids)
-			@UpdatesManager.processUncompressedUpdatesForProject @project_id, () =>
-				@callback()
-				done()
-
-		it "should get all the docs with history ops", ->
-			@RedisManager.getDocIdsWithHistoryOps
-				.calledWith(@project_id)
-				.should.equal true
-
-		it "should process the doc ops for the each doc_id", ->
-			for doc_id in @doc_ids
-				@UpdatesManager._processUncompressedUpdatesForDocWithLock
-					.calledWith(@project_id, doc_id, @temporary)
-					.should.equal true
-
-		it "should call the callback", ->
-			@callback.called.should.equal true
-
-	describe "getSummarizedProjectUpdates", ->
-		beforeEach ->
-			@updates = [{doc_id: 123, v:456, op: "mock-updates", meta: {user_id: 123, start_ts: 1233, end_ts:1234}}]
-			@options = { before: "mock-before", limit: "mock-limit" }
-			@summarizedUpdates = [
+				.should.equal(true);
+		});
+
+		return it("should call the callback", function() {
+			return this.callback.called.should.equal(true);
+		});
+	});
+
+	describe("getDocUpdates", function() {
+		beforeEach(function() {
+			this.updates = ["mock-updates"];
+			this.options = { to: "mock-to", limit: "mock-limit" };
+			this.PackManager.getOpsByVersionRange = sinon.stub().callsArgWith(4, null, this.updates);
+			this.UpdatesManager.processUncompressedUpdatesWithLock = sinon.stub().callsArg(2);
+			return this.UpdatesManager.getDocUpdates(this.project_id, this.doc_id, this.options, this.callback);
+		});
+
+		it("should process outstanding updates", function() {
+			return this.UpdatesManager.processUncompressedUpdatesWithLock
+				.calledWith(this.project_id, this.doc_id)
+				.should.equal(true);
+		});
+
+		it("should get the updates from the database", function() {
+			return this.PackManager.getOpsByVersionRange
+				.calledWith(this.project_id, this.doc_id, this.options.from, this.options.to)
+				.should.equal(true);
+		});
+
+		return it("should return the updates", function() {
+			return this.callback
+				.calledWith(null, this.updates)
+				.should.equal(true);
+		});
+	});
+
+	describe("getDocUpdatesWithUserInfo", function() {
+		beforeEach(function() {
+			this.updates = ["mock-updates"];
+			this.options = { to: "mock-to", limit: "mock-limit" };
+			this.updatesWithUserInfo = ["updates-with-user-info"];
+			this.UpdatesManager.getDocUpdates = sinon.stub().callsArgWith(3, null, this.updates);
+			this.UpdatesManager.fillUserInfo = sinon.stub().callsArgWith(1, null, this.updatesWithUserInfo);
+			return this.UpdatesManager.getDocUpdatesWithUserInfo(this.project_id, this.doc_id, this.options, this.callback);
+		});
+
+		it("should get the updates", function() {
+			return this.UpdatesManager.getDocUpdates
+				.calledWith(this.project_id, this.doc_id, this.options)
+				.should.equal(true);
+		});
+
+		it("should file the updates with the user info", function() {
+			return this.UpdatesManager.fillUserInfo
+				.calledWith(this.updates)
+				.should.equal(true);
+		});
+
+		return it("should return the updates with the filled details", function() {
+			return this.callback.calledWith(null, this.updatesWithUserInfo).should.equal(true);
+		});
+	});
+
+	describe("processUncompressedUpdatesForProject", function() {
+		beforeEach(function(done) {
+			this.doc_ids = ["mock-id-1", "mock-id-2"];
+			this.UpdateTrimmer.shouldTrimUpdates = sinon.stub().callsArgWith(1, null, (this.temporary = "temp mock"));
+			this.MongoManager.backportProjectId = sinon.stub().callsArg(2);
+			this.UpdatesManager._processUncompressedUpdatesForDocWithLock = sinon.stub().callsArg(3);
+			this.RedisManager.getDocIdsWithHistoryOps = sinon.stub().callsArgWith(1, null, this.doc_ids);
+			return this.UpdatesManager.processUncompressedUpdatesForProject(this.project_id, () => {
+				this.callback();
+				return done();
+			});
+		});
+
+		it("should get all the docs with history ops", function() {
+			return this.RedisManager.getDocIdsWithHistoryOps
+				.calledWith(this.project_id)
+				.should.equal(true);
+		});
+
+		it("should process the doc ops for the each doc_id", function() {
+			return Array.from(this.doc_ids).map((doc_id) =>
+				this.UpdatesManager._processUncompressedUpdatesForDocWithLock
+					.calledWith(this.project_id, doc_id, this.temporary)
+					.should.equal(true));
+		});
+
+		return it("should call the callback", function() {
+			return this.callback.called.should.equal(true);
+		});
+	});
+
+	describe("getSummarizedProjectUpdates", function() {
+		beforeEach(function() {
+			this.updates = [{doc_id: 123, v:456, op: "mock-updates", meta: {user_id: 123, start_ts: 1233, end_ts:1234}}];
+			this.options = { before: "mock-before", limit: "mock-limit" };
+			this.summarizedUpdates = [
 				{meta: {user_ids: [123], start_ts: 1233, end_ts:1234},docs:{"123":{fromV:456,toV:456}}}
-			]
-			@updatesWithUserInfo = ["updates-with-user-info"]
-			@done_state = false
-			@iterator =
-				next: (cb) =>
-					@done_state = true
-					cb(null, @updates)
-				done: () =>
-					@done_state
-			@PackManager.makeProjectIterator = sinon.stub().callsArgWith(2, null, @iterator)
-			@UpdatesManager.processUncompressedUpdatesForProject = sinon.stub().callsArg(1)
-			@UpdatesManager.fillSummarizedUserInfo = sinon.stub().callsArgWith(1, null, @updatesWithUserInfo)
-			@UpdatesManager.getSummarizedProjectUpdates @project_id, @options, @callback
-
-		it "should process any outstanding updates", ->
-			@UpdatesManager.processUncompressedUpdatesForProject
-				.calledWith(@project_id)
-				.should.equal true
-
-		it "should get the updates", ->
-			@PackManager.makeProjectIterator
-				.calledWith(@project_id, @options.before)
-				.should.equal true
-
-		it "should fill the updates with the user info", ->
-			@UpdatesManager.fillSummarizedUserInfo
-				.calledWith(@summarizedUpdates)
-				.should.equal true
-
-		it "should return the updates with the filled details", ->
-			@callback.calledWith(null, @updatesWithUserInfo).should.equal true
-
-	# describe "_extendBatchOfSummarizedUpdates", ->
-	# 	beforeEach ->
-	# 		@before = Date.now()
-	# 		@min_count = 2
-	# 		@existingSummarizedUpdates = ["summarized-updates-3"]
-	# 		@summarizedUpdates = ["summarized-updates-3", "summarized-update-2", "summarized-update-1"]
-
-	# 	describe "when there are updates to get", ->
-	# 		beforeEach ->
-	# 			@updates = [
-	# 				{op: "mock-op-1", meta: end_ts: @before - 10},
-	# 				{op: "mock-op-1", meta: end_ts: @nextBeforeTimestamp = @before - 20}
-	# 			]
-	# 			@existingSummarizedUpdates = ["summarized-updates-3"]
-	# 			@summarizedUpdates = ["summarized-updates-3", "summarized-update-2", "summarized-update-1"]
-	# 			@UpdatesManager._summarizeUpdates = sinon.stub().returns(@summarizedUpdates)
-	# 			@UpdatesManager.getProjectUpdatesWithUserInfo = sinon.stub().callsArgWith(2, null, @updates)
-	# 			@UpdatesManager._extendBatchOfSummarizedUpdates @project_id, @existingSummarizedUpdates, @before, @min_count, @callback
-
-	# 		it "should get the updates", ->
-	# 			@UpdatesManager.getProjectUpdatesWithUserInfo
-	# 				.calledWith(@project_id, { before: @before, limit: 3 * @min_count })
-	# 				.should.equal true
-
-	# 		it "should summarize the updates", ->
-	# 			@UpdatesManager._summarizeUpdates
-	# 				.calledWith(@updates, @existingSummarizedUpdates)
-	# 				.should.equal true
-
-	# 		it "should call the callback with the summarized updates and the next before timestamp", ->
-	# 			@callback.calledWith(null, @summarizedUpdates, @nextBeforeTimestamp).should.equal true
-
-	# 	describe "when there are no more updates", ->
-	# 		beforeEach ->
-	# 			@updates = []
-	# 			@UpdatesManager._summarizeUpdates = sinon.stub().returns(@summarizedUpdates)
-	# 			@UpdatesManager.getProjectUpdatesWithUserInfo = sinon.stub().callsArgWith(2, null, @updates)
-	# 			@UpdatesManager._extendBatchOfSummarizedUpdates @project_id, @existingSummarizedUpdates, @before, @min_count, @callback
-
-	# 		it "should call the callback with the summarized updates and null for nextBeforeTimestamp", ->
-	# 			@callback.calledWith(null, @summarizedUpdates, null).should.equal true
-
-	# describe "getSummarizedProjectUpdates", ->
-	# 	describe "when one batch of updates is enough to meet the limit", ->
-	# 		beforeEach ->
-	# 			@before = Date.now()
-	# 			@min_count = 2
-	# 			@updates = ["summarized-updates-3", "summarized-updates-2"]
-	# 			@nextBeforeTimestamp = @before - 100
-	# 			@UpdatesManager._extendBatchOfSummarizedUpdates = sinon.stub().callsArgWith(4, null, @updates, @nextBeforeTimestamp)
-	# 			@UpdatesManager.getSummarizedProjectUpdates @project_id, { before: @before, min_count: @min_count }, @callback
-
-	# 		it "should get the batch of summarized updates", ->
-	# 			@UpdatesManager._extendBatchOfSummarizedUpdates
-	# 				.calledWith(@project_id, [], @before, @min_count)
-	# 				.should.equal true
-
-	# 		it "should call the callback with the updates", ->
-	# 			@callback.calledWith(null, @updates, @nextBeforeTimestamp).should.equal true
-
-	# 	describe "when multiple batches are needed to meet the limit", ->
-	# 		beforeEach ->
-	# 			@before = Date.now()
-	# 			@min_count = 4
-	# 			@firstBatch =  [{ toV: 6, fromV: 6 }, { toV: 5, fromV: 5 }]
-	# 			@nextBeforeTimestamp = @before - 100
-	# 			@secondBatch = [{ toV: 4, fromV: 4 }, { toV: 3, fromV: 3 }]
-	# 			@nextNextBeforeTimestamp = @before - 200
-	# 			@UpdatesManager._extendBatchOfSummarizedUpdates = (project_id, existingUpdates, before, desiredLength, callback) =>
-	# 				if existingUpdates.length == 0
-	# 					callback null, @firstBatch, @nextBeforeTimestamp
-	# 				else
-	# 					callback null, @firstBatch.concat(@secondBatch), @nextNextBeforeTimestamp
-	# 			sinon.spy @UpdatesManager, "_extendBatchOfSummarizedUpdates"
-	# 			@UpdatesManager.getSummarizedProjectUpdates @project_id, { before: @before, min_count: @min_count }, @callback
-
-	# 		it "should get the first batch of summarized updates", ->
-	# 			@UpdatesManager._extendBatchOfSummarizedUpdates
-	# 				.calledWith(@project_id, [], @before, @min_count)
-	# 				.should.equal true
-
-	# 		it "should get the second batch of summarized updates", ->
-	# 			@UpdatesManager._extendBatchOfSummarizedUpdates
-	# 				.calledWith(@project_id, @firstBatch, @nextBeforeTimestamp, @min_count)
-	# 				.should.equal true
-
-	# 		it "should call the callback with all the updates", ->
-	# 			@callback.calledWith(null, @firstBatch.concat(@secondBatch), @nextNextBeforeTimestamp).should.equal true
-
-	# 	describe "when the end of the database is hit", ->
-	# 		beforeEach ->
-	# 			@before = Date.now()
-	# 			@min_count = 4
-	# 			@updates =  [{ toV: 6, fromV: 6 }, { toV: 5, fromV: 5 }]
-	# 			@UpdatesManager._extendBatchOfSummarizedUpdates = sinon.stub().callsArgWith(4, null, @updates, null)
-	# 			@UpdatesManager.getSummarizedProjectUpdates @project_id, { before: @before, min_count: @min_count }, @callback
-
-	# 		it "should get the batch of summarized updates", ->
-	# 			@UpdatesManager._extendBatchOfSummarizedUpdates
-	# 				.calledWith(@project_id, [], @before, @min_count)
-	# 				.should.equal true
-
-	# 		it "should call the callback with the updates", ->
-	# 			@callback.calledWith(null, @updates, null).should.equal true
-
-	describe "fillUserInfo", ->
-		describe "with valid users", ->
-			beforeEach (done) ->
-				{ObjectId} = require "mongojs"
-				@user_id_1 = ObjectId().toString()
-				@user_id_2 = ObjectId().toString()
-				@updates = [{
-					meta:
-						user_id: @user_id_1
+			];
+			this.updatesWithUserInfo = ["updates-with-user-info"];
+			this.done_state = false;
+			this.iterator = {
+				next: cb => {
+					this.done_state = true;
+					return cb(null, this.updates);
+				},
+				done: () => {
+					return this.done_state;
+				}
+			};
+			this.PackManager.makeProjectIterator = sinon.stub().callsArgWith(2, null, this.iterator);
+			this.UpdatesManager.processUncompressedUpdatesForProject = sinon.stub().callsArg(1);
+			this.UpdatesManager.fillSummarizedUserInfo = sinon.stub().callsArgWith(1, null, this.updatesWithUserInfo);
+			return this.UpdatesManager.getSummarizedProjectUpdates(this.project_id, this.options, this.callback);
+		});
+
+		it("should process any outstanding updates", function() {
+			return this.UpdatesManager.processUncompressedUpdatesForProject
+				.calledWith(this.project_id)
+				.should.equal(true);
+		});
+
+		it("should get the updates", function() {
+			return this.PackManager.makeProjectIterator
+				.calledWith(this.project_id, this.options.before)
+				.should.equal(true);
+		});
+
+		it("should fill the updates with the user info", function() {
+			return this.UpdatesManager.fillSummarizedUserInfo
+				.calledWith(this.summarizedUpdates)
+				.should.equal(true);
+		});
+
+		return it("should return the updates with the filled details", function() {
+			return this.callback.calledWith(null, this.updatesWithUserInfo).should.equal(true);
+		});
+	});
+
+	// describe "_extendBatchOfSummarizedUpdates", ->
+	// 	beforeEach ->
+	// 		@before = Date.now()
+	// 		@min_count = 2
+	// 		@existingSummarizedUpdates = ["summarized-updates-3"]
+	// 		@summarizedUpdates = ["summarized-updates-3", "summarized-update-2", "summarized-update-1"]
+
+	// 	describe "when there are updates to get", ->
+	// 		beforeEach ->
+	// 			@updates = [
+	// 				{op: "mock-op-1", meta: end_ts: @before - 10},
+	// 				{op: "mock-op-1", meta: end_ts: @nextBeforeTimestamp = @before - 20}
+	// 			]
+	// 			@existingSummarizedUpdates = ["summarized-updates-3"]
+	// 			@summarizedUpdates = ["summarized-updates-3", "summarized-update-2", "summarized-update-1"]
+	// 			@UpdatesManager._summarizeUpdates = sinon.stub().returns(@summarizedUpdates)
+	// 			@UpdatesManager.getProjectUpdatesWithUserInfo = sinon.stub().callsArgWith(2, null, @updates)
+	// 			@UpdatesManager._extendBatchOfSummarizedUpdates @project_id, @existingSummarizedUpdates, @before, @min_count, @callback
+
+	// 		it "should get the updates", ->
+	// 			@UpdatesManager.getProjectUpdatesWithUserInfo
+	// 				.calledWith(@project_id, { before: @before, limit: 3 * @min_count })
+	// 				.should.equal true
+
+	// 		it "should summarize the updates", ->
+	// 			@UpdatesManager._summarizeUpdates
+	// 				.calledWith(@updates, @existingSummarizedUpdates)
+	// 				.should.equal true
+
+	// 		it "should call the callback with the summarized updates and the next before timestamp", ->
+	// 			@callback.calledWith(null, @summarizedUpdates, @nextBeforeTimestamp).should.equal true
+
+	// 	describe "when there are no more updates", ->
+	// 		beforeEach ->
+	// 			@updates = []
+	// 			@UpdatesManager._summarizeUpdates = sinon.stub().returns(@summarizedUpdates)
+	// 			@UpdatesManager.getProjectUpdatesWithUserInfo = sinon.stub().callsArgWith(2, null, @updates)
+	// 			@UpdatesManager._extendBatchOfSummarizedUpdates @project_id, @existingSummarizedUpdates, @before, @min_count, @callback
+
+	// 		it "should call the callback with the summarized updates and null for nextBeforeTimestamp", ->
+	// 			@callback.calledWith(null, @summarizedUpdates, null).should.equal true
+
+	// describe "getSummarizedProjectUpdates", ->
+	// 	describe "when one batch of updates is enough to meet the limit", ->
+	// 		beforeEach ->
+	// 			@before = Date.now()
+	// 			@min_count = 2
+	// 			@updates = ["summarized-updates-3", "summarized-updates-2"]
+	// 			@nextBeforeTimestamp = @before - 100
+	// 			@UpdatesManager._extendBatchOfSummarizedUpdates = sinon.stub().callsArgWith(4, null, @updates, @nextBeforeTimestamp)
+	// 			@UpdatesManager.getSummarizedProjectUpdates @project_id, { before: @before, min_count: @min_count }, @callback
+
+	// 		it "should get the batch of summarized updates", ->
+	// 			@UpdatesManager._extendBatchOfSummarizedUpdates
+	// 				.calledWith(@project_id, [], @before, @min_count)
+	// 				.should.equal true
+
+	// 		it "should call the callback with the updates", ->
+	// 			@callback.calledWith(null, @updates, @nextBeforeTimestamp).should.equal true
+
+	// 	describe "when multiple batches are needed to meet the limit", ->
+	// 		beforeEach ->
+	// 			@before = Date.now()
+	// 			@min_count = 4
+	// 			@firstBatch =  [{ toV: 6, fromV: 6 }, { toV: 5, fromV: 5 }]
+	// 			@nextBeforeTimestamp = @before - 100
+	// 			@secondBatch = [{ toV: 4, fromV: 4 }, { toV: 3, fromV: 3 }]
+	// 			@nextNextBeforeTimestamp = @before - 200
+	// 			@UpdatesManager._extendBatchOfSummarizedUpdates = (project_id, existingUpdates, before, desiredLength, callback) =>
+	// 				if existingUpdates.length == 0
+	// 					callback null, @firstBatch, @nextBeforeTimestamp
+	// 				else
+	// 					callback null, @firstBatch.concat(@secondBatch), @nextNextBeforeTimestamp
+	// 			sinon.spy @UpdatesManager, "_extendBatchOfSummarizedUpdates"
+	// 			@UpdatesManager.getSummarizedProjectUpdates @project_id, { before: @before, min_count: @min_count }, @callback
+
+	// 		it "should get the first batch of summarized updates", ->
+	// 			@UpdatesManager._extendBatchOfSummarizedUpdates
+	// 				.calledWith(@project_id, [], @before, @min_count)
+	// 				.should.equal true
+
+	// 		it "should get the second batch of summarized updates", ->
+	// 			@UpdatesManager._extendBatchOfSummarizedUpdates
+	// 				.calledWith(@project_id, @firstBatch, @nextBeforeTimestamp, @min_count)
+	// 				.should.equal true
+
+	// 		it "should call the callback with all the updates", ->
+	// 			@callback.calledWith(null, @firstBatch.concat(@secondBatch), @nextNextBeforeTimestamp).should.equal true
+
+	// 	describe "when the end of the database is hit", ->
+	// 		beforeEach ->
+	// 			@before = Date.now()
+	// 			@min_count = 4
+	// 			@updates =  [{ toV: 6, fromV: 6 }, { toV: 5, fromV: 5 }]
+	// 			@UpdatesManager._extendBatchOfSummarizedUpdates = sinon.stub().callsArgWith(4, null, @updates, null)
+	// 			@UpdatesManager.getSummarizedProjectUpdates @project_id, { before: @before, min_count: @min_count }, @callback
+
+	// 		it "should get the batch of summarized updates", ->
+	// 			@UpdatesManager._extendBatchOfSummarizedUpdates
+	// 				.calledWith(@project_id, [], @before, @min_count)
+	// 				.should.equal true
+
+	// 		it "should call the callback with the updates", ->
+	// 			@callback.calledWith(null, @updates, null).should.equal true
+
+	describe("fillUserInfo", function() {
+		describe("with valid users", function() {
+			beforeEach(function(done) {
+				const {ObjectId} = require("mongojs");
+				this.user_id_1 = ObjectId().toString();
+				this.user_id_2 = ObjectId().toString();
+				this.updates = [{
+					meta: {
+						user_id: this.user_id_1
+					},
 					op: "mock-op-1"
 				}, {
-					meta:
-						user_id: @user_id_1
+					meta: {
+						user_id: this.user_id_1
+					},
 					op: "mock-op-2"
 				}, {
-					meta:
-						user_id: @user_id_2
+					meta: {
+						user_id: this.user_id_2
+					},
 					op: "mock-op-3"
-				}]
-				@user_info = {}
-				@user_info[@user_id_1] = email: "user1@sharelatex.com"
-				@user_info[@user_id_2] = email: "user2@sharelatex.com"
+				}];
+				this.user_info = {};
+				this.user_info[this.user_id_1] = {email: "user1@sharelatex.com"};
+				this.user_info[this.user_id_2] = {email: "user2@sharelatex.com"};
 				
-				@WebApiManager.getUserInfo = (user_id, callback = (error, userInfo) ->) =>
-					callback null, @user_info[user_id]
-				sinon.spy @WebApiManager, "getUserInfo"
-
-				@UpdatesManager.fillUserInfo @updates, (error, @results) =>
-					done()
-
-			it "should only call getUserInfo once for each user_id", ->
-				@WebApiManager.getUserInfo.calledTwice.should.equal true
-				@WebApiManager.getUserInfo
-					.calledWith(@user_id_1)
-					.should.equal true
-				@WebApiManager.getUserInfo
-					.calledWith(@user_id_2)
-					.should.equal true
-
-			it "should return the updates with the user info filled", ->
-				expect(@results).to.deep.equal [{
-					meta:
-						user:
+				this.WebApiManager.getUserInfo = (user_id, callback) => {
+					if (callback == null) { callback = function(error, userInfo) {}; }
+					return callback(null, this.user_info[user_id]);
+				};
+				sinon.spy(this.WebApiManager, "getUserInfo");
+
+				return this.UpdatesManager.fillUserInfo(this.updates, (error, results) => {
+					this.results = results;
+					return done();
+				});
+			});
+
+			it("should only call getUserInfo once for each user_id", function() {
+				this.WebApiManager.getUserInfo.calledTwice.should.equal(true);
+				this.WebApiManager.getUserInfo
+					.calledWith(this.user_id_1)
+					.should.equal(true);
+				return this.WebApiManager.getUserInfo
+					.calledWith(this.user_id_2)
+					.should.equal(true);
+			});
+
+			return it("should return the updates with the user info filled", function() {
+				return expect(this.results).to.deep.equal([{
+					meta: {
+						user: {
 							email: "user1@sharelatex.com"
+						}
+					},
 					op: "mock-op-1"
 				}, {
-					meta:
-						user:
+					meta: {
+						user: {
 							email: "user1@sharelatex.com"
+						}
+					},
 					op: "mock-op-2"
 				}, {
-					meta:
-						user:
+					meta: {
+						user: {
 							email: "user2@sharelatex.com"
+						}
+					},
 					op: "mock-op-3"
-				}]
+				}]);
+		});
+	});
 
 
-		describe "with invalid user ids", ->
-			beforeEach (done) ->
-				@updates = [{
-					meta:
+		return describe("with invalid user ids", function() {
+			beforeEach(function(done) {
+				this.updates = [{
+					meta: {
 						user_id: null
+					},
 					op: "mock-op-1"
 				}, {
-					meta:
+					meta: {
 						user_id: "anonymous-user"
+					},
 					op: "mock-op-2"
-				}]
-				@WebApiManager.getUserInfo = (user_id, callback = (error, userInfo) ->) =>
-					callback null, @user_info[user_id]
-				sinon.spy @WebApiManager, "getUserInfo"
-
-				@UpdatesManager.fillUserInfo @updates, (error, @results) =>
-					done()
-
-			it "should not call getUserInfo", ->
-				@WebApiManager.getUserInfo.called.should.equal false
-
-			it "should return the updates without the user info filled", ->
-				expect(@results).to.deep.equal [{
-					meta: {}
+				}];
+				this.WebApiManager.getUserInfo = (user_id, callback) => {
+					if (callback == null) { callback = function(error, userInfo) {}; }
+					return callback(null, this.user_info[user_id]);
+				};
+				sinon.spy(this.WebApiManager, "getUserInfo");
+
+				return this.UpdatesManager.fillUserInfo(this.updates, (error, results) => {
+					this.results = results;
+					return done();
+				});
+			});
+
+			it("should not call getUserInfo", function() {
+				return this.WebApiManager.getUserInfo.called.should.equal(false);
+			});
+
+			return it("should return the updates without the user info filled", function() {
+				return expect(this.results).to.deep.equal([{
+					meta: {},
 					op: "mock-op-1"
 				}, {
-					meta: {}
+					meta: {},
 					op: "mock-op-2"
-				}]
-
-	describe "_summarizeUpdates", ->
-		beforeEach ->
-			@now = Date.now()
-			@user_1 = { id: "mock-user-1" }
-			@user_2 = { id: "mock-user-2" }
-
-		it "should concat updates that are close in time", ->
-			result = @UpdatesManager._summarizeUpdates [{
-				doc_id: "doc-id-1"
-				meta:
-					user_id: @user_1.id
-					start_ts: @now + 20
-					end_ts:   @now + 30
+				}]);
+		});
+	});
+});
+
+	return describe("_summarizeUpdates", function() {
+		beforeEach(function() {
+			this.now = Date.now();
+			this.user_1 = { id: "mock-user-1" };
+			return this.user_2 = { id: "mock-user-2" };});
+
+		it("should concat updates that are close in time", function() {
+			const result = this.UpdatesManager._summarizeUpdates([{
+				doc_id: "doc-id-1",
+				meta: {
+					user_id: this.user_1.id,
+					start_ts: this.now + 20,
+					end_ts:   this.now + 30
+				},
 				v: 5
 			}, {
-				doc_id: "doc-id-1"
-				meta:
-					user_id: @user_2.id
-					start_ts: @now
-					end_ts:   @now + 10
+				doc_id: "doc-id-1",
+				meta: {
+					user_id: this.user_2.id,
+					start_ts: this.now,
+					end_ts:   this.now + 10
+				},
 				v: 4
-			}]
+			}]);
 
-			expect(result).to.deep.equal [{
-				docs:
-					"doc-id-1":
-						fromV: 4
+			return expect(result).to.deep.equal([{
+				docs: {
+					"doc-id-1": {
+						fromV: 4,
 						toV: 5
-				meta:
-					user_ids: [@user_1.id, @user_2.id]
-					start_ts: @now
-					end_ts:   @now + 30
-			}]
-
-		it "should leave updates that are far apart in time", ->
-			oneDay = 1000 * 60 * 60 * 24
-			result = @UpdatesManager._summarizeUpdates [{
-				doc_id: "doc-id-1"
-				meta:
-					user_id: @user_2.id
-					start_ts: @now + oneDay
-					end_ts:   @now + oneDay + 10
+					}
+				},
+				meta: {
+					user_ids: [this.user_1.id, this.user_2.id],
+					start_ts: this.now,
+					end_ts:   this.now + 30
+				}
+			}]);
+	});
+
+		it("should leave updates that are far apart in time", function() {
+			const oneDay = 1000 * 60 * 60 * 24;
+			const result = this.UpdatesManager._summarizeUpdates([{
+				doc_id: "doc-id-1",
+				meta: {
+					user_id: this.user_2.id,
+					start_ts: this.now + oneDay,
+					end_ts:   this.now + oneDay + 10
+				},
 				v: 5
 			}, {
-				doc_id: "doc-id-1"
-				meta:
-					user_id: @user_1.id
-					start_ts: @now
-					end_ts:   @now + 10
+				doc_id: "doc-id-1",
+				meta: {
+					user_id: this.user_1.id,
+					start_ts: this.now,
+					end_ts:   this.now + 10
+				},
 				v: 4
-			}]
-			expect(result).to.deep.equal [{
-				docs:
-					"doc-id-1":
-						fromV: 5
+			}]);
+			return expect(result).to.deep.equal([{
+				docs: {
+					"doc-id-1": {
+						fromV: 5,
 						toV: 5
-				meta:
-					user_ids: [@user_2.id]
-					start_ts: @now + oneDay
-					end_ts:   @now + oneDay + 10
+					}
+				},
+				meta: {
+					user_ids: [this.user_2.id],
+					start_ts: this.now + oneDay,
+					end_ts:   this.now + oneDay + 10
+				}
 			}, {
-				docs:
-					"doc-id-1":
-						fromV: 4
+				docs: {
+					"doc-id-1": {
+						fromV: 4,
 						toV: 4
-				meta:
-					user_ids: [@user_1.id]
-					start_ts: @now
-					end_ts:   @now + 10
-			}]
-
-		it "should concat onto existing summarized updates", ->
-			result = @UpdatesManager._summarizeUpdates [{
-				doc_id: "doc-id-2"
-				meta:
-					user_id: @user_1.id
-					start_ts: @now + 20
-					end_ts:   @now + 30
+					}
+				},
+				meta: {
+					user_ids: [this.user_1.id],
+					start_ts: this.now,
+					end_ts:   this.now + 10
+				}
+			}]);
+	});
+
+		it("should concat onto existing summarized updates", function() {
+			const result = this.UpdatesManager._summarizeUpdates([{
+				doc_id: "doc-id-2",
+				meta: {
+					user_id: this.user_1.id,
+					start_ts: this.now + 20,
+					end_ts:   this.now + 30
+				},
 				v: 5
 			}, {
-				doc_id: "doc-id-2"
-				meta:
-					user_id: @user_2.id
-					start_ts: @now
-					end_ts:   @now + 10
+				doc_id: "doc-id-2",
+				meta: {
+					user_id: this.user_2.id,
+					start_ts: this.now,
+					end_ts:   this.now + 10
+				},
 				v: 4
 			}], [{
-				docs: 
-					"doc-id-1": 
-						fromV: 6
-						toV: 8
-				meta:
-					user_ids: [@user_1.id]
-					start_ts: @now + 40
-					end_ts:   @now + 50
-			}]
-			expect(result).to.deep.equal [{
-				docs:
-					"doc-id-1":
+				docs: { 
+					"doc-id-1": { 
+						fromV: 6,
 						toV: 8
+					}
+				},
+				meta: {
+					user_ids: [this.user_1.id],
+					start_ts: this.now + 40,
+					end_ts:   this.now + 50
+				}
+			}]);
+			return expect(result).to.deep.equal([{
+				docs: {
+					"doc-id-1": {
+						toV: 8,
 						fromV: 6
-					"doc-id-2":
-						toV: 5
+					},
+					"doc-id-2": {
+						toV: 5,
 						fromV: 4
-				meta:
-					user_ids: [@user_1.id, @user_2.id]
-					start_ts: @now
-					end_ts:   @now + 50
-			}]
-
-		it "should include null user values", ->
-			result = @UpdatesManager._summarizeUpdates [{
-				doc_id: "doc-id-1"
-				meta:
-					user_id: @user_1.id
-					start_ts: @now + 20
-					end_ts:   @now + 30
+					}
+				},
+				meta: {
+					user_ids: [this.user_1.id, this.user_2.id],
+					start_ts: this.now,
+					end_ts:   this.now + 50
+				}
+			}]);
+	});
+
+		it("should include null user values", function() {
+			const result = this.UpdatesManager._summarizeUpdates([{
+				doc_id: "doc-id-1",
+				meta: {
+					user_id: this.user_1.id,
+					start_ts: this.now + 20,
+					end_ts:   this.now + 30
+				},
 				v: 5
 			}, {
-				doc_id: "doc-id-1"
-				meta:
-					user_id: null
-					start_ts: @now
-					end_ts:   @now + 10
+				doc_id: "doc-id-1",
+				meta: {
+					user_id: null,
+					start_ts: this.now,
+					end_ts:   this.now + 10
+				},
 				v: 4
-			}]
-			expect(result).to.deep.equal [{
-				docs:
-					"doc-id-1":
-						fromV: 4
+			}]);
+			return expect(result).to.deep.equal([{
+				docs: {
+					"doc-id-1": {
+						fromV: 4,
 						toV: 5
-				meta:
-					user_ids: [@user_1.id, null]
-					start_ts: @now
-					end_ts:   @now + 30
-			}]
-
-		it "should include null user values, when the null is earlier in the updates list", ->
-			result = @UpdatesManager._summarizeUpdates [{
-				doc_id: "doc-id-1"
-				meta:
-					user_id: null
-					start_ts: @now
-					end_ts:   @now + 10
+					}
+				},
+				meta: {
+					user_ids: [this.user_1.id, null],
+					start_ts: this.now,
+					end_ts:   this.now + 30
+				}
+			}]);
+	});
+
+		it("should include null user values, when the null is earlier in the updates list", function() {
+			const result = this.UpdatesManager._summarizeUpdates([{
+				doc_id: "doc-id-1",
+				meta: {
+					user_id: null,
+					start_ts: this.now,
+					end_ts:   this.now + 10
+				},
 				v: 4
 			}, {
-				doc_id: "doc-id-1"
-				meta:
-					user_id: @user_1.id
-					start_ts: @now + 20
-					end_ts:   @now + 30
+				doc_id: "doc-id-1",
+				meta: {
+					user_id: this.user_1.id,
+					start_ts: this.now + 20,
+					end_ts:   this.now + 30
+				},
 				v: 5
-			}]
-			expect(result).to.deep.equal [{
-				docs:
-					"doc-id-1":
-						fromV: 4
+			}]);
+			return expect(result).to.deep.equal([{
+				docs: {
+					"doc-id-1": {
+						fromV: 4,
 						toV: 5
-				meta:
-					user_ids: [null, @user_1.id]
-					start_ts: @now
-					end_ts:   @now + 30
-			}]
-
-		it "should roll several null user values into one", ->
-			result = @UpdatesManager._summarizeUpdates [{
-				doc_id: "doc-id-1"
-				meta:
-					user_id: @user_1.id
-					start_ts: @now + 20
-					end_ts:   @now + 30
+					}
+				},
+				meta: {
+					user_ids: [null, this.user_1.id],
+					start_ts: this.now,
+					end_ts:   this.now + 30
+				}
+			}]);
+	});
+
+		it("should roll several null user values into one", function() {
+			const result = this.UpdatesManager._summarizeUpdates([{
+				doc_id: "doc-id-1",
+				meta: {
+					user_id: this.user_1.id,
+					start_ts: this.now + 20,
+					end_ts:   this.now + 30
+				},
 				v: 5
 			}, {
-				doc_id: "doc-id-1"
-				meta:
-					user_id: null
-					start_ts: @now
-					end_ts:   @now + 10
+				doc_id: "doc-id-1",
+				meta: {
+					user_id: null,
+					start_ts: this.now,
+					end_ts:   this.now + 10
+				},
 				v: 4
 			}, {
-				doc_id: "doc-id-1"
-				meta:
-					user_id: null
-					start_ts: @now + 2
-					end_ts:   @now + 4
+				doc_id: "doc-id-1",
+				meta: {
+					user_id: null,
+					start_ts: this.now + 2,
+					end_ts:   this.now + 4
+				},
 				v: 4
-			}]
-			expect(result).to.deep.equal [{
-				docs:
-					"doc-id-1":
-						fromV: 4
+			}]);
+			return expect(result).to.deep.equal([{
+				docs: {
+					"doc-id-1": {
+						fromV: 4,
 						toV: 5
-				meta:
-					user_ids: [@user_1.id, null]
-					start_ts: @now
-					end_ts:   @now + 30
-			}]
-
-		it "should split updates before a big delete", ->
-			result = @UpdatesManager._summarizeUpdates [{
-				doc_id: "doc-id-1"
-				op: [{ d: "this is a long long long long long delete", p: 34 }]
-				meta:
-					user_id: @user_1.id
-					start_ts: @now + 20
-					end_ts:   @now + 30
+					}
+				},
+				meta: {
+					user_ids: [this.user_1.id, null],
+					start_ts: this.now,
+					end_ts:   this.now + 30
+				}
+			}]);
+	});
+
+		return it("should split updates before a big delete", function() {
+			const result = this.UpdatesManager._summarizeUpdates([{
+				doc_id: "doc-id-1",
+				op: [{ d: "this is a long long long long long delete", p: 34 }],
+				meta: {
+					user_id: this.user_1.id,
+					start_ts: this.now + 20,
+					end_ts:   this.now + 30
+				},
 				v: 5
 			}, {
-				doc_id: "doc-id-1"
-				meta:
-					user_id: @user_2.id
-					start_ts: @now
-					end_ts:   @now + 10
+				doc_id: "doc-id-1",
+				meta: {
+					user_id: this.user_2.id,
+					start_ts: this.now,
+					end_ts:   this.now + 10
+				},
 				v: 4
-			}]
+			}]);
 
-			expect(result).to.deep.equal [{
-				docs:
-					"doc-id-1":
-						fromV: 5
+			return expect(result).to.deep.equal([{
+				docs: {
+					"doc-id-1": {
+						fromV: 5,
 						toV: 5
-				meta:
-					user_ids: [@user_1.id]
-					start_ts: @now + 20
-					end_ts:   @now + 30
+					}
+				},
+				meta: {
+					user_ids: [this.user_1.id],
+					start_ts: this.now + 20,
+					end_ts:   this.now + 30
+				}
 			}, {
-				docs:
-					"doc-id-1":
-						fromV: 4
+				docs: {
+					"doc-id-1": {
+						fromV: 4,
 						toV: 4
-				meta:
-					user_ids: [@user_2.id]
-					start_ts: @now
-					end_ts:   @now + 10
-			}]
+					}
+				},
+				meta: {
+					user_ids: [this.user_2.id],
+					start_ts: this.now,
+					end_ts:   this.now + 10
+				}
+			}]);
+	});
+});
+});

+ 153 - 112
services/track-changes/test/unit/coffee/WebApiManager/WebApiManagerTests.js

@@ -1,123 +1,164 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-expect = chai.expect
-modulePath = "../../../../app/js/WebApiManager.js"
-SandboxedModule = require('sandboxed-module')
-
-describe "WebApiManager", ->
-	beforeEach ->
-		@WebApiManager = SandboxedModule.require modulePath, requires:
-			"requestretry": @request = {}
-			"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
-			'settings-sharelatex': @settings =
-				apis:
-					web:
-						url: "http://example.com"
-						user: "sharelatex"
+/*
+ * 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/WebApiManager.js";
+const SandboxedModule = require('sandboxed-module');
+
+describe("WebApiManager", function() {
+	beforeEach(function() {
+		this.WebApiManager = SandboxedModule.require(modulePath, { requires: {
+			"requestretry": (this.request = {}),
+			"logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }),
+			'settings-sharelatex': (this.settings = {
+				apis: {
+					web: {
+						url: "http://example.com",
+						user: "sharelatex",
 						pass: "password"
-		@callback = sinon.stub()
-		@user_id = "mock-user-id"
-		@project_id = "mock-project-id"
-		@user_info =
-			email: "leo@sharelatex.com"
-			id: @user_id
-			first_name: "Leo"
-			last_nane: "Lion"
+					}
+				}
+			})
+		}
+	}
+		);
+		this.callback = sinon.stub();
+		this.user_id = "mock-user-id";
+		this.project_id = "mock-project-id";
+		this.user_info = {
+			email: "leo@sharelatex.com",
+			id: this.user_id,
+			first_name: "Leo",
+			last_nane: "Lion",
 			extra_param: "blah"
-		@project =
-			features: "mock-features"
-
-	describe "getUserInfo", ->
-		describe "successfully", ->
-			beforeEach ->
-				@body = JSON.stringify @user_info
-				@request.get = sinon.stub().callsArgWith(1, null, {statusCode: 200}, @body)
-				@WebApiManager.getUserInfo @user_id, @callback
-
-			it 'should get the user from the web api', ->
-				@request.get
+		};
+		return this.project =
+			{features: "mock-features"};
+	});
+
+	describe("getUserInfo", function() {
+		describe("successfully", function() {
+			beforeEach(function() {
+				this.body = JSON.stringify(this.user_info);
+				this.request.get = sinon.stub().callsArgWith(1, null, {statusCode: 200}, this.body);
+				return this.WebApiManager.getUserInfo(this.user_id, this.callback);
+			});
+
+			it('should get the user from the web api', function() {
+				return this.request.get
 					.calledWithMatch({
-						url: "#{@settings.apis.web.url}/user/#{@user_id}/personal_info"
-						auth:
-							user: @settings.apis.web.user
-							pass: @settings.apis.web.pass
+						url: `${this.settings.apis.web.url}/user/${this.user_id}/personal_info`,
+						auth: {
+							user: this.settings.apis.web.user,
+							pass: this.settings.apis.web.pass,
 							sendImmediately: true
+						}
 					})
-					.should.equal true
-
-			it "should call the callback with only the email, id and names", ->
-				@callback.calledWith(null, {
-					id:         @user_id
-					email:      @user_info.email
-					first_name: @user_info.first_name
-					last_name:  @user_info.last_name
-				}).should.equal true
-
-		describe "when the web API returns an error", ->
-			beforeEach ->
-				@request.get = sinon.stub().callsArgWith(1, @error = new Error("something went wrong"), null, null)
-				@WebApiManager.getUserInfo @user_id, @callback
-
-			it "should return an error to the callback", ->
-				@callback.calledWith(@error).should.equal true
-
-		describe "when the web returns a failure error code", ->
-			beforeEach ->
-				@request.get = sinon.stub().callsArgWith(1, null, { statusCode: 500, attempts: 42}, "")
-				@WebApiManager.getUserInfo @user_id, @callback
-
-			it "should return the callback with an error", ->
-				@callback
+					.should.equal(true);
+			});
+
+			return it("should call the callback with only the email, id and names", function() {
+				return this.callback.calledWith(null, {
+					id:         this.user_id,
+					email:      this.user_info.email,
+					first_name: this.user_info.first_name,
+					last_name:  this.user_info.last_name
+				}).should.equal(true);
+			});
+		});
+
+		describe("when the web API returns an error", function() {
+			beforeEach(function() {
+				this.request.get = sinon.stub().callsArgWith(1, (this.error = new Error("something went wrong")), null, null);
+				return this.WebApiManager.getUserInfo(this.user_id, this.callback);
+			});
+
+			return it("should return an error to the callback", function() {
+				return this.callback.calledWith(this.error).should.equal(true);
+			});
+		});
+
+		describe("when the web returns a failure error code", function() {
+			beforeEach(function() {
+				this.request.get = sinon.stub().callsArgWith(1, null, { statusCode: 500, attempts: 42}, "");
+				return this.WebApiManager.getUserInfo(this.user_id, this.callback);
+			});
+
+			return it("should return the callback with an error", function() {
+				return this.callback
 					.calledWith(sinon.match.has('message', "web returned a non-success status code: 500 (attempts: 42)"))
-					.should.equal true
-
-		describe "when the user cannot be found", ->
-			beforeEach ->
-				@request.get = sinon.stub().callsArgWith(1, null, {statusCode: 404}, "nothing")
-				@WebApiManager.getUserInfo @user_id, @callback
-
-			it "should return a null value", ->
-				@callback
+					.should.equal(true);
+			});
+		});
+
+		return describe("when the user cannot be found", function() {
+			beforeEach(function() {
+				this.request.get = sinon.stub().callsArgWith(1, null, {statusCode: 404}, "nothing");
+				return this.WebApiManager.getUserInfo(this.user_id, this.callback);
+			});
+
+			return it("should return a null value", function() {
+				return this.callback
 					.calledWith(null, null)
-					.should.equal true
-
-
-	describe "getProjectDetails", ->
-		describe "successfully", ->
-			beforeEach ->
-				@body = JSON.stringify @project
-				@request.get = sinon.stub().callsArgWith(1, null, {statusCode: 200}, @body)
-				@WebApiManager.getProjectDetails @project_id, @callback
-
-			it 'should get the project from the web api', ->
-				@request.get
+					.should.equal(true);
+			});
+		});
+	});
+
+
+	return describe("getProjectDetails", function() {
+		describe("successfully", function() {
+			beforeEach(function() {
+				this.body = JSON.stringify(this.project);
+				this.request.get = sinon.stub().callsArgWith(1, null, {statusCode: 200}, this.body);
+				return this.WebApiManager.getProjectDetails(this.project_id, this.callback);
+			});
+
+			it('should get the project from the web api', function() {
+				return this.request.get
 					.calledWithMatch({
-						url: "#{@settings.apis.web.url}/project/#{@project_id}/details"
-						auth:
-							user: @settings.apis.web.user
-							pass: @settings.apis.web.pass
+						url: `${this.settings.apis.web.url}/project/${this.project_id}/details`,
+						auth: {
+							user: this.settings.apis.web.user,
+							pass: this.settings.apis.web.pass,
 							sendImmediately: true
+						}
 					})
-					.should.equal true
-
-			it "should call the callback with the project", ->
-				@callback.calledWith(null, @project).should.equal true
-
-		describe "when the web API returns an error", ->
-			beforeEach ->
-				@request.get = sinon.stub().callsArgWith(1, @error = new Error("something went wrong"), null, null)
-				@WebApiManager.getProjectDetails @project_id, @callback
-
-			it "should return an error to the callback", ->
-				@callback.calledWith(@error).should.equal true
-
-		describe "when the web returns a failure error code", ->
-			beforeEach ->
-				@request.get = sinon.stub().callsArgWith(1, null, { statusCode: 500, attempts: 42 }, "")
-				@WebApiManager.getProjectDetails @project_id, @callback
-
-			it "should return the callback with an error", ->
-				@callback
+					.should.equal(true);
+			});
+
+			return it("should call the callback with the project", function() {
+				return this.callback.calledWith(null, this.project).should.equal(true);
+			});
+		});
+
+		describe("when the web API returns an error", function() {
+			beforeEach(function() {
+				this.request.get = sinon.stub().callsArgWith(1, (this.error = new Error("something went wrong")), null, null);
+				return this.WebApiManager.getProjectDetails(this.project_id, this.callback);
+			});
+
+			return it("should return an error to the callback", function() {
+				return this.callback.calledWith(this.error).should.equal(true);
+			});
+		});
+
+		return describe("when the web returns a failure error code", function() {
+			beforeEach(function() {
+				this.request.get = sinon.stub().callsArgWith(1, null, { statusCode: 500, attempts: 42 }, "");
+				return this.WebApiManager.getProjectDetails(this.project_id, this.callback);
+			});
+
+			return it("should return the callback with an error", function() {
+				return this.callback
 					.calledWith(sinon.match.has('message', "web returned a non-success status code: 500 (attempts: 42)"))
-					.should.equal true
+					.should.equal(true);
+			});
+		});
+	});
+});