Przeglądaj źródła

prettier: convert test/unit decaffeinated files to Prettier format

Simon Detheridge 6 lat temu
rodzic
commit
088fd29391

+ 563 - 438
services/docstore/test/unit/js/DocArchiveManagerTests.js

@@ -12,441 +12,566 @@
  * DS102: Remove unnecessary code created because of implicit returns
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-const {
-    assert
-} = require("chai");
-const sinon = require('sinon');
-const chai = require('chai');
-const should = chai.should();
-const {
-    expect
-} = chai;
-const modulePath = "../../../app/js/DocArchiveManager.js";
-const SandboxedModule = require('sandboxed-module');
-const {
-    ObjectId
-} = require("mongojs");
-const Errors = require("../../../app/js/Errors");
-const crypto = require("crypto");
-
-describe("DocArchiveManager", function() {
-
-	beforeEach(function() {
-
-		this.settings = {
-			docstore: {
-				s3: {
-					secret: "secret",
-					key: "this_key",
-					bucket:"doc-archive-unit-test"
-				}
-			}
-		};
-
-		this.request = { 
-			put: {},
-			get: {},
-			del: {}
-		};
-
-		this.archivedDocs = [{
-			_id: ObjectId(),
-			inS3:true,
-			rev: 2
-		}, {
-			_id: ObjectId(),
-			inS3:true,
-			rev: 4
-		}, {
-			_id: ObjectId(),
-			inS3:true,
-			rev: 6
-		}];
-
-		this.mongoDocs = [{
-			_id: ObjectId(),
-			lines: ["one", "two", "three"],
-			rev: 2
-		}, {
-			_id: ObjectId(),
-			lines: ["aaa", "bbb", "ccc"],
-			rev: 4
-		}, {
-			_id: ObjectId(),
-			inS3: true,
-			rev: 6
-		}, {
-			_id: ObjectId(),
-			inS3: true,
-			rev: 6
-		}, {
-			_id: ObjectId(),
-			lines: ["111", "222", "333"],
-			rev: 6
-		}];
-
-		this.unarchivedDocs = [{
-			_id: ObjectId(),
-			lines: ["wombat", "potato", "banana"],
-			rev: 2
-		}, {
-			_id: ObjectId(),
-			lines: ["llama", "turnip", "apple"],
-			rev: 4
-		}, {
-			_id: ObjectId(),
-			lines: ["elephant", "swede", "nectarine"],
-			rev: 6
-		}];
-
-		this.mixedDocs = this.archivedDocs.concat(this.unarchivedDocs);
-
-		this.MongoManager = {
-			markDocAsArchived: sinon.stub().callsArgWith(2, null),
-			upsertIntoDocCollection: sinon.stub().callsArgWith(3, null),
-			getProjectsDocs: sinon.stub().callsArgWith(3, null, this.mongoDocs),
-			getArchivedProjectDocs: sinon.stub().callsArgWith(2, null, this.mongoDocs)
-		};
-
-		this.requires = { 
-			"settings-sharelatex": this.settings,
-			"./MongoManager": this.MongoManager,
-			"request": this.request,
-			"./RangeManager": (this.RangeManager = {}),
-			"logger-sharelatex": {
-				log() {},
-				err() {}
-			}
-		};
-		this.globals =
-			{JSON};
-
-		this.error = "my errror";
-		this.project_id = ObjectId().toString();
-		this.stubbedError = new Errors.NotFoundError("Error in S3 request");
-		return this.DocArchiveManager = SandboxedModule.require(modulePath, {requires: this.requires, globals: this.globals});
-	});
-
-	describe("archiveDoc", function() {
-
-		it("should use correct options", function(done){
-			this.request.put = sinon.stub().callsArgWith(1,  null, {statusCode:200,headers:{etag:""}});
-			return this.DocArchiveManager.archiveDoc(this.project_id, this.mongoDocs[0], err=> {
-				const opts = this.request.put.args[0][0];
-				assert.deepEqual(opts.aws, {key:this.settings.docstore.s3.key, secret:this.settings.docstore.s3.secret, bucket:this.settings.docstore.s3.bucket});
-				opts.body.should.equal(JSON.stringify({
-					lines: this.mongoDocs[0].lines,
-					ranges: this.mongoDocs[0].ranges,
-					schema_v: 1
-				})
-				);
-				opts.timeout.should.equal((30*1000));
-				opts.uri.should.equal(`https://${this.settings.docstore.s3.bucket}.s3.amazonaws.com/${this.project_id}/${this.mongoDocs[0]._id}`);
-				return done();
-			});
-		});
-
-		it("should return no md5 error", function(done){
-			const data = JSON.stringify({
-				lines: this.mongoDocs[0].lines,
-				ranges: this.mongoDocs[0].ranges,
-				schema_v: 1
-			});
-			this.md5 = crypto.createHash("md5").update(data).digest("hex");
-			this.request.put = sinon.stub().callsArgWith(1,  null, {statusCode:200,headers:{etag:this.md5}});
-			return this.DocArchiveManager.archiveDoc(this.project_id, this.mongoDocs[0], err=> {
-				should.not.exist(err);
-				return done();
-			});
-		});
-
-		return it("should return the error", function(done){
-			this.request.put = sinon.stub().callsArgWith(1, this.stubbedError, {statusCode:400,headers:{etag:""}});
-			return this.DocArchiveManager.archiveDoc(this.project_id, this.mongoDocs[0], err=> {
-				should.exist(err);
-				return done();
-			});
-		});
-	});
-
-	describe("unarchiveDoc", function() {
-
-		it("should use correct options", function(done){
-			this.request.get = sinon.stub().callsArgWith(1, null, {statusCode:200}, this.mongoDocs[0].lines);
-			this.request.del = sinon.stub().callsArgWith(1, null, {statusCode:204}, {});
-			return this.DocArchiveManager.unarchiveDoc(this.project_id, this.mongoDocs[0]._id, err=> {
-				const opts = this.request.get.args[0][0];
-				assert.deepEqual(opts.aws, {key:this.settings.docstore.s3.key, secret:this.settings.docstore.s3.secret, bucket:this.settings.docstore.s3.bucket});
-				opts.json.should.equal(true);
-				opts.timeout.should.equal((30*1000));
-				opts.uri.should.equal(`https://${this.settings.docstore.s3.bucket}.s3.amazonaws.com/${this.project_id}/${this.mongoDocs[0]._id}`);
-				return done();
-			});
-		});
-
-		it("should return the error", function(done){
-			this.request.get = sinon.stub().callsArgWith(1, this.stubbedError, {}, {});
-			return this.DocArchiveManager.unarchiveDoc(this.project_id, this.mongoDocs[0], err=> {
-				should.exist(err);
-				return done();
-			});
-		});
-
-		return it("should error if the doc lines are a string not an array", function(done){
-			this.request.get = sinon.stub().callsArgWith(1, null, {statusCode:200}, "this is a string");
-			this.request.del = sinon.stub();
-			return this.DocArchiveManager.unarchiveDoc(this.project_id, this.mongoDocs[0], err=> {
-				should.exist(err);
-				this.request.del.called.should.equal(false);
-				return done();
-			});
-		});
-	});
-
-	describe("archiveAllDocs", function() {
-
-		it("should archive all project docs which are not in s3", function(done){
-			this.MongoManager.getProjectsDocs = sinon.stub().callsArgWith(3, null, this.mongoDocs);
-			this.DocArchiveManager.archiveDoc = sinon.stub().callsArgWith(2, null);
-
-			return this.DocArchiveManager.archiveAllDocs(this.project_id, err=> {
-				this.DocArchiveManager.archiveDoc.calledWith(this.project_id, this.mongoDocs[0]).should.equal(true);
-				this.DocArchiveManager.archiveDoc.calledWith(this.project_id, this.mongoDocs[1]).should.equal(true);
-				this.DocArchiveManager.archiveDoc.calledWith(this.project_id, this.mongoDocs[4]).should.equal(true);
-
-				this.DocArchiveManager.archiveDoc.calledWith(this.project_id, this.mongoDocs[2]).should.equal(false);
-				this.DocArchiveManager.archiveDoc.calledWith(this.project_id, this.mongoDocs[3]).should.equal(false);
-				
-				should.not.exist(err);
-				return done();
-			});
-		});
-
-		it("should return error if have no docs", function(done){
-			this.MongoManager.getProjectsDocs = sinon.stub().callsArgWith(3, null, null);
-
-			return this.DocArchiveManager.archiveAllDocs(this.project_id, err=> {
-				should.exist(err);
-				return done();
-			});
-		});
-
-		it("should return the error", function(done){
-			this.MongoManager.getProjectsDocs = sinon.stub().callsArgWith(3, this.error, null);
-
-			return this.DocArchiveManager.archiveAllDocs(this.project_id, err=> {
-				err.should.equal(this.error);
-				return done();
-			});
-		});
-
-		return describe("when most have been already put in s3", function() {
-
-			beforeEach(function() {
-				let numberOfDocs = 10 * 1000;
-				this.mongoDocs = [];
-				while (--numberOfDocs !== 0) {
-					this.mongoDocs.push({inS3:true, _id: ObjectId()});
-				}
-
-				this.MongoManager.getProjectsDocs = sinon.stub().callsArgWith(3, null, this.mongoDocs);
-				return this.DocArchiveManager.archiveDoc = sinon.stub().callsArgWith(2, null);
-			});	
-
-			return it("should not throw and error", function(done){
-				return this.DocArchiveManager.archiveAllDocs(this.project_id, err=> {
-					should.not.exist(err);
-					return done();
-				});
-			});
-		});
-	});
-
-
-	describe("unArchiveAllDocs", function() {
-
-		it("should unarchive all inS3 docs", function(done){
-			this.MongoManager.getArchivedProjectDocs = sinon.stub().callsArgWith(1, null, this.archivedDocs);
-			this.DocArchiveManager.unarchiveDoc = sinon.stub().callsArgWith(2, null);
-			return this.DocArchiveManager.unArchiveAllDocs(this.project_id, err=> {
-				for (const doc of Array.from(this.archivedDocs)) {
-					this.DocArchiveManager.unarchiveDoc.calledWith(this.project_id, doc._id).should.equal(true);
-				}
-				should.not.exist(err);
-				return done();
-			});
-		});
-
-		it("should return error if have no docs", function(done){
-			this.MongoManager.getArchivedProjectDocs = sinon.stub().callsArgWith(1, null, null);
-			return this.DocArchiveManager.unArchiveAllDocs(this.project_id, err=> {
-				should.exist(err);
-				return done();
-			});
-		});
-
-		return it("should return the error", function(done){
-			this.MongoManager.getArchivedProjectDocs = sinon.stub().callsArgWith(1, this.error, null);
-			return this.DocArchiveManager.unArchiveAllDocs(this.project_id, err=> {
-				err.should.equal(this.error);
-				return done();
-			});
-		});
-	});
-
-	describe("destroyAllDocs", function() {
-		beforeEach(function() {
-			this.request.del = sinon.stub().callsArgWith(1, null, {statusCode:204}, {});
-			this.MongoManager.getProjectsDocs = sinon.stub().callsArgWith(3, null, this.mixedDocs);
-			this.MongoManager.findDoc = sinon.stub().callsArgWith(3, null, null);
-			this.MongoManager.destroyDoc = sinon.stub().yields();
-			return Array.from(this.mixedDocs).map((doc) =>
-				this.MongoManager.findDoc.withArgs(this.project_id, doc._id).callsArgWith(3, null, doc));
-		});
-
-		it("should destroy all the docs", function(done){
-			this.DocArchiveManager.destroyDoc = sinon.stub().callsArgWith(2, null);
-			return this.DocArchiveManager.destroyAllDocs(this.project_id, err=> {
-				for (const doc of Array.from(this.mixedDocs)) {
-					this.DocArchiveManager.destroyDoc.calledWith(this.project_id, doc._id).should.equal(true);
-				}
-				should.not.exist(err);
-				return done();
-			});
-		});
-
-		it("should only the s3 docs from s3", function(done){
-			const docOpts = doc => {
-				return JSON.parse(JSON.stringify({
-					aws: {key:this.settings.docstore.s3.key, secret:this.settings.docstore.s3.secret, bucket:this.settings.docstore.s3.bucket},
-					json: true,
-					timeout: 30 * 1000,
-					uri:`https://${this.settings.docstore.s3.bucket}.s3.amazonaws.com/${this.project_id}/${doc._id}`
-				}));
-			};
-
-			return this.DocArchiveManager.destroyAllDocs(this.project_id, err=> {
-				let doc;
-				expect(err).not.to.exist;
-
-				for (doc of Array.from(this.archivedDocs)) {
-					sinon.assert.calledWith(this.request.del, docOpts(doc));
-				}
-				for (doc of Array.from(this.unarchivedDocs)) {
-					expect(this.request.del.calledWith(docOpts(doc))).to.equal(false);
-				}  // no notCalledWith
-
-				return done();
-			});
-		});
-
-		return it("should remove the docs from mongo", function(done){
-			this.DocArchiveManager.destroyAllDocs(this.project_id, err=> {
-				return expect(err).not.to.exist;
-			});
-
-			for (const doc of Array.from(this.mixedDocs)) {
-				sinon.assert.calledWith(this.MongoManager.destroyDoc, doc._id);
-			}
-
-			return done();
-		});
-	});
-	
-	describe("_s3DocToMongoDoc", function() {
-		describe("with the old schema", function() { return it("should return the docs lines", function(done) {
-            return this.DocArchiveManager._s3DocToMongoDoc(["doc", "lines"], (error, doc) => {
-                expect(doc).to.deep.equal({
-                    lines: ["doc", "lines"]
-                });
-                return done();
-            });
-        }); });
-		
-		describe("with the new schema", function() {
-			it("should return the doc lines and ranges", function(done) {
-				this.RangeManager.jsonRangesToMongo = sinon.stub().returns({"mongo": "ranges"});
-				return this.DocArchiveManager._s3DocToMongoDoc({
-					lines: ["doc", "lines"],
-					ranges: {"json": "ranges"},
-					schema_v: 1
-				}, (error, doc) => {
-					expect(doc).to.deep.equal({
-						lines: ["doc", "lines"],
-						ranges: {"mongo": "ranges"}
-					});
-					return done();
-				});
-			});
-					
-			return it("should return just the doc lines when there are no ranges", function(done) {
-				return this.DocArchiveManager._s3DocToMongoDoc({
-					lines: ["doc", "lines"],
-					schema_v: 1
-				}, (error, doc) => {
-					expect(doc).to.deep.equal({
-						lines: ["doc", "lines"]
-					});
-					return done();
-				});
-			});
-		});
-		
-		return describe("with an unrecognised schema", function() { return it("should return an error", function(done) {
-            return this.DocArchiveManager._s3DocToMongoDoc({
-                schema_v: 2
-            }, (error, doc) => {
-                expect(error).to.exist;
-                return done();
-            });
-        }); });
-	});
-	
-	return describe("_mongoDocToS3Doc", function() {
-		describe("with a valid doc", function() { return it("should return the json version", function(done) {
-            let doc;
-            return this.DocArchiveManager._mongoDocToS3Doc((doc = {
-                lines: ["doc", "lines"],
-                ranges: { "mock": "ranges" }
-            }), (err, s3_doc) => {
-                expect(s3_doc).to.equal(JSON.stringify({
-                    lines: ["doc", "lines"],
-                    ranges: { "mock": "ranges" },
-                    schema_v: 1
-                })
-                );
-                return done();
-            });
-        }); });
-			
-		describe("with null bytes in the result", function() {
-			beforeEach(function() {
-				this._stringify = JSON.stringify;
-				return JSON.stringify = sinon.stub().returns('{"bad": "\u0000"}');
-			});
-			
-			afterEach(function() {
-				return JSON.stringify = this._stringify;
-			});
-				
-			return it("should return an error", function(done) {
-				return this.DocArchiveManager._mongoDocToS3Doc({
-					lines: ["doc", "lines"],
-					ranges: { "mock": "ranges" }
-				}, (err, s3_doc) => {
-					expect(err).to.exist;
-					return done();
-				});
-			});
-		});
-		
-		return describe("without doc lines", function() { return it("should return an error", function(done) {
-            return this.DocArchiveManager._mongoDocToS3Doc({}, (err, s3_doc) => {
-                expect(err).to.exist;
-                return done();
-            });
-        }); });
-	});
-});
-			
-			
+const { assert } = require('chai')
+const sinon = require('sinon')
+const chai = require('chai')
+const should = chai.should()
+const { expect } = chai
+const modulePath = '../../../app/js/DocArchiveManager.js'
+const SandboxedModule = require('sandboxed-module')
+const { ObjectId } = require('mongojs')
+const Errors = require('../../../app/js/Errors')
+const crypto = require('crypto')
+
+describe('DocArchiveManager', function() {
+  beforeEach(function() {
+    this.settings = {
+      docstore: {
+        s3: {
+          secret: 'secret',
+          key: 'this_key',
+          bucket: 'doc-archive-unit-test'
+        }
+      }
+    }
+
+    this.request = {
+      put: {},
+      get: {},
+      del: {}
+    }
+
+    this.archivedDocs = [
+      {
+        _id: ObjectId(),
+        inS3: true,
+        rev: 2
+      },
+      {
+        _id: ObjectId(),
+        inS3: true,
+        rev: 4
+      },
+      {
+        _id: ObjectId(),
+        inS3: true,
+        rev: 6
+      }
+    ]
+
+    this.mongoDocs = [
+      {
+        _id: ObjectId(),
+        lines: ['one', 'two', 'three'],
+        rev: 2
+      },
+      {
+        _id: ObjectId(),
+        lines: ['aaa', 'bbb', 'ccc'],
+        rev: 4
+      },
+      {
+        _id: ObjectId(),
+        inS3: true,
+        rev: 6
+      },
+      {
+        _id: ObjectId(),
+        inS3: true,
+        rev: 6
+      },
+      {
+        _id: ObjectId(),
+        lines: ['111', '222', '333'],
+        rev: 6
+      }
+    ]
+
+    this.unarchivedDocs = [
+      {
+        _id: ObjectId(),
+        lines: ['wombat', 'potato', 'banana'],
+        rev: 2
+      },
+      {
+        _id: ObjectId(),
+        lines: ['llama', 'turnip', 'apple'],
+        rev: 4
+      },
+      {
+        _id: ObjectId(),
+        lines: ['elephant', 'swede', 'nectarine'],
+        rev: 6
+      }
+    ]
+
+    this.mixedDocs = this.archivedDocs.concat(this.unarchivedDocs)
+
+    this.MongoManager = {
+      markDocAsArchived: sinon.stub().callsArgWith(2, null),
+      upsertIntoDocCollection: sinon.stub().callsArgWith(3, null),
+      getProjectsDocs: sinon.stub().callsArgWith(3, null, this.mongoDocs),
+      getArchivedProjectDocs: sinon.stub().callsArgWith(2, null, this.mongoDocs)
+    }
+
+    this.requires = {
+      'settings-sharelatex': this.settings,
+      './MongoManager': this.MongoManager,
+      request: this.request,
+      './RangeManager': (this.RangeManager = {}),
+      'logger-sharelatex': {
+        log() {},
+        err() {}
+      }
+    }
+    this.globals = { JSON }
+
+    this.error = 'my errror'
+    this.project_id = ObjectId().toString()
+    this.stubbedError = new Errors.NotFoundError('Error in S3 request')
+    return (this.DocArchiveManager = SandboxedModule.require(modulePath, {
+      requires: this.requires,
+      globals: this.globals
+    }))
+  })
+
+  describe('archiveDoc', function() {
+    it('should use correct options', function(done) {
+      this.request.put = sinon
+        .stub()
+        .callsArgWith(1, null, { statusCode: 200, headers: { etag: '' } })
+      return this.DocArchiveManager.archiveDoc(
+        this.project_id,
+        this.mongoDocs[0],
+        err => {
+          const opts = this.request.put.args[0][0]
+          assert.deepEqual(opts.aws, {
+            key: this.settings.docstore.s3.key,
+            secret: this.settings.docstore.s3.secret,
+            bucket: this.settings.docstore.s3.bucket
+          })
+          opts.body.should.equal(
+            JSON.stringify({
+              lines: this.mongoDocs[0].lines,
+              ranges: this.mongoDocs[0].ranges,
+              schema_v: 1
+            })
+          )
+          opts.timeout.should.equal(30 * 1000)
+          opts.uri.should.equal(
+            `https://${this.settings.docstore.s3.bucket}.s3.amazonaws.com/${this.project_id}/${this.mongoDocs[0]._id}`
+          )
+          return done()
+        }
+      )
+    })
+
+    it('should return no md5 error', function(done) {
+      const data = JSON.stringify({
+        lines: this.mongoDocs[0].lines,
+        ranges: this.mongoDocs[0].ranges,
+        schema_v: 1
+      })
+      this.md5 = crypto
+        .createHash('md5')
+        .update(data)
+        .digest('hex')
+      this.request.put = sinon
+        .stub()
+        .callsArgWith(1, null, { statusCode: 200, headers: { etag: this.md5 } })
+      return this.DocArchiveManager.archiveDoc(
+        this.project_id,
+        this.mongoDocs[0],
+        err => {
+          should.not.exist(err)
+          return done()
+        }
+      )
+    })
+
+    return it('should return the error', function(done) {
+      this.request.put = sinon.stub().callsArgWith(1, this.stubbedError, {
+        statusCode: 400,
+        headers: { etag: '' }
+      })
+      return this.DocArchiveManager.archiveDoc(
+        this.project_id,
+        this.mongoDocs[0],
+        err => {
+          should.exist(err)
+          return done()
+        }
+      )
+    })
+  })
+
+  describe('unarchiveDoc', function() {
+    it('should use correct options', function(done) {
+      this.request.get = sinon
+        .stub()
+        .callsArgWith(1, null, { statusCode: 200 }, this.mongoDocs[0].lines)
+      this.request.del = sinon
+        .stub()
+        .callsArgWith(1, null, { statusCode: 204 }, {})
+      return this.DocArchiveManager.unarchiveDoc(
+        this.project_id,
+        this.mongoDocs[0]._id,
+        err => {
+          const opts = this.request.get.args[0][0]
+          assert.deepEqual(opts.aws, {
+            key: this.settings.docstore.s3.key,
+            secret: this.settings.docstore.s3.secret,
+            bucket: this.settings.docstore.s3.bucket
+          })
+          opts.json.should.equal(true)
+          opts.timeout.should.equal(30 * 1000)
+          opts.uri.should.equal(
+            `https://${this.settings.docstore.s3.bucket}.s3.amazonaws.com/${this.project_id}/${this.mongoDocs[0]._id}`
+          )
+          return done()
+        }
+      )
+    })
+
+    it('should return the error', function(done) {
+      this.request.get = sinon.stub().callsArgWith(1, this.stubbedError, {}, {})
+      return this.DocArchiveManager.unarchiveDoc(
+        this.project_id,
+        this.mongoDocs[0],
+        err => {
+          should.exist(err)
+          return done()
+        }
+      )
+    })
+
+    return it('should error if the doc lines are a string not an array', function(done) {
+      this.request.get = sinon
+        .stub()
+        .callsArgWith(1, null, { statusCode: 200 }, 'this is a string')
+      this.request.del = sinon.stub()
+      return this.DocArchiveManager.unarchiveDoc(
+        this.project_id,
+        this.mongoDocs[0],
+        err => {
+          should.exist(err)
+          this.request.del.called.should.equal(false)
+          return done()
+        }
+      )
+    })
+  })
+
+  describe('archiveAllDocs', function() {
+    it('should archive all project docs which are not in s3', function(done) {
+      this.MongoManager.getProjectsDocs = sinon
+        .stub()
+        .callsArgWith(3, null, this.mongoDocs)
+      this.DocArchiveManager.archiveDoc = sinon.stub().callsArgWith(2, null)
+
+      return this.DocArchiveManager.archiveAllDocs(this.project_id, err => {
+        this.DocArchiveManager.archiveDoc
+          .calledWith(this.project_id, this.mongoDocs[0])
+          .should.equal(true)
+        this.DocArchiveManager.archiveDoc
+          .calledWith(this.project_id, this.mongoDocs[1])
+          .should.equal(true)
+        this.DocArchiveManager.archiveDoc
+          .calledWith(this.project_id, this.mongoDocs[4])
+          .should.equal(true)
+
+        this.DocArchiveManager.archiveDoc
+          .calledWith(this.project_id, this.mongoDocs[2])
+          .should.equal(false)
+        this.DocArchiveManager.archiveDoc
+          .calledWith(this.project_id, this.mongoDocs[3])
+          .should.equal(false)
+
+        should.not.exist(err)
+        return done()
+      })
+    })
+
+    it('should return error if have no docs', function(done) {
+      this.MongoManager.getProjectsDocs = sinon
+        .stub()
+        .callsArgWith(3, null, null)
+
+      return this.DocArchiveManager.archiveAllDocs(this.project_id, err => {
+        should.exist(err)
+        return done()
+      })
+    })
+
+    it('should return the error', function(done) {
+      this.MongoManager.getProjectsDocs = sinon
+        .stub()
+        .callsArgWith(3, this.error, null)
+
+      return this.DocArchiveManager.archiveAllDocs(this.project_id, err => {
+        err.should.equal(this.error)
+        return done()
+      })
+    })
+
+    return describe('when most have been already put in s3', function() {
+      beforeEach(function() {
+        let numberOfDocs = 10 * 1000
+        this.mongoDocs = []
+        while (--numberOfDocs !== 0) {
+          this.mongoDocs.push({ inS3: true, _id: ObjectId() })
+        }
+
+        this.MongoManager.getProjectsDocs = sinon
+          .stub()
+          .callsArgWith(3, null, this.mongoDocs)
+        return (this.DocArchiveManager.archiveDoc = sinon
+          .stub()
+          .callsArgWith(2, null))
+      })
+
+      return it('should not throw and error', function(done) {
+        return this.DocArchiveManager.archiveAllDocs(this.project_id, err => {
+          should.not.exist(err)
+          return done()
+        })
+      })
+    })
+  })
+
+  describe('unArchiveAllDocs', function() {
+    it('should unarchive all inS3 docs', function(done) {
+      this.MongoManager.getArchivedProjectDocs = sinon
+        .stub()
+        .callsArgWith(1, null, this.archivedDocs)
+      this.DocArchiveManager.unarchiveDoc = sinon.stub().callsArgWith(2, null)
+      return this.DocArchiveManager.unArchiveAllDocs(this.project_id, err => {
+        for (const doc of Array.from(this.archivedDocs)) {
+          this.DocArchiveManager.unarchiveDoc
+            .calledWith(this.project_id, doc._id)
+            .should.equal(true)
+        }
+        should.not.exist(err)
+        return done()
+      })
+    })
+
+    it('should return error if have no docs', function(done) {
+      this.MongoManager.getArchivedProjectDocs = sinon
+        .stub()
+        .callsArgWith(1, null, null)
+      return this.DocArchiveManager.unArchiveAllDocs(this.project_id, err => {
+        should.exist(err)
+        return done()
+      })
+    })
+
+    return it('should return the error', function(done) {
+      this.MongoManager.getArchivedProjectDocs = sinon
+        .stub()
+        .callsArgWith(1, this.error, null)
+      return this.DocArchiveManager.unArchiveAllDocs(this.project_id, err => {
+        err.should.equal(this.error)
+        return done()
+      })
+    })
+  })
+
+  describe('destroyAllDocs', function() {
+    beforeEach(function() {
+      this.request.del = sinon
+        .stub()
+        .callsArgWith(1, null, { statusCode: 204 }, {})
+      this.MongoManager.getProjectsDocs = sinon
+        .stub()
+        .callsArgWith(3, null, this.mixedDocs)
+      this.MongoManager.findDoc = sinon.stub().callsArgWith(3, null, null)
+      this.MongoManager.destroyDoc = sinon.stub().yields()
+      return Array.from(this.mixedDocs).map(doc =>
+        this.MongoManager.findDoc
+          .withArgs(this.project_id, doc._id)
+          .callsArgWith(3, null, doc)
+      )
+    })
+
+    it('should destroy all the docs', function(done) {
+      this.DocArchiveManager.destroyDoc = sinon.stub().callsArgWith(2, null)
+      return this.DocArchiveManager.destroyAllDocs(this.project_id, err => {
+        for (const doc of Array.from(this.mixedDocs)) {
+          this.DocArchiveManager.destroyDoc
+            .calledWith(this.project_id, doc._id)
+            .should.equal(true)
+        }
+        should.not.exist(err)
+        return done()
+      })
+    })
+
+    it('should only the s3 docs from s3', function(done) {
+      const docOpts = doc => {
+        return JSON.parse(
+          JSON.stringify({
+            aws: {
+              key: this.settings.docstore.s3.key,
+              secret: this.settings.docstore.s3.secret,
+              bucket: this.settings.docstore.s3.bucket
+            },
+            json: true,
+            timeout: 30 * 1000,
+            uri: `https://${this.settings.docstore.s3.bucket}.s3.amazonaws.com/${this.project_id}/${doc._id}`
+          })
+        )
+      }
+
+      return this.DocArchiveManager.destroyAllDocs(this.project_id, err => {
+        let doc
+        expect(err).not.to.exist
+
+        for (doc of Array.from(this.archivedDocs)) {
+          sinon.assert.calledWith(this.request.del, docOpts(doc))
+        }
+        for (doc of Array.from(this.unarchivedDocs)) {
+          expect(this.request.del.calledWith(docOpts(doc))).to.equal(false)
+        } // no notCalledWith
+
+        return done()
+      })
+    })
+
+    return it('should remove the docs from mongo', function(done) {
+      this.DocArchiveManager.destroyAllDocs(this.project_id, err => {
+        return expect(err).not.to.exist
+      })
+
+      for (const doc of Array.from(this.mixedDocs)) {
+        sinon.assert.calledWith(this.MongoManager.destroyDoc, doc._id)
+      }
+
+      return done()
+    })
+  })
+
+  describe('_s3DocToMongoDoc', function() {
+    describe('with the old schema', function() {
+      return it('should return the docs lines', function(done) {
+        return this.DocArchiveManager._s3DocToMongoDoc(
+          ['doc', 'lines'],
+          (error, doc) => {
+            expect(doc).to.deep.equal({
+              lines: ['doc', 'lines']
+            })
+            return done()
+          }
+        )
+      })
+    })
+
+    describe('with the new schema', function() {
+      it('should return the doc lines and ranges', function(done) {
+        this.RangeManager.jsonRangesToMongo = sinon
+          .stub()
+          .returns({ mongo: 'ranges' })
+        return this.DocArchiveManager._s3DocToMongoDoc(
+          {
+            lines: ['doc', 'lines'],
+            ranges: { json: 'ranges' },
+            schema_v: 1
+          },
+          (error, doc) => {
+            expect(doc).to.deep.equal({
+              lines: ['doc', 'lines'],
+              ranges: { mongo: 'ranges' }
+            })
+            return done()
+          }
+        )
+      })
+
+      return it('should return just the doc lines when there are no ranges', function(done) {
+        return this.DocArchiveManager._s3DocToMongoDoc(
+          {
+            lines: ['doc', 'lines'],
+            schema_v: 1
+          },
+          (error, doc) => {
+            expect(doc).to.deep.equal({
+              lines: ['doc', 'lines']
+            })
+            return done()
+          }
+        )
+      })
+    })
+
+    return describe('with an unrecognised schema', function() {
+      return it('should return an error', function(done) {
+        return this.DocArchiveManager._s3DocToMongoDoc(
+          {
+            schema_v: 2
+          },
+          (error, doc) => {
+            expect(error).to.exist
+            return done()
+          }
+        )
+      })
+    })
+  })
+
+  return describe('_mongoDocToS3Doc', function() {
+    describe('with a valid doc', function() {
+      return it('should return the json version', function(done) {
+        let doc
+        return this.DocArchiveManager._mongoDocToS3Doc(
+          (doc = {
+            lines: ['doc', 'lines'],
+            ranges: { mock: 'ranges' }
+          }),
+          (err, s3_doc) => {
+            expect(s3_doc).to.equal(
+              JSON.stringify({
+                lines: ['doc', 'lines'],
+                ranges: { mock: 'ranges' },
+                schema_v: 1
+              })
+            )
+            return done()
+          }
+        )
+      })
+    })
+
+    describe('with null bytes in the result', function() {
+      beforeEach(function() {
+        this._stringify = JSON.stringify
+        return (JSON.stringify = sinon.stub().returns('{"bad": "\u0000"}'))
+      })
+
+      afterEach(function() {
+        return (JSON.stringify = this._stringify)
+      })
+
+      return it('should return an error', function(done) {
+        return this.DocArchiveManager._mongoDocToS3Doc(
+          {
+            lines: ['doc', 'lines'],
+            ranges: { mock: 'ranges' }
+          },
+          (err, s3_doc) => {
+            expect(err).to.exist
+            return done()
+          }
+        )
+      })
+    })
+
+    return describe('without doc lines', function() {
+      return it('should return an error', function(done) {
+        return this.DocArchiveManager._mongoDocToS3Doc({}, (err, s3_doc) => {
+          expect(err).to.exist
+          return done()
+        })
+      })
+    })
+  })
+})

+ 793 - 532
services/docstore/test/unit/js/DocManagerTests.js

@@ -12,535 +12,796 @@
  * DS102: Remove unnecessary code created because of implicit returns
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-const SandboxedModule = require('sandboxed-module');
-const sinon = require('sinon');
-const chai = require('chai');
-const {
-    assert
-} = require("chai");
-chai.should();
-const {
-    expect
-} = chai;
-const modulePath = require('path').join(__dirname, '../../../app/js/DocManager');
-const {
-    ObjectId
-} = require("mongojs");
-const Errors = require("../../../app/js/Errors");
-
-describe("DocManager", function() {
-	beforeEach(function() {
-		this.DocManager = SandboxedModule.require(modulePath, { requires: {
-			"./MongoManager": (this.MongoManager = {}),
-			"./DocArchiveManager": (this.DocArchiveManager = {}),
-			"./RangeManager": (this.RangeManager = {
-				jsonRangesToMongo(r) { return r; },
-				shouldUpdateRanges: sinon.stub().returns(false)
-			}),
-			"logger-sharelatex": (this.logger = { 
-				log: sinon.stub(),
-				warn() {},
-				err() {}
-			})
-		}
-	}
-		);
-		this.doc_id = ObjectId().toString();
-		this.project_id = ObjectId().toString();
-		this.another_project_id = ObjectId().toString();
-		this.callback = sinon.stub();
-		return this.stubbedError = new Error("blew up");
-	});
-
-
-	describe("checkDocExists", function() {
-		beforeEach(function() {
-			return this.DocManager._getDoc = sinon.stub();
-		});
-
-		it("should call get doc with a quick filter", function(done){
-			this.DocManager._getDoc.callsArgWith(3, null, {_id:this.doc_id});
-			return this.DocManager.checkDocExists(this.project_id, this.doc_id, (err, exist)=> {
-				exist.should.equal(true);
-				this.DocManager._getDoc.calledWith(this.project_id, this.doc_id, {_id:1, inS3:true}).should.equal(true);
-				return done();
-			});
-		});
-
-		it("should return false when doc is not there", function(done){
-			this.DocManager._getDoc.callsArgWith(3, null);
-			return this.DocManager.checkDocExists(this.project_id, this.doc_id, (err, exist)=> {
-				exist.should.equal(false);
-				return done();
-			});
-		});
-
-		return it("should return error when get doc errors", function(done){
-			this.DocManager._getDoc.callsArgWith(3, "error");
-			return this.DocManager.checkDocExists(this.project_id, this.doc_id, (err, exist)=> {
-				err.should.equal("error");
-				return done();
-			});
-		});
-	});
-
-	describe("getFullDoc", function() {
-		beforeEach(function() {
-			this.DocManager._getDoc = sinon.stub();
-			return this.doc = { 
-				_id: this.doc_id,
-				lines:["2134"]
-			};});
-
-		it("should call get doc with a quick filter", function(done){
-			this.DocManager._getDoc.callsArgWith(3, null, this.doc);
-			return this.DocManager.getFullDoc(this.project_id, this.doc_id, (err, doc)=> {
-				doc.should.equal(this.doc);
-				this.DocManager._getDoc.calledWith(this.project_id, this.doc_id, {lines: true, rev: true, deleted: true, version: true, ranges: true, inS3:true}).should.equal(true);
-				return done();
-			});
-		});
-	
-		return it("should return error when get doc errors", function(done){
-			this.DocManager._getDoc.callsArgWith(3, "error");
-			return this.DocManager.getFullDoc(this.project_id, this.doc_id, (err, exist)=> {
-				err.should.equal("error");
-				return done();
-			});
-		});
-	});
-
-	describe("getRawDoc", function() {
-
-		beforeEach(function() {
-			this.DocManager._getDoc = sinon.stub();
-			return this.doc = 
-				{lines:["2134"]};});
-
-		it("should call get doc with a quick filter", function(done){
-			this.DocManager._getDoc.callsArgWith(3, null, this.doc);
-			return this.DocManager.getDocLines(this.project_id, this.doc_id, (err, doc)=> {
-				doc.should.equal(this.doc);
-				this.DocManager._getDoc.calledWith(this.project_id, this.doc_id, {lines: true, inS3:true}).should.equal(true);
-				return done();
-			});
-		});
-	
-		return it("should return error when get doc errors", function(done){
-			this.DocManager._getDoc.callsArgWith(3, "error");
-			return this.DocManager.getDocLines(this.project_id, this.doc_id, (err, exist)=> {
-				err.should.equal("error");
-				return done();
-			});
-		});
-	});
-
-
-
-	describe("getDoc", function() {
-		beforeEach(function() {
-			this.project = { name: "mock-project" };
-			this.doc = { _id: this.doc_id, project_id: this.project_id, lines: ["mock-lines"] };
-			this.version = 42;
-			this.MongoManager.findDoc = sinon.stub();
-			return this.MongoManager.getDocVersion = sinon.stub().yields(null, this.version);
-		});
-
-		describe("when using a filter", function() {
-			beforeEach(function() {
-				return this.MongoManager.findDoc.yields(null, this.doc);
-			});
-
-			it("should error if inS3 is not set to true", function(done){
-				return this.DocManager._getDoc(this.project_id, this.doc_id, {inS3: false}, (err) => {
-					expect(err).to.exist;
-					return done();
-				});
-			});
-
-			it("should always get inS3 even when no filter is passed", function(done){
-				return this.DocManager._getDoc(this.project_id, this.doc_id, undefined, err=> {
-					this.MongoManager.findDoc.called.should.equal(false);
-					expect(err).to.exist;
-					return done();
-				});
-			});
-
-			return it("should not error if inS3 is set to true", function(done){
-				return this.DocManager._getDoc(this.project_id, this.doc_id, {inS3: true}, (err) => {
-					expect(err).to.not.exist;
-					return done();
-				});
-			});
-		});
-
-		describe("when the doc is in the doc collection", function() {
-			beforeEach(function() {
-				this.MongoManager.findDoc.yields(null, this.doc);
-				return this.DocManager._getDoc(this.project_id, this.doc_id, {version: true, inS3:true}, this.callback);
-			});
-
-			it("should get the doc from the doc collection", function() {
-				return this.MongoManager.findDoc
-					.calledWith(this.project_id, this.doc_id)
-					.should.equal(true);
-			});
-
-			it("should get the doc version from the docOps collection", function() {
-				return this.MongoManager.getDocVersion
-					.calledWith(this.doc_id)
-					.should.equal(true);
-			});
-			
-			return it("should return the callback with the doc with the version", function() {
-				this.callback.called.should.equal(true);
-				const doc = this.callback.args[0][1];
-				doc.lines.should.equal(this.doc.lines);
-				return doc.version.should.equal(this.version);
-			});
-		});
-
-		describe("without the version filter", function() {
-			beforeEach(function() {
-				this.MongoManager.findDoc.yields(null, this.doc);
-				return this.DocManager._getDoc(this.project_id, this.doc_id, {version: false, inS3:true}, this.callback);
-			});
-
-			return it("should not get the doc version from the docOps collection", function() {
-				return this.MongoManager.getDocVersion.called.should.equal(false);
-			});
-		});
-		
-		describe("when MongoManager.findDoc errors", function() {
-			beforeEach(function() {
-				this.MongoManager.findDoc.yields(this.stubbedError);
-				return this.DocManager._getDoc(this.project_id, this.doc_id, {version: true, inS3:true}, this.callback);
-			});
-
-			return it("should return the error", function() {
-				return this.callback.calledWith(this.stubbedError).should.equal(true);
-			});
-		});
-
-		describe("when the doc is archived", function() {
-			beforeEach(function() { 
-				this.doc = { _id: this.doc_id, project_id: this.project_id, lines: ["mock-lines"], inS3: true };
-				this.MongoManager.findDoc.yields(null, this.doc);
-				this.DocArchiveManager.unarchiveDoc = (project_id, doc_id, callback) => {
-					this.doc.inS3 = false;
-					return callback();
-				};
-				sinon.spy(this.DocArchiveManager, "unarchiveDoc");
-				return this.DocManager._getDoc(this.project_id, this.doc_id, {version: true, inS3:true}, this.callback);
-			});
-
-			it("should call the DocArchive to unarchive the doc", function() {
-				return this.DocArchiveManager.unarchiveDoc.calledWith(this.project_id, this.doc_id).should.equal(true);
-			});
-			
-			it("should look up the doc twice", function() {
-				return this.MongoManager.findDoc.calledTwice.should.equal(true);
-			});
-
-			return it("should return the doc", function() {
-				return this.callback.calledWith(null, this.doc).should.equal(true);
-			});
-		});
-
-		return describe("when the doc does not exist in the docs collection", function() {
-			beforeEach(function() { 
-				this.MongoManager.findDoc = sinon.stub().yields(null, null);
-				return this.DocManager._getDoc(this.project_id, this.doc_id, {version: true, inS3:true}, this.callback);
-			});
-
-			return it("should return a NotFoundError", function() {
-				return this.callback
-					.calledWith(sinon.match.has('message', `No such doc: ${this.doc_id} in project ${this.project_id}`))
-					.should.equal(true);
-			});
-		});
-	});
-
-	describe("getAllNonDeletedDocs", function() {
-		describe("when the project exists", function() {
-			beforeEach(function() { 
-				this.docs = [{ _id: this.doc_id, project_id: this.project_id, lines: ["mock-lines"] }];
-				this.MongoManager.getProjectsDocs = sinon.stub().callsArgWith(3, null, this.docs);
-				this.DocArchiveManager.unArchiveAllDocs = sinon.stub().callsArgWith(1, null, this.docs);
-				this.filter = { lines: true };
-				return this.DocManager.getAllNonDeletedDocs(this.project_id, this.filter, this.callback);
-			});
-
-			it("should get the project from the database", function() {
-				return this.MongoManager.getProjectsDocs
-					.calledWith(this.project_id, {include_deleted: false}, this.filter)
-					.should.equal(true);
-			});
-
-			return it("should return the docs", function() {
-				return this.callback.calledWith(null, this.docs).should.equal(true);
-			});
-		});
-
-		return describe("when there are no docs for the project", function() {
-			beforeEach(function() { 
-				this.MongoManager.getProjectsDocs = sinon.stub().callsArgWith(3, null, null);
-				this.DocArchiveManager.unArchiveAllDocs = sinon.stub().callsArgWith(1, null);
-				return this.DocManager.getAllNonDeletedDocs(this.project_id, this.filter, this.callback);
-			});
-
-			return it("should return a NotFoundError", function() {
-				return this.callback
-					.calledWith(sinon.match.has('message', `No docs for project ${this.project_id}`))
-					.should.equal(true);
-			});
-		});
-	});
-
-	describe("deleteDoc", function() {
-		describe("when the doc exists", function() {
-			beforeEach(function() {
-				this.lines = ["mock", "doc", "lines"];
-				this.rev = 77;
-				this.DocManager.checkDocExists = sinon.stub().callsArgWith(2, null, true);
-				this.MongoManager.markDocAsDeleted = sinon.stub().callsArg(2);
-				return this.DocManager.deleteDoc(this.project_id, this.doc_id, this.callback);
-			});
-
-			it("should get the doc", function() {
-				return this.DocManager.checkDocExists
-					.calledWith(this.project_id, this.doc_id)
-					.should.equal(true);
-			});
-
-			it("should mark doc as deleted", function() {
-				return this.MongoManager.markDocAsDeleted
-					.calledWith(this.project_id, this.doc_id)
-					.should.equal(true);
-			});
-
-			return it("should return the callback", function() {
-				return this.callback.called.should.equal(true);
-			});
-		});
-
-		return describe("when the doc does not exist", function() {
-			beforeEach(function() { 
-				this.DocManager.checkDocExists = sinon.stub().callsArgWith(2, null, false);
-				return this.DocManager.deleteDoc(this.project_id, this.doc_id, this.callback);
-			});
-
-			return it("should return a NotFoundError", function() {
-				return this.callback
-					.calledWith(sinon.match.has('message', `No such project/doc to delete: ${this.project_id}/${this.doc_id}`))
-					.should.equal(true);
-			});
-		});
-	});
-
-	return describe("updateDoc", function() {
-		beforeEach(function() {
-			this.oldDocLines = ["old", "doc", "lines"];
-			this.newDocLines = ["new", "doc", "lines"];
-			this.originalRanges = {
-				changes: [{
-					id: ObjectId().toString(),
-					op: { i: "foo", p: 3 },
-					meta: {
-						user_id: ObjectId().toString(),
-						ts: new Date().toString()
-					}
-				}]
-			};
-			this.newRanges = {
-				changes: [{
-					id: ObjectId().toString(),
-					op: { i: "bar", p: 6 },
-					meta: {
-						user_id: ObjectId().toString(),
-						ts: new Date().toString()
-					}
-				}]
-			};
-			this.version = 42;
-			this.doc = { _id: this.doc_id, project_id: this.project_id, lines: this.oldDocLines, rev: (this.rev = 5), version: this.version, ranges: this.originalRanges };
-
-			this.MongoManager.upsertIntoDocCollection = sinon.stub().callsArg(3);
-			this.MongoManager.setDocVersion = sinon.stub().yields();
-			return this.DocManager._getDoc = sinon.stub();
-		});
-
-		describe("when only the doc lines have changed", function() {
-			beforeEach(function() {
-				this.DocManager._getDoc = sinon.stub().callsArgWith(3, null, this.doc);
-				return this.DocManager.updateDoc(this.project_id, this.doc_id, this.newDocLines, this.version, this.originalRanges, this.callback);
-			});
-
-			it("should get the existing doc", function() {
-				return this.DocManager._getDoc
-					.calledWith(this.project_id, this.doc_id, {version: true, rev: true, lines: true, version: true, ranges: true, inS3:true})
-					.should.equal(true);
-			});
-
-			it("should upsert the document to the doc collection", function() {
-				return this.MongoManager.upsertIntoDocCollection
-					.calledWith(this.project_id, this.doc_id, {lines: this.newDocLines})
-					.should.equal(true);
-			});
-			
-			it("should not update the version", function() {
-				return this.MongoManager.setDocVersion.called.should.equal(false);
-			});
-
-			return it("should return the callback with the new rev", function() {
-				return this.callback.calledWith(null, true, this.rev + 1).should.equal(true);
-			});
-		});
-
-		describe("when the doc ranges have changed", function() {
-			beforeEach(function() {
-				this.DocManager._getDoc = sinon.stub().callsArgWith(3, null, this.doc);
-				this.RangeManager.shouldUpdateRanges.returns(true);
-				return this.DocManager.updateDoc(this.project_id, this.doc_id, this.oldDocLines, this.version, this.newRanges, this.callback);
-			});
-
-			it("should upsert the ranges", function() {
-				return this.MongoManager.upsertIntoDocCollection
-					.calledWith(this.project_id, this.doc_id, {ranges: this.newRanges})
-					.should.equal(true);
-			});
-			
-			it("should not update the version", function() {
-				return this.MongoManager.setDocVersion.called.should.equal(false);
-			});
-
-			return it("should return the callback with the new rev", function() {
-				return this.callback.calledWith(null, true, this.rev + 1).should.equal(true);
-			});
-		});
-
-		describe("when only the version has changed", function() {
-			beforeEach(function() {
-				this.DocManager._getDoc = sinon.stub().callsArgWith(3, null, this.doc);
-				return this.DocManager.updateDoc(this.project_id, this.doc_id, this.oldDocLines, this.version + 1, this.originalRanges, this.callback);
-			});
-
-			it("should not change the lines or ranges", function() {
-				return this.MongoManager.upsertIntoDocCollection.called.should.equal(false);
-			});
-			
-			it("should update the version", function() {
-				return this.MongoManager.setDocVersion
-					.calledWith(this.doc_id, this.version + 1)
-					.should.equal(true);
-			});
-
-			return it("should return the callback with the old rev", function() {
-				return this.callback.calledWith(null, true, this.rev).should.equal(true);
-			});
-		});
-
-		describe("when the doc has not changed at all", function() {
-			beforeEach(function() {
-				this.DocManager._getDoc = sinon.stub().callsArgWith(3, null, this.doc);
-				return this.DocManager.updateDoc(this.project_id, this.doc_id, this.oldDocLines, this.version, this.originalRanges, this.callback);
-			});
-
-			it("should not update the ranges or lines", function() {
-				return this.MongoManager.upsertIntoDocCollection.called.should.equal(false);
-			});
-			
-			it("should not update the version", function() {
-				return this.MongoManager.setDocVersion.called.should.equal(false);
-			});
-
-			return it("should return the callback with the old rev and modified == false", function() {
-				return this.callback.calledWith(null, false, this.rev).should.equal(true);
-			});
-		});
-
-		describe("when the version is null", function() {
-			beforeEach(function() {
-				return this.DocManager.updateDoc(this.project_id, this.doc_id, this.newDocLines, null, this.originalRanges, this.callback);
-			});
-
-			return it("should return an error", function() {
-				return this.callback.calledWith(sinon.match.has('message', "no lines, version or ranges provided")).should.equal(true);
-			});
-		});
-
-		describe("when the lines are null", function() {
-			beforeEach(function() {
-				return this.DocManager.updateDoc(this.project_id, this.doc_id, null, this.version, this.originalRanges, this.callback);
-			});
-
-			return it("should return an error", function() {
-				return this.callback.calledWith(sinon.match.has('message', "no lines, version or ranges provided")).should.equal(true);
-			});
-		});
-
-		describe("when the ranges are null", function() {
-			beforeEach(function() {
-				return this.DocManager.updateDoc(this.project_id, this.doc_id, this.newDocLines, this.version, null, this.callback);
-			});
-
-			return it("should return an error", function() {
-				return this.callback.calledWith(sinon.match.has('message', "no lines, version or ranges provided")).should.equal(true);
-			});
-		});
-
-		describe("when there is a generic error getting the doc", function() {
-			beforeEach(function() {
-				this.error =  new Error("doc could not be found");
-				this.DocManager._getDoc = sinon.stub().callsArgWith(3, this.error, null, null);
-				return this.DocManager.updateDoc(this.project_id, this.doc_id, this.newDocLines, this.version, this.originalRanges, this.callback);
-			});
-			
-			it("should not upsert the document to the doc collection", function() {
-				return this.MongoManager.upsertIntoDocCollection.called.should.equal(false);
-			});
-
-			return it("should return the callback with the error", function() {
-				return this.callback.calledWith(this.error).should.equal(true);
-			});
-		});
-
-		describe("when the doc lines have not changed", function() {
-			beforeEach(function() {
-				this.DocManager._getDoc = sinon.stub().callsArgWith(3, null, this.doc);
-				return this.DocManager.updateDoc(this.project_id, this.doc_id, this.oldDocLines.slice(), this.version, this.originalRanges, this.callback);
-			});
-
-			it("should not update the doc", function() {
-				return this.MongoManager.upsertIntoDocCollection.called.should.equal(false);
-			});
-
-			return it("should return the callback with the existing rev", function() {
-				return this.callback.calledWith(null, false, this.rev).should.equal(true);
-			});
-		});
-
-		return describe("when the doc does not exist", function() {
-			beforeEach(function() {
-				this.DocManager._getDoc = sinon.stub().callsArgWith(3, null, null, null);
-				return this.DocManager.updateDoc(this.project_id, this.doc_id, this.newDocLines, this.version, this.originalRanges, this.callback);
-			});
-			
-			it("should upsert the document to the doc collection", function() {
-				return this.MongoManager.upsertIntoDocCollection
-					.calledWith(this.project_id, this.doc_id, {lines: this.newDocLines, ranges: this.originalRanges})
-					.should.equal(true);
-			});
-				
-			it("should set the version", function() {
-				return this.MongoManager.setDocVersion
-					.calledWith(this.doc_id, this.version)
-					.should.equal(true);
-			});
-
-			return it("should return the callback with the new rev", function() {
-				return this.callback.calledWith(null, true, 1).should.equal(true);
-			});
-		});
-	});
-});
+const SandboxedModule = require('sandboxed-module')
+const sinon = require('sinon')
+const chai = require('chai')
+const { assert } = require('chai')
+chai.should()
+const { expect } = chai
+const modulePath = require('path').join(__dirname, '../../../app/js/DocManager')
+const { ObjectId } = require('mongojs')
+const Errors = require('../../../app/js/Errors')
+
+describe('DocManager', function() {
+  beforeEach(function() {
+    this.DocManager = SandboxedModule.require(modulePath, {
+      requires: {
+        './MongoManager': (this.MongoManager = {}),
+        './DocArchiveManager': (this.DocArchiveManager = {}),
+        './RangeManager': (this.RangeManager = {
+          jsonRangesToMongo(r) {
+            return r
+          },
+          shouldUpdateRanges: sinon.stub().returns(false)
+        }),
+        'logger-sharelatex': (this.logger = {
+          log: sinon.stub(),
+          warn() {},
+          err() {}
+        })
+      }
+    })
+    this.doc_id = ObjectId().toString()
+    this.project_id = ObjectId().toString()
+    this.another_project_id = ObjectId().toString()
+    this.callback = sinon.stub()
+    return (this.stubbedError = new Error('blew up'))
+  })
+
+  describe('checkDocExists', function() {
+    beforeEach(function() {
+      return (this.DocManager._getDoc = sinon.stub())
+    })
+
+    it('should call get doc with a quick filter', function(done) {
+      this.DocManager._getDoc.callsArgWith(3, null, { _id: this.doc_id })
+      return this.DocManager.checkDocExists(
+        this.project_id,
+        this.doc_id,
+        (err, exist) => {
+          exist.should.equal(true)
+          this.DocManager._getDoc
+            .calledWith(this.project_id, this.doc_id, { _id: 1, inS3: true })
+            .should.equal(true)
+          return done()
+        }
+      )
+    })
+
+    it('should return false when doc is not there', function(done) {
+      this.DocManager._getDoc.callsArgWith(3, null)
+      return this.DocManager.checkDocExists(
+        this.project_id,
+        this.doc_id,
+        (err, exist) => {
+          exist.should.equal(false)
+          return done()
+        }
+      )
+    })
+
+    return it('should return error when get doc errors', function(done) {
+      this.DocManager._getDoc.callsArgWith(3, 'error')
+      return this.DocManager.checkDocExists(
+        this.project_id,
+        this.doc_id,
+        (err, exist) => {
+          err.should.equal('error')
+          return done()
+        }
+      )
+    })
+  })
+
+  describe('getFullDoc', function() {
+    beforeEach(function() {
+      this.DocManager._getDoc = sinon.stub()
+      return (this.doc = {
+        _id: this.doc_id,
+        lines: ['2134']
+      })
+    })
+
+    it('should call get doc with a quick filter', function(done) {
+      this.DocManager._getDoc.callsArgWith(3, null, this.doc)
+      return this.DocManager.getFullDoc(
+        this.project_id,
+        this.doc_id,
+        (err, doc) => {
+          doc.should.equal(this.doc)
+          this.DocManager._getDoc
+            .calledWith(this.project_id, this.doc_id, {
+              lines: true,
+              rev: true,
+              deleted: true,
+              version: true,
+              ranges: true,
+              inS3: true
+            })
+            .should.equal(true)
+          return done()
+        }
+      )
+    })
+
+    return it('should return error when get doc errors', function(done) {
+      this.DocManager._getDoc.callsArgWith(3, 'error')
+      return this.DocManager.getFullDoc(
+        this.project_id,
+        this.doc_id,
+        (err, exist) => {
+          err.should.equal('error')
+          return done()
+        }
+      )
+    })
+  })
+
+  describe('getRawDoc', function() {
+    beforeEach(function() {
+      this.DocManager._getDoc = sinon.stub()
+      return (this.doc = { lines: ['2134'] })
+    })
+
+    it('should call get doc with a quick filter', function(done) {
+      this.DocManager._getDoc.callsArgWith(3, null, this.doc)
+      return this.DocManager.getDocLines(
+        this.project_id,
+        this.doc_id,
+        (err, doc) => {
+          doc.should.equal(this.doc)
+          this.DocManager._getDoc
+            .calledWith(this.project_id, this.doc_id, {
+              lines: true,
+              inS3: true
+            })
+            .should.equal(true)
+          return done()
+        }
+      )
+    })
+
+    return it('should return error when get doc errors', function(done) {
+      this.DocManager._getDoc.callsArgWith(3, 'error')
+      return this.DocManager.getDocLines(
+        this.project_id,
+        this.doc_id,
+        (err, exist) => {
+          err.should.equal('error')
+          return done()
+        }
+      )
+    })
+  })
+
+  describe('getDoc', function() {
+    beforeEach(function() {
+      this.project = { name: 'mock-project' }
+      this.doc = {
+        _id: this.doc_id,
+        project_id: this.project_id,
+        lines: ['mock-lines']
+      }
+      this.version = 42
+      this.MongoManager.findDoc = sinon.stub()
+      return (this.MongoManager.getDocVersion = sinon
+        .stub()
+        .yields(null, this.version))
+    })
+
+    describe('when using a filter', function() {
+      beforeEach(function() {
+        return this.MongoManager.findDoc.yields(null, this.doc)
+      })
+
+      it('should error if inS3 is not set to true', function(done) {
+        return this.DocManager._getDoc(
+          this.project_id,
+          this.doc_id,
+          { inS3: false },
+          err => {
+            expect(err).to.exist
+            return done()
+          }
+        )
+      })
+
+      it('should always get inS3 even when no filter is passed', function(done) {
+        return this.DocManager._getDoc(
+          this.project_id,
+          this.doc_id,
+          undefined,
+          err => {
+            this.MongoManager.findDoc.called.should.equal(false)
+            expect(err).to.exist
+            return done()
+          }
+        )
+      })
+
+      return it('should not error if inS3 is set to true', function(done) {
+        return this.DocManager._getDoc(
+          this.project_id,
+          this.doc_id,
+          { inS3: true },
+          err => {
+            expect(err).to.not.exist
+            return done()
+          }
+        )
+      })
+    })
+
+    describe('when the doc is in the doc collection', function() {
+      beforeEach(function() {
+        this.MongoManager.findDoc.yields(null, this.doc)
+        return this.DocManager._getDoc(
+          this.project_id,
+          this.doc_id,
+          { version: true, inS3: true },
+          this.callback
+        )
+      })
+
+      it('should get the doc from the doc collection', function() {
+        return this.MongoManager.findDoc
+          .calledWith(this.project_id, this.doc_id)
+          .should.equal(true)
+      })
+
+      it('should get the doc version from the docOps collection', function() {
+        return this.MongoManager.getDocVersion
+          .calledWith(this.doc_id)
+          .should.equal(true)
+      })
+
+      return it('should return the callback with the doc with the version', function() {
+        this.callback.called.should.equal(true)
+        const doc = this.callback.args[0][1]
+        doc.lines.should.equal(this.doc.lines)
+        return doc.version.should.equal(this.version)
+      })
+    })
+
+    describe('without the version filter', function() {
+      beforeEach(function() {
+        this.MongoManager.findDoc.yields(null, this.doc)
+        return this.DocManager._getDoc(
+          this.project_id,
+          this.doc_id,
+          { version: false, inS3: true },
+          this.callback
+        )
+      })
+
+      return it('should not get the doc version from the docOps collection', function() {
+        return this.MongoManager.getDocVersion.called.should.equal(false)
+      })
+    })
+
+    describe('when MongoManager.findDoc errors', function() {
+      beforeEach(function() {
+        this.MongoManager.findDoc.yields(this.stubbedError)
+        return this.DocManager._getDoc(
+          this.project_id,
+          this.doc_id,
+          { version: true, inS3: true },
+          this.callback
+        )
+      })
+
+      return it('should return the error', function() {
+        return this.callback.calledWith(this.stubbedError).should.equal(true)
+      })
+    })
+
+    describe('when the doc is archived', function() {
+      beforeEach(function() {
+        this.doc = {
+          _id: this.doc_id,
+          project_id: this.project_id,
+          lines: ['mock-lines'],
+          inS3: true
+        }
+        this.MongoManager.findDoc.yields(null, this.doc)
+        this.DocArchiveManager.unarchiveDoc = (
+          project_id,
+          doc_id,
+          callback
+        ) => {
+          this.doc.inS3 = false
+          return callback()
+        }
+        sinon.spy(this.DocArchiveManager, 'unarchiveDoc')
+        return this.DocManager._getDoc(
+          this.project_id,
+          this.doc_id,
+          { version: true, inS3: true },
+          this.callback
+        )
+      })
+
+      it('should call the DocArchive to unarchive the doc', function() {
+        return this.DocArchiveManager.unarchiveDoc
+          .calledWith(this.project_id, this.doc_id)
+          .should.equal(true)
+      })
+
+      it('should look up the doc twice', function() {
+        return this.MongoManager.findDoc.calledTwice.should.equal(true)
+      })
+
+      return it('should return the doc', function() {
+        return this.callback.calledWith(null, this.doc).should.equal(true)
+      })
+    })
+
+    return describe('when the doc does not exist in the docs collection', function() {
+      beforeEach(function() {
+        this.MongoManager.findDoc = sinon.stub().yields(null, null)
+        return this.DocManager._getDoc(
+          this.project_id,
+          this.doc_id,
+          { version: true, inS3: true },
+          this.callback
+        )
+      })
+
+      return it('should return a NotFoundError', function() {
+        return this.callback
+          .calledWith(
+            sinon.match.has(
+              'message',
+              `No such doc: ${this.doc_id} in project ${this.project_id}`
+            )
+          )
+          .should.equal(true)
+      })
+    })
+  })
+
+  describe('getAllNonDeletedDocs', function() {
+    describe('when the project exists', function() {
+      beforeEach(function() {
+        this.docs = [
+          {
+            _id: this.doc_id,
+            project_id: this.project_id,
+            lines: ['mock-lines']
+          }
+        ]
+        this.MongoManager.getProjectsDocs = sinon
+          .stub()
+          .callsArgWith(3, null, this.docs)
+        this.DocArchiveManager.unArchiveAllDocs = sinon
+          .stub()
+          .callsArgWith(1, null, this.docs)
+        this.filter = { lines: true }
+        return this.DocManager.getAllNonDeletedDocs(
+          this.project_id,
+          this.filter,
+          this.callback
+        )
+      })
+
+      it('should get the project from the database', function() {
+        return this.MongoManager.getProjectsDocs
+          .calledWith(this.project_id, { include_deleted: false }, this.filter)
+          .should.equal(true)
+      })
+
+      return it('should return the docs', function() {
+        return this.callback.calledWith(null, this.docs).should.equal(true)
+      })
+    })
+
+    return describe('when there are no docs for the project', function() {
+      beforeEach(function() {
+        this.MongoManager.getProjectsDocs = sinon
+          .stub()
+          .callsArgWith(3, null, null)
+        this.DocArchiveManager.unArchiveAllDocs = sinon
+          .stub()
+          .callsArgWith(1, null)
+        return this.DocManager.getAllNonDeletedDocs(
+          this.project_id,
+          this.filter,
+          this.callback
+        )
+      })
+
+      return it('should return a NotFoundError', function() {
+        return this.callback
+          .calledWith(
+            sinon.match.has('message', `No docs for project ${this.project_id}`)
+          )
+          .should.equal(true)
+      })
+    })
+  })
+
+  describe('deleteDoc', function() {
+    describe('when the doc exists', function() {
+      beforeEach(function() {
+        this.lines = ['mock', 'doc', 'lines']
+        this.rev = 77
+        this.DocManager.checkDocExists = sinon
+          .stub()
+          .callsArgWith(2, null, true)
+        this.MongoManager.markDocAsDeleted = sinon.stub().callsArg(2)
+        return this.DocManager.deleteDoc(
+          this.project_id,
+          this.doc_id,
+          this.callback
+        )
+      })
+
+      it('should get the doc', function() {
+        return this.DocManager.checkDocExists
+          .calledWith(this.project_id, this.doc_id)
+          .should.equal(true)
+      })
+
+      it('should mark doc as deleted', function() {
+        return this.MongoManager.markDocAsDeleted
+          .calledWith(this.project_id, this.doc_id)
+          .should.equal(true)
+      })
+
+      return it('should return the callback', function() {
+        return this.callback.called.should.equal(true)
+      })
+    })
+
+    return describe('when the doc does not exist', function() {
+      beforeEach(function() {
+        this.DocManager.checkDocExists = sinon
+          .stub()
+          .callsArgWith(2, null, false)
+        return this.DocManager.deleteDoc(
+          this.project_id,
+          this.doc_id,
+          this.callback
+        )
+      })
+
+      return it('should return a NotFoundError', function() {
+        return this.callback
+          .calledWith(
+            sinon.match.has(
+              'message',
+              `No such project/doc to delete: ${this.project_id}/${this.doc_id}`
+            )
+          )
+          .should.equal(true)
+      })
+    })
+  })
+
+  return describe('updateDoc', function() {
+    beforeEach(function() {
+      this.oldDocLines = ['old', 'doc', 'lines']
+      this.newDocLines = ['new', 'doc', 'lines']
+      this.originalRanges = {
+        changes: [
+          {
+            id: ObjectId().toString(),
+            op: { i: 'foo', p: 3 },
+            meta: {
+              user_id: ObjectId().toString(),
+              ts: new Date().toString()
+            }
+          }
+        ]
+      }
+      this.newRanges = {
+        changes: [
+          {
+            id: ObjectId().toString(),
+            op: { i: 'bar', p: 6 },
+            meta: {
+              user_id: ObjectId().toString(),
+              ts: new Date().toString()
+            }
+          }
+        ]
+      }
+      this.version = 42
+      this.doc = {
+        _id: this.doc_id,
+        project_id: this.project_id,
+        lines: this.oldDocLines,
+        rev: (this.rev = 5),
+        version: this.version,
+        ranges: this.originalRanges
+      }
+
+      this.MongoManager.upsertIntoDocCollection = sinon.stub().callsArg(3)
+      this.MongoManager.setDocVersion = sinon.stub().yields()
+      return (this.DocManager._getDoc = sinon.stub())
+    })
+
+    describe('when only the doc lines have changed', function() {
+      beforeEach(function() {
+        this.DocManager._getDoc = sinon.stub().callsArgWith(3, null, this.doc)
+        return this.DocManager.updateDoc(
+          this.project_id,
+          this.doc_id,
+          this.newDocLines,
+          this.version,
+          this.originalRanges,
+          this.callback
+        )
+      })
+
+      it('should get the existing doc', function() {
+        return this.DocManager._getDoc
+          .calledWith(this.project_id, this.doc_id, {
+            version: true,
+            rev: true,
+            lines: true,
+            version: true,
+            ranges: true,
+            inS3: true
+          })
+          .should.equal(true)
+      })
+
+      it('should upsert the document to the doc collection', function() {
+        return this.MongoManager.upsertIntoDocCollection
+          .calledWith(this.project_id, this.doc_id, { lines: this.newDocLines })
+          .should.equal(true)
+      })
+
+      it('should not update the version', function() {
+        return this.MongoManager.setDocVersion.called.should.equal(false)
+      })
+
+      return it('should return the callback with the new rev', function() {
+        return this.callback
+          .calledWith(null, true, this.rev + 1)
+          .should.equal(true)
+      })
+    })
+
+    describe('when the doc ranges have changed', function() {
+      beforeEach(function() {
+        this.DocManager._getDoc = sinon.stub().callsArgWith(3, null, this.doc)
+        this.RangeManager.shouldUpdateRanges.returns(true)
+        return this.DocManager.updateDoc(
+          this.project_id,
+          this.doc_id,
+          this.oldDocLines,
+          this.version,
+          this.newRanges,
+          this.callback
+        )
+      })
+
+      it('should upsert the ranges', function() {
+        return this.MongoManager.upsertIntoDocCollection
+          .calledWith(this.project_id, this.doc_id, { ranges: this.newRanges })
+          .should.equal(true)
+      })
+
+      it('should not update the version', function() {
+        return this.MongoManager.setDocVersion.called.should.equal(false)
+      })
+
+      return it('should return the callback with the new rev', function() {
+        return this.callback
+          .calledWith(null, true, this.rev + 1)
+          .should.equal(true)
+      })
+    })
+
+    describe('when only the version has changed', function() {
+      beforeEach(function() {
+        this.DocManager._getDoc = sinon.stub().callsArgWith(3, null, this.doc)
+        return this.DocManager.updateDoc(
+          this.project_id,
+          this.doc_id,
+          this.oldDocLines,
+          this.version + 1,
+          this.originalRanges,
+          this.callback
+        )
+      })
+
+      it('should not change the lines or ranges', function() {
+        return this.MongoManager.upsertIntoDocCollection.called.should.equal(
+          false
+        )
+      })
+
+      it('should update the version', function() {
+        return this.MongoManager.setDocVersion
+          .calledWith(this.doc_id, this.version + 1)
+          .should.equal(true)
+      })
+
+      return it('should return the callback with the old rev', function() {
+        return this.callback.calledWith(null, true, this.rev).should.equal(true)
+      })
+    })
+
+    describe('when the doc has not changed at all', function() {
+      beforeEach(function() {
+        this.DocManager._getDoc = sinon.stub().callsArgWith(3, null, this.doc)
+        return this.DocManager.updateDoc(
+          this.project_id,
+          this.doc_id,
+          this.oldDocLines,
+          this.version,
+          this.originalRanges,
+          this.callback
+        )
+      })
+
+      it('should not update the ranges or lines', function() {
+        return this.MongoManager.upsertIntoDocCollection.called.should.equal(
+          false
+        )
+      })
+
+      it('should not update the version', function() {
+        return this.MongoManager.setDocVersion.called.should.equal(false)
+      })
+
+      return it('should return the callback with the old rev and modified == false', function() {
+        return this.callback
+          .calledWith(null, false, this.rev)
+          .should.equal(true)
+      })
+    })
+
+    describe('when the version is null', function() {
+      beforeEach(function() {
+        return this.DocManager.updateDoc(
+          this.project_id,
+          this.doc_id,
+          this.newDocLines,
+          null,
+          this.originalRanges,
+          this.callback
+        )
+      })
+
+      return it('should return an error', function() {
+        return this.callback
+          .calledWith(
+            sinon.match.has('message', 'no lines, version or ranges provided')
+          )
+          .should.equal(true)
+      })
+    })
+
+    describe('when the lines are null', function() {
+      beforeEach(function() {
+        return this.DocManager.updateDoc(
+          this.project_id,
+          this.doc_id,
+          null,
+          this.version,
+          this.originalRanges,
+          this.callback
+        )
+      })
+
+      return it('should return an error', function() {
+        return this.callback
+          .calledWith(
+            sinon.match.has('message', 'no lines, version or ranges provided')
+          )
+          .should.equal(true)
+      })
+    })
+
+    describe('when the ranges are null', function() {
+      beforeEach(function() {
+        return this.DocManager.updateDoc(
+          this.project_id,
+          this.doc_id,
+          this.newDocLines,
+          this.version,
+          null,
+          this.callback
+        )
+      })
+
+      return it('should return an error', function() {
+        return this.callback
+          .calledWith(
+            sinon.match.has('message', 'no lines, version or ranges provided')
+          )
+          .should.equal(true)
+      })
+    })
+
+    describe('when there is a generic error getting the doc', function() {
+      beforeEach(function() {
+        this.error = new Error('doc could not be found')
+        this.DocManager._getDoc = sinon
+          .stub()
+          .callsArgWith(3, this.error, null, null)
+        return this.DocManager.updateDoc(
+          this.project_id,
+          this.doc_id,
+          this.newDocLines,
+          this.version,
+          this.originalRanges,
+          this.callback
+        )
+      })
+
+      it('should not upsert the document to the doc collection', function() {
+        return this.MongoManager.upsertIntoDocCollection.called.should.equal(
+          false
+        )
+      })
+
+      return it('should return the callback with the error', function() {
+        return this.callback.calledWith(this.error).should.equal(true)
+      })
+    })
+
+    describe('when the doc lines have not changed', function() {
+      beforeEach(function() {
+        this.DocManager._getDoc = sinon.stub().callsArgWith(3, null, this.doc)
+        return this.DocManager.updateDoc(
+          this.project_id,
+          this.doc_id,
+          this.oldDocLines.slice(),
+          this.version,
+          this.originalRanges,
+          this.callback
+        )
+      })
+
+      it('should not update the doc', function() {
+        return this.MongoManager.upsertIntoDocCollection.called.should.equal(
+          false
+        )
+      })
+
+      return it('should return the callback with the existing rev', function() {
+        return this.callback
+          .calledWith(null, false, this.rev)
+          .should.equal(true)
+      })
+    })
+
+    return describe('when the doc does not exist', function() {
+      beforeEach(function() {
+        this.DocManager._getDoc = sinon.stub().callsArgWith(3, null, null, null)
+        return this.DocManager.updateDoc(
+          this.project_id,
+          this.doc_id,
+          this.newDocLines,
+          this.version,
+          this.originalRanges,
+          this.callback
+        )
+      })
+
+      it('should upsert the document to the doc collection', function() {
+        return this.MongoManager.upsertIntoDocCollection
+          .calledWith(this.project_id, this.doc_id, {
+            lines: this.newDocLines,
+            ranges: this.originalRanges
+          })
+          .should.equal(true)
+      })
+
+      it('should set the version', function() {
+        return this.MongoManager.setDocVersion
+          .calledWith(this.doc_id, this.version)
+          .should.equal(true)
+      })
+
+      return it('should return the callback with the new rev', function() {
+        return this.callback.calledWith(null, true, 1).should.equal(true)
+      })
+    })
+  })
+})

+ 478 - 439
services/docstore/test/unit/js/HttpControllerTests.js

@@ -9,442 +9,481 @@
  * DS102: Remove unnecessary code created because of implicit returns
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-const SandboxedModule = require('sandboxed-module');
-const {
-    assert
-} = require("chai");
-const sinon = require('sinon');
-const chai = require('chai');
-chai.should();
-const {
-    expect
-} = chai;
-const modulePath = require('path').join(__dirname, '../../../app/js/HttpController');
-const {
-    ObjectId
-} = require("mongojs");
-
-describe("HttpController", function() {
-	beforeEach(function() {
-		this.HttpController = SandboxedModule.require(modulePath, { requires: {
-			"./DocManager": (this.DocManager = {}),
-			"./DocArchiveManager": (this.DocArchiveManager = {}),
-			"logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }),
-			"./HealthChecker": {}
-		}
-	});
-		this.res = { send: sinon.stub(), json: sinon.stub(), setHeader:sinon.stub() };
-		this.res.status = sinon.stub().returns(this.res);
-		this.req = { query:{}};
-		this.next = sinon.stub();
-		this.project_id = "mock-project-id";
-		this.doc_id = "mock-doc-id";
-		this.doc = {
-			_id: this.doc_id,
-			lines: ["mock", "lines", " here", "", "", " spaces "],
-			version: 42,
-			rev: 5
-		};
-		return this.deletedDoc = {
-			deleted:true,
-			_id: this.doc_id,
-			lines: ["mock", "lines", " here", "", "", " spaces "],
-			version: 42,
-			rev: 5
-		};});
-
-	describe("getDoc", function() {
-
-		describe("without deleted docs", function() {
-			beforeEach(function() {
-				this.req.params = {
-					project_id: this.project_id,
-					doc_id: this.doc_id
-				};
-				this.DocManager.getFullDoc = sinon.stub().callsArgWith(2, null, this.doc);
-				return this.HttpController.getDoc(this.req, this.res, this.next);
-			});
-
-			it("should get the document with the version (including deleted)", function() {
-				return this.DocManager.getFullDoc
-					.calledWith(this.project_id, this.doc_id)
-					.should.equal(true);
-			});
-
-			return it("should return the doc as JSON", function() {
-				return this.res.json
-					.calledWith({
-						_id: this.doc_id,
-						lines: this.doc.lines,
-						rev: this.doc.rev,
-						version: this.doc.version
-					})
-					.should.equal(true);
-			});
-		});
-
-		return describe("which is deleted", function() {
-			beforeEach(function() {
-				this.req.params = {
-					project_id: this.project_id,
-					doc_id: this.doc_id
-				};
-				return this.DocManager.getFullDoc = sinon.stub().callsArgWith(2, null, this.deletedDoc);
-			});
-
-			it("should get the doc from the doc manager", function() {
-				this.HttpController.getDoc(this.req, this.res, this.next);
-				return this.DocManager.getFullDoc.calledWith(this.project_id, this.doc_id).should.equal(true);
-			});
-
-			it("should return 404 if the query string delete is not set ", function() {
-				this.HttpController.getDoc(this.req, this.res, this.next);
-				return this.res.send.calledWith(404).should.equal(true);
-			});
-
-			return it("should return the doc as JSON if include_deleted is set to true", function() {
-				this.req.query.include_deleted = "true";
-				this.HttpController.getDoc(this.req, this.res, this.next);
-				return this.res.json
-					.calledWith({
-						_id: this.doc_id,
-						lines: this.doc.lines,
-						rev: this.doc.rev,
-						deleted: true,
-						version: this.doc.version
-					})
-					.should.equal(true);
-			});
-		});
-	});
-
-	describe("getRawDoc", function() {
-		beforeEach(function() {
-			this.req.params = {
-				project_id: this.project_id,
-				doc_id: this.doc_id
-			};
-			this.DocManager.getDocLines = sinon.stub().callsArgWith(2, null, this.doc);
-			return this.HttpController.getRawDoc(this.req, this.res, this.next);
-		});
-
-		it("should get the document without the version", function() {
-			return this.DocManager.getDocLines
-				.calledWith(this.project_id, this.doc_id)
-				.should.equal(true);
-		});
-
-		it("should set the content type header", function() {
-			return this.res.setHeader.calledWith('content-type', 'text/plain').should.equal(true);
-		});
-
-		return it("should send the raw version of the doc", function() {
-			return assert.deepEqual(this.res.send.args[0][0], `${this.doc.lines[0]}\n${this.doc.lines[1]}\n${this.doc.lines[2]}\n${this.doc.lines[3]}\n${this.doc.lines[4]}\n${this.doc.lines[5]}`);
-		});
-	});
-
-	describe("getAllDocs", function() {
-		describe("normally", function() {
-			beforeEach(function() {
-				this.req.params =
-					{project_id: this.project_id};
-				this.docs = [{
-					_id: ObjectId(),
-					lines: ["mock", "lines", "one"],
-					rev: 2
-				}, {
-					_id: ObjectId(),
-					lines: ["mock", "lines", "two"],
-					rev: 4
-				}];
-				this.DocManager.getAllNonDeletedDocs = sinon.stub().callsArgWith(2, null, this.docs);
-				return this.HttpController.getAllDocs(this.req, this.res, this.next);
-			});
-
-			it("should get all the (non-deleted) docs", function() {
-				return this.DocManager.getAllNonDeletedDocs
-					.calledWith(this.project_id, {lines: true, rev: true})
-					.should.equal(true);
-			});
-
-			return it("should return the doc as JSON", function() {
-				return this.res.json
-					.calledWith([{
-						_id:   this.docs[0]._id.toString(),
-						lines: this.docs[0].lines,
-						rev:   this.docs[0].rev
-					}, {
-						_id:   this.docs[1]._id.toString(),
-						lines: this.docs[1].lines,
-						rev:   this.docs[1].rev
-					}])
-					.should.equal(true);
-			});
-		});
-
-		return describe("with a null doc", function() {
-			beforeEach(function() {
-				this.req.params =
-					{project_id: this.project_id};
-				this.docs = [{
-					_id: ObjectId(),
-					lines: ["mock", "lines", "one"],
-					rev: 2
-				},
-				null,
-				{
-					_id: ObjectId(),
-					lines: ["mock", "lines", "two"],
-					rev: 4
-				}];
-				this.DocManager.getAllNonDeletedDocs = sinon.stub().callsArgWith(2, null, this.docs);
-				return this.HttpController.getAllDocs(this.req, this.res, this.next);
-			});
-
-			it("should return the non null docs as JSON", function() {
-				return this.res.json
-					.calledWith([{
-						_id:   this.docs[0]._id.toString(),
-						lines: this.docs[0].lines,
-						rev:   this.docs[0].rev
-					}, {
-						_id:   this.docs[2]._id.toString(),
-						lines: this.docs[2].lines,
-						rev:   this.docs[2].rev
-					}])
-					.should.equal(true);
-			});
-
-			return it("should log out an error", function() {
-				return this.logger.error
-					.calledWith({
-						err: sinon.match.has('message', "null doc"),
-						project_id: this.project_id
-					},
-						"encountered null doc"
-					)
-					.should.equal(true);
-			});
-		});
-	});
-
-	describe("getAllRanges", function() { return describe("normally", function() {
-        beforeEach(function() {
-            this.req.params =
-                {project_id: this.project_id};
-            this.docs = [{
-                _id: ObjectId(),
-                ranges: {"mock_ranges": "one"}
-            }, {
-                _id: ObjectId(),
-                ranges: {"mock_ranges": "two"}
-            }];
-            this.DocManager.getAllNonDeletedDocs = sinon.stub().callsArgWith(2, null, this.docs);
-            return this.HttpController.getAllRanges(this.req, this.res, this.next);
-        });
-
-        it("should get all the (non-deleted) doc ranges", function() {
-            return this.DocManager.getAllNonDeletedDocs
-                .calledWith(this.project_id, {ranges: true})
-                .should.equal(true);
-        });
-
-        return it("should return the doc as JSON", function() {
-            return this.res.json
-                .calledWith([{
-                    _id:    this.docs[0]._id.toString(),
-                    ranges: this.docs[0].ranges
-                }, {
-                    _id:    this.docs[1]._id.toString(),
-                    ranges: this.docs[1].ranges
-                }])
-                .should.equal(true);
-        });
-    }); });
-
-	describe("updateDoc", function() {
-		beforeEach(function() {
-			return this.req.params = {
-				project_id: this.project_id,
-				doc_id: this.doc_id
-			};
-		});
-
-		describe("when the doc lines exist and were updated", function() {
-			beforeEach(function() {
-				this.req.body = {
-					lines: (this.lines = ["hello", "world"]),
-					version: (this.version = 42),
-					ranges: (this.ranges = { changes: "mock" })
-				};
-				this.DocManager.updateDoc = sinon.stub().yields(null, true, (this.rev = 5));
-				return this.HttpController.updateDoc(this.req, this.res, this.next);
-			});
-
-			it("should update the document", function() {
-				return this.DocManager.updateDoc
-					.calledWith(this.project_id, this.doc_id, this.lines, this.version, this.ranges)
-					.should.equal(true);
-			});
-
-			return it("should return a modified status", function() {
-				return this.res.json
-					.calledWith({modified: true, rev: this.rev})
-					.should.equal(true);
-			});
-		});
-
-		describe("when the doc lines exist and were not updated", function() {
-			beforeEach(function() {
-				this.req.body = {
-					lines: (this.lines = ["hello", "world"]),
-					version: (this.version = 42),
-					ranges: {}
-				};
-				this.DocManager.updateDoc = sinon.stub().yields(null, false, (this.rev = 5));
-				return this.HttpController.updateDoc(this.req, this.res, this.next);
-			});
-
-			return it("should return a modified status", function() {
-				return this.res.json
-					.calledWith({modified: false, rev: this.rev})
-					.should.equal(true);
-			});
-		});
-
-		describe("when the doc lines are not provided", function() {
-			beforeEach(function() {
-				this.req.body = { version: 42, ranges: {} };
-				this.DocManager.updateDoc = sinon.stub().yields(null, false);
-				return this.HttpController.updateDoc(this.req, this.res, this.next);
-			});
-
-			it("should not update the document", function() {
-				return this.DocManager.updateDoc.called.should.equal(false);
-			});
-
-			return it("should return a 400 (bad request) response", function() {
-				return this.res.send
-					.calledWith(400)
-					.should.equal(true);
-			});
-		});
-
-		describe("when the doc version are not provided", function() {
-			beforeEach(function() {
-				this.req.body = { version: 42, lines: ["hello world"] };
-				this.DocManager.updateDoc = sinon.stub().yields(null, false);
-				return this.HttpController.updateDoc(this.req, this.res, this.next);
-			});
-
-			it("should not update the document", function() {
-				return this.DocManager.updateDoc.called.should.equal(false);
-			});
-
-			return it("should return a 400 (bad request) response", function() {
-				return this.res.send
-					.calledWith(400)
-					.should.equal(true);
-			});
-		});
-
-		describe("when the doc ranges is not provided", function() {
-			beforeEach(function() {
-				this.req.body = { lines : [ "foo" ], version: 42 };
-				this.DocManager.updateDoc = sinon.stub().yields(null, false);
-				return this.HttpController.updateDoc(this.req, this.res, this.next);
-			});
-
-			it("should not update the document", function() {
-				return this.DocManager.updateDoc.called.should.equal(false);
-			});
-
-			return it("should return a 400 (bad request) response", function() {
-				return this.res.send
-					.calledWith(400)
-					.should.equal(true);
-			});
-		});
-
-		return describe("when the doc body is too large", function() {
-			beforeEach(function() {
-				this.req.body = {
-					lines: (this.lines = Array(2049).fill('a'.repeat(1024))),
-					version: (this.version = 42),
-					ranges: (this.ranges = { changes: "mock" })
-				};
-				return this.HttpController.updateDoc(this.req, this.res, this.next);
-			});
-
-			it("should return a 413 (too large) response", function() {
-				return sinon.assert.calledWith(this.res.status, 413);
-			});
-
-			return it("should report that the document body is too large", function() {
-				return sinon.assert.calledWith(this.res.send, "document body too large");
-			});
-		});
-	});
-
-	describe("deleteDoc", function() {
-		beforeEach(function() {
-			this.req.params = {
-				project_id: this.project_id,
-				doc_id: this.doc_id
-			};
-			this.DocManager.deleteDoc = sinon.stub().callsArg(2);
-			return this.HttpController.deleteDoc(this.req, this.res, this.next);
-		});
-
-		it("should delete the document", function() {
-			return this.DocManager.deleteDoc
-				.calledWith(this.project_id, this.doc_id)
-				.should.equal(true);
-		});
-
-		return it("should return a 204 (No Content)", function() {
-			return this.res.send
-				.calledWith(204)
-				.should.equal(true);
-		});
-	});
-
-	describe("archiveAllDocs", function() {
-		beforeEach(function() {
-			this.req.params =
-				{project_id: this.project_id};
-			this.DocArchiveManager.archiveAllDocs = sinon.stub().callsArg(1);
-			return this.HttpController.archiveAllDocs(this.req, this.res, this.next);
-		});
-
-		it("should archive the project", function() {
-			return this.DocArchiveManager.archiveAllDocs
-				.calledWith(this.project_id)
-				.should.equal(true);
-		});
-
-		return it("should return a 204 (No Content)", function() {
-			return this.res.send
-				.calledWith(204)
-				.should.equal(true);
-		});
-	});
-
-	return describe("destroyAllDocs", function() {
-		beforeEach(function() {
-			this.req.params =
-				{project_id: this.project_id};
-			this.DocArchiveManager.destroyAllDocs = sinon.stub().callsArg(1);
-			return this.HttpController.destroyAllDocs(this.req, this.res, this.next);
-		});
-
-		it("should destroy the docs", function() {
-			return sinon.assert.calledWith(this.DocArchiveManager.destroyAllDocs, this.project_id);
-		});
-
-		return it("should return 204", function() {
-			return sinon.assert.calledWith(this.res.send, 204);
-		});
-	});
-});
-
+const SandboxedModule = require('sandboxed-module')
+const { assert } = require('chai')
+const sinon = require('sinon')
+const chai = require('chai')
+chai.should()
+const { expect } = chai
+const modulePath = require('path').join(
+  __dirname,
+  '../../../app/js/HttpController'
+)
+const { ObjectId } = require('mongojs')
+
+describe('HttpController', function() {
+  beforeEach(function() {
+    this.HttpController = SandboxedModule.require(modulePath, {
+      requires: {
+        './DocManager': (this.DocManager = {}),
+        './DocArchiveManager': (this.DocArchiveManager = {}),
+        'logger-sharelatex': (this.logger = {
+          log: sinon.stub(),
+          error: sinon.stub()
+        }),
+        './HealthChecker': {}
+      }
+    })
+    this.res = {
+      send: sinon.stub(),
+      json: sinon.stub(),
+      setHeader: sinon.stub()
+    }
+    this.res.status = sinon.stub().returns(this.res)
+    this.req = { query: {} }
+    this.next = sinon.stub()
+    this.project_id = 'mock-project-id'
+    this.doc_id = 'mock-doc-id'
+    this.doc = {
+      _id: this.doc_id,
+      lines: ['mock', 'lines', ' here', '', '', ' spaces '],
+      version: 42,
+      rev: 5
+    }
+    return (this.deletedDoc = {
+      deleted: true,
+      _id: this.doc_id,
+      lines: ['mock', 'lines', ' here', '', '', ' spaces '],
+      version: 42,
+      rev: 5
+    })
+  })
+
+  describe('getDoc', function() {
+    describe('without deleted docs', function() {
+      beforeEach(function() {
+        this.req.params = {
+          project_id: this.project_id,
+          doc_id: this.doc_id
+        }
+        this.DocManager.getFullDoc = sinon
+          .stub()
+          .callsArgWith(2, null, this.doc)
+        return this.HttpController.getDoc(this.req, this.res, this.next)
+      })
+
+      it('should get the document with the version (including deleted)', function() {
+        return this.DocManager.getFullDoc
+          .calledWith(this.project_id, this.doc_id)
+          .should.equal(true)
+      })
+
+      return it('should return the doc as JSON', function() {
+        return this.res.json
+          .calledWith({
+            _id: this.doc_id,
+            lines: this.doc.lines,
+            rev: this.doc.rev,
+            version: this.doc.version
+          })
+          .should.equal(true)
+      })
+    })
+
+    return describe('which is deleted', function() {
+      beforeEach(function() {
+        this.req.params = {
+          project_id: this.project_id,
+          doc_id: this.doc_id
+        }
+        return (this.DocManager.getFullDoc = sinon
+          .stub()
+          .callsArgWith(2, null, this.deletedDoc))
+      })
+
+      it('should get the doc from the doc manager', function() {
+        this.HttpController.getDoc(this.req, this.res, this.next)
+        return this.DocManager.getFullDoc
+          .calledWith(this.project_id, this.doc_id)
+          .should.equal(true)
+      })
+
+      it('should return 404 if the query string delete is not set ', function() {
+        this.HttpController.getDoc(this.req, this.res, this.next)
+        return this.res.send.calledWith(404).should.equal(true)
+      })
+
+      return it('should return the doc as JSON if include_deleted is set to true', function() {
+        this.req.query.include_deleted = 'true'
+        this.HttpController.getDoc(this.req, this.res, this.next)
+        return this.res.json
+          .calledWith({
+            _id: this.doc_id,
+            lines: this.doc.lines,
+            rev: this.doc.rev,
+            deleted: true,
+            version: this.doc.version
+          })
+          .should.equal(true)
+      })
+    })
+  })
+
+  describe('getRawDoc', function() {
+    beforeEach(function() {
+      this.req.params = {
+        project_id: this.project_id,
+        doc_id: this.doc_id
+      }
+      this.DocManager.getDocLines = sinon.stub().callsArgWith(2, null, this.doc)
+      return this.HttpController.getRawDoc(this.req, this.res, this.next)
+    })
+
+    it('should get the document without the version', function() {
+      return this.DocManager.getDocLines
+        .calledWith(this.project_id, this.doc_id)
+        .should.equal(true)
+    })
+
+    it('should set the content type header', function() {
+      return this.res.setHeader
+        .calledWith('content-type', 'text/plain')
+        .should.equal(true)
+    })
+
+    return it('should send the raw version of the doc', function() {
+      return assert.deepEqual(
+        this.res.send.args[0][0],
+        `${this.doc.lines[0]}\n${this.doc.lines[1]}\n${this.doc.lines[2]}\n${this.doc.lines[3]}\n${this.doc.lines[4]}\n${this.doc.lines[5]}`
+      )
+    })
+  })
+
+  describe('getAllDocs', function() {
+    describe('normally', function() {
+      beforeEach(function() {
+        this.req.params = { project_id: this.project_id }
+        this.docs = [
+          {
+            _id: ObjectId(),
+            lines: ['mock', 'lines', 'one'],
+            rev: 2
+          },
+          {
+            _id: ObjectId(),
+            lines: ['mock', 'lines', 'two'],
+            rev: 4
+          }
+        ]
+        this.DocManager.getAllNonDeletedDocs = sinon
+          .stub()
+          .callsArgWith(2, null, this.docs)
+        return this.HttpController.getAllDocs(this.req, this.res, this.next)
+      })
+
+      it('should get all the (non-deleted) docs', function() {
+        return this.DocManager.getAllNonDeletedDocs
+          .calledWith(this.project_id, { lines: true, rev: true })
+          .should.equal(true)
+      })
+
+      return it('should return the doc as JSON', function() {
+        return this.res.json
+          .calledWith([
+            {
+              _id: this.docs[0]._id.toString(),
+              lines: this.docs[0].lines,
+              rev: this.docs[0].rev
+            },
+            {
+              _id: this.docs[1]._id.toString(),
+              lines: this.docs[1].lines,
+              rev: this.docs[1].rev
+            }
+          ])
+          .should.equal(true)
+      })
+    })
+
+    return describe('with a null doc', function() {
+      beforeEach(function() {
+        this.req.params = { project_id: this.project_id }
+        this.docs = [
+          {
+            _id: ObjectId(),
+            lines: ['mock', 'lines', 'one'],
+            rev: 2
+          },
+          null,
+          {
+            _id: ObjectId(),
+            lines: ['mock', 'lines', 'two'],
+            rev: 4
+          }
+        ]
+        this.DocManager.getAllNonDeletedDocs = sinon
+          .stub()
+          .callsArgWith(2, null, this.docs)
+        return this.HttpController.getAllDocs(this.req, this.res, this.next)
+      })
+
+      it('should return the non null docs as JSON', function() {
+        return this.res.json
+          .calledWith([
+            {
+              _id: this.docs[0]._id.toString(),
+              lines: this.docs[0].lines,
+              rev: this.docs[0].rev
+            },
+            {
+              _id: this.docs[2]._id.toString(),
+              lines: this.docs[2].lines,
+              rev: this.docs[2].rev
+            }
+          ])
+          .should.equal(true)
+      })
+
+      return it('should log out an error', function() {
+        return this.logger.error
+          .calledWith(
+            {
+              err: sinon.match.has('message', 'null doc'),
+              project_id: this.project_id
+            },
+            'encountered null doc'
+          )
+          .should.equal(true)
+      })
+    })
+  })
+
+  describe('getAllRanges', function() {
+    return describe('normally', function() {
+      beforeEach(function() {
+        this.req.params = { project_id: this.project_id }
+        this.docs = [
+          {
+            _id: ObjectId(),
+            ranges: { mock_ranges: 'one' }
+          },
+          {
+            _id: ObjectId(),
+            ranges: { mock_ranges: 'two' }
+          }
+        ]
+        this.DocManager.getAllNonDeletedDocs = sinon
+          .stub()
+          .callsArgWith(2, null, this.docs)
+        return this.HttpController.getAllRanges(this.req, this.res, this.next)
+      })
+
+      it('should get all the (non-deleted) doc ranges', function() {
+        return this.DocManager.getAllNonDeletedDocs
+          .calledWith(this.project_id, { ranges: true })
+          .should.equal(true)
+      })
+
+      return it('should return the doc as JSON', function() {
+        return this.res.json
+          .calledWith([
+            {
+              _id: this.docs[0]._id.toString(),
+              ranges: this.docs[0].ranges
+            },
+            {
+              _id: this.docs[1]._id.toString(),
+              ranges: this.docs[1].ranges
+            }
+          ])
+          .should.equal(true)
+      })
+    })
+  })
+
+  describe('updateDoc', function() {
+    beforeEach(function() {
+      return (this.req.params = {
+        project_id: this.project_id,
+        doc_id: this.doc_id
+      })
+    })
+
+    describe('when the doc lines exist and were updated', function() {
+      beforeEach(function() {
+        this.req.body = {
+          lines: (this.lines = ['hello', 'world']),
+          version: (this.version = 42),
+          ranges: (this.ranges = { changes: 'mock' })
+        }
+        this.DocManager.updateDoc = sinon
+          .stub()
+          .yields(null, true, (this.rev = 5))
+        return this.HttpController.updateDoc(this.req, this.res, this.next)
+      })
+
+      it('should update the document', function() {
+        return this.DocManager.updateDoc
+          .calledWith(
+            this.project_id,
+            this.doc_id,
+            this.lines,
+            this.version,
+            this.ranges
+          )
+          .should.equal(true)
+      })
+
+      return it('should return a modified status', function() {
+        return this.res.json
+          .calledWith({ modified: true, rev: this.rev })
+          .should.equal(true)
+      })
+    })
+
+    describe('when the doc lines exist and were not updated', function() {
+      beforeEach(function() {
+        this.req.body = {
+          lines: (this.lines = ['hello', 'world']),
+          version: (this.version = 42),
+          ranges: {}
+        }
+        this.DocManager.updateDoc = sinon
+          .stub()
+          .yields(null, false, (this.rev = 5))
+        return this.HttpController.updateDoc(this.req, this.res, this.next)
+      })
+
+      return it('should return a modified status', function() {
+        return this.res.json
+          .calledWith({ modified: false, rev: this.rev })
+          .should.equal(true)
+      })
+    })
+
+    describe('when the doc lines are not provided', function() {
+      beforeEach(function() {
+        this.req.body = { version: 42, ranges: {} }
+        this.DocManager.updateDoc = sinon.stub().yields(null, false)
+        return this.HttpController.updateDoc(this.req, this.res, this.next)
+      })
+
+      it('should not update the document', function() {
+        return this.DocManager.updateDoc.called.should.equal(false)
+      })
+
+      return it('should return a 400 (bad request) response', function() {
+        return this.res.send.calledWith(400).should.equal(true)
+      })
+    })
+
+    describe('when the doc version are not provided', function() {
+      beforeEach(function() {
+        this.req.body = { version: 42, lines: ['hello world'] }
+        this.DocManager.updateDoc = sinon.stub().yields(null, false)
+        return this.HttpController.updateDoc(this.req, this.res, this.next)
+      })
+
+      it('should not update the document', function() {
+        return this.DocManager.updateDoc.called.should.equal(false)
+      })
+
+      return it('should return a 400 (bad request) response', function() {
+        return this.res.send.calledWith(400).should.equal(true)
+      })
+    })
+
+    describe('when the doc ranges is not provided', function() {
+      beforeEach(function() {
+        this.req.body = { lines: ['foo'], version: 42 }
+        this.DocManager.updateDoc = sinon.stub().yields(null, false)
+        return this.HttpController.updateDoc(this.req, this.res, this.next)
+      })
+
+      it('should not update the document', function() {
+        return this.DocManager.updateDoc.called.should.equal(false)
+      })
+
+      return it('should return a 400 (bad request) response', function() {
+        return this.res.send.calledWith(400).should.equal(true)
+      })
+    })
+
+    return describe('when the doc body is too large', function() {
+      beforeEach(function() {
+        this.req.body = {
+          lines: (this.lines = Array(2049).fill('a'.repeat(1024))),
+          version: (this.version = 42),
+          ranges: (this.ranges = { changes: 'mock' })
+        }
+        return this.HttpController.updateDoc(this.req, this.res, this.next)
+      })
+
+      it('should return a 413 (too large) response', function() {
+        return sinon.assert.calledWith(this.res.status, 413)
+      })
+
+      return it('should report that the document body is too large', function() {
+        return sinon.assert.calledWith(this.res.send, 'document body too large')
+      })
+    })
+  })
+
+  describe('deleteDoc', function() {
+    beforeEach(function() {
+      this.req.params = {
+        project_id: this.project_id,
+        doc_id: this.doc_id
+      }
+      this.DocManager.deleteDoc = sinon.stub().callsArg(2)
+      return this.HttpController.deleteDoc(this.req, this.res, this.next)
+    })
+
+    it('should delete the document', function() {
+      return this.DocManager.deleteDoc
+        .calledWith(this.project_id, this.doc_id)
+        .should.equal(true)
+    })
+
+    return it('should return a 204 (No Content)', function() {
+      return this.res.send.calledWith(204).should.equal(true)
+    })
+  })
+
+  describe('archiveAllDocs', function() {
+    beforeEach(function() {
+      this.req.params = { project_id: this.project_id }
+      this.DocArchiveManager.archiveAllDocs = sinon.stub().callsArg(1)
+      return this.HttpController.archiveAllDocs(this.req, this.res, this.next)
+    })
+
+    it('should archive the project', function() {
+      return this.DocArchiveManager.archiveAllDocs
+        .calledWith(this.project_id)
+        .should.equal(true)
+    })
+
+    return it('should return a 204 (No Content)', function() {
+      return this.res.send.calledWith(204).should.equal(true)
+    })
+  })
+
+  return describe('destroyAllDocs', function() {
+    beforeEach(function() {
+      this.req.params = { project_id: this.project_id }
+      this.DocArchiveManager.destroyAllDocs = sinon.stub().callsArg(1)
+      return this.HttpController.destroyAllDocs(this.req, this.res, this.next)
+    })
+
+    it('should destroy the docs', function() {
+      return sinon.assert.calledWith(
+        this.DocArchiveManager.destroyAllDocs,
+        this.project_id
+      )
+    })
+
+    return it('should return 204', function() {
+      return sinon.assert.calledWith(this.res.send, 204)
+    })
+  })
+})

+ 282 - 220
services/docstore/test/unit/js/MongoManagerTests.js

@@ -9,223 +9,285 @@
  * DS102: Remove unnecessary code created because of implicit returns
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-const SandboxedModule = require('sandboxed-module');
-const sinon = require('sinon');
-require('chai').should();
-const modulePath = require('path').join(__dirname, '../../../app/js/MongoManager');
-const {
-    ObjectId
-} = require("mongojs");
-const {
-    assert
-} = require("chai");
-
-describe("MongoManager", function() {
-	beforeEach(function() {
-		this.MongoManager = SandboxedModule.require(modulePath, { requires: {
-			"./mongojs": {
-				db: (this.db = { docs: {}, docOps: {} }),
-				ObjectId
-			},
-			'metrics-sharelatex': {timeAsyncMethod: sinon.stub()},
-			'logger-sharelatex': {log(){}}
-		}
-	});
-		this.project_id = ObjectId().toString();
-		this.doc_id = ObjectId().toString();
-		this.callback = sinon.stub();
-		return this.stubbedErr = new Error("hello world");
-	});
-
-	describe("findDoc", function() {
-		beforeEach(function() {
-			this.doc = { name: "mock-doc"};
-			this.db.docs.find = sinon.stub().callsArgWith(2, null, [this.doc]);
-			this.filter = { lines: true };
-			return this.MongoManager.findDoc(this.project_id, this.doc_id, this.filter, this.callback);
-		});
-
-		it("should find the doc", function() {
-			return this.db.docs.find
-				.calledWith({
-					_id: ObjectId(this.doc_id),
-					project_id: ObjectId(this.project_id)
-				}, this.filter)
-				.should.equal(true);
-		});
-
-		return it("should call the callback with the doc", function() {
-			return this.callback.calledWith(null, this.doc).should.equal(true);
-		});
-	});
-
-	describe("getProjectsDocs", function() {
-		beforeEach(function() {
-			this.filter = {lines: true};
-			this.doc1 = { name: "mock-doc1" };
-			this.doc2 = { name: "mock-doc2" };
-			this.doc3 = { name: "mock-doc3" };
-			this.doc4 = { name: "mock-doc4" };
-			return this.db.docs.find = sinon.stub().callsArgWith(2, null, [this.doc, this.doc3, this.doc4]);
-		});
-		
-		describe("with included_deleted = false", function() {
-			beforeEach(function() { 
-				return this.MongoManager.getProjectsDocs(this.project_id, {include_deleted: false}, this.filter, this.callback);
-			});
-
-			it("should find the non-deleted docs via the project_id", function() {
-				return this.db.docs.find
-					.calledWith({
-						project_id: ObjectId(this.project_id),
-						deleted: { $ne: true }
-					}, this.filter)
-					.should.equal(true);
-			});
-
-			return it("should call the callback with the docs", function() {
-				return this.callback.calledWith(null, [this.doc, this.doc3, this.doc4]).should.equal(true);
-			});
-		});
-				
-		return describe("with included_deleted = true", function() {
-			beforeEach(function() {
-				return this.MongoManager.getProjectsDocs(this.project_id, {include_deleted: true}, this.filter, this.callback);
-			});
-
-			it("should find all via the project_id", function() {
-				return this.db.docs.find
-					.calledWith({
-						project_id: ObjectId(this.project_id)
-					}, this.filter)
-					.should.equal(true);
-			});
-
-			return it("should call the callback with the docs", function() {
-				return this.callback.calledWith(null, [this.doc, this.doc3, this.doc4]).should.equal(true);
-			});
-		});
-	});
-
-	describe("upsertIntoDocCollection", function() {
-		beforeEach(function() {
-			this.db.docs.update = sinon.stub().callsArgWith(3, this.stubbedErr);
-			return this.oldRev = 77;
-		});
-
-		it("should upsert the document", function(done){	
-			return this.MongoManager.upsertIntoDocCollection(this.project_id, this.doc_id, {lines: this.lines}, err=> {
-				const args = this.db.docs.update.args[0];
-				assert.deepEqual(args[0], {_id: ObjectId(this.doc_id)});
-				assert.equal(args[1].$set.lines, this.lines);
-				assert.equal(args[1].$inc.rev, 1);
-				assert.deepEqual(args[1].$set.project_id, ObjectId(this.project_id));
-				return done();
-			});
-		});
-
-		return it("should return the error", function(done){
-			return this.MongoManager.upsertIntoDocCollection(this.project_id, this.doc_id, {lines: this.lines}, err=> {
-				err.should.equal(this.stubbedErr);
-				return done();
-			});
-		});
-	});
-
-	describe("markDocAsDeleted", function() {
-		beforeEach(function() {
-			this.db.docs.update = sinon.stub().callsArgWith(2, this.stubbedErr);
-			return this.oldRev = 77;
-		});
-
-		it("should process the update", function(done) {
-			return this.MongoManager.markDocAsDeleted(this.project_id, this.doc_id, err=> {
-				const args = this.db.docs.update.args[0];
-				assert.deepEqual(args[0], {_id: ObjectId(this.doc_id), project_id: ObjectId(this.project_id)});
-				assert.equal(args[1].$set.deleted, true);
-				return done();
-			});
-		});
-
-		return it("should return the error", function(done){
-			return this.MongoManager.markDocAsDeleted(this.project_id, this.doc_id, err=> {
-				err.should.equal(this.stubbedErr);
-				return done();
-			});
-		});
-	});
-
-	describe("destroyDoc", function() {
-		beforeEach(function(done) {
-			this.db.docs.remove = sinon.stub().yields();
-			this.db.docOps.remove = sinon.stub().yields();
-			return this.MongoManager.destroyDoc('123456789012', done);
-		});
-
-		it("should destroy the doc", function() {
-			return sinon.assert.calledWith(this.db.docs.remove, {_id: ObjectId('123456789012')});
-		});
-
-		return it("should destroy the docOps", function() {
-			return sinon.assert.calledWith(this.db.docOps.remove, {doc_id: ObjectId('123456789012')});
-		});
-	});
-
-	describe("getDocVersion", function() {
-		describe("when the doc exists", function() {
-			beforeEach(function() {
-				this.doc =
-					{version: (this.version = 42)};
-				this.db.docOps.find = sinon.stub().callsArgWith(2, null, [this.doc]);
-				return this.MongoManager.getDocVersion(this.doc_id, this.callback);
-			});
-
-			it("should look for the doc in the database", function() {
-				return this.db.docOps.find
-					.calledWith({ doc_id: ObjectId(this.doc_id) }, {version: 1})
-					.should.equal(true);
-			});
-
-			return it("should call the callback with the version", function() {
-				return this.callback.calledWith(null, this.version).should.equal(true);
-			});
-		});
-
-		return describe("when the doc doesn't exist", function() {
-			beforeEach(function() {
-				this.db.docOps.find = sinon.stub().callsArgWith(2, null, []);
-				return this.MongoManager.getDocVersion(this.doc_id, this.callback);
-			});
-
-			return it("should call the callback with 0", function() {
-				return this.callback.calledWith(null, 0).should.equal(true);
-			});
-		});
-	});
-
-	return describe("setDocVersion", function() {
-		beforeEach(function() {
-			this.version = 42;
-			this.db.docOps.update = sinon.stub().callsArg(3);
-			return this.MongoManager.setDocVersion(this.doc_id, this.version, this.callback);
-		});
-
-		it("should update the doc version", function() {
-			return this.db.docOps.update
-				.calledWith({
-					doc_id: ObjectId(this.doc_id)
-				}, {
-					$set: {
-						version: this.version
-					}
-				}, {
-					upsert: true 
-				})
-				.should.equal(true);
-		});
-
-		return it("should call the callback", function() {
-			return this.callback.called.should.equal(true);
-		});
-	});
-});
+const SandboxedModule = require('sandboxed-module')
+const sinon = require('sinon')
+require('chai').should()
+const modulePath = require('path').join(
+  __dirname,
+  '../../../app/js/MongoManager'
+)
+const { ObjectId } = require('mongojs')
+const { assert } = require('chai')
+
+describe('MongoManager', function() {
+  beforeEach(function() {
+    this.MongoManager = SandboxedModule.require(modulePath, {
+      requires: {
+        './mongojs': {
+          db: (this.db = { docs: {}, docOps: {} }),
+          ObjectId
+        },
+        'metrics-sharelatex': { timeAsyncMethod: sinon.stub() },
+        'logger-sharelatex': { log() {} }
+      }
+    })
+    this.project_id = ObjectId().toString()
+    this.doc_id = ObjectId().toString()
+    this.callback = sinon.stub()
+    return (this.stubbedErr = new Error('hello world'))
+  })
+
+  describe('findDoc', function() {
+    beforeEach(function() {
+      this.doc = { name: 'mock-doc' }
+      this.db.docs.find = sinon.stub().callsArgWith(2, null, [this.doc])
+      this.filter = { lines: true }
+      return this.MongoManager.findDoc(
+        this.project_id,
+        this.doc_id,
+        this.filter,
+        this.callback
+      )
+    })
+
+    it('should find the doc', function() {
+      return this.db.docs.find
+        .calledWith(
+          {
+            _id: ObjectId(this.doc_id),
+            project_id: ObjectId(this.project_id)
+          },
+          this.filter
+        )
+        .should.equal(true)
+    })
+
+    return it('should call the callback with the doc', function() {
+      return this.callback.calledWith(null, this.doc).should.equal(true)
+    })
+  })
+
+  describe('getProjectsDocs', function() {
+    beforeEach(function() {
+      this.filter = { lines: true }
+      this.doc1 = { name: 'mock-doc1' }
+      this.doc2 = { name: 'mock-doc2' }
+      this.doc3 = { name: 'mock-doc3' }
+      this.doc4 = { name: 'mock-doc4' }
+      return (this.db.docs.find = sinon
+        .stub()
+        .callsArgWith(2, null, [this.doc, this.doc3, this.doc4]))
+    })
+
+    describe('with included_deleted = false', function() {
+      beforeEach(function() {
+        return this.MongoManager.getProjectsDocs(
+          this.project_id,
+          { include_deleted: false },
+          this.filter,
+          this.callback
+        )
+      })
+
+      it('should find the non-deleted docs via the project_id', function() {
+        return this.db.docs.find
+          .calledWith(
+            {
+              project_id: ObjectId(this.project_id),
+              deleted: { $ne: true }
+            },
+            this.filter
+          )
+          .should.equal(true)
+      })
+
+      return it('should call the callback with the docs', function() {
+        return this.callback
+          .calledWith(null, [this.doc, this.doc3, this.doc4])
+          .should.equal(true)
+      })
+    })
+
+    return describe('with included_deleted = true', function() {
+      beforeEach(function() {
+        return this.MongoManager.getProjectsDocs(
+          this.project_id,
+          { include_deleted: true },
+          this.filter,
+          this.callback
+        )
+      })
+
+      it('should find all via the project_id', function() {
+        return this.db.docs.find
+          .calledWith(
+            {
+              project_id: ObjectId(this.project_id)
+            },
+            this.filter
+          )
+          .should.equal(true)
+      })
+
+      return it('should call the callback with the docs', function() {
+        return this.callback
+          .calledWith(null, [this.doc, this.doc3, this.doc4])
+          .should.equal(true)
+      })
+    })
+  })
+
+  describe('upsertIntoDocCollection', function() {
+    beforeEach(function() {
+      this.db.docs.update = sinon.stub().callsArgWith(3, this.stubbedErr)
+      return (this.oldRev = 77)
+    })
+
+    it('should upsert the document', function(done) {
+      return this.MongoManager.upsertIntoDocCollection(
+        this.project_id,
+        this.doc_id,
+        { lines: this.lines },
+        err => {
+          const args = this.db.docs.update.args[0]
+          assert.deepEqual(args[0], { _id: ObjectId(this.doc_id) })
+          assert.equal(args[1].$set.lines, this.lines)
+          assert.equal(args[1].$inc.rev, 1)
+          assert.deepEqual(args[1].$set.project_id, ObjectId(this.project_id))
+          return done()
+        }
+      )
+    })
+
+    return it('should return the error', function(done) {
+      return this.MongoManager.upsertIntoDocCollection(
+        this.project_id,
+        this.doc_id,
+        { lines: this.lines },
+        err => {
+          err.should.equal(this.stubbedErr)
+          return done()
+        }
+      )
+    })
+  })
+
+  describe('markDocAsDeleted', function() {
+    beforeEach(function() {
+      this.db.docs.update = sinon.stub().callsArgWith(2, this.stubbedErr)
+      return (this.oldRev = 77)
+    })
+
+    it('should process the update', function(done) {
+      return this.MongoManager.markDocAsDeleted(
+        this.project_id,
+        this.doc_id,
+        err => {
+          const args = this.db.docs.update.args[0]
+          assert.deepEqual(args[0], {
+            _id: ObjectId(this.doc_id),
+            project_id: ObjectId(this.project_id)
+          })
+          assert.equal(args[1].$set.deleted, true)
+          return done()
+        }
+      )
+    })
+
+    return it('should return the error', function(done) {
+      return this.MongoManager.markDocAsDeleted(
+        this.project_id,
+        this.doc_id,
+        err => {
+          err.should.equal(this.stubbedErr)
+          return done()
+        }
+      )
+    })
+  })
+
+  describe('destroyDoc', function() {
+    beforeEach(function(done) {
+      this.db.docs.remove = sinon.stub().yields()
+      this.db.docOps.remove = sinon.stub().yields()
+      return this.MongoManager.destroyDoc('123456789012', done)
+    })
+
+    it('should destroy the doc', function() {
+      return sinon.assert.calledWith(this.db.docs.remove, {
+        _id: ObjectId('123456789012')
+      })
+    })
+
+    return it('should destroy the docOps', function() {
+      return sinon.assert.calledWith(this.db.docOps.remove, {
+        doc_id: ObjectId('123456789012')
+      })
+    })
+  })
+
+  describe('getDocVersion', function() {
+    describe('when the doc exists', function() {
+      beforeEach(function() {
+        this.doc = { version: (this.version = 42) }
+        this.db.docOps.find = sinon.stub().callsArgWith(2, null, [this.doc])
+        return this.MongoManager.getDocVersion(this.doc_id, this.callback)
+      })
+
+      it('should look for the doc in the database', function() {
+        return this.db.docOps.find
+          .calledWith({ doc_id: ObjectId(this.doc_id) }, { version: 1 })
+          .should.equal(true)
+      })
+
+      return it('should call the callback with the version', function() {
+        return this.callback.calledWith(null, this.version).should.equal(true)
+      })
+    })
+
+    return describe("when the doc doesn't exist", function() {
+      beforeEach(function() {
+        this.db.docOps.find = sinon.stub().callsArgWith(2, null, [])
+        return this.MongoManager.getDocVersion(this.doc_id, this.callback)
+      })
+
+      return it('should call the callback with 0', function() {
+        return this.callback.calledWith(null, 0).should.equal(true)
+      })
+    })
+  })
+
+  return describe('setDocVersion', function() {
+    beforeEach(function() {
+      this.version = 42
+      this.db.docOps.update = sinon.stub().callsArg(3)
+      return this.MongoManager.setDocVersion(
+        this.doc_id,
+        this.version,
+        this.callback
+      )
+    })
+
+    it('should update the doc version', function() {
+      return this.db.docOps.update
+        .calledWith(
+          {
+            doc_id: ObjectId(this.doc_id)
+          },
+          {
+            $set: {
+              version: this.version
+            }
+          },
+          {
+            upsert: true
+          }
+        )
+        .should.equal(true)
+    })
+
+    return it('should call the callback', function() {
+      return this.callback.called.should.equal(true)
+    })
+  })
+})

+ 236 - 178
services/docstore/test/unit/js/RangeManagerTests.js

@@ -10,190 +10,248 @@
  * DS102: Remove unnecessary code created because of implicit returns
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-const SandboxedModule = require('sandboxed-module');
-const sinon = require('sinon');
-require('chai').should();
-const {
-    expect
-} = require('chai');
-const modulePath = require('path').join(__dirname, '../../../app/js/RangeManager');
-const {
-    ObjectId
-} = require("mongojs");
-const {
-    assert
-} = require("chai");
-const _ = require("underscore");
+const SandboxedModule = require('sandboxed-module')
+const sinon = require('sinon')
+require('chai').should()
+const { expect } = require('chai')
+const modulePath = require('path').join(
+  __dirname,
+  '../../../app/js/RangeManager'
+)
+const { ObjectId } = require('mongojs')
+const { assert } = require('chai')
+const _ = require('underscore')
 
-describe("RangeManager", function() {
-	beforeEach(function() {
-		return this.RangeManager = SandboxedModule.require(modulePath, { requires: {
-			"./mongojs": {
-				ObjectId
-			}
-		}
-	}
-		);
-	});
+describe('RangeManager', function() {
+  beforeEach(function() {
+    return (this.RangeManager = SandboxedModule.require(modulePath, {
+      requires: {
+        './mongojs': {
+          ObjectId
+        }
+      }
+    }))
+  })
 
-	describe("jsonRangesToMongo", function() {
-		it("should convert ObjectIds and dates to proper objects", function() {
-			const change_id = ObjectId().toString();
-			const comment_id = ObjectId().toString();
-			const user_id = ObjectId().toString();
-			const thread_id = ObjectId().toString();
-			const ts = new Date().toJSON();
-			return this.RangeManager.jsonRangesToMongo({
-				changes: [{
-					id: change_id,
-					op: { i: "foo", p: 3 },
-					metadata: {
-						user_id,
-						ts
-					}
-				}],
-				comments: [{
-					id: comment_id,
-					op: { c: "foo", p: 3, t: thread_id }
-				}]
-			}).should.deep.equal({
-				changes: [{
-					id: ObjectId(change_id),
-					op: { i: "foo", p: 3 },
-					metadata: {
-						user_id: ObjectId(user_id),
-						ts: new Date(ts)
-					}
-				}],
-				comments: [{
-					id: ObjectId(comment_id),
-					op: { c: "foo", p: 3, t: ObjectId(thread_id) }
-				}]
-			});
-	});
-		
-		it("should leave malformed ObjectIds as they are", function() {
-			const change_id = "foo";
-			const comment_id = "bar";
-			const user_id = "baz";
-			return this.RangeManager.jsonRangesToMongo({
-				changes: [{
-					id: change_id,
-					metadata: {
-						user_id
-					}
-				}],
-				comments: [{
-					id: comment_id
-				}]
-			}).should.deep.equal({
-				changes: [{
-					id: change_id,
-					metadata: {
-						user_id
-					}
-				}],
-				comments: [{
-					id: comment_id
-				}]
-			});
-	});
-		
-		return it("should be consistent when transformed through json -> mongo -> json", function() {
-			const change_id = ObjectId().toString();
-			const comment_id = ObjectId().toString();
-			const user_id = ObjectId().toString();
-			const thread_id = ObjectId().toString();
-			const ts = new Date().toJSON();
-			const ranges1 = {
-				changes: [{
-					id: change_id,
-					op: { i: "foo", p: 3 },
-					metadata: {
-						user_id,
-						ts
-					}
-				}],
-				comments: [{
-					id: comment_id,
-					op: { c: "foo", p: 3, t: thread_id }
-				}]
-			};
-			const ranges1_copy = JSON.parse(JSON.stringify(ranges1)); // jsonRangesToMongo modifies in place
-			const ranges2 = JSON.parse(JSON.stringify(this.RangeManager.jsonRangesToMongo(ranges1_copy)));
-			return ranges1.should.deep.equal(ranges2);
-		});
-	});
-			
-	return describe("shouldUpdateRanges", function() {
-		beforeEach(function() {
-			this.ranges = {
-				changes: [{
-					id: ObjectId(),
-					op: { i: "foo", p: 3 },
-					metadata: {
-						user_id: ObjectId(),
-						ts: new Date()
-					}
-				}],
-				comments: [{
-					id: ObjectId(),
-					op: { c: "foo", p: 3, t: ObjectId() }
-				}]
-			};
-			return this.ranges_copy = this.RangeManager.jsonRangesToMongo(JSON.parse(JSON.stringify(this.ranges)));
-		});
+  describe('jsonRangesToMongo', function() {
+    it('should convert ObjectIds and dates to proper objects', function() {
+      const change_id = ObjectId().toString()
+      const comment_id = ObjectId().toString()
+      const user_id = ObjectId().toString()
+      const thread_id = ObjectId().toString()
+      const ts = new Date().toJSON()
+      return this.RangeManager.jsonRangesToMongo({
+        changes: [
+          {
+            id: change_id,
+            op: { i: 'foo', p: 3 },
+            metadata: {
+              user_id,
+              ts
+            }
+          }
+        ],
+        comments: [
+          {
+            id: comment_id,
+            op: { c: 'foo', p: 3, t: thread_id }
+          }
+        ]
+      }).should.deep.equal({
+        changes: [
+          {
+            id: ObjectId(change_id),
+            op: { i: 'foo', p: 3 },
+            metadata: {
+              user_id: ObjectId(user_id),
+              ts: new Date(ts)
+            }
+          }
+        ],
+        comments: [
+          {
+            id: ObjectId(comment_id),
+            op: { c: 'foo', p: 3, t: ObjectId(thread_id) }
+          }
+        ]
+      })
+    })
 
-		describe("with a blank new range", function() { return it("should throw an error", function() {
-            return expect(() => {
-                return this.RangeManager.shouldUpdateRanges(this.ranges, null);
-            }).to.throw(Error);
-        }); });
-		
-		describe("with a blank old range", function() { return it("should treat it like {}", function() {
-            this.RangeManager.shouldUpdateRanges(null, {}).should.equal(false);
-            return this.RangeManager.shouldUpdateRanges(null, this.ranges).should.equal(true);
-        }); });
-		
-		describe("with no changes", function() { return it("should return false", function() {
-            return this.RangeManager.shouldUpdateRanges(this.ranges, this.ranges_copy).should.equal(false);
-        }); });
-		
-		return describe("with changes", function() {
-			it("should return true when the change id changes", function() {
-				this.ranges_copy.changes[0].id = ObjectId();
-				return this.RangeManager.shouldUpdateRanges(this.ranges, this.ranges_copy).should.equal(true);
-			});
+    it('should leave malformed ObjectIds as they are', function() {
+      const change_id = 'foo'
+      const comment_id = 'bar'
+      const user_id = 'baz'
+      return this.RangeManager.jsonRangesToMongo({
+        changes: [
+          {
+            id: change_id,
+            metadata: {
+              user_id
+            }
+          }
+        ],
+        comments: [
+          {
+            id: comment_id
+          }
+        ]
+      }).should.deep.equal({
+        changes: [
+          {
+            id: change_id,
+            metadata: {
+              user_id
+            }
+          }
+        ],
+        comments: [
+          {
+            id: comment_id
+          }
+        ]
+      })
+    })
 
-			it("should return true when the change user id changes", function() {
-				this.ranges_copy.changes[0].metadata.user_id = ObjectId();
-				return this.RangeManager.shouldUpdateRanges(this.ranges, this.ranges_copy).should.equal(true);
-			});
+    return it('should be consistent when transformed through json -> mongo -> json', function() {
+      const change_id = ObjectId().toString()
+      const comment_id = ObjectId().toString()
+      const user_id = ObjectId().toString()
+      const thread_id = ObjectId().toString()
+      const ts = new Date().toJSON()
+      const ranges1 = {
+        changes: [
+          {
+            id: change_id,
+            op: { i: 'foo', p: 3 },
+            metadata: {
+              user_id,
+              ts
+            }
+          }
+        ],
+        comments: [
+          {
+            id: comment_id,
+            op: { c: 'foo', p: 3, t: thread_id }
+          }
+        ]
+      }
+      const ranges1_copy = JSON.parse(JSON.stringify(ranges1)) // jsonRangesToMongo modifies in place
+      const ranges2 = JSON.parse(
+        JSON.stringify(this.RangeManager.jsonRangesToMongo(ranges1_copy))
+      )
+      return ranges1.should.deep.equal(ranges2)
+    })
+  })
 
-			it("should return true when the change ts changes", function() {
-				this.ranges_copy.changes[0].metadata.ts = new Date(Date.now() + 1000);
-				return this.RangeManager.shouldUpdateRanges(this.ranges, this.ranges_copy).should.equal(true);
-			});
+  return describe('shouldUpdateRanges', function() {
+    beforeEach(function() {
+      this.ranges = {
+        changes: [
+          {
+            id: ObjectId(),
+            op: { i: 'foo', p: 3 },
+            metadata: {
+              user_id: ObjectId(),
+              ts: new Date()
+            }
+          }
+        ],
+        comments: [
+          {
+            id: ObjectId(),
+            op: { c: 'foo', p: 3, t: ObjectId() }
+          }
+        ]
+      }
+      return (this.ranges_copy = this.RangeManager.jsonRangesToMongo(
+        JSON.parse(JSON.stringify(this.ranges))
+      ))
+    })
 
-			it("should return true when the change op changes", function() {
-				this.ranges_copy.changes[0].op.i = "bar";
-				return this.RangeManager.shouldUpdateRanges(this.ranges, this.ranges_copy).should.equal(true);
-			});
+    describe('with a blank new range', function() {
+      return it('should throw an error', function() {
+        return expect(() => {
+          return this.RangeManager.shouldUpdateRanges(this.ranges, null)
+        }).to.throw(Error)
+      })
+    })
 
-			it("should return true when the comment id changes", function() {
-				this.ranges_copy.comments[0].id = ObjectId();
-				return this.RangeManager.shouldUpdateRanges(this.ranges, this.ranges_copy).should.equal(true);
-			});
+    describe('with a blank old range', function() {
+      return it('should treat it like {}', function() {
+        this.RangeManager.shouldUpdateRanges(null, {}).should.equal(false)
+        return this.RangeManager.shouldUpdateRanges(
+          null,
+          this.ranges
+        ).should.equal(true)
+      })
+    })
 
-			it("should return true when the comment offset changes", function() {
-				this.ranges_copy.comments[0].op.p = 17;
-				return this.RangeManager.shouldUpdateRanges(this.ranges, this.ranges_copy).should.equal(true);
-			});
+    describe('with no changes', function() {
+      return it('should return false', function() {
+        return this.RangeManager.shouldUpdateRanges(
+          this.ranges,
+          this.ranges_copy
+        ).should.equal(false)
+      })
+    })
 
-			return it("should return true when the comment content changes", function() {
-				this.ranges_copy.comments[0].op.c = "bar";
-				return this.RangeManager.shouldUpdateRanges(this.ranges, this.ranges_copy).should.equal(true);
-			});
-		});
-	});
-});
+    return describe('with changes', function() {
+      it('should return true when the change id changes', function() {
+        this.ranges_copy.changes[0].id = ObjectId()
+        return this.RangeManager.shouldUpdateRanges(
+          this.ranges,
+          this.ranges_copy
+        ).should.equal(true)
+      })
+
+      it('should return true when the change user id changes', function() {
+        this.ranges_copy.changes[0].metadata.user_id = ObjectId()
+        return this.RangeManager.shouldUpdateRanges(
+          this.ranges,
+          this.ranges_copy
+        ).should.equal(true)
+      })
+
+      it('should return true when the change ts changes', function() {
+        this.ranges_copy.changes[0].metadata.ts = new Date(Date.now() + 1000)
+        return this.RangeManager.shouldUpdateRanges(
+          this.ranges,
+          this.ranges_copy
+        ).should.equal(true)
+      })
+
+      it('should return true when the change op changes', function() {
+        this.ranges_copy.changes[0].op.i = 'bar'
+        return this.RangeManager.shouldUpdateRanges(
+          this.ranges,
+          this.ranges_copy
+        ).should.equal(true)
+      })
+
+      it('should return true when the comment id changes', function() {
+        this.ranges_copy.comments[0].id = ObjectId()
+        return this.RangeManager.shouldUpdateRanges(
+          this.ranges,
+          this.ranges_copy
+        ).should.equal(true)
+      })
+
+      it('should return true when the comment offset changes', function() {
+        this.ranges_copy.comments[0].op.p = 17
+        return this.RangeManager.shouldUpdateRanges(
+          this.ranges,
+          this.ranges_copy
+        ).should.equal(true)
+      })
+
+      return it('should return true when the comment content changes', function() {
+        this.ranges_copy.comments[0].op.c = 'bar'
+        return this.RangeManager.shouldUpdateRanges(
+          this.ranges,
+          this.ranges_copy
+        ).should.equal(true)
+      })
+    })
+  })
+})