Browse Source

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

decaffeinate 6 years ago
parent
commit
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() }
 			"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: 3, i: "bbb" }] },
 				{ op: [{ p: 6, i: "ccc" }] }
 				{ 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" }
 				{ 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([{
 				.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)
 					.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" } ],
 					[ { 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" }
 					{ 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" } ],
 					[ { 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" }
 					{ 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" } ],
 					[ { 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" }
 						{ 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" }
 					{ 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" } ],
 						[ { 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" }
 						{ 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" } ],
 						[ { 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" }
 						{ 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" } ],
 						[ { 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" } ],
 						[ { 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" }
 						{ 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" }
 						{ 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" }
 						{ 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" } ],
 							[ { 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" } ],
 							[ { 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" } ],
 							[ { 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
 					.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
 					.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
 					.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"
 						key: "s3-key"
-					stores:
+					},
+					stores: {
 						doc_history: "s3-bucket"
 						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"))
 					.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({
 					.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
 							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"))
 					.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:{}
 				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)
 					.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)
 				.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({
 				.calledWith({
-					doc_id: ObjectId(@doc_id)
+					doc_id: ObjectId(this.doc_id),
 					project_id: { $exists: false }
 					project_id: { $exists: false }
 				}, {
 				}, {
-					$set: { project_id: ObjectId(@project_id) }
+					$set: { project_id: ObjectId(this.project_id) }
 				}, {
 				}, {
 					multi: true
 					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({
 				.calledWith({
-					project_id: ObjectId(@project_id)
+					project_id: ObjectId(this.project_id)
 				}, {
 				}, {
-					$set: @metadata
+					$set: this.metadata
 				}, {
 				}, {
 					upsert: true
 					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: [
 				pack: [
 					{ op: "op-1", meta: "meta-1", v: 1},
 					{ op: "op-1", meta: "meta-1", v: 1},
 					{ op: "op-2", meta: "meta-2", v: 2}
 					{ op: "op-2", meta: "meta-2", v: 2}
-				]
-				n : 2
+				],
+				n : 2,
 				sz : 100
 				sz : 100
-			}
-			@newUpdates = [
+			};
+			this.newUpdates = [
 				{ op: "op-3", meta: "meta-3", v: 3},
 				{ op: "op-3", meta: "meta-3", v: 3},
 				{ op: "op-4", meta: "meta-4", v: 4}
 				{ 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)
 				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: [
 					pack: [
 						{ op: "op-1", meta: "meta-1", v: 1},
 						{ op: "op-1", meta: "meta-1", v: 1},
 						{ op: "op-2", meta: "meta-2", v: 2}
 						{ 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())
 					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: [
 					pack: [
 						{ op: "op-1", meta: "meta-1", v: 1},
 						{ op: "op-1", meta: "meta-1", v: 1},
 						{ op: "op-2", meta: "meta-2", v: 2}
 						{ 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())
 					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: [
 					pack: [
 						{ op: "op-1", meta: "meta-1", v: 1},
 						{ op: "op-1", meta: "meta-1", v: 1},
 						{ op: "op-2", meta: "meta-2", v: 2}
 						{ 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")
 			"../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
 				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
 				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
 				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
 				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
 				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
 				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
 				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
 				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
 				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
 				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
 				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
 				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
 				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
 				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
 				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
 				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
 				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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					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
 					.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"))
 						.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'))
 						.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(
 				.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}}}
 				{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"
 					op: "mock-op-1"
 				}, {
 				}, {
-					meta:
-						user_id: @user_id_1
+					meta: {
+						user_id: this.user_id_1
+					},
 					op: "mock-op-2"
 					op: "mock-op-2"
 				}, {
 				}, {
-					meta:
-						user_id: @user_id_2
+					meta: {
+						user_id: this.user_id_2
+					},
 					op: "mock-op-3"
 					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"
 							email: "user1@sharelatex.com"
+						}
+					},
 					op: "mock-op-1"
 					op: "mock-op-1"
 				}, {
 				}, {
-					meta:
-						user:
+					meta: {
+						user: {
 							email: "user1@sharelatex.com"
 							email: "user1@sharelatex.com"
+						}
+					},
 					op: "mock-op-2"
 					op: "mock-op-2"
 				}, {
 				}, {
-					meta:
-						user:
+					meta: {
+						user: {
 							email: "user2@sharelatex.com"
 							email: "user2@sharelatex.com"
+						}
+					},
 					op: "mock-op-3"
 					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
 						user_id: null
+					},
 					op: "mock-op-1"
 					op: "mock-op-1"
 				}, {
 				}, {
-					meta:
+					meta: {
 						user_id: "anonymous-user"
 						user_id: "anonymous-user"
+					},
 					op: "mock-op-2"
 					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"
 					op: "mock-op-1"
 				}, {
 				}, {
-					meta: {}
+					meta: {},
 					op: "mock-op-2"
 					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
 				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
 				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
 						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
 				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
 				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
 						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
 						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
 				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
 				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
 						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
 						fromV: 6
-					"doc-id-2":
-						toV: 5
+					},
+					"doc-id-2": {
+						toV: 5,
 						fromV: 4
 						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
 				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
 				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
 						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
 				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
 				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
 						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
 				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
 				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
 				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
 						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
 				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
 				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
 						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
 						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"
 						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"
 			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({
 					.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
 							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)"))
 					.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)
 					.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({
 					.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
 							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)"))
 					.calledWith(sinon.match.has('message', "web returned a non-success status code: 500 (attempts: 42)"))
-					.should.equal true
+					.should.equal(true);
+			});
+		});
+	});
+});