Răsfoiți Sursa

prettier: convert test/acceptance decaffeinated files to Prettier format

mserranom 6 ani în urmă
părinte
comite
3183aca524

+ 578 - 381
services/track-changes/test/acceptance/js/AppendingUpdatesTests.js

@@ -10,384 +10,581 @@
  * DS207: Consider shorter variations of null checks
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-const sinon = require("sinon");
-const chai = require("chai");
-chai.should();
-const { expect } = chai;
-const mongojs = require("../../../app/js/mongojs");
-const { ObjectId } = mongojs;
-const Settings = require("settings-sharelatex");
-const request = require("request");
-const rclient = require("redis").createClient(Settings.redis.history); // Only works locally for now
-
-const TrackChangesApp = require("./helpers/TrackChangesApp");
-const TrackChangesClient = require("./helpers/TrackChangesClient");
-const MockWebApi = require("./helpers/MockWebApi");
-
-describe("Appending doc ops to the history", function() {
-	before(function(done) { return TrackChangesApp.ensureRunning(done); });
-
-	describe("when the history does not exist yet", function() {
-		before(function(done) {
-			this.project_id = ObjectId().toString();
-			this.doc_id = ObjectId().toString();
-			this.user_id = ObjectId().toString();
-			MockWebApi.projects[this.project_id] = {features: {versioning: false}};
-			TrackChangesClient.pushRawUpdates(this.project_id, this.doc_id, [{
-				op: [{ i: "f", p: 3 }],
-				meta: { ts: Date.now(), user_id: this.user_id },
-				v: 3
-			}, {
-				op: [{ i: "o", p: 4 }],
-				meta: { ts: Date.now(), user_id: this.user_id },
-				v: 4
-			}, {
-				op: [{ i: "o", p: 5 }],
-				meta: { ts: Date.now(), user_id: this.user_id },
-				v: 5
-			}], error => {
-				if (error != null) { throw error; }
-				return TrackChangesClient.flushAndGetCompressedUpdates(this.project_id, this.doc_id, (error, updates) => {
-					this.updates = updates;
-					if (error != null) { throw error; }
-					return done();
-				});
-			});
-			return null;
-		});
-
-		it("should insert the compressed op into mongo", function() {
-			return expect(this.updates[0].pack[0].op).to.deep.equal([{
-				p: 3, i: "foo"
-			}]);
-	});
-
-		it("should insert the correct version number into mongo", function() {
-			return expect(this.updates[0].v).to.equal(5);
-		});
-
-		it("should store the doc id", function() {
-			return expect(this.updates[0].doc_id.toString()).to.equal(this.doc_id);
-		});
-
-		it("should store the project id", function() {
-			return expect(this.updates[0].project_id.toString()).to.equal(this.project_id);
-		});
-
-		return it("should clear the doc from the DocsWithHistoryOps set", function(done) {
-			rclient.sismember(`DocsWithHistoryOps:${this.project_id}`, this.doc_id, (error, member) => {
-				member.should.equal(0);
-				return done();
-			});
-			return null;
-		});
-	});
-
-	describe("when the history has already been started", function() {
-		beforeEach(function(done) {
-			this.project_id = ObjectId().toString();
-			this.doc_id = ObjectId().toString();
-			this.user_id = ObjectId().toString();
-			MockWebApi.projects[this.project_id] = {features: {versioning: false}};
-			TrackChangesClient.pushRawUpdates(this.project_id, this.doc_id, [{
-				op: [{ i: "f", p: 3 }],
-				meta: { ts: Date.now(), user_id: this.user_id },
-				v: 3
-			}, {
-				op: [{ i: "o", p: 4 }],
-				meta: { ts: Date.now(), user_id: this.user_id },
-				v: 4
-			}, {
-				op: [{ i: "o", p: 5 }],
-				meta: { ts: Date.now(), user_id: this.user_id },
-				v: 5
-			}], error => {
-				if (error != null) { throw error; }
-				return TrackChangesClient.flushAndGetCompressedUpdates(this.project_id, this.doc_id, (error, updates) => {
-					if (error != null) { throw error; }
-					return done();
-				});
-			});
-			return null;
-		});
-
-		describe("when the updates are recent and from the same user", function() {
-			beforeEach(function(done) {
-				TrackChangesClient.pushRawUpdates(this.project_id, this.doc_id, [{
-					op: [{ i: "b", p: 6 }],
-					meta: { ts: Date.now(), user_id: this.user_id },
-					v: 6
-				}, {
-					op: [{ i: "a", p: 7 }],
-					meta: { ts: Date.now(), user_id: this.user_id },
-					v: 7
-				}, {
-					op: [{ i: "r", p: 8 }],
-					meta: { ts: Date.now(), user_id: this.user_id },
-					v: 8
-				}], error => {
-					if (error != null) { throw error; }
-					return TrackChangesClient.flushAndGetCompressedUpdates(this.project_id, this.doc_id, (error, updates) => {
-						this.updates = updates;
-						if (error != null) { throw error; }
-						return done();
-					});
-				});
-				return null;
-			});
-
-			it("should combine all the updates into one pack", function() {
-				return expect(this.updates[0].pack[1].op).to.deep.equal([{
-					p: 6, i: "bar"
-				}]);
-		});
-
-			return it("should insert the correct version number into mongo", function() {
-				return expect(this.updates[0].v_end).to.equal(8);
-			});
-		});
-
-
-		return describe("when the updates are far apart", function() {
-			beforeEach(function(done) {
-				const oneDay = 24 * 60 * 60 * 1000;
-				TrackChangesClient.pushRawUpdates(this.project_id, this.doc_id, [{
-					op: [{ i: "b", p: 6 }],
-					meta: { ts: Date.now() + oneDay, user_id: this.user_id },
-					v: 6
-				}, {
-					op: [{ i: "a", p: 7 }],
-					meta: { ts: Date.now() + oneDay, user_id: this.user_id },
-					v: 7
-				}, {
-					op: [{ i: "r", p: 8 }],
-					meta: { ts: Date.now() + oneDay, user_id: this.user_id },
-					v: 8
-				}], error => {
-					if (error != null) { throw error; }
-					return TrackChangesClient.flushAndGetCompressedUpdates(this.project_id, this.doc_id, (error, updates) => {
-						this.updates = updates;
-						if (error != null) { throw error; }
-						return done();
-					});
-				});
-				return null;
-			});
-
-			return it("should combine the updates into one pack", function() {
-				expect(this.updates[0].pack[0].op).to.deep.equal([{
-					p: 3, i: "foo"
-				}]);
-				return expect(this.updates[0].pack[1].op).to.deep.equal([{
-					p: 6, i: "bar"
-				}]);
-		});
-	});
-});
-
-	describe("when the updates need processing in batches", function() {
-		before(function(done) {
-			this.project_id = ObjectId().toString();
-			this.doc_id = ObjectId().toString();
-			this.user_id = ObjectId().toString();
-			MockWebApi.projects[this.project_id] = {features: {versioning: false}};
-			const updates = [];
-			this.expectedOp = [{ p:0, i: "" }];
-			for (let i = 0; i <= 250; i++) {
-				updates.push({
-					op: [{i: "a", p: 0}],
-					meta: { ts: Date.now(), user_id: this.user_id },
-					v: i
-				});
-				this.expectedOp[0].i = `a${this.expectedOp[0].i}`;
-			}
-
-			TrackChangesClient.pushRawUpdates(this.project_id, this.doc_id, updates, error => {
-				if (error != null) { throw error; }
-				return TrackChangesClient.flushAndGetCompressedUpdates(this.project_id, this.doc_id, (error, updates1) => {
-					this.updates = updates1;
-					if (error != null) { throw error; }
-					return done();
-				});
-			});
-			return null;
-		});
-
-		it("should concat the compressed op into mongo", function() {
-			return expect(this.updates[0].pack.length).to.deep.equal(3);
-		});  // batch size is 100
-
-		return it("should insert the correct version number into mongo", function() {
-			return expect(this.updates[0].v_end).to.equal(250);
-		});
-	});
-
-
-	describe("when there are multiple ops in each update", function() {
-		before(function(done) {
-			this.project_id = ObjectId().toString();
-			this.doc_id = ObjectId().toString();
-			this.user_id = ObjectId().toString();
-			MockWebApi.projects[this.project_id] = {features: {versioning: false}};
-			const oneDay = 24 * 60 * 60 * 1000;
-			TrackChangesClient.pushRawUpdates(this.project_id, this.doc_id, [{
-				op: [{ i: "f", p: 3 }, { i: "o", p: 4 }, { i: "o", p: 5 }],
-				meta: { ts: Date.now(), user_id: this.user_id },
-				v: 3
-			}, {
-				op: [{ i: "b", p: 6 }, { i: "a", p: 7 }, { i: "r", p: 8 }],
-				meta: { ts: Date.now() + oneDay, user_id: this.user_id },
-				v: 4
-			}], error => {
-				if (error != null) { throw error; }
-				return TrackChangesClient.flushAndGetCompressedUpdates(this.project_id, this.doc_id, (error, updates) => {
-					this.updates = updates;
-					if (error != null) { throw error; }
-					return done();
-				});
-			});
-			return null;
-		});
-
-		it("should insert the compressed ops into mongo", function() {
-			expect(this.updates[0].pack[0].op).to.deep.equal([{
-				p: 3, i: "foo"
-			}]);
-			return expect(this.updates[0].pack[1].op).to.deep.equal([{
-				p: 6, i: "bar"
-			}]);
-	});
-
-		return it("should insert the correct version numbers into mongo", function() {
-			expect(this.updates[0].pack[0].v).to.equal(3);
-			return expect(this.updates[0].pack[1].v).to.equal(4);
-		});
-	});
-
-	describe("when there is a no-op update", function() {
-		before(function(done) {
-			this.project_id = ObjectId().toString();
-			this.doc_id = ObjectId().toString();
-			this.user_id = ObjectId().toString();
-			MockWebApi.projects[this.project_id] = {features: {versioning: false}};
-			const oneDay = 24 * 60 * 60 * 1000;
-			TrackChangesClient.pushRawUpdates(this.project_id, this.doc_id, [{
-				op: [],
-				meta: { ts: Date.now(), user_id: this.user_id },
-				v: 3
-			}, {
-				op: [{ i: "foo", p: 3 }],
-				meta: { ts: Date.now() + oneDay, user_id: this.user_id },
-				v: 4
-			}], error => {
-				if (error != null) { throw error; }
-				return TrackChangesClient.flushAndGetCompressedUpdates(this.project_id, this.doc_id, (error, updates) => {
-					this.updates = updates;
-					if (error != null) { throw error; }
-					return done();
-				});
-			});
-			return null;
-		});
-
-		it("should insert the compressed no-op into mongo", function() {
-			return expect(this.updates[0].pack[0].op).to.deep.equal([]);
-	});
-
-
-		it("should insert the compressed next update into mongo", function() {
-			return expect(this.updates[0].pack[1].op).to.deep.equal([{
-				p: 3, i: "foo"
-			}]);
-	});
-
-		return it("should insert the correct version numbers into mongo", function() {
-			expect(this.updates[0].pack[0].v).to.equal(3);
-			return expect(this.updates[0].pack[1].v).to.equal(4);
-		});
-	});
-
-	describe("when there is a comment update", function() {
-		before(function(done) {
-			this.project_id = ObjectId().toString();
-			this.doc_id = ObjectId().toString();
-			this.user_id = ObjectId().toString();
-			MockWebApi.projects[this.project_id] = {features: {versioning: false}};
-			TrackChangesClient.pushRawUpdates(this.project_id, this.doc_id, [{
-				op: [{ c: "foo", p: 3 }, {d: "bar", p: 6}],
-				meta: { ts: Date.now(), user_id: this.user_id },
-				v: 3
-			}], error => {
-				if (error != null) { throw error; }
-				return TrackChangesClient.flushAndGetCompressedUpdates(this.project_id, this.doc_id, (error, updates) => {
-					this.updates = updates;
-					if (error != null) { throw error; }
-					return done();
-				});
-			});
-			return null;
-		});
-
-		it("should ignore the comment op", function() {
-			return expect(this.updates[0].pack[0].op).to.deep.equal([{d: "bar", p: 6}]);
-	});
-
-		return it("should insert the correct version numbers into mongo", function() {
-			return expect(this.updates[0].pack[0].v).to.equal(3);
-		});
-	});
-
-	describe("when the project has versioning enabled", function() {
-		before(function(done) {
-			this.project_id = ObjectId().toString();
-			this.doc_id = ObjectId().toString();
-			this.user_id = ObjectId().toString();
-			MockWebApi.projects[this.project_id] = {features: {versioning: true}};
-
-			TrackChangesClient.pushRawUpdates(this.project_id, this.doc_id, [{
-				op: [{ i: "f", p: 3 }],
-				meta: { ts: Date.now(), user_id: this.user_id },
-				v: 3
-			}], error => {
-				if (error != null) { throw error; }
-				return TrackChangesClient.flushAndGetCompressedUpdates(this.project_id, this.doc_id, (error, updates) => {
-					this.updates = updates;
-					if (error != null) { throw error; }
-					return done();
-				});
-			});
-			return null;
-		});
-
-		return it("should not add a expiresAt entry in the update in mongo", function() {
-			return expect(this.updates[0].expiresAt).to.be.undefined;
-		});
-	});
-
-	return describe("when the project does not have versioning enabled", function() {
-		before(function(done) {
-			this.project_id = ObjectId().toString();
-			this.doc_id = ObjectId().toString();
-			this.user_id = ObjectId().toString();
-			MockWebApi.projects[this.project_id] = {features: {versioning: false}};
-
-			TrackChangesClient.pushRawUpdates(this.project_id, this.doc_id, [{
-				op: [{ i: "f", p: 3 }],
-				meta: { ts: Date.now(), user_id: this.user_id },
-				v: 3
-			}], error => {
-				if (error != null) { throw error; }
-				return TrackChangesClient.flushAndGetCompressedUpdates(this.project_id, this.doc_id, (error, updates) => {
-					this.updates = updates;
-					if (error != null) { throw error; }
-					return done();
-				});
-			});
-			return null;
-		});
-
-		return it("should add a expiresAt entry in the update in mongo", function() {
-			return expect(this.updates[0].expiresAt).to.exist;
-		});
-	});
-});
+const sinon = require('sinon')
+const chai = require('chai')
+chai.should()
+const { expect } = chai
+const mongojs = require('../../../app/js/mongojs')
+const { ObjectId } = mongojs
+const Settings = require('settings-sharelatex')
+const request = require('request')
+const rclient = require('redis').createClient(Settings.redis.history) // Only works locally for now
+
+const TrackChangesApp = require('./helpers/TrackChangesApp')
+const TrackChangesClient = require('./helpers/TrackChangesClient')
+const MockWebApi = require('./helpers/MockWebApi')
+
+describe('Appending doc ops to the history', function() {
+  before(function(done) {
+    return TrackChangesApp.ensureRunning(done)
+  })
+
+  describe('when the history does not exist yet', function() {
+    before(function(done) {
+      this.project_id = ObjectId().toString()
+      this.doc_id = ObjectId().toString()
+      this.user_id = ObjectId().toString()
+      MockWebApi.projects[this.project_id] = { features: { versioning: false } }
+      TrackChangesClient.pushRawUpdates(
+        this.project_id,
+        this.doc_id,
+        [
+          {
+            op: [{ i: 'f', p: 3 }],
+            meta: { ts: Date.now(), user_id: this.user_id },
+            v: 3
+          },
+          {
+            op: [{ i: 'o', p: 4 }],
+            meta: { ts: Date.now(), user_id: this.user_id },
+            v: 4
+          },
+          {
+            op: [{ i: 'o', p: 5 }],
+            meta: { ts: Date.now(), user_id: this.user_id },
+            v: 5
+          }
+        ],
+        error => {
+          if (error != null) {
+            throw error
+          }
+          return TrackChangesClient.flushAndGetCompressedUpdates(
+            this.project_id,
+            this.doc_id,
+            (error, updates) => {
+              this.updates = updates
+              if (error != null) {
+                throw error
+              }
+              return done()
+            }
+          )
+        }
+      )
+      return null
+    })
+
+    it('should insert the compressed op into mongo', function() {
+      return expect(this.updates[0].pack[0].op).to.deep.equal([
+        {
+          p: 3,
+          i: 'foo'
+        }
+      ])
+    })
+
+    it('should insert the correct version number into mongo', function() {
+      return expect(this.updates[0].v).to.equal(5)
+    })
+
+    it('should store the doc id', function() {
+      return expect(this.updates[0].doc_id.toString()).to.equal(this.doc_id)
+    })
+
+    it('should store the project id', function() {
+      return expect(this.updates[0].project_id.toString()).to.equal(
+        this.project_id
+      )
+    })
+
+    return it('should clear the doc from the DocsWithHistoryOps set', function(done) {
+      rclient.sismember(
+        `DocsWithHistoryOps:${this.project_id}`,
+        this.doc_id,
+        (error, member) => {
+          member.should.equal(0)
+          return done()
+        }
+      )
+      return null
+    })
+  })
+
+  describe('when the history has already been started', function() {
+    beforeEach(function(done) {
+      this.project_id = ObjectId().toString()
+      this.doc_id = ObjectId().toString()
+      this.user_id = ObjectId().toString()
+      MockWebApi.projects[this.project_id] = { features: { versioning: false } }
+      TrackChangesClient.pushRawUpdates(
+        this.project_id,
+        this.doc_id,
+        [
+          {
+            op: [{ i: 'f', p: 3 }],
+            meta: { ts: Date.now(), user_id: this.user_id },
+            v: 3
+          },
+          {
+            op: [{ i: 'o', p: 4 }],
+            meta: { ts: Date.now(), user_id: this.user_id },
+            v: 4
+          },
+          {
+            op: [{ i: 'o', p: 5 }],
+            meta: { ts: Date.now(), user_id: this.user_id },
+            v: 5
+          }
+        ],
+        error => {
+          if (error != null) {
+            throw error
+          }
+          return TrackChangesClient.flushAndGetCompressedUpdates(
+            this.project_id,
+            this.doc_id,
+            (error, updates) => {
+              if (error != null) {
+                throw error
+              }
+              return done()
+            }
+          )
+        }
+      )
+      return null
+    })
+
+    describe('when the updates are recent and from the same user', function() {
+      beforeEach(function(done) {
+        TrackChangesClient.pushRawUpdates(
+          this.project_id,
+          this.doc_id,
+          [
+            {
+              op: [{ i: 'b', p: 6 }],
+              meta: { ts: Date.now(), user_id: this.user_id },
+              v: 6
+            },
+            {
+              op: [{ i: 'a', p: 7 }],
+              meta: { ts: Date.now(), user_id: this.user_id },
+              v: 7
+            },
+            {
+              op: [{ i: 'r', p: 8 }],
+              meta: { ts: Date.now(), user_id: this.user_id },
+              v: 8
+            }
+          ],
+          error => {
+            if (error != null) {
+              throw error
+            }
+            return TrackChangesClient.flushAndGetCompressedUpdates(
+              this.project_id,
+              this.doc_id,
+              (error, updates) => {
+                this.updates = updates
+                if (error != null) {
+                  throw error
+                }
+                return done()
+              }
+            )
+          }
+        )
+        return null
+      })
+
+      it('should combine all the updates into one pack', function() {
+        return expect(this.updates[0].pack[1].op).to.deep.equal([
+          {
+            p: 6,
+            i: 'bar'
+          }
+        ])
+      })
+
+      return it('should insert the correct version number into mongo', function() {
+        return expect(this.updates[0].v_end).to.equal(8)
+      })
+    })
+
+    return describe('when the updates are far apart', function() {
+      beforeEach(function(done) {
+        const oneDay = 24 * 60 * 60 * 1000
+        TrackChangesClient.pushRawUpdates(
+          this.project_id,
+          this.doc_id,
+          [
+            {
+              op: [{ i: 'b', p: 6 }],
+              meta: { ts: Date.now() + oneDay, user_id: this.user_id },
+              v: 6
+            },
+            {
+              op: [{ i: 'a', p: 7 }],
+              meta: { ts: Date.now() + oneDay, user_id: this.user_id },
+              v: 7
+            },
+            {
+              op: [{ i: 'r', p: 8 }],
+              meta: { ts: Date.now() + oneDay, user_id: this.user_id },
+              v: 8
+            }
+          ],
+          error => {
+            if (error != null) {
+              throw error
+            }
+            return TrackChangesClient.flushAndGetCompressedUpdates(
+              this.project_id,
+              this.doc_id,
+              (error, updates) => {
+                this.updates = updates
+                if (error != null) {
+                  throw error
+                }
+                return done()
+              }
+            )
+          }
+        )
+        return null
+      })
+
+      return it('should combine the updates into one pack', function() {
+        expect(this.updates[0].pack[0].op).to.deep.equal([
+          {
+            p: 3,
+            i: 'foo'
+          }
+        ])
+        return expect(this.updates[0].pack[1].op).to.deep.equal([
+          {
+            p: 6,
+            i: 'bar'
+          }
+        ])
+      })
+    })
+  })
+
+  describe('when the updates need processing in batches', function() {
+    before(function(done) {
+      this.project_id = ObjectId().toString()
+      this.doc_id = ObjectId().toString()
+      this.user_id = ObjectId().toString()
+      MockWebApi.projects[this.project_id] = { features: { versioning: false } }
+      const updates = []
+      this.expectedOp = [{ p: 0, i: '' }]
+      for (let i = 0; i <= 250; i++) {
+        updates.push({
+          op: [{ i: 'a', p: 0 }],
+          meta: { ts: Date.now(), user_id: this.user_id },
+          v: i
+        })
+        this.expectedOp[0].i = `a${this.expectedOp[0].i}`
+      }
+
+      TrackChangesClient.pushRawUpdates(
+        this.project_id,
+        this.doc_id,
+        updates,
+        error => {
+          if (error != null) {
+            throw error
+          }
+          return TrackChangesClient.flushAndGetCompressedUpdates(
+            this.project_id,
+            this.doc_id,
+            (error, updates1) => {
+              this.updates = updates1
+              if (error != null) {
+                throw error
+              }
+              return done()
+            }
+          )
+        }
+      )
+      return null
+    })
+
+    it('should concat the compressed op into mongo', function() {
+      return expect(this.updates[0].pack.length).to.deep.equal(3)
+    }) // batch size is 100
+
+    return it('should insert the correct version number into mongo', function() {
+      return expect(this.updates[0].v_end).to.equal(250)
+    })
+  })
+
+  describe('when there are multiple ops in each update', function() {
+    before(function(done) {
+      this.project_id = ObjectId().toString()
+      this.doc_id = ObjectId().toString()
+      this.user_id = ObjectId().toString()
+      MockWebApi.projects[this.project_id] = { features: { versioning: false } }
+      const oneDay = 24 * 60 * 60 * 1000
+      TrackChangesClient.pushRawUpdates(
+        this.project_id,
+        this.doc_id,
+        [
+          {
+            op: [
+              { i: 'f', p: 3 },
+              { i: 'o', p: 4 },
+              { i: 'o', p: 5 }
+            ],
+            meta: { ts: Date.now(), user_id: this.user_id },
+            v: 3
+          },
+          {
+            op: [
+              { i: 'b', p: 6 },
+              { i: 'a', p: 7 },
+              { i: 'r', p: 8 }
+            ],
+            meta: { ts: Date.now() + oneDay, user_id: this.user_id },
+            v: 4
+          }
+        ],
+        error => {
+          if (error != null) {
+            throw error
+          }
+          return TrackChangesClient.flushAndGetCompressedUpdates(
+            this.project_id,
+            this.doc_id,
+            (error, updates) => {
+              this.updates = updates
+              if (error != null) {
+                throw error
+              }
+              return done()
+            }
+          )
+        }
+      )
+      return null
+    })
+
+    it('should insert the compressed ops into mongo', function() {
+      expect(this.updates[0].pack[0].op).to.deep.equal([
+        {
+          p: 3,
+          i: 'foo'
+        }
+      ])
+      return expect(this.updates[0].pack[1].op).to.deep.equal([
+        {
+          p: 6,
+          i: 'bar'
+        }
+      ])
+    })
+
+    return it('should insert the correct version numbers into mongo', function() {
+      expect(this.updates[0].pack[0].v).to.equal(3)
+      return expect(this.updates[0].pack[1].v).to.equal(4)
+    })
+  })
+
+  describe('when there is a no-op update', function() {
+    before(function(done) {
+      this.project_id = ObjectId().toString()
+      this.doc_id = ObjectId().toString()
+      this.user_id = ObjectId().toString()
+      MockWebApi.projects[this.project_id] = { features: { versioning: false } }
+      const oneDay = 24 * 60 * 60 * 1000
+      TrackChangesClient.pushRawUpdates(
+        this.project_id,
+        this.doc_id,
+        [
+          {
+            op: [],
+            meta: { ts: Date.now(), user_id: this.user_id },
+            v: 3
+          },
+          {
+            op: [{ i: 'foo', p: 3 }],
+            meta: { ts: Date.now() + oneDay, user_id: this.user_id },
+            v: 4
+          }
+        ],
+        error => {
+          if (error != null) {
+            throw error
+          }
+          return TrackChangesClient.flushAndGetCompressedUpdates(
+            this.project_id,
+            this.doc_id,
+            (error, updates) => {
+              this.updates = updates
+              if (error != null) {
+                throw error
+              }
+              return done()
+            }
+          )
+        }
+      )
+      return null
+    })
+
+    it('should insert the compressed no-op into mongo', function() {
+      return expect(this.updates[0].pack[0].op).to.deep.equal([])
+    })
+
+    it('should insert the compressed next update into mongo', function() {
+      return expect(this.updates[0].pack[1].op).to.deep.equal([
+        {
+          p: 3,
+          i: 'foo'
+        }
+      ])
+    })
+
+    return it('should insert the correct version numbers into mongo', function() {
+      expect(this.updates[0].pack[0].v).to.equal(3)
+      return expect(this.updates[0].pack[1].v).to.equal(4)
+    })
+  })
+
+  describe('when there is a comment update', function() {
+    before(function(done) {
+      this.project_id = ObjectId().toString()
+      this.doc_id = ObjectId().toString()
+      this.user_id = ObjectId().toString()
+      MockWebApi.projects[this.project_id] = { features: { versioning: false } }
+      TrackChangesClient.pushRawUpdates(
+        this.project_id,
+        this.doc_id,
+        [
+          {
+            op: [
+              { c: 'foo', p: 3 },
+              { d: 'bar', p: 6 }
+            ],
+            meta: { ts: Date.now(), user_id: this.user_id },
+            v: 3
+          }
+        ],
+        error => {
+          if (error != null) {
+            throw error
+          }
+          return TrackChangesClient.flushAndGetCompressedUpdates(
+            this.project_id,
+            this.doc_id,
+            (error, updates) => {
+              this.updates = updates
+              if (error != null) {
+                throw error
+              }
+              return done()
+            }
+          )
+        }
+      )
+      return null
+    })
+
+    it('should ignore the comment op', function() {
+      return expect(this.updates[0].pack[0].op).to.deep.equal([
+        { d: 'bar', p: 6 }
+      ])
+    })
+
+    return it('should insert the correct version numbers into mongo', function() {
+      return expect(this.updates[0].pack[0].v).to.equal(3)
+    })
+  })
+
+  describe('when the project has versioning enabled', function() {
+    before(function(done) {
+      this.project_id = ObjectId().toString()
+      this.doc_id = ObjectId().toString()
+      this.user_id = ObjectId().toString()
+      MockWebApi.projects[this.project_id] = { features: { versioning: true } }
+
+      TrackChangesClient.pushRawUpdates(
+        this.project_id,
+        this.doc_id,
+        [
+          {
+            op: [{ i: 'f', p: 3 }],
+            meta: { ts: Date.now(), user_id: this.user_id },
+            v: 3
+          }
+        ],
+        error => {
+          if (error != null) {
+            throw error
+          }
+          return TrackChangesClient.flushAndGetCompressedUpdates(
+            this.project_id,
+            this.doc_id,
+            (error, updates) => {
+              this.updates = updates
+              if (error != null) {
+                throw error
+              }
+              return done()
+            }
+          )
+        }
+      )
+      return null
+    })
+
+    return it('should not add a expiresAt entry in the update in mongo', function() {
+      return expect(this.updates[0].expiresAt).to.be.undefined
+    })
+  })
+
+  return describe('when the project does not have versioning enabled', function() {
+    before(function(done) {
+      this.project_id = ObjectId().toString()
+      this.doc_id = ObjectId().toString()
+      this.user_id = ObjectId().toString()
+      MockWebApi.projects[this.project_id] = { features: { versioning: false } }
+
+      TrackChangesClient.pushRawUpdates(
+        this.project_id,
+        this.doc_id,
+        [
+          {
+            op: [{ i: 'f', p: 3 }],
+            meta: { ts: Date.now(), user_id: this.user_id },
+            v: 3
+          }
+        ],
+        error => {
+          if (error != null) {
+            throw error
+          }
+          return TrackChangesClient.flushAndGetCompressedUpdates(
+            this.project_id,
+            this.doc_id,
+            (error, updates) => {
+              this.updates = updates
+              if (error != null) {
+                throw error
+              }
+              return done()
+            }
+          )
+        }
+      )
+      return null
+    })
+
+    return it('should add a expiresAt entry in the update in mongo', function() {
+      return expect(this.updates[0].expiresAt).to.exist
+    })
+  })
+})

+ 255 - 171
services/track-changes/test/acceptance/js/ArchivingUpdatesTests.js

@@ -14,176 +14,260 @@
  * DS207: Consider shorter variations of null checks
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-const sinon = require("sinon");
-const chai = require("chai");
-chai.should();
-const { expect } = chai;
-const mongojs = require("../../../app/js/mongojs");
-const { db } = mongojs;
-const { ObjectId } = mongojs;
-const Settings = require("settings-sharelatex");
-const request = require("request");
-const rclient = require("redis").createClient(Settings.redis.history); // Only works locally for now
-
-const TrackChangesApp = require("./helpers/TrackChangesApp");
-const TrackChangesClient = require("./helpers/TrackChangesClient");
-const MockDocStoreApi = require("./helpers/MockDocStoreApi");
-const MockWebApi = require("./helpers/MockWebApi");
-
-describe("Archiving updates", function() {
-	before(function(done) {
-		if (__guard__(__guard__(Settings != null ? Settings.trackchanges : undefined, x1 => x1.s3), x => x.key.length) < 1) {
-			const message = new Error("s3 keys not setup, this test setup will fail");
-			return done(message);
-		}
-
-		return TrackChangesClient.waitForS3(done);
-	});
-
-	before(function(done) {
-		this.now = Date.now();
-		this.to = this.now;
-		this.user_id = ObjectId().toString();
-		this.doc_id = ObjectId().toString();
-		this.project_id = ObjectId().toString();
-
-		this.minutes = 60 * 1000;
-		this.hours = 60 * this.minutes;
-
-		MockWebApi.projects[this.project_id] = {
-			features: {
-				versioning: true
-			}
-		};
-		sinon.spy(MockWebApi, "getProjectDetails");
-
-		MockWebApi.users[this.user_id] = (this.user = {
-			email: "user@sharelatex.com",
-			first_name: "Leo",
-			last_name: "Lion",
-			id: this.user_id
-		});
-		sinon.spy(MockWebApi, "getUserInfo");
-
-		MockDocStoreApi.docs[this.doc_id] = (this.doc = { 
-			_id: this.doc_id,
-			project_id: this.project_id
-		});
-		sinon.spy(MockDocStoreApi, "getAllDoc");
-
-		this.updates = [];
-		for (let i = 0, end = 512+10, asc = end >= 0; asc ? i <= end : i >= end; asc ? i++ : i--) {
-			this.updates.push({
-				op: [{ i: "a", p: 0 }],
-				meta: { ts: this.now + ((i-2048) * this.hours), user_id: this.user_id },
-				v: (2 * i) + 1
-			});
-			this.updates.push({
-				op: [{ i: "b", p: 0 }],
-				meta: { ts: this.now + ((i-2048) * this.hours) + (10*this.minutes), user_id: this.user_id },
-				v: (2 * i) + 2
-			});
-		}
-		TrackChangesApp.ensureRunning(() => {
-			return TrackChangesClient.pushRawUpdates(this.project_id, this.doc_id, this.updates, error => {
-				if (error != null) { throw error; }
-				return TrackChangesClient.flushDoc(this.project_id, this.doc_id, (error) => {
-					if (error != null) { throw error; }
-					return done();
-				});
-			});
-		});
-		return null;
-	});
-
-	after(function(done) {
-		MockWebApi.getUserInfo.restore();
-		return db.docHistory.remove({project_id: ObjectId(this.project_id)}, () => {
-			return db.docHistoryIndex.remove({project_id: ObjectId(this.project_id)}, () => {
-				return TrackChangesClient.removeS3Doc(this.project_id, this.doc_id, done);
-			});
-		});
-	});
-
-	describe("archiving a doc's updates", function() {
-		before(function(done) {
-			TrackChangesClient.pushDocHistory(this.project_id, this.doc_id, (error) => {
-				if (error != null) { throw error; }
-				return done();
-			});
-			return null;
-		});
-
-		it("should have one cached pack", function(done) {
-			return db.docHistory.count({ doc_id: ObjectId(this.doc_id), expiresAt:{$exists:true}}, (error, count) => {
-				if (error != null) { throw error; }
-				count.should.equal(1);
-				return done();
-			});
-		});
-
-		it("should have one remaining pack after cache is expired", function(done) {
-			return db.docHistory.remove({
-				doc_id: ObjectId(this.doc_id),
-				expiresAt:{$exists:true}
-			}, (err, result) => {
-				if (typeof error !== 'undefined' && error !== null) { throw error; }
-				return db.docHistory.count({ doc_id: ObjectId(this.doc_id)}, (error, count) => {
-					if (error != null) { throw error; }
-					count.should.equal(1);
-					return done();
-				});
-			});
-		});
-
-		it("should have a docHistoryIndex entry marked as inS3", function(done) {
-			return db.docHistoryIndex.findOne({ _id: ObjectId(this.doc_id) }, (error, index) => {
-				if (error != null) { throw error; }
-				index.packs[0].inS3.should.equal(true);
-				return done();
-			});
-		});
-
-		it("should have a docHistoryIndex entry with the last version", function(done) {
-			return db.docHistoryIndex.findOne({ _id: ObjectId(this.doc_id) }, (error, index) => {
-				if (error != null) { throw error; }
-				index.packs[0].v_end.should.equal(1024);
-				return done();
-			});
-		});
-
-		return it("should store 1024 doc changes in S3 in one pack", function(done) {
-			return db.docHistoryIndex.findOne({ _id: ObjectId(this.doc_id) }, (error, index) => {
-				if (error != null) { throw error; }
-				const pack_id = index.packs[0]._id;
-				return TrackChangesClient.getS3Doc(this.project_id, this.doc_id, pack_id, (error, doc) => {
-					doc.n.should.equal(1024);
-					doc.pack.length.should.equal(1024);
-					return done();
-				});
-			});
-		});
-	});
-
-	return describe("unarchiving a doc's updates", function() {
-		before(function(done) {
-			TrackChangesClient.pullDocHistory(this.project_id, this.doc_id, (error) => {
-				if (error != null) { throw error; }
-				return done();
-			});
-			return null;
-		});
-
-		return it("should restore both packs", function(done) {
-			return db.docHistory.count({ doc_id: ObjectId(this.doc_id) }, (error, count) => {
-				if (error != null) { throw error; }
-				count.should.equal(2);
-				return done();
-			});
-		});
-	});
-});
+const sinon = require('sinon')
+const chai = require('chai')
+chai.should()
+const { expect } = chai
+const mongojs = require('../../../app/js/mongojs')
+const { db } = mongojs
+const { ObjectId } = mongojs
+const Settings = require('settings-sharelatex')
+const request = require('request')
+const rclient = require('redis').createClient(Settings.redis.history) // Only works locally for now
+
+const TrackChangesApp = require('./helpers/TrackChangesApp')
+const TrackChangesClient = require('./helpers/TrackChangesClient')
+const MockDocStoreApi = require('./helpers/MockDocStoreApi')
+const MockWebApi = require('./helpers/MockWebApi')
+
+describe('Archiving updates', function() {
+  before(function(done) {
+    if (
+      __guard__(
+        __guard__(
+          Settings != null ? Settings.trackchanges : undefined,
+          x1 => x1.s3
+        ),
+        x => x.key.length
+      ) < 1
+    ) {
+      const message = new Error('s3 keys not setup, this test setup will fail')
+      return done(message)
+    }
+
+    return TrackChangesClient.waitForS3(done)
+  })
+
+  before(function(done) {
+    this.now = Date.now()
+    this.to = this.now
+    this.user_id = ObjectId().toString()
+    this.doc_id = ObjectId().toString()
+    this.project_id = ObjectId().toString()
+
+    this.minutes = 60 * 1000
+    this.hours = 60 * this.minutes
+
+    MockWebApi.projects[this.project_id] = {
+      features: {
+        versioning: true
+      }
+    }
+    sinon.spy(MockWebApi, 'getProjectDetails')
+
+    MockWebApi.users[this.user_id] = this.user = {
+      email: 'user@sharelatex.com',
+      first_name: 'Leo',
+      last_name: 'Lion',
+      id: this.user_id
+    }
+    sinon.spy(MockWebApi, 'getUserInfo')
+
+    MockDocStoreApi.docs[this.doc_id] = this.doc = {
+      _id: this.doc_id,
+      project_id: this.project_id
+    }
+    sinon.spy(MockDocStoreApi, 'getAllDoc')
+
+    this.updates = []
+    for (
+      let i = 0, end = 512 + 10, asc = end >= 0;
+      asc ? i <= end : i >= end;
+      asc ? i++ : i--
+    ) {
+      this.updates.push({
+        op: [{ i: 'a', p: 0 }],
+        meta: { ts: this.now + (i - 2048) * this.hours, user_id: this.user_id },
+        v: 2 * i + 1
+      })
+      this.updates.push({
+        op: [{ i: 'b', p: 0 }],
+        meta: {
+          ts: this.now + (i - 2048) * this.hours + 10 * this.minutes,
+          user_id: this.user_id
+        },
+        v: 2 * i + 2
+      })
+    }
+    TrackChangesApp.ensureRunning(() => {
+      return TrackChangesClient.pushRawUpdates(
+        this.project_id,
+        this.doc_id,
+        this.updates,
+        error => {
+          if (error != null) {
+            throw error
+          }
+          return TrackChangesClient.flushDoc(
+            this.project_id,
+            this.doc_id,
+            error => {
+              if (error != null) {
+                throw error
+              }
+              return done()
+            }
+          )
+        }
+      )
+    })
+    return null
+  })
+
+  after(function(done) {
+    MockWebApi.getUserInfo.restore()
+    return db.docHistory.remove(
+      { project_id: ObjectId(this.project_id) },
+      () => {
+        return db.docHistoryIndex.remove(
+          { project_id: ObjectId(this.project_id) },
+          () => {
+            return TrackChangesClient.removeS3Doc(
+              this.project_id,
+              this.doc_id,
+              done
+            )
+          }
+        )
+      }
+    )
+  })
+
+  describe("archiving a doc's updates", function() {
+    before(function(done) {
+      TrackChangesClient.pushDocHistory(this.project_id, this.doc_id, error => {
+        if (error != null) {
+          throw error
+        }
+        return done()
+      })
+      return null
+    })
+
+    it('should have one cached pack', function(done) {
+      return db.docHistory.count(
+        { doc_id: ObjectId(this.doc_id), expiresAt: { $exists: true } },
+        (error, count) => {
+          if (error != null) {
+            throw error
+          }
+          count.should.equal(1)
+          return done()
+        }
+      )
+    })
+
+    it('should have one remaining pack after cache is expired', function(done) {
+      return db.docHistory.remove(
+        {
+          doc_id: ObjectId(this.doc_id),
+          expiresAt: { $exists: true }
+        },
+        (err, result) => {
+          if (typeof error !== 'undefined' && error !== null) {
+            throw error
+          }
+          return db.docHistory.count(
+            { doc_id: ObjectId(this.doc_id) },
+            (error, count) => {
+              if (error != null) {
+                throw error
+              }
+              count.should.equal(1)
+              return done()
+            }
+          )
+        }
+      )
+    })
+
+    it('should have a docHistoryIndex entry marked as inS3', function(done) {
+      return db.docHistoryIndex.findOne(
+        { _id: ObjectId(this.doc_id) },
+        (error, index) => {
+          if (error != null) {
+            throw error
+          }
+          index.packs[0].inS3.should.equal(true)
+          return done()
+        }
+      )
+    })
+
+    it('should have a docHistoryIndex entry with the last version', function(done) {
+      return db.docHistoryIndex.findOne(
+        { _id: ObjectId(this.doc_id) },
+        (error, index) => {
+          if (error != null) {
+            throw error
+          }
+          index.packs[0].v_end.should.equal(1024)
+          return done()
+        }
+      )
+    })
+
+    return it('should store 1024 doc changes in S3 in one pack', function(done) {
+      return db.docHistoryIndex.findOne(
+        { _id: ObjectId(this.doc_id) },
+        (error, index) => {
+          if (error != null) {
+            throw error
+          }
+          const pack_id = index.packs[0]._id
+          return TrackChangesClient.getS3Doc(
+            this.project_id,
+            this.doc_id,
+            pack_id,
+            (error, doc) => {
+              doc.n.should.equal(1024)
+              doc.pack.length.should.equal(1024)
+              return done()
+            }
+          )
+        }
+      )
+    })
+  })
+
+  return describe("unarchiving a doc's updates", function() {
+    before(function(done) {
+      TrackChangesClient.pullDocHistory(this.project_id, this.doc_id, error => {
+        if (error != null) {
+          throw error
+        }
+        return done()
+      })
+      return null
+    })
+
+    return it('should restore both packs', function(done) {
+      return db.docHistory.count(
+        { doc_id: ObjectId(this.doc_id) },
+        (error, count) => {
+          if (error != null) {
+            throw error
+          }
+          count.should.equal(2)
+          return done()
+        }
+      )
+    })
+  })
+})
 
 function __guard__(value, transform) {
-  return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
-}
+  return typeof value !== 'undefined' && value !== null
+    ? transform(value)
+    : undefined
+}

+ 264 - 185
services/track-changes/test/acceptance/js/FlushingUpdatesTests.js

@@ -10,188 +10,267 @@
  * DS207: Consider shorter variations of null checks
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-const sinon = require("sinon");
-const chai = require("chai");
-chai.should();
-const { expect } = chai;
-const mongojs = require("../../../app/js/mongojs");
-const { ObjectId } = mongojs;
-const Settings = require("settings-sharelatex");
-const request = require("request");
-const rclient = require("redis").createClient(Settings.redis.history); // Only works locally for now
-
-const TrackChangesApp = require("./helpers/TrackChangesApp");
-const TrackChangesClient = require("./helpers/TrackChangesClient");
-const MockWebApi = require("./helpers/MockWebApi");
-
-describe("Flushing updates", function() {
-	before(function(done) { return TrackChangesApp.ensureRunning(done); });
-
-	describe("flushing a doc's updates", function() {
-		before(function(done) {
-			this.project_id = ObjectId().toString();
-			this.doc_id = ObjectId().toString();
-			this.user_id = ObjectId().toString();
-			MockWebApi.projects[this.project_id] = {features: {versioning: true}};
-
-			TrackChangesClient.pushRawUpdates(this.project_id, this.doc_id, [{
-				op: [{ i: "f", p: 3 }],
-				meta: { ts: Date.now(), user_id: this.user_id },
-				v: 3
-			}], error => {
-				if (error != null) { throw error; }
-				return TrackChangesClient.flushDoc(this.project_id, this.doc_id, (error) => {
-					if (error != null) { throw error; }
-					return done();
-				});
-			});
-			return null;
-		});
-
-		return it("should flush the op into mongo", function(done) {
-			TrackChangesClient.getCompressedUpdates(this.doc_id, (error, updates) => {
-				expect(updates[0].pack[0].op).to.deep.equal([{
-					p: 3, i: "f"
-				}]);
-				return done();
-			});
-			return null;
-		});
-	});
-
-	return describe("flushing a project's updates", function() {
-		describe("with versioning enabled", function() {
-			before(function(done) {
-				this.project_id = ObjectId().toString();
-				this.doc_id = ObjectId().toString();
-				this.user_id = ObjectId().toString();
-
-				this.weeks = 7 * 24 * 60 * 60 * 1000;
-
-				MockWebApi.projects[this.project_id] = {
-					features: {
-						versioning: true
-					}
-				};
-
-				TrackChangesClient.pushRawUpdates(this.project_id, this.doc_id, [{
-					op: [{ i: "g", p: 2 }],
-					meta: { ts: Date.now() - (2 * this.weeks), user_id: this.user_id },
-					v: 2
-				}, {
-					op: [{ i: "f", p: 3 }],
-					meta: { ts: Date.now(), user_id: this.user_id },
-					v: 3
-				}], error => {
-					if (error != null) { throw error; }
-					return TrackChangesClient.flushProject(this.project_id, (error) => {
-						if (error != null) { throw error; }
-						return done();
-					});
-				});
-				return null;
-			});
-
-			it("should not mark the updates for deletion", function(done) {
-				TrackChangesClient.getCompressedUpdates(this.doc_id, (error, updates) => {
-					expect(updates[0].expiresAt).to.not.exist;
-					return done();
-				});
-				return null;
-			});
-
-			return it("should preserve history forever", function(done) {
-				TrackChangesClient.getProjectMetaData(this.project_id, (error, project) => {
-					expect(project.preserveHistory).to.equal(true);
-					return done();
-				});
-				return null;
-			});
-		});
-
-		describe("without versioning enabled", function() {
-			before(function(done) {
-				this.project_id = ObjectId().toString();
-				this.doc_id = ObjectId().toString();
-				this.user_id = ObjectId().toString();
-
-				this.weeks = 7 * 24 * 60 * 60 * 1000;
-
-				MockWebApi.projects[this.project_id] = {
-					features: {
-						versioning: false
-					}
-				};
-
-				TrackChangesClient.pushRawUpdates(this.project_id, this.doc_id, [{
-					op: [{ i: "g", p: 2 }],
-					meta: { ts: Date.now() - (2 * this.weeks), user_id: this.user_id },
-					v: 2
-				}, {
-					op: [{ i: "f", p: 3 }],
-					meta: { ts: Date.now(), user_id: this.user_id },
-					v: 3
-				}], error => {
-					if (error != null) { throw error; }
-					return TrackChangesClient.flushProject(this.project_id, (error) => {
-						if (error != null) { throw error; }
-						return done();
-					});
-				});
-				return null;
-			});
-
-			return it("should mark the updates for deletion", function(done) {
-				TrackChangesClient.getCompressedUpdates(this.doc_id, (error, updates) => {
-					expect(updates[0].expiresAt).to.exist;
-					return done();
-				});
-				return null;
-			});
-		});
-
-		return describe("without versioning enabled but with preserveHistory set to true", function() {
-			before(function(done) {
-				this.project_id = ObjectId().toString();
-				this.doc_id = ObjectId().toString();
-				this.user_id = ObjectId().toString();
-
-				this.weeks = 7 * 24 * 60 * 60 * 1000;
-
-				MockWebApi.projects[this.project_id] = {
-					features: {
-						versioning: false
-					}
-				};
-
-				TrackChangesClient.setPreserveHistoryForProject(this.project_id, error => {
-					if (error != null) { throw error; }
-					return TrackChangesClient.pushRawUpdates(this.project_id, this.doc_id, [{
-						op: [{ i: "g", p: 2 }],
-						meta: { ts: Date.now() - (2 * this.weeks), user_id: this.user_id },
-						v: 2
-					}, {
-						op: [{ i: "f", p: 3 }],
-						meta: { ts: Date.now(), user_id: this.user_id },
-						v: 3
-					}], error => {
-						if (error != null) { throw error; }
-						return TrackChangesClient.flushProject(this.project_id, (error) => {
-							if (error != null) { throw error; }
-							return done();
-						});
-					});
-				});
-				return null;
-			});
-
-			return it("should not mark the updates for deletion", function(done) {
-				TrackChangesClient.getCompressedUpdates(this.doc_id, (error, updates) => {
-					expect(updates[0].expiresAt).to.not.exist;
-					return done();
-				});
-				return null;
-			});
-		});
-	});
-});
+const sinon = require('sinon')
+const chai = require('chai')
+chai.should()
+const { expect } = chai
+const mongojs = require('../../../app/js/mongojs')
+const { ObjectId } = mongojs
+const Settings = require('settings-sharelatex')
+const request = require('request')
+const rclient = require('redis').createClient(Settings.redis.history) // Only works locally for now
+
+const TrackChangesApp = require('./helpers/TrackChangesApp')
+const TrackChangesClient = require('./helpers/TrackChangesClient')
+const MockWebApi = require('./helpers/MockWebApi')
+
+describe('Flushing updates', function() {
+  before(function(done) {
+    return TrackChangesApp.ensureRunning(done)
+  })
+
+  describe("flushing a doc's updates", function() {
+    before(function(done) {
+      this.project_id = ObjectId().toString()
+      this.doc_id = ObjectId().toString()
+      this.user_id = ObjectId().toString()
+      MockWebApi.projects[this.project_id] = { features: { versioning: true } }
+
+      TrackChangesClient.pushRawUpdates(
+        this.project_id,
+        this.doc_id,
+        [
+          {
+            op: [{ i: 'f', p: 3 }],
+            meta: { ts: Date.now(), user_id: this.user_id },
+            v: 3
+          }
+        ],
+        error => {
+          if (error != null) {
+            throw error
+          }
+          return TrackChangesClient.flushDoc(
+            this.project_id,
+            this.doc_id,
+            error => {
+              if (error != null) {
+                throw error
+              }
+              return done()
+            }
+          )
+        }
+      )
+      return null
+    })
+
+    return it('should flush the op into mongo', function(done) {
+      TrackChangesClient.getCompressedUpdates(this.doc_id, (error, updates) => {
+        expect(updates[0].pack[0].op).to.deep.equal([
+          {
+            p: 3,
+            i: 'f'
+          }
+        ])
+        return done()
+      })
+      return null
+    })
+  })
+
+  return describe("flushing a project's updates", function() {
+    describe('with versioning enabled', function() {
+      before(function(done) {
+        this.project_id = ObjectId().toString()
+        this.doc_id = ObjectId().toString()
+        this.user_id = ObjectId().toString()
+
+        this.weeks = 7 * 24 * 60 * 60 * 1000
+
+        MockWebApi.projects[this.project_id] = {
+          features: {
+            versioning: true
+          }
+        }
+
+        TrackChangesClient.pushRawUpdates(
+          this.project_id,
+          this.doc_id,
+          [
+            {
+              op: [{ i: 'g', p: 2 }],
+              meta: { ts: Date.now() - 2 * this.weeks, user_id: this.user_id },
+              v: 2
+            },
+            {
+              op: [{ i: 'f', p: 3 }],
+              meta: { ts: Date.now(), user_id: this.user_id },
+              v: 3
+            }
+          ],
+          error => {
+            if (error != null) {
+              throw error
+            }
+            return TrackChangesClient.flushProject(this.project_id, error => {
+              if (error != null) {
+                throw error
+              }
+              return done()
+            })
+          }
+        )
+        return null
+      })
+
+      it('should not mark the updates for deletion', function(done) {
+        TrackChangesClient.getCompressedUpdates(
+          this.doc_id,
+          (error, updates) => {
+            expect(updates[0].expiresAt).to.not.exist
+            return done()
+          }
+        )
+        return null
+      })
+
+      return it('should preserve history forever', function(done) {
+        TrackChangesClient.getProjectMetaData(
+          this.project_id,
+          (error, project) => {
+            expect(project.preserveHistory).to.equal(true)
+            return done()
+          }
+        )
+        return null
+      })
+    })
+
+    describe('without versioning enabled', function() {
+      before(function(done) {
+        this.project_id = ObjectId().toString()
+        this.doc_id = ObjectId().toString()
+        this.user_id = ObjectId().toString()
+
+        this.weeks = 7 * 24 * 60 * 60 * 1000
+
+        MockWebApi.projects[this.project_id] = {
+          features: {
+            versioning: false
+          }
+        }
+
+        TrackChangesClient.pushRawUpdates(
+          this.project_id,
+          this.doc_id,
+          [
+            {
+              op: [{ i: 'g', p: 2 }],
+              meta: { ts: Date.now() - 2 * this.weeks, user_id: this.user_id },
+              v: 2
+            },
+            {
+              op: [{ i: 'f', p: 3 }],
+              meta: { ts: Date.now(), user_id: this.user_id },
+              v: 3
+            }
+          ],
+          error => {
+            if (error != null) {
+              throw error
+            }
+            return TrackChangesClient.flushProject(this.project_id, error => {
+              if (error != null) {
+                throw error
+              }
+              return done()
+            })
+          }
+        )
+        return null
+      })
+
+      return it('should mark the updates for deletion', function(done) {
+        TrackChangesClient.getCompressedUpdates(
+          this.doc_id,
+          (error, updates) => {
+            expect(updates[0].expiresAt).to.exist
+            return done()
+          }
+        )
+        return null
+      })
+    })
+
+    return describe('without versioning enabled but with preserveHistory set to true', function() {
+      before(function(done) {
+        this.project_id = ObjectId().toString()
+        this.doc_id = ObjectId().toString()
+        this.user_id = ObjectId().toString()
+
+        this.weeks = 7 * 24 * 60 * 60 * 1000
+
+        MockWebApi.projects[this.project_id] = {
+          features: {
+            versioning: false
+          }
+        }
+
+        TrackChangesClient.setPreserveHistoryForProject(
+          this.project_id,
+          error => {
+            if (error != null) {
+              throw error
+            }
+            return TrackChangesClient.pushRawUpdates(
+              this.project_id,
+              this.doc_id,
+              [
+                {
+                  op: [{ i: 'g', p: 2 }],
+                  meta: {
+                    ts: Date.now() - 2 * this.weeks,
+                    user_id: this.user_id
+                  },
+                  v: 2
+                },
+                {
+                  op: [{ i: 'f', p: 3 }],
+                  meta: { ts: Date.now(), user_id: this.user_id },
+                  v: 3
+                }
+              ],
+              error => {
+                if (error != null) {
+                  throw error
+                }
+                return TrackChangesClient.flushProject(
+                  this.project_id,
+                  error => {
+                    if (error != null) {
+                      throw error
+                    }
+                    return done()
+                  }
+                )
+              }
+            )
+          }
+        )
+        return null
+      })
+
+      return it('should not mark the updates for deletion', function(done) {
+        TrackChangesClient.getCompressedUpdates(
+          this.doc_id,
+          (error, updates) => {
+            expect(updates[0].expiresAt).to.not.exist
+            return done()
+          }
+        )
+        return null
+      })
+    })
+  })
+})

+ 110 - 84
services/track-changes/test/acceptance/js/GettingADiffTests.js

@@ -9,97 +9,123 @@
  * DS207: Consider shorter variations of null checks
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-const sinon = require("sinon");
-const chai = require("chai");
-chai.should();
-const { expect } = chai;
-const mongojs = require("../../../app/js/mongojs");
-const { db } = mongojs;
-const { ObjectId } = mongojs;
-const Settings = require("settings-sharelatex");
+const sinon = require('sinon')
+const chai = require('chai')
+chai.should()
+const { expect } = chai
+const mongojs = require('../../../app/js/mongojs')
+const { db } = mongojs
+const { ObjectId } = mongojs
+const Settings = require('settings-sharelatex')
 
-const TrackChangesApp = require("./helpers/TrackChangesApp");
-const TrackChangesClient = require("./helpers/TrackChangesClient");
-const MockDocUpdaterApi = require("./helpers/MockDocUpdaterApi");
-const MockWebApi = require("./helpers/MockWebApi");
+const TrackChangesApp = require('./helpers/TrackChangesApp')
+const TrackChangesClient = require('./helpers/TrackChangesClient')
+const MockDocUpdaterApi = require('./helpers/MockDocUpdaterApi')
+const MockWebApi = require('./helpers/MockWebApi')
 
-describe("Getting a diff", function() {
+describe('Getting a diff', function() {
+  beforeEach(function(done) {
+    sinon.spy(MockDocUpdaterApi, 'getDoc')
 
-	beforeEach(function(done) {
-		sinon.spy(MockDocUpdaterApi, "getDoc");
+    this.now = Date.now()
+    this.from = this.now - 100000000
+    this.to = this.now
+    this.user_id = ObjectId().toString()
+    this.doc_id = ObjectId().toString()
+    this.project_id = ObjectId().toString()
+    MockWebApi.projects[this.project_id] = { features: { versioning: true } }
 
-		this.now = Date.now();
-		this.from = this.now - 100000000;
-		this.to = this.now;
-		this.user_id = ObjectId().toString();
-		this.doc_id = ObjectId().toString();
-		this.project_id = ObjectId().toString();
-		MockWebApi.projects[this.project_id] = {features: {versioning: true}};
+    MockWebApi.users[this.user_id] = this.user = {
+      email: 'user@sharelatex.com',
+      first_name: 'Leo',
+      last_name: 'Lion',
+      id: this.user_id
+    }
+    sinon.spy(MockWebApi, 'getUserInfo')
 
-		MockWebApi.users[this.user_id] = (this.user = {
-			email: "user@sharelatex.com",
-			first_name: "Leo",
-			last_name: "Lion",
-			id: this.user_id
-		});
-		sinon.spy(MockWebApi, "getUserInfo");
+    const twoMinutes = 2 * 60 * 1000
 
-		const twoMinutes = 2 * 60 * 1000;
+    this.updates = [
+      {
+        op: [{ i: 'one ', p: 0 }],
+        meta: { ts: this.from - twoMinutes, user_id: this.user_id },
+        v: 3
+      },
+      {
+        op: [{ i: 'two ', p: 4 }],
+        meta: { ts: this.from + twoMinutes, user_id: this.user_id },
+        v: (this.fromVersion = 4)
+      },
+      {
+        op: [{ i: 'three ', p: 8 }],
+        meta: { ts: this.to - twoMinutes, user_id: this.user_id },
+        v: (this.toVersion = 5)
+      },
+      {
+        op: [{ i: 'four', p: 14 }],
+        meta: { ts: this.to + twoMinutes, user_id: this.user_id },
+        v: 6
+      }
+    ]
+    this.lines = ['one two three four']
+    this.expected_diff = [
+      { u: 'one ' },
+      {
+        i: 'two three ',
+        meta: {
+          start_ts: this.from + twoMinutes,
+          end_ts: this.to - twoMinutes,
+          user: this.user
+        }
+      }
+    ]
 
-		this.updates = [{
-			op: [{ i: "one ", p: 0 }],
-			meta: { ts: this.from - twoMinutes, user_id: this.user_id },
-			v: 3
-		}, {
-			op: [{ i: "two ", p: 4 }],
-			meta: { ts: this.from + twoMinutes, user_id: this.user_id },
-			v: (this.fromVersion = 4)
-		}, {
-			op: [{ i: "three ", p: 8 }],
-			meta: { ts: this.to - twoMinutes, user_id: this.user_id },
-			v: (this.toVersion = 5)
-		}, {
-			op: [{ i: "four", p: 14 }],
-			meta: { ts: this.to + twoMinutes, user_id: this.user_id },
-			v: 6
-		}];
-		this.lines = ["one two three four"];
-		this.expected_diff = [
-			{ u: "one " },
-			{ i: "two three ", meta: { start_ts: this.from + twoMinutes, end_ts: this.to - twoMinutes, user: this.user } }
-		];
+    MockDocUpdaterApi.docs[this.doc_id] = {
+      lines: this.lines,
+      version: 7
+    }
+    TrackChangesApp.ensureRunning(() => {
+      return TrackChangesClient.pushRawUpdates(
+        this.project_id,
+        this.doc_id,
+        this.updates,
+        error => {
+          if (error != null) {
+            throw error
+          }
+          return TrackChangesClient.getDiff(
+            this.project_id,
+            this.doc_id,
+            this.fromVersion,
+            this.toVersion,
+            (error, diff) => {
+              if (error != null) {
+                throw error
+              }
+              this.diff = diff.diff
+              return done()
+            }
+          )
+        }
+      )
+    })
+    return null
+  })
 
-		MockDocUpdaterApi.docs[this.doc_id] = {
-			lines: this.lines,
-			version: 7
-		};
-		TrackChangesApp.ensureRunning(() => {
-			return TrackChangesClient.pushRawUpdates(this.project_id, this.doc_id, this.updates, error => {
-				if (error != null) { throw error; }
-				return TrackChangesClient.getDiff(this.project_id, this.doc_id, this.fromVersion, this.toVersion, (error, diff) => {
-					if (error != null) { throw error; }
-					this.diff = diff.diff;
-					return done();
-				});
-			});
-		});
-		return null;
-	});
+  afterEach(function() {
+    MockDocUpdaterApi.getDoc.restore()
+    MockWebApi.getUserInfo.restore()
+    return null
+  })
 
-	afterEach(function() {
-		MockDocUpdaterApi.getDoc.restore();
-		MockWebApi.getUserInfo.restore();
-		return null;
-	});
+  it('should return the diff', function() {
+    return expect(this.diff).to.deep.equal(this.expected_diff)
+  })
 
-	it("should return the diff", function() {
-		return expect(this.diff).to.deep.equal(this.expected_diff);
-	});
-
-	return it("should get the doc from the doc updater", function() {
-		MockDocUpdaterApi.getDoc
-			.calledWith(this.project_id, this.doc_id)
-			.should.equal(true);
-		return null;
-	});
-});
+  return it('should get the doc from the doc updater', function() {
+    MockDocUpdaterApi.getDoc
+      .calledWith(this.project_id, this.doc_id)
+      .should.equal(true)
+    return null
+  })
+})

+ 165 - 138
services/track-changes/test/acceptance/js/GettingUpdatesTests.js

@@ -10,154 +10,181 @@
  * DS207: Consider shorter variations of null checks
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-const sinon = require("sinon");
-const chai = require("chai");
-chai.should();
-const { expect } = chai;
-const mongojs = require("../../../app/js/mongojs");
-const { db } = mongojs;
-const { ObjectId } = mongojs;
-const Settings = require("settings-sharelatex");
+const sinon = require('sinon')
+const chai = require('chai')
+chai.should()
+const { expect } = chai
+const mongojs = require('../../../app/js/mongojs')
+const { db } = mongojs
+const { ObjectId } = mongojs
+const Settings = require('settings-sharelatex')
 
-const TrackChangesApp = require("./helpers/TrackChangesApp");
-const TrackChangesClient = require("./helpers/TrackChangesClient");
-const MockWebApi = require("./helpers/MockWebApi");
+const TrackChangesApp = require('./helpers/TrackChangesApp')
+const TrackChangesClient = require('./helpers/TrackChangesClient')
+const MockWebApi = require('./helpers/MockWebApi')
 
-describe("Getting updates", function() {
-	before(function(done) {
-		this.now = Date.now();
-		this.to = this.now;
-		this.user_id = ObjectId().toString();
-		this.deleted_user_id = 'deleted_user';
-		this.doc_id = ObjectId().toString();
-		this.project_id = ObjectId().toString();
+describe('Getting updates', function() {
+  before(function(done) {
+    this.now = Date.now()
+    this.to = this.now
+    this.user_id = ObjectId().toString()
+    this.deleted_user_id = 'deleted_user'
+    this.doc_id = ObjectId().toString()
+    this.project_id = ObjectId().toString()
 
-		this.minutes = 60 * 1000;
-		this.hours = 60 * this.minutes;
+    this.minutes = 60 * 1000
+    this.hours = 60 * this.minutes
 
-		MockWebApi.projects[this.project_id] = {
-			features: {
-				versioning: true
-			}
-		};
+    MockWebApi.projects[this.project_id] = {
+      features: {
+        versioning: true
+      }
+    }
 
-		MockWebApi.users[this.user_id] = (this.user = {
-			email: "user@sharelatex.com",
-			first_name: "Leo",
-			last_name: "Lion",
-			id: this.user_id
-		});
-		sinon.spy(MockWebApi, "getUserInfo");
+    MockWebApi.users[this.user_id] = this.user = {
+      email: 'user@sharelatex.com',
+      first_name: 'Leo',
+      last_name: 'Lion',
+      id: this.user_id
+    }
+    sinon.spy(MockWebApi, 'getUserInfo')
 
-		this.updates = [];
-		for (let i = 0; i <= 9; i++) {
-			this.updates.push({
-				op: [{ i: "a", p: 0 }],
-				meta: { ts: this.now - ((9 - i) * this.hours) - (2 * this.minutes), user_id: this.user_id },
-				v: (2 * i) + 1
-			});
-			this.updates.push({
-				op: [{ i: "b", p: 0 }],
-				meta: { ts: this.now - ((9 - i) * this.hours), user_id: this.user_id },
-				v: (2 * i) + 2
-			});
-		}
-		this.updates[0].meta.user_id = this.deleted_user_id;
+    this.updates = []
+    for (let i = 0; i <= 9; i++) {
+      this.updates.push({
+        op: [{ i: 'a', p: 0 }],
+        meta: {
+          ts: this.now - (9 - i) * this.hours - 2 * this.minutes,
+          user_id: this.user_id
+        },
+        v: 2 * i + 1
+      })
+      this.updates.push({
+        op: [{ i: 'b', p: 0 }],
+        meta: { ts: this.now - (9 - i) * this.hours, user_id: this.user_id },
+        v: 2 * i + 2
+      })
+    }
+    this.updates[0].meta.user_id = this.deleted_user_id
 
-		TrackChangesApp.ensureRunning(() => {
-			return TrackChangesClient.pushRawUpdates(this.project_id, this.doc_id, this.updates, error => {
-				if (error != null) { throw error; }
-				return done();
-			});
-		});
-		return null;
-	});
+    TrackChangesApp.ensureRunning(() => {
+      return TrackChangesClient.pushRawUpdates(
+        this.project_id,
+        this.doc_id,
+        this.updates,
+        error => {
+          if (error != null) {
+            throw error
+          }
+          return done()
+        }
+      )
+    })
+    return null
+  })
 
-	({
-		after() {
-			MockWebApi.getUserInfo.restore();
-			return null;
-		}
-	});
+  ;({
+    after() {
+      MockWebApi.getUserInfo.restore()
+      return null
+    }
+  })
 
-	describe("getting updates up to the limit", function() {
-		before(function(done) {
-			TrackChangesClient.getUpdates(this.project_id, { before: this.to + 1, min_count: 3 }, (error, body) => {
-				if (error != null) { throw error; }
-				this.updates = body.updates;
-				return done();
-			});
-			return null;
-		});
+  describe('getting updates up to the limit', function() {
+    before(function(done) {
+      TrackChangesClient.getUpdates(
+        this.project_id,
+        { before: this.to + 1, min_count: 3 },
+        (error, body) => {
+          if (error != null) {
+            throw error
+          }
+          this.updates = body.updates
+          return done()
+        }
+      )
+      return null
+    })
 
-		it("should fetch the user details from the web api", function() {
-			return MockWebApi.getUserInfo
-				.calledWith(this.user_id)
-				.should.equal(true);
-		});
+    it('should fetch the user details from the web api', function() {
+      return MockWebApi.getUserInfo.calledWith(this.user_id).should.equal(true)
+    })
 
-		return it("should return at least the min_count number of summarized updates", function() {
-			const docs1 = {};
-			docs1[this.doc_id] = {toV: 20, fromV: 19};
-			const docs2 = {};
-			docs2[this.doc_id] = {toV: 18, fromV: 17};
-			const docs3 = {};
-			docs3[this.doc_id] = {toV: 16, fromV: 15};
-			return expect(this.updates.slice(0,3)).to.deep.equal([{
-				docs: docs1,
-				meta: {
-					start_ts: this.to - (2 * this.minutes),
-					end_ts: this.to,
-					users: [this.user]
-				}
-			}, {
-				docs: docs2,
-				meta: {
-					start_ts: this.to - (1 * this.hours) - (2 * this.minutes),
-					end_ts: this.to - (1 * this.hours),
-					users: [this.user]
-				}
-			}, {
-				docs: docs3,
-				meta: {
-					start_ts: this.to - (2 * this.hours) - (2 * this.minutes),
-					end_ts: this.to - (2 * this.hours),
-					users: [this.user]
-				}
-			}]);
-	});
-});
+    return it('should return at least the min_count number of summarized updates', function() {
+      const docs1 = {}
+      docs1[this.doc_id] = { toV: 20, fromV: 19 }
+      const docs2 = {}
+      docs2[this.doc_id] = { toV: 18, fromV: 17 }
+      const docs3 = {}
+      docs3[this.doc_id] = { toV: 16, fromV: 15 }
+      return expect(this.updates.slice(0, 3)).to.deep.equal([
+        {
+          docs: docs1,
+          meta: {
+            start_ts: this.to - 2 * this.minutes,
+            end_ts: this.to,
+            users: [this.user]
+          }
+        },
+        {
+          docs: docs2,
+          meta: {
+            start_ts: this.to - 1 * this.hours - 2 * this.minutes,
+            end_ts: this.to - 1 * this.hours,
+            users: [this.user]
+          }
+        },
+        {
+          docs: docs3,
+          meta: {
+            start_ts: this.to - 2 * this.hours - 2 * this.minutes,
+            end_ts: this.to - 2 * this.hours,
+            users: [this.user]
+          }
+        }
+      ])
+    })
+  })
 
-	return describe("getting updates beyond the end of the database", function() {
-		before(function(done) {
-			TrackChangesClient.getUpdates(this.project_id, { before: (this.to - (8 * this.hours)) + 1, min_count: 30 }, (error, body) => {
-				if (error != null) { throw error; }
-				this.updates = body.updates;
-				return done();
-			});
-			return null;
-		});
+  return describe('getting updates beyond the end of the database', function() {
+    before(function(done) {
+      TrackChangesClient.getUpdates(
+        this.project_id,
+        { before: this.to - 8 * this.hours + 1, min_count: 30 },
+        (error, body) => {
+          if (error != null) {
+            throw error
+          }
+          this.updates = body.updates
+          return done()
+        }
+      )
+      return null
+    })
 
-		return it("should return as many updates as it can", function() {
-			const docs1 = {};
-			docs1[this.doc_id] = {toV: 4, fromV: 3};
-			const docs2 = {};
-			docs2[this.doc_id] = {toV: 2, fromV: 1};
-			return expect(this.updates).to.deep.equal([{
-				docs: docs1,
-				meta: {
-					start_ts: this.to - (8 * this.hours) - (2 * this.minutes),
-					end_ts: this.to - (8 * this.hours),
-					users: [this.user]
-				}
-			}, {
-				docs: docs2,
-				meta: {
-					start_ts: this.to - (9 * this.hours) - (2 * this.minutes),
-					end_ts: this.to - (9 * this.hours),
-					users: [this.user, null]
-				}
-			}]);
-	});
-});
-});
+    return it('should return as many updates as it can', function() {
+      const docs1 = {}
+      docs1[this.doc_id] = { toV: 4, fromV: 3 }
+      const docs2 = {}
+      docs2[this.doc_id] = { toV: 2, fromV: 1 }
+      return expect(this.updates).to.deep.equal([
+        {
+          docs: docs1,
+          meta: {
+            start_ts: this.to - 8 * this.hours - 2 * this.minutes,
+            end_ts: this.to - 8 * this.hours,
+            users: [this.user]
+          }
+        },
+        {
+          docs: docs2,
+          meta: {
+            start_ts: this.to - 9 * this.hours - 2 * this.minutes,
+            end_ts: this.to - 9 * this.hours,
+            users: [this.user, null]
+          }
+        }
+      ])
+    })
+  })
+})

+ 52 - 46
services/track-changes/test/acceptance/js/LockManagerTests.js

@@ -9,52 +9,58 @@
  * 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");
-chai.should();
-const { expect } = chai;
-const mongojs = require("../../../app/js/mongojs");
-const { ObjectId } = mongojs;
-const Settings = require("settings-sharelatex");
-const LockManager = require("../../../app/js/LockManager");
-const rclient = require("redis").createClient(Settings.redis.history); // Only works locally for now
-const TrackChangesApp = require("./helpers/TrackChangesApp");
+const sinon = require('sinon')
+const chai = require('chai')
+chai.should()
+const { expect } = chai
+const mongojs = require('../../../app/js/mongojs')
+const { ObjectId } = mongojs
+const Settings = require('settings-sharelatex')
+const LockManager = require('../../../app/js/LockManager')
+const rclient = require('redis').createClient(Settings.redis.history) // Only works locally for now
+const TrackChangesApp = require('./helpers/TrackChangesApp')
 
-describe("Locking document", function() {
+describe('Locking document', function() {
+  before(function(done) {
+    TrackChangesApp.ensureRunning(done)
+    return null
+  })
 
-	before(function(done){
-		TrackChangesApp.ensureRunning(done);
-		return null;
-	});
-		
-	return describe("when the lock has expired in redis", function() {
-		before(function(done) {
-			LockManager.LOCK_TTL = 1; // second
-			LockManager.runWithLock("doc123", releaseA => {
-				// we create a lock A and allow it to expire in redis
-				return setTimeout(() =>
-					// now we create a new lock B and try to release A
-					LockManager.runWithLock("doc123", releaseB => {
-						return releaseA();
-					}  // try to release lock A to see if it wipes out lock B
-					, (error) => {})
-				
-						// we never release lock B so nothing should happen here
-				, 1500);
-			} // enough time to wait until the lock has expired
-			, error =>
-				// we get here after trying to release lock A
-				done()
-			);
-			return null;
-		});
+  return describe('when the lock has expired in redis', function() {
+    before(function(done) {
+      LockManager.LOCK_TTL = 1 // second
+      LockManager.runWithLock(
+        'doc123',
+        releaseA => {
+          // we create a lock A and allow it to expire in redis
+          return setTimeout(
+            () =>
+              // now we create a new lock B and try to release A
+              LockManager.runWithLock(
+                'doc123',
+                releaseB => {
+                  return releaseA()
+                }, // try to release lock A to see if it wipes out lock B
+                error => {}
+              ),
 
-		return it("the new lock should not be removed by the expired locker", function(done) {
-			LockManager.checkLock("doc123", (err, isFree) => {
-					expect(isFree).to.equal(false);
-					return done();
-			});
-			return null;
-		});
-	});
-});
+            // we never release lock B so nothing should happen here
+            1500
+          )
+        }, // enough time to wait until the lock has expired
+        error =>
+          // we get here after trying to release lock A
+          done()
+      )
+      return null
+    })
+
+    return it('the new lock should not be removed by the expired locker', function(done) {
+      LockManager.checkLock('doc123', (err, isFree) => {
+        expect(isFree).to.equal(false)
+        return done()
+      })
+      return null
+    })
+  })
+})

+ 100 - 74
services/track-changes/test/acceptance/js/RestoringVersions.js

@@ -9,86 +9,112 @@
  * DS207: Consider shorter variations of null checks
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-const sinon = require("sinon");
-const chai = require("chai");
-chai.should();
-const { expect } = chai;
-const mongojs = require("../../../app/js/mongojs");
-const { db } = mongojs;
-const { ObjectId } = mongojs;
-const Settings = require("settings-sharelatex");
+const sinon = require('sinon')
+const chai = require('chai')
+chai.should()
+const { expect } = chai
+const mongojs = require('../../../app/js/mongojs')
+const { db } = mongojs
+const { ObjectId } = mongojs
+const Settings = require('settings-sharelatex')
 
-const TrackChangesApp = require("./helpers/TrackChangesApp");
-const TrackChangesClient = require("./helpers/TrackChangesClient");
-const MockDocUpdaterApi = require("./helpers/MockDocUpdaterApi");
-const MockWebApi = require("./helpers/MockWebApi");
+const TrackChangesApp = require('./helpers/TrackChangesApp')
+const TrackChangesClient = require('./helpers/TrackChangesClient')
+const MockDocUpdaterApi = require('./helpers/MockDocUpdaterApi')
+const MockWebApi = require('./helpers/MockWebApi')
 
-describe("Restoring a version", function() {
-	before(function(done) {
-		sinon.spy(MockDocUpdaterApi, "setDoc");
+describe('Restoring a version', function() {
+  before(function(done) {
+    sinon.spy(MockDocUpdaterApi, 'setDoc')
 
-		this.now = Date.now();
-		this.user_id = ObjectId().toString();
-		this.doc_id = ObjectId().toString();
-		this.project_id = ObjectId().toString();
-		MockWebApi.projects[this.project_id] = {features: {versioning: true}};
-		
-		const minutes = 60 * 1000;
+    this.now = Date.now()
+    this.user_id = ObjectId().toString()
+    this.doc_id = ObjectId().toString()
+    this.project_id = ObjectId().toString()
+    MockWebApi.projects[this.project_id] = { features: { versioning: true } }
 
-		this.updates = [{
-			op: [{ i: "one ", p: 0 }],
-			meta: { ts: this.now - (6 * minutes), user_id: this.user_id },
-			v: 3
-		}, {
-			op: [{ i: "two ", p: 4 }],
-			meta: { ts: this.now - (4 * minutes), user_id: this.user_id },
-			v: 4
-		}, {
-			op: [{ i: "three ", p: 8 }],
-			meta: { ts: this.now - (2 * minutes), user_id: this.user_id },
-			v: 5
-		}, {
-			op: [{ i: "four", p: 14 }],
-			meta: { ts: this.now, user_id: this.user_id },
-			v: 6
-		}];
-		this.lines = ["one two three four"];
-		this.restored_lines = ["one two "];
-		this.beforeVersion = 5;
+    const minutes = 60 * 1000
 
-		MockWebApi.users[this.user_id] = (this.user = {
-			email: "user@sharelatex.com",
-			first_name: "Leo",
-			last_name: "Lion",
-			id: this.user_id
-		});
+    this.updates = [
+      {
+        op: [{ i: 'one ', p: 0 }],
+        meta: { ts: this.now - 6 * minutes, user_id: this.user_id },
+        v: 3
+      },
+      {
+        op: [{ i: 'two ', p: 4 }],
+        meta: { ts: this.now - 4 * minutes, user_id: this.user_id },
+        v: 4
+      },
+      {
+        op: [{ i: 'three ', p: 8 }],
+        meta: { ts: this.now - 2 * minutes, user_id: this.user_id },
+        v: 5
+      },
+      {
+        op: [{ i: 'four', p: 14 }],
+        meta: { ts: this.now, user_id: this.user_id },
+        v: 6
+      }
+    ]
+    this.lines = ['one two three four']
+    this.restored_lines = ['one two ']
+    this.beforeVersion = 5
 
-		MockDocUpdaterApi.docs[this.doc_id] = {
-			lines: this.lines,
-			version: 7
-		};
+    MockWebApi.users[this.user_id] = this.user = {
+      email: 'user@sharelatex.com',
+      first_name: 'Leo',
+      last_name: 'Lion',
+      id: this.user_id
+    }
 
-		TrackChangesApp.ensureRunning(() => {
-			return TrackChangesClient.pushRawUpdates(this.project_id, this.doc_id, this.updates, error => {
-				if (error != null) { throw error; }
-				return TrackChangesClient.restoreDoc(this.project_id, this.doc_id, this.beforeVersion, this.user_id, error => {
-					if (error != null) { throw error; }
-					return done();
-				});
-			});
-		});
-		return null;
-	});
+    MockDocUpdaterApi.docs[this.doc_id] = {
+      lines: this.lines,
+      version: 7
+    }
 
-	after(function() {
-		MockDocUpdaterApi.setDoc.restore();
-		return null;
-	});
+    TrackChangesApp.ensureRunning(() => {
+      return TrackChangesClient.pushRawUpdates(
+        this.project_id,
+        this.doc_id,
+        this.updates,
+        error => {
+          if (error != null) {
+            throw error
+          }
+          return TrackChangesClient.restoreDoc(
+            this.project_id,
+            this.doc_id,
+            this.beforeVersion,
+            this.user_id,
+            error => {
+              if (error != null) {
+                throw error
+              }
+              return done()
+            }
+          )
+        }
+      )
+    })
+    return null
+  })
 
-	return it("should set the doc in the doc updater", function() {
-		MockDocUpdaterApi.setDoc
-			.calledWith(this.project_id, this.doc_id, this.restored_lines, this.user_id, true)
-			.should.equal(true);
-		return null;
-	});
-});
+  after(function() {
+    MockDocUpdaterApi.setDoc.restore()
+    return null
+  })
+
+  return it('should set the doc in the doc updater', function() {
+    MockDocUpdaterApi.setDoc
+      .calledWith(
+        this.project_id,
+        this.doc_id,
+        this.restored_lines,
+        this.user_id,
+        true
+      )
+      .should.equal(true)
+    return null
+  })
+})

+ 37 - 32
services/track-changes/test/acceptance/js/helpers/MockDocStoreApi.js

@@ -10,40 +10,45 @@
  * DS207: Consider shorter variations of null checks
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-let MockDocUpdaterApi;
-const express = require("express");
-const app = express();
+let MockDocUpdaterApi
+const express = require('express')
+const app = express()
 
-module.exports = (MockDocUpdaterApi = {
-	docs: {},
+module.exports = MockDocUpdaterApi = {
+  docs: {},
 
-	getAllDoc(project_id, callback) {
-		if (callback == null) { callback = function(error) {}; }
-		return callback(null, this.docs);
-	},
+  getAllDoc(project_id, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return callback(null, this.docs)
+  },
 
-	run() {
-		app.get("/project/:project_id/doc", (req, res, next) => {
-			return this.getAllDoc(req.params.project_id, (error, docs) => {
-				if (error != null) {
-					res.send(500);
-				}
-				if ((docs == null)) {
-					return res.send(404);
-				} else {
-					return res.send(JSON.stringify(docs));
-				}
-			});
-		});
+  run() {
+    app.get('/project/:project_id/doc', (req, res, next) => {
+      return this.getAllDoc(req.params.project_id, (error, docs) => {
+        if (error != null) {
+          res.send(500)
+        }
+        if (docs == null) {
+          return res.send(404)
+        } else {
+          return res.send(JSON.stringify(docs))
+        }
+      })
+    })
 
-		return app.listen(3016, (error) => {
-			if (error != null) { throw error; }
-	}).on("error", (error) => {
-			console.error("error starting MockDocStoreApi:", error.message);
-			return process.exit(1);
-		});
-	}
-});
-
-MockDocUpdaterApi.run();
+    return app
+      .listen(3016, error => {
+        if (error != null) {
+          throw error
+        }
+      })
+      .on('error', error => {
+        console.error('error starting MockDocStoreApi:', error.message)
+        return process.exit(1)
+      })
+  }
+}
 
+MockDocUpdaterApi.run()

+ 71 - 48
services/track-changes/test/acceptance/js/helpers/MockDocUpdaterApi.js

@@ -11,58 +11,81 @@
  * DS207: Consider shorter variations of null checks
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-let MockDocUpdaterApi;
-const express = require("express");
-const app = express();
+let MockDocUpdaterApi
+const express = require('express')
+const app = express()
 
-module.exports = (MockDocUpdaterApi = {
-	docs: {},
+module.exports = MockDocUpdaterApi = {
+  docs: {},
 
-	getDoc(project_id, doc_id, callback) {
-		if (callback == null) { callback = function(error) {}; }
-		return callback(null, this.docs[doc_id]);
-	},
+  getDoc(project_id, doc_id, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return callback(null, this.docs[doc_id])
+  },
 
-	setDoc(project_id, doc_id, lines, user_id, undoing, callback) {
-		if (callback == null) { callback = function(error) {}; }
-		if (!this.docs[doc_id]) { this.docs[doc_id] = {}; }
-		this.docs[doc_id].lines = lines;
-		return callback();
-	},
+  setDoc(project_id, doc_id, lines, user_id, undoing, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    if (!this.docs[doc_id]) {
+      this.docs[doc_id] = {}
+    }
+    this.docs[doc_id].lines = lines
+    return callback()
+  },
 
-	run() {
-		app.get("/project/:project_id/doc/:doc_id", (req, res, next) => {
-			return this.getDoc(req.params.project_id, req.params.doc_id, (error, doc) => {
-				if (error != null) {
-					res.send(500);
-				}
-				if ((doc == null)) {
-					return res.send(404);
-				} else {
-					return res.send(JSON.stringify(doc));
-				}
-			});
-		});
+  run() {
+    app.get('/project/:project_id/doc/:doc_id', (req, res, next) => {
+      return this.getDoc(
+        req.params.project_id,
+        req.params.doc_id,
+        (error, doc) => {
+          if (error != null) {
+            res.send(500)
+          }
+          if (doc == null) {
+            return res.send(404)
+          } else {
+            return res.send(JSON.stringify(doc))
+          }
+        }
+      )
+    })
 
-		app.post("/project/:project_id/doc/:doc_id", express.bodyParser(), (req, res, next) => {
-			return this.setDoc(req.params.project_id, req.params.doc_id, req.body.lines, req.body.user_id, req.body.undoing, (errr, doc) => {
-				if (typeof error !== 'undefined' && error !== null) {
-					return res.send(500);
-				} else {
-					return res.send(204);
-				}
-			});
-		});
+    app.post(
+      '/project/:project_id/doc/:doc_id',
+      express.bodyParser(),
+      (req, res, next) => {
+        return this.setDoc(
+          req.params.project_id,
+          req.params.doc_id,
+          req.body.lines,
+          req.body.user_id,
+          req.body.undoing,
+          (errr, doc) => {
+            if (typeof error !== 'undefined' && error !== null) {
+              return res.send(500)
+            } else {
+              return res.send(204)
+            }
+          }
+        )
+      }
+    )
 
-		return app.listen(3003, (error) => {
-			if (error != null) { throw error; }
-	}).on("error", (error) => {
-			console.error("error starting MockDocUpdaterApi:", error.message);
-			return process.exit(1);
-		});
-	}
-});
-
-
-MockDocUpdaterApi.run();
+    return app
+      .listen(3003, error => {
+        if (error != null) {
+          throw error
+        }
+      })
+      .on('error', error => {
+        console.error('error starting MockDocUpdaterApi:', error.message)
+        return process.exit(1)
+      })
+  }
+}
 
+MockDocUpdaterApi.run()

+ 56 - 49
services/track-changes/test/acceptance/js/helpers/MockWebApi.js

@@ -10,60 +10,67 @@
  * DS207: Consider shorter variations of null checks
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-let MockWebApi;
-const express = require("express");
-const app = express();
+let MockWebApi
+const express = require('express')
+const app = express()
 
-module.exports = (MockWebApi = {
-	users: {},
+module.exports = MockWebApi = {
+  users: {},
 
-	projects: {},
+  projects: {},
 
-	getUserInfo(user_id, callback) {
-		if (callback == null) { callback = function(error) {}; }
-		return callback(null, this.users[user_id] || null);
-	},
+  getUserInfo(user_id, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return callback(null, this.users[user_id] || null)
+  },
 
-	getProjectDetails(project_id, callback) {
-		if (callback == null) { callback = function(error, project) {}; }
-		return callback(null, this.projects[project_id]);
-	},
+  getProjectDetails(project_id, callback) {
+    if (callback == null) {
+      callback = function(error, project) {}
+    }
+    return callback(null, this.projects[project_id])
+  },
 
-	run() {
-		app.get("/user/:user_id/personal_info", (req, res, next) => {
-			return this.getUserInfo(req.params.user_id, (error, user) => {
-				if (error != null) {
-					res.send(500);
-				}
-				if ((user == null)) {
-					return res.send(404);
-				} else {
-					return res.send(JSON.stringify(user));
-				}
-			});
-		});
+  run() {
+    app.get('/user/:user_id/personal_info', (req, res, next) => {
+      return this.getUserInfo(req.params.user_id, (error, user) => {
+        if (error != null) {
+          res.send(500)
+        }
+        if (user == null) {
+          return res.send(404)
+        } else {
+          return res.send(JSON.stringify(user))
+        }
+      })
+    })
 
-		app.get("/project/:project_id/details", (req, res, next) => {
-			return this.getProjectDetails(req.params.project_id, (error, project) => {
-				if (error != null) {
-					res.send(500);
-				}
-				if ((project == null)) {
-					return res.send(404);
-				} else {
-					return res.send(JSON.stringify(project));
-				}
-			});
-		});
+    app.get('/project/:project_id/details', (req, res, next) => {
+      return this.getProjectDetails(req.params.project_id, (error, project) => {
+        if (error != null) {
+          res.send(500)
+        }
+        if (project == null) {
+          return res.send(404)
+        } else {
+          return res.send(JSON.stringify(project))
+        }
+      })
+    })
 
-		return app.listen(3000, (error) => {
-			if (error != null) { throw error; }
-	}).on("error", (error) => {
-			console.error("error starting MockWebApiServer:", error.message);
-			return process.exit(1);
-		});
-	}
-});
-
-MockWebApi.run();
+    return app
+      .listen(3000, error => {
+        if (error != null) {
+          throw error
+        }
+      })
+      .on('error', error => {
+        console.error('error starting MockWebApiServer:', error.message)
+        return process.exit(1)
+      })
+  }
+}
 
+MockWebApi.run()

+ 48 - 33
services/track-changes/test/acceptance/js/helpers/TrackChangesApp.js

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

+ 273 - 194
services/track-changes/test/acceptance/js/helpers/TrackChangesClient.js

@@ -12,198 +12,277 @@
  * DS207: Consider shorter variations of null checks
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-let TrackChangesClient;
-const async = require('async');
-const zlib = require('zlib');
-const request = require("request");
-const Settings = require("settings-sharelatex");
-const rclient = require("redis-sharelatex").createClient(Settings.redis.history); // Only works locally for now
-const Keys = Settings.redis.history.key_schema;
-const {db, ObjectId} = require("../../../../app/js/mongojs");
-
-const aws = require("aws-sdk");
+let TrackChangesClient
+const async = require('async')
+const zlib = require('zlib')
+const request = require('request')
+const Settings = require('settings-sharelatex')
+const rclient = require('redis-sharelatex').createClient(Settings.redis.history) // Only works locally for now
+const Keys = Settings.redis.history.key_schema
+const { db, ObjectId } = require('../../../../app/js/mongojs')
+
+const aws = require('aws-sdk')
 const s3 = new aws.S3({
-	accessKeyId: Settings.trackchanges.s3.key,
-	secretAccessKey: Settings.trackchanges.s3.secret,
-	endpoint: Settings.trackchanges.s3.endpoint,
-	s3ForcePathStyle: Settings.trackchanges.s3.pathStyle
-});
-const S3_BUCKET = Settings.trackchanges.stores.doc_history;
-
-module.exports = (TrackChangesClient = {
-	flushAndGetCompressedUpdates(project_id, doc_id, callback) {
-		if (callback == null) { callback = function(error, updates) {}; }
-		return TrackChangesClient.flushDoc(project_id, doc_id, (error) => {
-			if (error != null) { return callback(error); }
-			return TrackChangesClient.getCompressedUpdates(doc_id, callback);
-		});
-	},
-
-	flushDoc(project_id, doc_id, callback) {
-		if (callback == null) { callback = function(error) {}; }
-		return request.post({
-			url: `http://localhost:3015/project/${project_id}/doc/${doc_id}/flush`
-		}, (error, response, body) => {
-			response.statusCode.should.equal(204);
-			return callback(error);
-		});
-	},
-
-	flushProject(project_id, callback) {
-		if (callback == null) { callback = function(error) {}; }
-		return request.post({
-			url: `http://localhost:3015/project/${project_id}/flush`
-		}, (error, response, body) => {
-			response.statusCode.should.equal(204);
-			return callback(error);
-		});
-	},
-
-	getCompressedUpdates(doc_id, callback) {
-		if (callback == null) { callback = function(error, updates) {}; }
-		return db.docHistory
-			.find({doc_id: ObjectId(doc_id)})
-			.sort({"meta.end_ts": 1})
-			.toArray(callback);
-	},
-
-	getProjectMetaData(project_id, callback) {
-		if (callback == null) { callback = function(error, updates) {}; }
-		return db.projectHistoryMetaData
-			.find({
-				project_id: ObjectId(project_id)
-			},
-			(error, projects) => callback(error, projects[0]));
-	},
-
-	setPreserveHistoryForProject(project_id, callback) {
-		if (callback == null) { callback = function(error) {}; }
-		return db.projectHistoryMetaData.update({
-			project_id: ObjectId(project_id)
-			}, {
-				$set: { preserveHistory: true }
-			}, {
-				upsert: true
-			}, callback);
-	},
-
-	pushRawUpdates(project_id, doc_id, updates, callback) {
-		if (callback == null) { callback = function(error) {}; }
-		return rclient.sadd(Keys.docsWithHistoryOps({project_id}), doc_id, (error) => {
-			if (error != null) { return callback(error); }
-			return rclient.rpush(Keys.uncompressedHistoryOps({doc_id}), ...Array.from(((Array.from(updates).map((u) => JSON.stringify(u))))), callback);
-		});
-	},
-
-	getDiff(project_id, doc_id, from, to, callback) {
-		if (callback == null) { callback = function(error, diff) {}; }
-		return request.get({
-			url: `http://localhost:3015/project/${project_id}/doc/${doc_id}/diff?from=${from}&to=${to}`
-		}, (error, response, body) => {
-			response.statusCode.should.equal(200);
-			return callback(null, JSON.parse(body));
-		});
-	},
-
-	getUpdates(project_id, options, callback) {
-		if (callback == null) { callback = function(error, body) {}; }
-		return request.get({
-			url: `http://localhost:3015/project/${project_id}/updates?before=${options.before}&min_count=${options.min_count}`
-		}, (error, response, body) => {
-			response.statusCode.should.equal(200);
-			return callback(null, JSON.parse(body));
-		});
-	},
-
-	restoreDoc(project_id, doc_id, version, user_id, callback) {
-		if (callback == null) { callback = function(error) {}; }
-		return request.post({
-			url: `http://localhost:3015/project/${project_id}/doc/${doc_id}/version/${version}/restore`,
-			headers: {
-				"X-User-Id": user_id
-			}
-		}, (error, response, body) => {
-			response.statusCode.should.equal(204);
-			return callback(null);
-		});
-	},
-
-	pushDocHistory(project_id, doc_id, callback) {
-		if (callback == null) { callback = function(error) {}; }
-		return request.post({
-			url: `http://localhost:3015/project/${project_id}/doc/${doc_id}/push`
-		}, (error, response, body) => {
-			response.statusCode.should.equal(204);
-			return callback(error);
-		});
-	},
-
-	pullDocHistory(project_id, doc_id, callback) {
-		if (callback == null) { callback = function(error) {}; }
-		return request.post({
-			url: `http://localhost:3015/project/${project_id}/doc/${doc_id}/pull`
-		}, (error, response, body) => {
-			response.statusCode.should.equal(204);
-			return callback(error);
-		});
-	},
-
-	waitForS3(done, retries) {
-		if (retries == null) { retries = 42; }
-		if (!Settings.trackchanges.s3.endpoint) {
-			return done();
-		}
-
-		return request.get(`${Settings.trackchanges.s3.endpoint}/`, (err, res) => {
-			if (res && (res.statusCode < 500)) {
-				return done();
-			}
-
-			if (retries === 0) {
-				return done(err || new Error(`s3 returned ${res.statusCode}`));
-			}
-
-			return setTimeout(() => TrackChangesClient.waitForS3(done, --retries)
-			, 1000);
-		});
-	},
-
-	getS3Doc(project_id, doc_id, pack_id, callback) {
-		if (callback == null) { callback = function(error, body) {}; }
-		const params = {
-			Bucket: S3_BUCKET,
-			Key: `${project_id}/changes-${doc_id}/pack-${pack_id}`
-		};
-
-		return s3.getObject(params, (error, data) => {
-			if (error != null) { return callback(error); }
-			const body = data.Body;
-			if ((body == null)) { return callback(new Error("empty response from s3")); }
-			return zlib.gunzip(body, (err, result) => {
-				if (err != null) { return callback(err); }
-				return callback(null, JSON.parse(result.toString()));
-			});
-		});
-	},
-
-	removeS3Doc(project_id, doc_id, callback) {
-		if (callback == null) { callback = function(error, res, body) {}; }
-		let params = {
-			Bucket: S3_BUCKET,
-			Prefix: `${project_id}/changes-${doc_id}`
-		};
-
-		return s3.listObjects(params, (error, data) => {
-			if (error != null) { return callback(error); }
-
-			params = {
-				Bucket: S3_BUCKET,
-				Delete: {
-					Objects: data.Contents.map(s3object => ({Key: s3object.Key}))
-				}
-			};
-
-			return s3.deleteObjects(params, callback);
-		});
-	}
-});
+  accessKeyId: Settings.trackchanges.s3.key,
+  secretAccessKey: Settings.trackchanges.s3.secret,
+  endpoint: Settings.trackchanges.s3.endpoint,
+  s3ForcePathStyle: Settings.trackchanges.s3.pathStyle
+})
+const S3_BUCKET = Settings.trackchanges.stores.doc_history
+
+module.exports = TrackChangesClient = {
+  flushAndGetCompressedUpdates(project_id, doc_id, callback) {
+    if (callback == null) {
+      callback = function(error, updates) {}
+    }
+    return TrackChangesClient.flushDoc(project_id, doc_id, error => {
+      if (error != null) {
+        return callback(error)
+      }
+      return TrackChangesClient.getCompressedUpdates(doc_id, callback)
+    })
+  },
+
+  flushDoc(project_id, doc_id, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return request.post(
+      {
+        url: `http://localhost:3015/project/${project_id}/doc/${doc_id}/flush`
+      },
+      (error, response, body) => {
+        response.statusCode.should.equal(204)
+        return callback(error)
+      }
+    )
+  },
+
+  flushProject(project_id, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return request.post(
+      {
+        url: `http://localhost:3015/project/${project_id}/flush`
+      },
+      (error, response, body) => {
+        response.statusCode.should.equal(204)
+        return callback(error)
+      }
+    )
+  },
+
+  getCompressedUpdates(doc_id, callback) {
+    if (callback == null) {
+      callback = function(error, updates) {}
+    }
+    return db.docHistory
+      .find({ doc_id: ObjectId(doc_id) })
+      .sort({ 'meta.end_ts': 1 })
+      .toArray(callback)
+  },
+
+  getProjectMetaData(project_id, callback) {
+    if (callback == null) {
+      callback = function(error, updates) {}
+    }
+    return db.projectHistoryMetaData.find(
+      {
+        project_id: ObjectId(project_id)
+      },
+      (error, projects) => callback(error, projects[0])
+    )
+  },
+
+  setPreserveHistoryForProject(project_id, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return db.projectHistoryMetaData.update(
+      {
+        project_id: ObjectId(project_id)
+      },
+      {
+        $set: { preserveHistory: true }
+      },
+      {
+        upsert: true
+      },
+      callback
+    )
+  },
+
+  pushRawUpdates(project_id, doc_id, updates, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return rclient.sadd(
+      Keys.docsWithHistoryOps({ project_id }),
+      doc_id,
+      error => {
+        if (error != null) {
+          return callback(error)
+        }
+        return rclient.rpush(
+          Keys.uncompressedHistoryOps({ doc_id }),
+          ...Array.from(Array.from(updates).map(u => JSON.stringify(u))),
+          callback
+        )
+      }
+    )
+  },
+
+  getDiff(project_id, doc_id, from, to, callback) {
+    if (callback == null) {
+      callback = function(error, diff) {}
+    }
+    return request.get(
+      {
+        url: `http://localhost:3015/project/${project_id}/doc/${doc_id}/diff?from=${from}&to=${to}`
+      },
+      (error, response, body) => {
+        response.statusCode.should.equal(200)
+        return callback(null, JSON.parse(body))
+      }
+    )
+  },
+
+  getUpdates(project_id, options, callback) {
+    if (callback == null) {
+      callback = function(error, body) {}
+    }
+    return request.get(
+      {
+        url: `http://localhost:3015/project/${project_id}/updates?before=${options.before}&min_count=${options.min_count}`
+      },
+      (error, response, body) => {
+        response.statusCode.should.equal(200)
+        return callback(null, JSON.parse(body))
+      }
+    )
+  },
+
+  restoreDoc(project_id, doc_id, version, user_id, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return request.post(
+      {
+        url: `http://localhost:3015/project/${project_id}/doc/${doc_id}/version/${version}/restore`,
+        headers: {
+          'X-User-Id': user_id
+        }
+      },
+      (error, response, body) => {
+        response.statusCode.should.equal(204)
+        return callback(null)
+      }
+    )
+  },
+
+  pushDocHistory(project_id, doc_id, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return request.post(
+      {
+        url: `http://localhost:3015/project/${project_id}/doc/${doc_id}/push`
+      },
+      (error, response, body) => {
+        response.statusCode.should.equal(204)
+        return callback(error)
+      }
+    )
+  },
+
+  pullDocHistory(project_id, doc_id, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return request.post(
+      {
+        url: `http://localhost:3015/project/${project_id}/doc/${doc_id}/pull`
+      },
+      (error, response, body) => {
+        response.statusCode.should.equal(204)
+        return callback(error)
+      }
+    )
+  },
+
+  waitForS3(done, retries) {
+    if (retries == null) {
+      retries = 42
+    }
+    if (!Settings.trackchanges.s3.endpoint) {
+      return done()
+    }
+
+    return request.get(`${Settings.trackchanges.s3.endpoint}/`, (err, res) => {
+      if (res && res.statusCode < 500) {
+        return done()
+      }
+
+      if (retries === 0) {
+        return done(err || new Error(`s3 returned ${res.statusCode}`))
+      }
+
+      return setTimeout(
+        () => TrackChangesClient.waitForS3(done, --retries),
+        1000
+      )
+    })
+  },
+
+  getS3Doc(project_id, doc_id, pack_id, callback) {
+    if (callback == null) {
+      callback = function(error, body) {}
+    }
+    const params = {
+      Bucket: S3_BUCKET,
+      Key: `${project_id}/changes-${doc_id}/pack-${pack_id}`
+    }
+
+    return s3.getObject(params, (error, data) => {
+      if (error != null) {
+        return callback(error)
+      }
+      const body = data.Body
+      if (body == null) {
+        return callback(new Error('empty response from s3'))
+      }
+      return zlib.gunzip(body, (err, result) => {
+        if (err != null) {
+          return callback(err)
+        }
+        return callback(null, JSON.parse(result.toString()))
+      })
+    })
+  },
+
+  removeS3Doc(project_id, doc_id, callback) {
+    if (callback == null) {
+      callback = function(error, res, body) {}
+    }
+    let params = {
+      Bucket: S3_BUCKET,
+      Prefix: `${project_id}/changes-${doc_id}`
+    }
+
+    return s3.listObjects(params, (error, data) => {
+      if (error != null) {
+        return callback(error)
+      }
+
+      params = {
+        Bucket: S3_BUCKET,
+        Delete: {
+          Objects: data.Contents.map(s3object => ({ Key: s3object.Key }))
+        }
+      }
+
+      return s3.deleteObjects(params, callback)
+    })
+  }
+}