diff --git a/services/track-changes/test/unit/js/DiffGenerator/DiffGeneratorTests.js b/services/track-changes/test/unit/js/DiffGenerator/DiffGeneratorTests.js index 7876a22254..b84775ddea 100644 --- a/services/track-changes/test/unit/js/DiffGenerator/DiffGeneratorTests.js +++ b/services/track-changes/test/unit/js/DiffGenerator/DiffGeneratorTests.js @@ -10,393 +10,453 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const sinon = require('sinon'); -const chai = require('chai'); -const should = chai.should(); -const { expect } = chai; -const modulePath = "../../../../app/js/DiffGenerator.js"; -const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/js/DiffGenerator.js' +const SandboxedModule = require('sandboxed-module') -describe("DiffGenerator", function() { - beforeEach(function() { - this.DiffGenerator = SandboxedModule.require(modulePath, { requires: { - "logger-sharelatex": { warn: sinon.stub() } - } - }); - this.ts = Date.now(); - this.user_id = "mock-user-id"; - this.user_id_2 = "mock-user-id-2"; - return this.meta = { - start_ts: this.ts, end_ts: this.ts, user_id: this.user_id - };}); +describe('DiffGenerator', function() { + beforeEach(function() { + this.DiffGenerator = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': { warn: sinon.stub() } + } + }) + this.ts = Date.now() + this.user_id = 'mock-user-id' + this.user_id_2 = 'mock-user-id-2' + return (this.meta = { + start_ts: this.ts, + end_ts: this.ts, + user_id: this.user_id + }) + }) - describe("rewindOp", function() { - describe("rewinding an insert", function() { return it("should undo the insert", function() { - const content = "hello world"; - const rewoundContent = this.DiffGenerator.rewindOp(content, { p: 6, i: "wo" }); - return rewoundContent.should.equal("hello rld"); - }); } - ); + describe('rewindOp', function() { + describe('rewinding an insert', function() { + return it('should undo the insert', function() { + const content = 'hello world' + const rewoundContent = this.DiffGenerator.rewindOp(content, { + p: 6, + i: 'wo' + }) + return rewoundContent.should.equal('hello rld') + }) + }) - describe("rewinding a delete", function() { return it("should undo the delete", function() { - const content = "hello rld"; - const rewoundContent = this.DiffGenerator.rewindOp(content, { p: 6, d: "wo" }); - return rewoundContent.should.equal("hello world"); - }); } - ); + describe('rewinding a delete', function() { + return it('should undo the delete', function() { + const content = 'hello rld' + const rewoundContent = this.DiffGenerator.rewindOp(content, { + p: 6, + d: 'wo' + }) + return rewoundContent.should.equal('hello world') + }) + }) - describe("with an inconsistent update", function() { return it("should throw an error", function() { - const content = "hello world"; - return expect( () => { - return this.DiffGenerator.rewindOp(content, { p: 6, i: "foo" }); - }).to.throw(this.DiffGenerator.ConsistencyError); - }); } - ); - - return describe("with an update which is beyond the length of the content", function() { return it("should undo the insert as if it were at the end of the content", function() { - const content = "foobar"; - const rewoundContent = this.DiffGenerator.rewindOp(content, { p: 4, i: "bar" }); - return rewoundContent.should.equal("foo"); - }); } - ); - }); + describe('with an inconsistent update', function() { + return it('should throw an error', function() { + const content = 'hello world' + return expect(() => { + return this.DiffGenerator.rewindOp(content, { p: 6, i: 'foo' }) + }).to.throw(this.DiffGenerator.ConsistencyError) + }) + }) - describe("rewindUpdate", function() { return it("should rewind ops in reverse", function() { - const content = "aaabbbccc"; - const update = - {op: [{ p: 3, i: "bbb" }, { p: 6, i: "ccc" }]}; - const rewoundContent = this.DiffGenerator.rewindUpdate(content, update); - return rewoundContent.should.equal("aaa"); - }); } - ); + return describe('with an update which is beyond the length of the content', function() { + return it('should undo the insert as if it were at the end of the content', function() { + const content = 'foobar' + const rewoundContent = this.DiffGenerator.rewindOp(content, { + p: 4, + i: 'bar' + }) + return rewoundContent.should.equal('foo') + }) + }) + }) - describe("rewindUpdates", function() { return it("should rewind updates in reverse", function() { - const content = "aaabbbccc"; - const updates = [ - { op: [{ p: 3, i: "bbb" }] }, - { op: [{ p: 6, i: "ccc" }] } - ]; - const rewoundContent = this.DiffGenerator.rewindUpdates(content, updates); - return rewoundContent.should.equal("aaa"); - }); } - ); + describe('rewindUpdate', function() { + return it('should rewind ops in reverse', function() { + const content = 'aaabbbccc' + const update = { + op: [ + { p: 3, i: 'bbb' }, + { p: 6, i: 'ccc' } + ] + } + const rewoundContent = this.DiffGenerator.rewindUpdate(content, update) + return rewoundContent.should.equal('aaa') + }) + }) - describe("buildDiff", function() { - beforeEach(function() { - this.diff = [ {u: "mock-diff"} ]; - this.content = "Hello world"; - this.updates = [ - { i: "mock-update-1" }, - { i: "mock-update-2" }, - { i: "mock-update-3" } - ]; - this.DiffGenerator.applyUpdateToDiff = sinon.stub().returns(this.diff); - this.DiffGenerator.compressDiff = sinon.stub().returns(this.diff); - return this.result = this.DiffGenerator.buildDiff(this.content, this.updates); - }); + describe('rewindUpdates', function() { + return it('should rewind updates in reverse', function() { + const content = 'aaabbbccc' + const updates = [ + { op: [{ p: 3, i: 'bbb' }] }, + { op: [{ p: 6, i: 'ccc' }] } + ] + const rewoundContent = this.DiffGenerator.rewindUpdates(content, updates) + return rewoundContent.should.equal('aaa') + }) + }) - it("should return the diff", function() { - return this.result.should.deep.equal(this.diff); - }); + describe('buildDiff', function() { + beforeEach(function() { + this.diff = [{ u: 'mock-diff' }] + this.content = 'Hello world' + this.updates = [ + { i: 'mock-update-1' }, + { i: 'mock-update-2' }, + { i: 'mock-update-3' } + ] + this.DiffGenerator.applyUpdateToDiff = sinon.stub().returns(this.diff) + this.DiffGenerator.compressDiff = sinon.stub().returns(this.diff) + return (this.result = this.DiffGenerator.buildDiff( + this.content, + this.updates + )) + }) - it("should build the content into an initial diff", function() { - return this.DiffGenerator.applyUpdateToDiff - .calledWith([{ - u: this.content - }], this.updates[0]) - .should.equal(true); - }); + it('should return the diff', function() { + return this.result.should.deep.equal(this.diff) + }) - it("should apply each update", function() { - return Array.from(this.updates).map((update) => - this.DiffGenerator.applyUpdateToDiff - .calledWith(sinon.match.any, update) - .should.equal(true)); - }); + it('should build the content into an initial diff', function() { + return this.DiffGenerator.applyUpdateToDiff + .calledWith( + [ + { + u: this.content + } + ], + this.updates[0] + ) + .should.equal(true) + }) - return it("should compress the diff", function() { - return this.DiffGenerator.compressDiff - .calledWith(this.diff) - .should.equal(true); - }); - }); + it('should apply each update', function() { + return Array.from(this.updates).map(update => + this.DiffGenerator.applyUpdateToDiff + .calledWith(sinon.match.any, update) + .should.equal(true) + ) + }) - describe("compressDiff", function() { - describe("with adjacent inserts with the same user_id", function() { return it("should create one update with combined meta data and min/max timestamps", function() { - const diff = this.DiffGenerator.compressDiff([ - { i: "foo", meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } }}, - { i: "bar", meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id } }} - ]); - return expect(diff).to.deep.equal([ - { i: "foobar", meta: { start_ts: 5, end_ts: 20, user: { id: this.user_id } }} - ]); - }); } - ); + return it('should compress the diff', function() { + return this.DiffGenerator.compressDiff + .calledWith(this.diff) + .should.equal(true) + }) + }) - describe("with adjacent inserts with different user_ids", function() { return it("should leave the inserts unchanged", function() { - const input = [ - { i: "foo", meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } }}, - { i: "bar", meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id_2 } }} - ]; - const output = this.DiffGenerator.compressDiff(input); - return expect(output).to.deep.equal(input); - }); } - ); + describe('compressDiff', function() { + describe('with adjacent inserts with the same user_id', function() { + return it('should create one update with combined meta data and min/max timestamps', function() { + const diff = this.DiffGenerator.compressDiff([ + { + i: 'foo', + meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } } + }, + { + i: 'bar', + meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id } } + } + ]) + return expect(diff).to.deep.equal([ + { + i: 'foobar', + meta: { start_ts: 5, end_ts: 20, user: { id: this.user_id } } + } + ]) + }) + }) - describe("with adjacent deletes with the same user_id", function() { return it("should create one update with combined meta data and min/max timestamps", function() { - const diff = this.DiffGenerator.compressDiff([ - { d: "foo", meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } }}, - { d: "bar", meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id } }} - ]); - return expect(diff).to.deep.equal([ - { d: "foobar", meta: { start_ts: 5, end_ts: 20, user: { id: this.user_id } }} - ]); - }); } - ); + describe('with adjacent inserts with different user_ids', function() { + return it('should leave the inserts unchanged', function() { + const input = [ + { + i: 'foo', + meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } } + }, + { + i: 'bar', + meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id_2 } } + } + ] + const output = this.DiffGenerator.compressDiff(input) + return expect(output).to.deep.equal(input) + }) + }) - return describe("with adjacent deletes with different user_ids", function() { return it("should leave the deletes unchanged", function() { - const input = [ - { d: "foo", meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } }}, - { d: "bar", meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id_2 } }} - ]; - const output = this.DiffGenerator.compressDiff(input); - return expect(output).to.deep.equal(input); - }); } - ); - }); + describe('with adjacent deletes with the same user_id', function() { + return it('should create one update with combined meta data and min/max timestamps', function() { + const diff = this.DiffGenerator.compressDiff([ + { + d: 'foo', + meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } } + }, + { + d: 'bar', + meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id } } + } + ]) + return expect(diff).to.deep.equal([ + { + d: 'foobar', + meta: { start_ts: 5, end_ts: 20, user: { id: this.user_id } } + } + ]) + }) + }) - return describe("applyUpdateToDiff", function() { - describe("an insert", function() { - it("should insert into the middle of (u)nchanged text", function() { - const diff = this.DiffGenerator.applyUpdateToDiff( - [ { u: "foobar" } ], - { op: [{ p: 3, i: "baz" }], meta: this.meta } - ); - return expect(diff).to.deep.equal([ - { u: "foo" }, - { i: "baz", meta: this.meta }, - { u: "bar" } - ]); - }); + return describe('with adjacent deletes with different user_ids', function() { + return it('should leave the deletes unchanged', function() { + const input = [ + { + d: 'foo', + meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } } + }, + { + d: 'bar', + meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id_2 } } + } + ] + const output = this.DiffGenerator.compressDiff(input) + return expect(output).to.deep.equal(input) + }) + }) + }) - it("should insert into the start of (u)changed text", function() { - const diff = this.DiffGenerator.applyUpdateToDiff( - [ { u: "foobar" } ], - { op: [{ p: 0, i: "baz" }], meta: this.meta } - ); - return expect(diff).to.deep.equal([ - { i: "baz", meta: this.meta }, - { u: "foobar" } - ]); - }); + return describe('applyUpdateToDiff', function() { + describe('an insert', function() { + it('should insert into the middle of (u)nchanged text', function() { + const diff = this.DiffGenerator.applyUpdateToDiff([{ u: 'foobar' }], { + op: [{ p: 3, i: 'baz' }], + meta: this.meta + }) + return expect(diff).to.deep.equal([ + { u: 'foo' }, + { i: 'baz', meta: this.meta }, + { u: 'bar' } + ]) + }) - it("should insert into the end of (u)changed text", function() { - const diff = this.DiffGenerator.applyUpdateToDiff( - [ { u: "foobar" } ], - { op: [{ p: 6, i: "baz" }], meta: this.meta } - ); - return expect(diff).to.deep.equal([ - { u: "foobar" }, - { i: "baz", meta: this.meta } - ]); - }); + it('should insert into the start of (u)changed text', function() { + const diff = this.DiffGenerator.applyUpdateToDiff([{ u: 'foobar' }], { + op: [{ p: 0, i: 'baz' }], + meta: this.meta + }) + return expect(diff).to.deep.equal([ + { i: 'baz', meta: this.meta }, + { u: 'foobar' } + ]) + }) - it("should insert into the middle of (i)inserted text", function() { - const diff = this.DiffGenerator.applyUpdateToDiff( - [ { i: "foobar", meta: this.meta } ], - { op: [{ p: 3, i: "baz" }], meta: this.meta } - ); - return expect(diff).to.deep.equal([ - { i: "foo", meta: this.meta }, - { i: "baz", meta: this.meta }, - { i: "bar", meta: this.meta } - ]); - }); + it('should insert into the end of (u)changed text', function() { + const diff = this.DiffGenerator.applyUpdateToDiff([{ u: 'foobar' }], { + op: [{ p: 6, i: 'baz' }], + meta: this.meta + }) + return expect(diff).to.deep.equal([ + { u: 'foobar' }, + { i: 'baz', meta: this.meta } + ]) + }) - return it("should not count deletes in the running length total", function() { - const diff = this.DiffGenerator.applyUpdateToDiff( - [ - { d: "deleted", meta: this.meta }, - { u: "foobar" } - ], - { op: [{ p: 3, i: "baz" }], meta: this.meta } - ); - return expect(diff).to.deep.equal([ - { d: "deleted", meta: this.meta }, - { u: "foo" }, - { i: "baz", meta: this.meta }, - { u: "bar" } - ]); - }); - }); + it('should insert into the middle of (i)inserted text', function() { + const diff = this.DiffGenerator.applyUpdateToDiff( + [{ i: 'foobar', meta: this.meta }], + { op: [{ p: 3, i: 'baz' }], meta: this.meta } + ) + return expect(diff).to.deep.equal([ + { i: 'foo', meta: this.meta }, + { i: 'baz', meta: this.meta }, + { i: 'bar', meta: this.meta } + ]) + }) - return describe("a delete", function() { - describe("deleting unchanged text", function() { - it("should delete from the middle of (u)nchanged text", function() { - const diff = this.DiffGenerator.applyUpdateToDiff( - [ { u: "foobazbar" } ], - { op: [{ p: 3, d: "baz" }], meta: this.meta } - ); - return expect(diff).to.deep.equal([ - { u: "foo" }, - { d: "baz", meta: this.meta }, - { u: "bar" } - ]); - }); + return it('should not count deletes in the running length total', function() { + const diff = this.DiffGenerator.applyUpdateToDiff( + [{ d: 'deleted', meta: this.meta }, { u: 'foobar' }], + { op: [{ p: 3, i: 'baz' }], meta: this.meta } + ) + return expect(diff).to.deep.equal([ + { d: 'deleted', meta: this.meta }, + { u: 'foo' }, + { i: 'baz', meta: this.meta }, + { u: 'bar' } + ]) + }) + }) - it("should delete from the start of (u)nchanged text", function() { - const diff = this.DiffGenerator.applyUpdateToDiff( - [ { u: "foobazbar" } ], - { op: [{ p: 0, d: "foo" }], meta: this.meta } - ); - return expect(diff).to.deep.equal([ - { d: "foo", meta: this.meta }, - { u: "bazbar" } - ]); - }); + return describe('a delete', function() { + describe('deleting unchanged text', function() { + it('should delete from the middle of (u)nchanged text', function() { + const diff = this.DiffGenerator.applyUpdateToDiff( + [{ u: 'foobazbar' }], + { op: [{ p: 3, d: 'baz' }], meta: this.meta } + ) + return expect(diff).to.deep.equal([ + { u: 'foo' }, + { d: 'baz', meta: this.meta }, + { u: 'bar' } + ]) + }) - it("should delete from the end of (u)nchanged text", function() { - const diff = this.DiffGenerator.applyUpdateToDiff( - [ { u: "foobazbar" } ], - { op: [{ p: 6, d: "bar" }], meta: this.meta } - ); - return expect(diff).to.deep.equal([ - { u: "foobaz" }, - { d: "bar", meta: this.meta } - ]); - }); + it('should delete from the start of (u)nchanged text', function() { + const diff = this.DiffGenerator.applyUpdateToDiff( + [{ u: 'foobazbar' }], + { op: [{ p: 0, d: 'foo' }], meta: this.meta } + ) + return expect(diff).to.deep.equal([ + { d: 'foo', meta: this.meta }, + { u: 'bazbar' } + ]) + }) - return it("should delete across multiple (u)changed text parts", function() { - const diff = this.DiffGenerator.applyUpdateToDiff( - [ { u: "foo" }, { u: "baz" }, { u: "bar" } ], - { op: [{ p: 2, d: "obazb" }], meta: this.meta } - ); - return expect(diff).to.deep.equal([ - { u: "fo" }, - { d: "o", meta: this.meta }, - { d: "baz", meta: this.meta }, - { d: "b", meta: this.meta }, - { u: "ar" } - ]); - }); - }); + it('should delete from the end of (u)nchanged text', function() { + const diff = this.DiffGenerator.applyUpdateToDiff( + [{ u: 'foobazbar' }], + { op: [{ p: 6, d: 'bar' }], meta: this.meta } + ) + return expect(diff).to.deep.equal([ + { u: 'foobaz' }, + { d: 'bar', meta: this.meta } + ]) + }) - describe("deleting inserts", function() { - it("should delete from the middle of (i)nserted text", function() { - const diff = this.DiffGenerator.applyUpdateToDiff( - [ { i: "foobazbar", meta: this.meta } ], - { op: [{ p: 3, d: "baz" }], meta: this.meta } - ); - return expect(diff).to.deep.equal([ - { i: "foo", meta: this.meta }, - { i: "bar", meta: this.meta } - ]); - }); + return it('should delete across multiple (u)changed text parts', function() { + const diff = this.DiffGenerator.applyUpdateToDiff( + [{ u: 'foo' }, { u: 'baz' }, { u: 'bar' }], + { op: [{ p: 2, d: 'obazb' }], meta: this.meta } + ) + return expect(diff).to.deep.equal([ + { u: 'fo' }, + { d: 'o', meta: this.meta }, + { d: 'baz', meta: this.meta }, + { d: 'b', meta: this.meta }, + { u: 'ar' } + ]) + }) + }) - it("should delete from the start of (u)nchanged text", function() { - const diff = this.DiffGenerator.applyUpdateToDiff( - [ { i: "foobazbar", meta: this.meta } ], - { op: [{ p: 0, d: "foo" }], meta: this.meta } - ); - return expect(diff).to.deep.equal([ - { i: "bazbar", meta: this.meta } - ]); - }); + describe('deleting inserts', function() { + it('should delete from the middle of (i)nserted text', function() { + const diff = this.DiffGenerator.applyUpdateToDiff( + [{ i: 'foobazbar', meta: this.meta }], + { op: [{ p: 3, d: 'baz' }], meta: this.meta } + ) + return expect(diff).to.deep.equal([ + { i: 'foo', meta: this.meta }, + { i: 'bar', meta: this.meta } + ]) + }) - it("should delete from the end of (u)nchanged text", function() { - const diff = this.DiffGenerator.applyUpdateToDiff( - [ { i: "foobazbar", meta: this.meta } ], - { op: [{ p: 6, d: "bar" }], meta: this.meta } - ); - return expect(diff).to.deep.equal([ - { i: "foobaz", meta: this.meta } - ]); - }); + it('should delete from the start of (u)nchanged text', function() { + const diff = this.DiffGenerator.applyUpdateToDiff( + [{ i: 'foobazbar', meta: this.meta }], + { op: [{ p: 0, d: 'foo' }], meta: this.meta } + ) + return expect(diff).to.deep.equal([{ i: 'bazbar', meta: this.meta }]) + }) - return it("should delete across multiple (u)changed and (i)nserted text parts", function() { - const diff = this.DiffGenerator.applyUpdateToDiff( - [ { u: "foo" }, { i: "baz", meta: this.meta }, { u: "bar" } ], - { op: [{ p: 2, d: "obazb" }], meta: this.meta } - ); - return expect(diff).to.deep.equal([ - { u: "fo" }, - { d: "o", meta: this.meta }, - { d: "b", meta: this.meta }, - { u: "ar" } - ]); - }); - }); + it('should delete from the end of (u)nchanged text', function() { + const diff = this.DiffGenerator.applyUpdateToDiff( + [{ i: 'foobazbar', meta: this.meta }], + { op: [{ p: 6, d: 'bar' }], meta: this.meta } + ) + return expect(diff).to.deep.equal([{ i: 'foobaz', meta: this.meta }]) + }) - describe("deleting over existing deletes", function() { return it("should delete across multiple (u)changed and (d)deleted text parts", function() { - const diff = this.DiffGenerator.applyUpdateToDiff( - [ { u: "foo" }, { d: "baz", meta: this.meta }, { u: "bar" } ], - { op: [{ p: 2, d: "ob" }], meta: this.meta } - ); - return expect(diff).to.deep.equal([ - { u: "fo" }, - { d: "o", meta: this.meta }, - { d: "baz", meta: this.meta }, - { d: "b", meta: this.meta }, - { u: "ar" } - ]); - }); } - ); + return it('should delete across multiple (u)changed and (i)nserted text parts', function() { + const diff = this.DiffGenerator.applyUpdateToDiff( + [{ u: 'foo' }, { i: 'baz', meta: this.meta }, { u: 'bar' }], + { op: [{ p: 2, d: 'obazb' }], meta: this.meta } + ) + return expect(diff).to.deep.equal([ + { u: 'fo' }, + { d: 'o', meta: this.meta }, + { d: 'b', meta: this.meta }, + { u: 'ar' } + ]) + }) + }) - describe("deleting when the text doesn't match", function() { - it("should throw an error when deleting from the middle of (u)nchanged text", function() { - return expect( - () => this.DiffGenerator.applyUpdateToDiff( - [ { u: "foobazbar" } ], - { op: [{ p: 3, d: "xxx" }], meta: this.meta } - ) - ).to.throw(this.DiffGenerator.ConsistencyError); - }); + describe('deleting over existing deletes', function() { + return it('should delete across multiple (u)changed and (d)deleted text parts', function() { + const diff = this.DiffGenerator.applyUpdateToDiff( + [{ u: 'foo' }, { d: 'baz', meta: this.meta }, { u: 'bar' }], + { op: [{ p: 2, d: 'ob' }], meta: this.meta } + ) + return expect(diff).to.deep.equal([ + { u: 'fo' }, + { d: 'o', meta: this.meta }, + { d: 'baz', meta: this.meta }, + { d: 'b', meta: this.meta }, + { u: 'ar' } + ]) + }) + }) - it("should throw an error when deleting from the start of (u)nchanged text", function() { - return expect( - () => this.DiffGenerator.applyUpdateToDiff( - [ { u: "foobazbar" } ], - { op: [{ p: 0, d: "xxx" }], meta: this.meta } - ) - ).to.throw(this.DiffGenerator.ConsistencyError); - }); + describe("deleting when the text doesn't match", function() { + it('should throw an error when deleting from the middle of (u)nchanged text', function() { + return expect(() => + this.DiffGenerator.applyUpdateToDiff([{ u: 'foobazbar' }], { + op: [{ p: 3, d: 'xxx' }], + meta: this.meta + }) + ).to.throw(this.DiffGenerator.ConsistencyError) + }) - return it("should throw an error when deleting from the end of (u)nchanged text", function() { - return expect( - () => this.DiffGenerator.applyUpdateToDiff( - [ { u: "foobazbar" } ], - { op: [{ p: 6, d: "xxx" }] , meta: this.meta } - ) - ).to.throw(this.DiffGenerator.ConsistencyError); - }); - }); + it('should throw an error when deleting from the start of (u)nchanged text', function() { + return expect(() => + this.DiffGenerator.applyUpdateToDiff([{ u: 'foobazbar' }], { + op: [{ p: 0, d: 'xxx' }], + meta: this.meta + }) + ).to.throw(this.DiffGenerator.ConsistencyError) + }) - describe("when the last update in the existing diff is a delete", function() { return it("should insert the new update before the delete", function() { - const diff = this.DiffGenerator.applyUpdateToDiff( - [ { u: "foo" }, { d: "bar", meta: this.meta } ], - { op: [{ p: 3, i: "baz" }], meta: this.meta } - ); - return expect(diff).to.deep.equal([ - { u: "foo" }, - { i: "baz", meta: this.meta }, - { d: "bar", meta: this.meta } - ]); - }); } - ); - - return describe("when the only update in the existing diff is a delete", function() { return it("should insert the new update after the delete", function() { - const diff = this.DiffGenerator.applyUpdateToDiff( - [ { d: "bar", meta: this.meta } ], - { op: [{ p: 0, i: "baz" }], meta: this.meta } - ); - return expect(diff).to.deep.equal([ - { d: "bar", meta: this.meta }, - { i: "baz", meta: this.meta } - ]); - }); } - ); - }); - }); -}); + return it('should throw an error when deleting from the end of (u)nchanged text', function() { + return expect(() => + this.DiffGenerator.applyUpdateToDiff([{ u: 'foobazbar' }], { + op: [{ p: 6, d: 'xxx' }], + meta: this.meta + }) + ).to.throw(this.DiffGenerator.ConsistencyError) + }) + }) + describe('when the last update in the existing diff is a delete', function() { + return it('should insert the new update before the delete', function() { + const diff = this.DiffGenerator.applyUpdateToDiff( + [{ u: 'foo' }, { d: 'bar', meta: this.meta }], + { op: [{ p: 3, i: 'baz' }], meta: this.meta } + ) + return expect(diff).to.deep.equal([ + { u: 'foo' }, + { i: 'baz', meta: this.meta }, + { d: 'bar', meta: this.meta } + ]) + }) + }) + return describe('when the only update in the existing diff is a delete', function() { + return it('should insert the new update after the delete', function() { + const diff = this.DiffGenerator.applyUpdateToDiff( + [{ d: 'bar', meta: this.meta }], + { op: [{ p: 0, i: 'baz' }], meta: this.meta } + ) + return expect(diff).to.deep.equal([ + { d: 'bar', meta: this.meta }, + { i: 'baz', meta: this.meta } + ]) + }) + }) + }) + }) +}) diff --git a/services/track-changes/test/unit/js/DiffManager/DiffManagerTests.js b/services/track-changes/test/unit/js/DiffManager/DiffManagerTests.js index cc8f29c6f6..f6768fa197 100644 --- a/services/track-changes/test/unit/js/DiffManager/DiffManagerTests.js +++ b/services/track-changes/test/unit/js/DiffManager/DiffManagerTests.js @@ -10,301 +10,439 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const sinon = require('sinon'); -const chai = require('chai'); -const should = chai.should(); -const { expect } = chai; -const modulePath = "../../../../app/js/DiffManager.js"; -const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/js/DiffManager.js' +const SandboxedModule = require('sandboxed-module') -describe("DiffManager", function() { - beforeEach(function() { - this.DiffManager = SandboxedModule.require(modulePath, { requires: { - "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() }), - "./UpdatesManager": (this.UpdatesManager = {}), - "./DocumentUpdaterManager": (this.DocumentUpdaterManager = {}), - "./DiffGenerator": (this.DiffGenerator = {}) - } - }); - this.callback = sinon.stub(); - this.from = new Date(); - this.to = new Date(Date.now() + 10000); - this.project_id = "mock-project-id"; - return this.doc_id = "mock-doc-id"; - }); +describe('DiffManager', function() { + beforeEach(function() { + this.DiffManager = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub(), + warn: sinon.stub() + }), + './UpdatesManager': (this.UpdatesManager = {}), + './DocumentUpdaterManager': (this.DocumentUpdaterManager = {}), + './DiffGenerator': (this.DiffGenerator = {}) + } + }) + this.callback = sinon.stub() + this.from = new Date() + this.to = new Date(Date.now() + 10000) + this.project_id = 'mock-project-id' + return (this.doc_id = 'mock-doc-id') + }) - describe("getLatestDocAndUpdates", function() { - beforeEach(function() { - this.content = "hello world"; - this.version = 42; - this.updates = [ "mock-update-1", "mock-update-2" ]; + describe('getLatestDocAndUpdates', function() { + beforeEach(function() { + this.content = 'hello world' + this.version = 42 + this.updates = ['mock-update-1', 'mock-update-2'] - this.DocumentUpdaterManager.getDocument = sinon.stub().callsArgWith(2, null, this.content, this.version); - return this.UpdatesManager.getDocUpdatesWithUserInfo = sinon.stub().callsArgWith(3, null, this.updates); - }); + this.DocumentUpdaterManager.getDocument = sinon + .stub() + .callsArgWith(2, null, this.content, this.version) + return (this.UpdatesManager.getDocUpdatesWithUserInfo = sinon + .stub() + .callsArgWith(3, null, this.updates)) + }) - describe("with a fromVersion", function() { - beforeEach(function() { - return this.DiffManager.getLatestDocAndUpdates(this.project_id, this.doc_id, this.from, this.callback); - }); + describe('with a fromVersion', function() { + beforeEach(function() { + return this.DiffManager.getLatestDocAndUpdates( + this.project_id, + this.doc_id, + this.from, + this.callback + ) + }) - it("should get the latest version of the doc", function() { - return this.DocumentUpdaterManager.getDocument - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); + it('should get the latest version of the doc', function() { + return this.DocumentUpdaterManager.getDocument + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) - it("should get the latest updates", function() { - return this.UpdatesManager.getDocUpdatesWithUserInfo - .calledWith(this.project_id, this.doc_id, {from: this.from}) - .should.equal(true); - }); + it('should get the latest updates', function() { + return this.UpdatesManager.getDocUpdatesWithUserInfo + .calledWith(this.project_id, this.doc_id, { from: this.from }) + .should.equal(true) + }) - return it("should call the callback with the content, version and updates", function() { - return this.callback.calledWith(null, this.content, this.version, this.updates).should.equal(true); - }); - }); + return it('should call the callback with the content, version and updates', function() { + return this.callback + .calledWith(null, this.content, this.version, this.updates) + .should.equal(true) + }) + }) - return describe("with no fromVersion", function() { - beforeEach(function() { - return this.DiffManager.getLatestDocAndUpdates(this.project_id, this.doc_id, null, this.callback); - }); + return describe('with no fromVersion', function() { + beforeEach(function() { + return this.DiffManager.getLatestDocAndUpdates( + this.project_id, + this.doc_id, + null, + this.callback + ) + }) - it("should get the latest version of the doc", function() { - return this.DocumentUpdaterManager.getDocument - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); + it('should get the latest version of the doc', function() { + return this.DocumentUpdaterManager.getDocument + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) - it("should not get the latest updates", function() { - return this.UpdatesManager.getDocUpdatesWithUserInfo - .called.should.equal(false); - }); + it('should not get the latest updates', function() { + return this.UpdatesManager.getDocUpdatesWithUserInfo.called.should.equal( + false + ) + }) - return it("should call the callback with the content, version and blank updates", function() { - return this.callback.calledWith(null, this.content, this.version, []).should.equal(true); - }); - }); - }); - + return it('should call the callback with the content, version and blank updates', function() { + return this.callback + .calledWith(null, this.content, this.version, []) + .should.equal(true) + }) + }) + }) - describe("getDiff", function() { - beforeEach(function() { - this.content = "hello world"; - // Op versions are the version they were applied to, so doc is always one version - // ahead.s - this.version = 43; - this.updates = [ - { op: "mock-4", v: 42, meta: { start_ts: new Date(this.to.getTime() + 20)} }, - { op: "mock-3", v: 41, meta: { start_ts: new Date(this.to.getTime() + 10)} }, - { op: "mock-2", v: 40, meta: { start_ts: new Date(this.to.getTime() - 10)} }, - { op: "mock-1", v: 39, meta: { start_ts: new Date(this.to.getTime() - 20)} } - ]; - this.fromVersion = 39; - this.toVersion = 40; - this.diffed_updates = this.updates.slice(2); - this.rewound_content = "rewound-content"; - return this.diff = [ {u: "mock-diff"} ];}); - - describe("with matching versions", function() { - beforeEach(function() { - this.DiffManager.getDocumentBeforeVersion = sinon.stub().callsArgWith(3, null, this.rewound_content, this.updates); - this.DiffGenerator.buildDiff = sinon.stub().returns(this.diff); - return this.DiffManager.getDiff(this.project_id, this.doc_id, this.fromVersion, this.toVersion, this.callback); - }); + describe('getDiff', function() { + beforeEach(function() { + this.content = 'hello world' + // Op versions are the version they were applied to, so doc is always one version + // ahead.s + this.version = 43 + this.updates = [ + { + op: 'mock-4', + v: 42, + meta: { start_ts: new Date(this.to.getTime() + 20) } + }, + { + op: 'mock-3', + v: 41, + meta: { start_ts: new Date(this.to.getTime() + 10) } + }, + { + op: 'mock-2', + v: 40, + meta: { start_ts: new Date(this.to.getTime() - 10) } + }, + { + op: 'mock-1', + v: 39, + meta: { start_ts: new Date(this.to.getTime() - 20) } + } + ] + this.fromVersion = 39 + this.toVersion = 40 + this.diffed_updates = this.updates.slice(2) + this.rewound_content = 'rewound-content' + return (this.diff = [{ u: 'mock-diff' }]) + }) - it("should get the latest doc and version with all recent updates", function() { - return this.DiffManager.getDocumentBeforeVersion - .calledWith(this.project_id, this.doc_id, this.fromVersion) - .should.equal(true); - }); + describe('with matching versions', function() { + beforeEach(function() { + this.DiffManager.getDocumentBeforeVersion = sinon + .stub() + .callsArgWith(3, null, this.rewound_content, this.updates) + this.DiffGenerator.buildDiff = sinon.stub().returns(this.diff) + return this.DiffManager.getDiff( + this.project_id, + this.doc_id, + this.fromVersion, + this.toVersion, + this.callback + ) + }) - it("should generate the diff", function() { - return this.DiffGenerator.buildDiff - .calledWith(this.rewound_content, this.diffed_updates.slice().reverse()) - .should.equal(true); - }); + it('should get the latest doc and version with all recent updates', function() { + return this.DiffManager.getDocumentBeforeVersion + .calledWith(this.project_id, this.doc_id, this.fromVersion) + .should.equal(true) + }) - return it("should call the callback with the diff", function() { - return this.callback.calledWith(null, this.diff).should.equal(true); - }); - }); + it('should generate the diff', function() { + return this.DiffGenerator.buildDiff + .calledWith( + this.rewound_content, + this.diffed_updates.slice().reverse() + ) + .should.equal(true) + }) - return describe("when the updates are inconsistent", function() { - beforeEach(function() { - this.DiffManager.getLatestDocAndUpdates = sinon.stub().callsArgWith(3, null, this.content, this.version, this.updates); - this.DiffGenerator.buildDiff = sinon.stub().throws(this.error = new Error("inconsistent!")); - return this.DiffManager.getDiff(this.project_id, this.doc_id, this.fromVersion, this.toVersion, this.callback); - }); + return it('should call the callback with the diff', function() { + return this.callback.calledWith(null, this.diff).should.equal(true) + }) + }) - return it("should call the callback with an error", function() { - return this.callback - .calledWith(this.error) - .should.equal(true); - }); - }); - }); + return describe('when the updates are inconsistent', function() { + beforeEach(function() { + this.DiffManager.getLatestDocAndUpdates = sinon + .stub() + .callsArgWith(3, null, this.content, this.version, this.updates) + this.DiffGenerator.buildDiff = sinon + .stub() + .throws((this.error = new Error('inconsistent!'))) + return this.DiffManager.getDiff( + this.project_id, + this.doc_id, + this.fromVersion, + this.toVersion, + this.callback + ) + }) - describe("getDocumentBeforeVersion", function() { - beforeEach(function() { - this.DiffManager._tryGetDocumentBeforeVersion = sinon.stub(); - this.document = "mock-documents"; - return this.rewound_updates = "mock-rewound-updates"; - }); + return it('should call the callback with an error', function() { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) + }) - describe("succesfully", function() { - beforeEach(function() { - this.DiffManager._tryGetDocumentBeforeVersion.yields(null, this.document, this.rewound_updates); - return this.DiffManager.getDocumentBeforeVersion(this.project_id, this.doc_id, this.version, this.callback); - }); - - it("should call _tryGetDocumentBeforeVersion", function() { - return this.DiffManager._tryGetDocumentBeforeVersion - .calledWith(this.project_id, this.doc_id, this.version) - .should.equal(true); - }); - - return it("should call the callback with the response", function() { - return this.callback.calledWith(null, this.document, this.rewound_updates).should.equal(true); - }); - }); - - describe("with a retry needed", function() { - beforeEach(function() { - let retried = false; - this.DiffManager._tryGetDocumentBeforeVersion = (project_id, doc_id, version, callback) => { - if (!retried) { - retried = true; - const error = new Error(); - error.retry = true; - return callback(error); - } else { - return callback(null, this.document, this.rewound_updates); - } - }; - sinon.spy(this.DiffManager, "_tryGetDocumentBeforeVersion"); - return this.DiffManager.getDocumentBeforeVersion(this.project_id, this.doc_id, this.version, this.callback); - }); - - it("should call _tryGetDocumentBeforeVersion twice", function() { - return this.DiffManager._tryGetDocumentBeforeVersion - .calledTwice - .should.equal(true); - }); - - return it("should call the callback with the response", function() { - return this.callback.calledWith(null, this.document, this.rewound_updates).should.equal(true); - }); - }); - - describe("with a non-retriable error", function() { - beforeEach(function() { - this.error = new Error("oops"); - this.DiffManager._tryGetDocumentBeforeVersion.yields(this.error); - return this.DiffManager.getDocumentBeforeVersion(this.project_id, this.doc_id, this.version, this.callback); - }); - - it("should call _tryGetDocumentBeforeVersion once", function() { - return this.DiffManager._tryGetDocumentBeforeVersion - .calledOnce - .should.equal(true); - }); - - return it("should call the callback with the error", function() { - return this.callback.calledWith(this.error).should.equal(true); - }); - }); - - return describe("when retry limit is matched", function() { - beforeEach(function() { - this.error = new Error("oops"); - this.error.retry = true; - this.DiffManager._tryGetDocumentBeforeVersion.yields(this.error); - return this.DiffManager.getDocumentBeforeVersion(this.project_id, this.doc_id, this.version, this.callback); - }); - - it("should call _tryGetDocumentBeforeVersion three times (max retries)", function() { - return this.DiffManager._tryGetDocumentBeforeVersion - .calledThrice - .should.equal(true); - }); - - return it("should call the callback with the error", function() { - return this.callback.calledWith(this.error).should.equal(true); - }); - }); - }); + describe('getDocumentBeforeVersion', function() { + beforeEach(function() { + this.DiffManager._tryGetDocumentBeforeVersion = sinon.stub() + this.document = 'mock-documents' + return (this.rewound_updates = 'mock-rewound-updates') + }) - return describe("_tryGetDocumentBeforeVersion", function() { - beforeEach(function() { - this.content = "hello world"; - // Op versions are the version they were applied to, so doc is always one version - // ahead.s - this.version = 43; - this.updates = [ - { op: "mock-4", v: 42, meta: { start_ts: new Date(this.to.getTime() + 20)} }, - { op: "mock-3", v: 41, meta: { start_ts: new Date(this.to.getTime() + 10)} }, - { op: "mock-2", v: 40, meta: { start_ts: new Date(this.to.getTime() - 10)} }, - { op: "mock-1", v: 39, meta: { start_ts: new Date(this.to.getTime() - 20)} } - ]; - this.fromVersion = 39; - this.rewound_content = "rewound-content"; - return this.diff = [ {u: "mock-diff"} ];}); - - describe("with matching versions", function() { - beforeEach(function() { - this.DiffManager.getLatestDocAndUpdates = sinon.stub().callsArgWith(3, null, this.content, this.version, this.updates); - this.DiffGenerator.rewindUpdates = sinon.spy((content, updates) => { - // the rewindUpdates method reverses the 'updates' array - updates.reverse(); - return this.rewound_content; - }); - this.rewindUpdatesWithArgs = this.DiffGenerator.rewindUpdates.withArgs(this.content, this.updates.slice().reverse()); - return this.DiffManager._tryGetDocumentBeforeVersion(this.project_id, this.doc_id, this.fromVersion, this.callback); - }); + describe('succesfully', function() { + beforeEach(function() { + this.DiffManager._tryGetDocumentBeforeVersion.yields( + null, + this.document, + this.rewound_updates + ) + return this.DiffManager.getDocumentBeforeVersion( + this.project_id, + this.doc_id, + this.version, + this.callback + ) + }) - it("should get the latest doc and version with all recent updates", function() { - return this.DiffManager.getLatestDocAndUpdates - .calledWith(this.project_id, this.doc_id, this.fromVersion) - .should.equal(true); - }); + it('should call _tryGetDocumentBeforeVersion', function() { + return this.DiffManager._tryGetDocumentBeforeVersion + .calledWith(this.project_id, this.doc_id, this.version) + .should.equal(true) + }) - it("should rewind the diff", function() { - return sinon.assert.calledOnce(this.rewindUpdatesWithArgs); - }); + return it('should call the callback with the response', function() { + return this.callback + .calledWith(null, this.document, this.rewound_updates) + .should.equal(true) + }) + }) - return it("should call the callback with the rewound document and updates", function() { - return this.callback.calledWith(null, this.rewound_content, this.updates).should.equal(true); - }); - }); + describe('with a retry needed', function() { + beforeEach(function() { + let retried = false + this.DiffManager._tryGetDocumentBeforeVersion = ( + project_id, + doc_id, + version, + callback + ) => { + if (!retried) { + retried = true + const error = new Error() + error.retry = true + return callback(error) + } else { + return callback(null, this.document, this.rewound_updates) + } + } + sinon.spy(this.DiffManager, '_tryGetDocumentBeforeVersion') + return this.DiffManager.getDocumentBeforeVersion( + this.project_id, + this.doc_id, + this.version, + this.callback + ) + }) - describe("with mismatching versions", function() { - beforeEach(function() { - this.version = 50; - this.updates = [ { op: "mock-1", v: 40 }, { op: "mock-1", v: 39 } ]; - this.DiffManager.getLatestDocAndUpdates = sinon.stub().callsArgWith(3, null, this.content, this.version, this.updates); - return this.DiffManager._tryGetDocumentBeforeVersion(this.project_id, this.doc_id, this.fromVersion, this.callback); - }); + it('should call _tryGetDocumentBeforeVersion twice', function() { + return this.DiffManager._tryGetDocumentBeforeVersion.calledTwice.should.equal( + true + ) + }) - return it("should call the callback with an error with retry = true set", function() { - this.callback.calledOnce.should.equal(true); - const error = this.callback.args[0][0]; - return expect(error.retry).to.equal(true); - }); - }); + return it('should call the callback with the response', function() { + return this.callback + .calledWith(null, this.document, this.rewound_updates) + .should.equal(true) + }) + }) - return describe("when the updates are inconsistent", function() { - beforeEach(function() { - this.DiffManager.getLatestDocAndUpdates = sinon.stub().callsArgWith(3, null, this.content, this.version, this.updates); - this.DiffGenerator.rewindUpdates = sinon.stub().throws(this.error = new Error("inconsistent!")); - return this.DiffManager.getDocumentBeforeVersion(this.project_id, this.doc_id, this.fromVersion, this.callback); - }); + describe('with a non-retriable error', function() { + beforeEach(function() { + this.error = new Error('oops') + this.DiffManager._tryGetDocumentBeforeVersion.yields(this.error) + return this.DiffManager.getDocumentBeforeVersion( + this.project_id, + this.doc_id, + this.version, + this.callback + ) + }) - return it("should call the callback with an error", function() { - return this.callback - .calledWith(this.error) - .should.equal(true); - }); - }); - }); -}); + it('should call _tryGetDocumentBeforeVersion once', function() { + return this.DiffManager._tryGetDocumentBeforeVersion.calledOnce.should.equal( + true + ) + }) + + return it('should call the callback with the error', function() { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) + + return describe('when retry limit is matched', function() { + beforeEach(function() { + this.error = new Error('oops') + this.error.retry = true + this.DiffManager._tryGetDocumentBeforeVersion.yields(this.error) + return this.DiffManager.getDocumentBeforeVersion( + this.project_id, + this.doc_id, + this.version, + this.callback + ) + }) + + it('should call _tryGetDocumentBeforeVersion three times (max retries)', function() { + return this.DiffManager._tryGetDocumentBeforeVersion.calledThrice.should.equal( + true + ) + }) + + return it('should call the callback with the error', function() { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) + }) + + return describe('_tryGetDocumentBeforeVersion', function() { + beforeEach(function() { + this.content = 'hello world' + // Op versions are the version they were applied to, so doc is always one version + // ahead.s + this.version = 43 + this.updates = [ + { + op: 'mock-4', + v: 42, + meta: { start_ts: new Date(this.to.getTime() + 20) } + }, + { + op: 'mock-3', + v: 41, + meta: { start_ts: new Date(this.to.getTime() + 10) } + }, + { + op: 'mock-2', + v: 40, + meta: { start_ts: new Date(this.to.getTime() - 10) } + }, + { + op: 'mock-1', + v: 39, + meta: { start_ts: new Date(this.to.getTime() - 20) } + } + ] + this.fromVersion = 39 + this.rewound_content = 'rewound-content' + return (this.diff = [{ u: 'mock-diff' }]) + }) + + describe('with matching versions', function() { + beforeEach(function() { + this.DiffManager.getLatestDocAndUpdates = sinon + .stub() + .callsArgWith(3, null, this.content, this.version, this.updates) + this.DiffGenerator.rewindUpdates = sinon.spy((content, updates) => { + // the rewindUpdates method reverses the 'updates' array + updates.reverse() + return this.rewound_content + }) + this.rewindUpdatesWithArgs = this.DiffGenerator.rewindUpdates.withArgs( + this.content, + this.updates.slice().reverse() + ) + return this.DiffManager._tryGetDocumentBeforeVersion( + this.project_id, + this.doc_id, + this.fromVersion, + this.callback + ) + }) + + it('should get the latest doc and version with all recent updates', function() { + return this.DiffManager.getLatestDocAndUpdates + .calledWith(this.project_id, this.doc_id, this.fromVersion) + .should.equal(true) + }) + + it('should rewind the diff', function() { + return sinon.assert.calledOnce(this.rewindUpdatesWithArgs) + }) + + return it('should call the callback with the rewound document and updates', function() { + return this.callback + .calledWith(null, this.rewound_content, this.updates) + .should.equal(true) + }) + }) + + describe('with mismatching versions', function() { + beforeEach(function() { + this.version = 50 + this.updates = [ + { op: 'mock-1', v: 40 }, + { op: 'mock-1', v: 39 } + ] + this.DiffManager.getLatestDocAndUpdates = sinon + .stub() + .callsArgWith(3, null, this.content, this.version, this.updates) + return this.DiffManager._tryGetDocumentBeforeVersion( + this.project_id, + this.doc_id, + this.fromVersion, + this.callback + ) + }) + + return it('should call the callback with an error with retry = true set', function() { + this.callback.calledOnce.should.equal(true) + const error = this.callback.args[0][0] + return expect(error.retry).to.equal(true) + }) + }) + + return describe('when the updates are inconsistent', function() { + beforeEach(function() { + this.DiffManager.getLatestDocAndUpdates = sinon + .stub() + .callsArgWith(3, null, this.content, this.version, this.updates) + this.DiffGenerator.rewindUpdates = sinon + .stub() + .throws((this.error = new Error('inconsistent!'))) + return this.DiffManager.getDocumentBeforeVersion( + this.project_id, + this.doc_id, + this.fromVersion, + this.callback + ) + }) + + return it('should call the callback with an error', function() { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) + }) +}) diff --git a/services/track-changes/test/unit/js/DocArchive/MongoAWS.js b/services/track-changes/test/unit/js/DocArchive/MongoAWS.js index e3d82e6370..27e18b1d6b 100644 --- a/services/track-changes/test/unit/js/DocArchive/MongoAWS.js +++ b/services/track-changes/test/unit/js/DocArchive/MongoAWS.js @@ -9,88 +9,104 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const chai = require('chai'); -chai.should(); -const sinon = require("sinon"); -const modulePath = "../../../../app/js/MongoAWS.js"; -const SandboxedModule = require('sandboxed-module'); -const {ObjectId} = require("mongojs"); -const MemoryStream = require('memorystream'); -const zlib = require("zlib"); +const chai = require('chai') +chai.should() +const sinon = require('sinon') +const modulePath = '../../../../app/js/MongoAWS.js' +const SandboxedModule = require('sandboxed-module') +const { ObjectId } = require('mongojs') +const MemoryStream = require('memorystream') +const zlib = require('zlib') -describe("MongoAWS", function() { - beforeEach(function() { - this.MongoAWS = SandboxedModule.require(modulePath, { requires: { - "settings-sharelatex": (this.settings = { - trackchanges: { - s3: { - secret: "s3-secret", - key: "s3-key" - }, - stores: { - doc_history: "s3-bucket" - } - } - }), - "child_process": (this.child_process = {}), - "mongo-uri": (this.mongouri = {}), - "logger-sharelatex": (this.logger = {log: sinon.stub(), error: sinon.stub(), err() {}}), - "aws-sdk": (this.awssdk = {}), - "fs": (this.fs = {}), - "s3-streams": (this.S3S = {}), - "./mongojs" : { db: (this.db = {}), ObjectId }, - "JSONStream": (this.JSONStream = {}), - "readline-stream": (this.readline = sinon.stub()), - 'metrics-sharelatex': {inc(){}} - } - }); +describe('MongoAWS', function() { + beforeEach(function() { + this.MongoAWS = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': (this.settings = { + trackchanges: { + s3: { + secret: 's3-secret', + key: 's3-key' + }, + stores: { + doc_history: 's3-bucket' + } + } + }), + child_process: (this.child_process = {}), + 'mongo-uri': (this.mongouri = {}), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub(), + err() {} + }), + 'aws-sdk': (this.awssdk = {}), + fs: (this.fs = {}), + 's3-streams': (this.S3S = {}), + './mongojs': { db: (this.db = {}), ObjectId }, + JSONStream: (this.JSONStream = {}), + 'readline-stream': (this.readline = sinon.stub()), + 'metrics-sharelatex': { inc() {} } + } + }) - this.project_id = ObjectId().toString(); - this.doc_id = ObjectId().toString(); - this.pack_id = ObjectId(); - this.update = { v:123 }; - return this.callback = sinon.stub(); - }); + this.project_id = ObjectId().toString() + this.doc_id = ObjectId().toString() + this.pack_id = ObjectId() + this.update = { v: 123 } + return (this.callback = sinon.stub()) + }) - describe("archivePack", function() { + describe('archivePack', function() { + beforeEach(function(done) { + this.awssdk.config = { update: sinon.stub() } + this.awssdk.S3 = sinon.stub() + this.S3S.WriteStream = () => MemoryStream.createWriteStream() + this.db.docHistory = {} + this.db.docHistory.findOne = sinon + .stub() + .callsArgWith(1, null, { pack: 'hello' }) - beforeEach(function(done) { - this.awssdk.config = { update: sinon.stub() }; - this.awssdk.S3 = sinon.stub(); - this.S3S.WriteStream = () => MemoryStream.createWriteStream(); - this.db.docHistory = {}; - this.db.docHistory.findOne = sinon.stub().callsArgWith(1, null, {"pack":"hello"}); + return this.MongoAWS.archivePack( + this.project_id, + this.doc_id, + this.pack_id, + (err, result) => { + this.callback() + return done() + } + ) + }) - return this.MongoAWS.archivePack(this.project_id, this.doc_id, this.pack_id, (err, result) => { - this.callback(); - return done(); - }); - }); + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); + return describe('unArchivePack', function() { + beforeEach(function(done) { + return zlib.gzip('{"pack":"123"}', (err, zbuf) => { + this.awssdk.config = { update: sinon.stub() } + this.awssdk.S3 = sinon.stub() + this.S3S.ReadStream = () => + MemoryStream.createReadStream(zbuf, { readable: true }) + this.db.docHistory = {} + this.db.docHistory.insert = sinon.stub().callsArgWith(1, null, 'pack') - return describe("unArchivePack", function() { + return this.MongoAWS.unArchivePack( + this.project_id, + this.doc_id, + this.pack_id, + (err, result) => { + this.callback() + return done() + } + ) + }) + }) - beforeEach(function(done) { - return zlib.gzip('{"pack":"123"}', (err, zbuf) => { - this.awssdk.config = { update: sinon.stub() }; - this.awssdk.S3 = sinon.stub(); - this.S3S.ReadStream = () => MemoryStream.createReadStream(zbuf, {readable:true}); - this.db.docHistory = {}; - this.db.docHistory.insert = sinon.stub().callsArgWith(1, null, "pack"); - - return this.MongoAWS.unArchivePack(this.project_id, this.doc_id, this.pack_id, (err, result) => { - this.callback(); - return done(); - }); - }); - }); - - return it("should call db.docHistory.insert", function() { - return this.db.docHistory.insert.called.should.equal(true); - }); - }); -}); + return it('should call db.docHistory.insert', function() { + return this.db.docHistory.insert.called.should.equal(true) + }) + }) +}) diff --git a/services/track-changes/test/unit/js/DocumentUpdaterManager/DocumentUpdaterManagerTests.js b/services/track-changes/test/unit/js/DocumentUpdaterManager/DocumentUpdaterManagerTests.js index 9b4ce3cc11..a1dcaa0ee5 100644 --- a/services/track-changes/test/unit/js/DocumentUpdaterManager/DocumentUpdaterManagerTests.js +++ b/services/track-changes/test/unit/js/DocumentUpdaterManager/DocumentUpdaterManagerTests.js @@ -9,127 +9,197 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const sinon = require('sinon'); -const chai = require('chai'); -const should = chai.should(); -const { expect } = chai; -const modulePath = "../../../../app/js/DocumentUpdaterManager.js"; -const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/js/DocumentUpdaterManager.js' +const SandboxedModule = require('sandboxed-module') -describe("DocumentUpdaterManager", function() { - beforeEach(function() { - this.DocumentUpdaterManager = SandboxedModule.require(modulePath, { requires: { - "request": (this.request = {}), - "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }), - 'settings-sharelatex': (this.settings = - {apis : {documentupdater: {url : "http://example.com"}}}) - } - } - ); - this.callback = sinon.stub(); - this.lines = ["one", "two", "three"]; - return this.version = 42; - }); +describe('DocumentUpdaterManager', function() { + beforeEach(function() { + this.DocumentUpdaterManager = SandboxedModule.require(modulePath, { + requires: { + request: (this.request = {}), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub() + }), + 'settings-sharelatex': (this.settings = { + apis: { documentupdater: { url: 'http://example.com' } } + }) + } + }) + this.callback = sinon.stub() + this.lines = ['one', 'two', 'three'] + return (this.version = 42) + }) - describe("getDocument", function() { - describe("successfully", function() { - beforeEach(function() { - this.body = JSON.stringify({ - lines: this.lines, - version: this.version, - ops: []}); - this.request.get = sinon.stub().callsArgWith(1, null, {statusCode: 200}, this.body); - return this.DocumentUpdaterManager.getDocument(this.project_id, this.doc_id, this.callback); - }); + describe('getDocument', function() { + describe('successfully', function() { + beforeEach(function() { + this.body = JSON.stringify({ + lines: this.lines, + version: this.version, + ops: [] + }) + this.request.get = sinon + .stub() + .callsArgWith(1, null, { statusCode: 200 }, this.body) + return this.DocumentUpdaterManager.getDocument( + this.project_id, + this.doc_id, + this.callback + ) + }) - it('should get the document from the document updater', function() { - const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}`; - return this.request.get.calledWith(url).should.equal(true); - }); + it('should get the document from the document updater', function() { + const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}` + return this.request.get.calledWith(url).should.equal(true) + }) - return it("should call the callback with the content and version", function() { - return this.callback.calledWith(null, this.lines.join("\n"), this.version).should.equal(true); - }); - }); + return it('should call the callback with the content and version', function() { + return this.callback + .calledWith(null, this.lines.join('\n'), this.version) + .should.equal(true) + }) + }) - describe("when the document updater API returns an error", function() { - beforeEach(function() { - this.request.get = sinon.stub().callsArgWith(1, (this.error = new Error("something went wrong")), null, null); - return this.DocumentUpdaterManager.getDocument(this.project_id, this.doc_id, this.callback); - }); + describe('when the document updater API returns an error', function() { + beforeEach(function() { + this.request.get = sinon + .stub() + .callsArgWith( + 1, + (this.error = new Error('something went wrong')), + null, + null + ) + return this.DocumentUpdaterManager.getDocument( + this.project_id, + this.doc_id, + this.callback + ) + }) - return it("should return an error to the callback", function() { - return this.callback.calledWith(this.error).should.equal(true); - }); - }); + return it('should return an error to the callback', function() { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) - return describe("when the document updater returns a failure error code", function() { - beforeEach(function() { - this.request.get = sinon.stub().callsArgWith(1, null, { statusCode: 500 }, ""); - return this.DocumentUpdaterManager.getDocument(this.project_id, this.doc_id, this.callback); - }); + return describe('when the document updater returns a failure error code', function() { + beforeEach(function() { + this.request.get = sinon + .stub() + .callsArgWith(1, null, { statusCode: 500 }, '') + return this.DocumentUpdaterManager.getDocument( + this.project_id, + this.doc_id, + this.callback + ) + }) - return it("should return the callback with an error", function() { - return this.callback - .calledWith(sinon.match.has('message', "doc updater returned a non-success status code: 500")) - .should.equal(true); - }); - }); - }); + return it('should return the callback with an error', function() { + return this.callback + .calledWith( + sinon.match.has( + 'message', + 'doc updater returned a non-success status code: 500' + ) + ) + .should.equal(true) + }) + }) + }) - return describe("setDocument", function() { - beforeEach(function() { - this.content = "mock content"; - return this.user_id = "user-id-123"; - }); + return describe('setDocument', function() { + beforeEach(function() { + this.content = 'mock content' + return (this.user_id = 'user-id-123') + }) - describe("successfully", function() { - beforeEach(function() { - this.request.post = sinon.stub().callsArgWith(1, null, {statusCode: 200}); - return this.DocumentUpdaterManager.setDocument(this.project_id, this.doc_id, this.content, this.user_id, this.callback); - }); + describe('successfully', function() { + beforeEach(function() { + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 200 }) + return this.DocumentUpdaterManager.setDocument( + this.project_id, + this.doc_id, + this.content, + this.user_id, + this.callback + ) + }) - it('should set the document in the document updater', function() { - const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}`; - return this.request.post - .calledWith({ - url, - json: { - lines: this.content.split("\n"), - source: "restore", - user_id: this.user_id, - undoing: true - } - }).should.equal(true); - }); + it('should set the document in the document updater', function() { + const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}` + return this.request.post + .calledWith({ + url, + json: { + lines: this.content.split('\n'), + source: 'restore', + user_id: this.user_id, + undoing: true + } + }) + .should.equal(true) + }) - return it("should call the callback", function() { - return this.callback.calledWith(null).should.equal(true); - }); - }); + return it('should call the callback', function() { + return this.callback.calledWith(null).should.equal(true) + }) + }) - describe("when the document updater API returns an error", function() { - beforeEach(function() { - this.request.post = sinon.stub().callsArgWith(1, (this.error = new Error("something went wrong")), null, null); - return this.DocumentUpdaterManager.setDocument(this.project_id, this.doc_id, this.content, this.user_id, this.callback); - }); + describe('when the document updater API returns an error', function() { + beforeEach(function() { + this.request.post = sinon + .stub() + .callsArgWith( + 1, + (this.error = new Error('something went wrong')), + null, + null + ) + return this.DocumentUpdaterManager.setDocument( + this.project_id, + this.doc_id, + this.content, + this.user_id, + this.callback + ) + }) - return it("should return an error to the callback", function() { - return this.callback.calledWith(this.error).should.equal(true); - }); - }); + return it('should return an error to the callback', function() { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) - return describe("when the document updater returns a failure error code", function() { - beforeEach(function() { - this.request.post = sinon.stub().callsArgWith(1, null, { statusCode: 500 }, ""); - return this.DocumentUpdaterManager.setDocument(this.project_id, this.doc_id, this.content, this.user_id, this.callback); - }); + return describe('when the document updater returns a failure error code', function() { + beforeEach(function() { + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 500 }, '') + return this.DocumentUpdaterManager.setDocument( + this.project_id, + this.doc_id, + this.content, + this.user_id, + this.callback + ) + }) - return it("should return the callback with an error", function() { - return this.callback - .calledWith(sinon.match.has('message', "doc updater returned a non-success status code: 500")) - .should.equal(true); - }); - }); - }); -}); + return it('should return the callback with an error', function() { + return this.callback + .calledWith( + sinon.match.has( + 'message', + 'doc updater returned a non-success status code: 500' + ) + ) + .should.equal(true) + }) + }) + }) +}) diff --git a/services/track-changes/test/unit/js/HttpController/HttpControllerTests.js b/services/track-changes/test/unit/js/HttpController/HttpControllerTests.js index 21a74a9661..68bab28da3 100644 --- a/services/track-changes/test/unit/js/HttpController/HttpControllerTests.js +++ b/services/track-changes/test/unit/js/HttpController/HttpControllerTests.js @@ -9,175 +9,193 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const sinon = require('sinon'); -const chai = require('chai'); -const should = chai.should(); -const { expect } = chai; -const modulePath = "../../../../app/js/HttpController.js"; -const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/js/HttpController.js' +const SandboxedModule = require('sandboxed-module') -describe("HttpController", function() { - beforeEach(function() { - this.HttpController = SandboxedModule.require(modulePath, { requires: { - "logger-sharelatex": { log: sinon.stub() }, - "./UpdatesManager": (this.UpdatesManager = {}), - "./DiffManager": (this.DiffManager = {}), - "./RestoreManager": (this.RestoreManager = {}), - "./PackManager": (this.PackManager = {}), - "./DocArchiveManager": (this.DocArchiveManager = {}), - "./HealthChecker": (this.HealthChecker = {}) - } - }); - this.doc_id = "doc-id-123"; - this.project_id = "project-id-123"; - this.next = sinon.stub(); - this.user_id = "mock-user-123"; - return this.now = Date.now(); - }); +describe('HttpController', function() { + beforeEach(function() { + this.HttpController = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': { log: sinon.stub() }, + './UpdatesManager': (this.UpdatesManager = {}), + './DiffManager': (this.DiffManager = {}), + './RestoreManager': (this.RestoreManager = {}), + './PackManager': (this.PackManager = {}), + './DocArchiveManager': (this.DocArchiveManager = {}), + './HealthChecker': (this.HealthChecker = {}) + } + }) + this.doc_id = 'doc-id-123' + this.project_id = 'project-id-123' + this.next = sinon.stub() + this.user_id = 'mock-user-123' + return (this.now = Date.now()) + }) - describe("flushDoc", function() { - beforeEach(function() { - this.req = { - params: { - doc_id: this.doc_id, - project_id: this.project_id - } - }; - this.res = - {send: sinon.stub()}; - this.UpdatesManager.processUncompressedUpdatesWithLock = sinon.stub().callsArg(2); - return this.HttpController.flushDoc(this.req, this.res, this.next); - }); + describe('flushDoc', function() { + beforeEach(function() { + this.req = { + params: { + doc_id: this.doc_id, + project_id: this.project_id + } + } + this.res = { send: sinon.stub() } + this.UpdatesManager.processUncompressedUpdatesWithLock = sinon + .stub() + .callsArg(2) + return this.HttpController.flushDoc(this.req, this.res, this.next) + }) - it("should process the updates", function() { - return this.UpdatesManager.processUncompressedUpdatesWithLock - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); + it('should process the updates', function() { + return this.UpdatesManager.processUncompressedUpdatesWithLock + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) - return it("should return a success code", function() { - return this.res.send.calledWith(204).should.equal(true); - }); - }); + return it('should return a success code', function() { + return this.res.send.calledWith(204).should.equal(true) + }) + }) - describe("flushProject", function() { - beforeEach(function() { - this.req = { - params: { - project_id: this.project_id - } - }; - this.res = - {send: sinon.stub()}; - this.UpdatesManager.processUncompressedUpdatesForProject = sinon.stub().callsArg(1); - return this.HttpController.flushProject(this.req, this.res, this.next); - }); + describe('flushProject', function() { + beforeEach(function() { + this.req = { + params: { + project_id: this.project_id + } + } + this.res = { send: sinon.stub() } + this.UpdatesManager.processUncompressedUpdatesForProject = sinon + .stub() + .callsArg(1) + return this.HttpController.flushProject(this.req, this.res, this.next) + }) - it("should process the updates", function() { - return this.UpdatesManager.processUncompressedUpdatesForProject - .calledWith(this.project_id) - .should.equal(true); - }); + it('should process the updates', function() { + return this.UpdatesManager.processUncompressedUpdatesForProject + .calledWith(this.project_id) + .should.equal(true) + }) - return it("should return a success code", function() { - return this.res.send.calledWith(204).should.equal(true); - }); - }); + return it('should return a success code', function() { + return this.res.send.calledWith(204).should.equal(true) + }) + }) + describe('getDiff', function() { + beforeEach(function() { + this.from = 42 + this.to = 45 + this.req = { + params: { + doc_id: this.doc_id, + project_id: this.project_id + }, + query: { + from: this.from.toString(), + to: this.to.toString() + } + } + this.res = { json: sinon.stub() } + this.diff = [{ u: 'mock-diff' }] + this.DiffManager.getDiff = sinon.stub().callsArgWith(4, null, this.diff) + return this.HttpController.getDiff(this.req, this.res, this.next) + }) - describe("getDiff", function() { - beforeEach(function() { - this.from = 42; - this.to = 45; - this.req = { - params: { - doc_id: this.doc_id, - project_id: this.project_id - }, - query: { - from: this.from.toString(), - to: this.to.toString() - } - }; - this.res = - {json: sinon.stub()}; - this.diff = [ {u: "mock-diff"} ]; - this.DiffManager.getDiff = sinon.stub().callsArgWith(4, null, this.diff); - return this.HttpController.getDiff(this.req, this.res, this.next); - }); + it('should get the diff', function() { + return this.DiffManager.getDiff + .calledWith( + this.project_id, + this.doc_id, + parseInt(this.from, 10), + parseInt(this.to, 10) + ) + .should.equal(true) + }) - it("should get the diff", function() { - return this.DiffManager.getDiff - .calledWith(this.project_id, this.doc_id, parseInt(this.from, 10), parseInt(this.to, 10)) - .should.equal(true); - }); + return it('should return the diff', function() { + return this.res.json.calledWith({ diff: this.diff }).should.equal(true) + }) + }) - return it("should return the diff", function() { - return this.res.json.calledWith({diff: this.diff}).should.equal(true); - }); - }); + describe('getUpdates', function() { + beforeEach(function() { + this.before = Date.now() + this.nextBeforeTimestamp = this.before - 100 + this.min_count = 10 + this.req = { + params: { + project_id: this.project_id + }, + query: { + before: this.before.toString(), + min_count: this.min_count.toString() + } + } + this.res = { json: sinon.stub() } + this.updates = ['mock-summarized-updates'] + this.UpdatesManager.getSummarizedProjectUpdates = sinon + .stub() + .callsArgWith(2, null, this.updates, this.nextBeforeTimestamp) + return this.HttpController.getUpdates(this.req, this.res, this.next) + }) - describe("getUpdates", function() { - beforeEach(function() { - this.before = Date.now(); - this.nextBeforeTimestamp = this.before - 100; - this.min_count = 10; - this.req = { - params: { - project_id: this.project_id - }, - query: { - before: this.before.toString(), - min_count: this.min_count.toString() - } - }; - this.res = - {json: sinon.stub()}; - this.updates = ["mock-summarized-updates"]; - this.UpdatesManager.getSummarizedProjectUpdates = sinon.stub().callsArgWith(2, null, this.updates, this.nextBeforeTimestamp); - return this.HttpController.getUpdates(this.req, this.res, this.next); - }); + it('should get the updates', function() { + return this.UpdatesManager.getSummarizedProjectUpdates + .calledWith(this.project_id, { + before: this.before, + min_count: this.min_count + }) + .should.equal(true) + }) - it("should get the updates", function() { - return this.UpdatesManager.getSummarizedProjectUpdates - .calledWith(this.project_id, {before: this.before, min_count: this.min_count}) - .should.equal(true); - }); + return it('should return the formatted updates', function() { + return this.res.json + .calledWith({ + updates: this.updates, + nextBeforeTimestamp: this.nextBeforeTimestamp + }) + .should.equal(true) + }) + }) - return it("should return the formatted updates", function() { - return this.res.json.calledWith({updates: this.updates, nextBeforeTimestamp: this.nextBeforeTimestamp}).should.equal(true); - }); - }); + return describe('RestoreManager', function() { + beforeEach(function() { + this.version = '42' + this.req = { + params: { + doc_id: this.doc_id, + project_id: this.project_id, + version: this.version + }, + headers: { + 'x-user-id': this.user_id + } + } + this.res = { send: sinon.stub() } - return describe("RestoreManager", function() { - beforeEach(function() { - this.version = "42"; - this.req = { - params: { - doc_id: this.doc_id, - project_id: this.project_id, - version: this.version - }, - headers: { - "x-user-id": this.user_id - } - }; - this.res = - {send: sinon.stub()}; + this.RestoreManager.restoreToBeforeVersion = sinon.stub().callsArg(4) + return this.HttpController.restore(this.req, this.res, this.next) + }) - this.RestoreManager.restoreToBeforeVersion = sinon.stub().callsArg(4); - return this.HttpController.restore(this.req, this.res, this.next); - }); - - it("should restore the document", function() { - return this.RestoreManager.restoreToBeforeVersion - .calledWith(this.project_id, this.doc_id, parseInt(this.version, 10), this.user_id) - .should.equal(true); - }); - - return it("should return a success code", function() { - return this.res.send.calledWith(204).should.equal(true); - }); - }); -}); + it('should restore the document', function() { + return this.RestoreManager.restoreToBeforeVersion + .calledWith( + this.project_id, + this.doc_id, + parseInt(this.version, 10), + this.user_id + ) + .should.equal(true) + }) + return it('should return a success code', function() { + return this.res.send.calledWith(204).should.equal(true) + }) + }) +}) diff --git a/services/track-changes/test/unit/js/LockManager/LockManagerTests.js b/services/track-changes/test/unit/js/LockManager/LockManagerTests.js index 2ad9072133..9c5a8fdebc 100644 --- a/services/track-changes/test/unit/js/LockManager/LockManagerTests.js +++ b/services/track-changes/test/unit/js/LockManager/LockManagerTests.js @@ -15,270 +15,306 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const sinon = require('sinon'); -const chai = require('chai'); -const should = chai.should(); -const { expect } = chai; -const modulePath = "../../../../app/js/LockManager.js"; -const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/js/LockManager.js' +const SandboxedModule = require('sandboxed-module') -describe("LockManager", function() { - beforeEach(function() { - this.Settings = { - redis: { - lock:{} - } - }; - this.LockManager = SandboxedModule.require(modulePath, { requires: { - "redis-sharelatex": { - createClient: () => { return this.rclient = - {auth: sinon.stub()}; } - }, - "settings-sharelatex": this.Settings, - "logger-sharelatex": {error() {}} - } - }); +describe('LockManager', function() { + beforeEach(function() { + this.Settings = { + redis: { + lock: {} + } + } + this.LockManager = SandboxedModule.require(modulePath, { + requires: { + 'redis-sharelatex': { + createClient: () => { + return (this.rclient = { auth: sinon.stub() }) + } + }, + 'settings-sharelatex': this.Settings, + 'logger-sharelatex': { error() {} } + } + }) - this.key = "lock-key"; - return this.callback = sinon.stub(); - }); + this.key = 'lock-key' + return (this.callback = sinon.stub()) + }) - describe("checkLock", function() { - describe("when the lock is taken", function() { - beforeEach(function() { - this.rclient.exists = sinon.stub().callsArgWith(1, null, "1"); - return this.LockManager.checkLock(this.key, this.callback); - }); + describe('checkLock', function() { + describe('when the lock is taken', function() { + beforeEach(function() { + this.rclient.exists = sinon.stub().callsArgWith(1, null, '1') + return this.LockManager.checkLock(this.key, this.callback) + }) - it("should check the lock in redis", function() { - return this.rclient.exists - .calledWith(this.key) - .should.equal(true); - }); + it('should check the lock in redis', function() { + return this.rclient.exists.calledWith(this.key).should.equal(true) + }) - return it("should return the callback with false", function() { - return this.callback.calledWith(null, false).should.equal(true); - }); - }); + return it('should return the callback with false', function() { + return this.callback.calledWith(null, false).should.equal(true) + }) + }) - return describe("when the lock is free", function() { - beforeEach(function() { - this.rclient.exists = sinon.stub().callsArgWith(1, null, "0"); - return this.LockManager.checkLock(this.key, this.callback); - }); + return describe('when the lock is free', function() { + beforeEach(function() { + this.rclient.exists = sinon.stub().callsArgWith(1, null, '0') + return this.LockManager.checkLock(this.key, this.callback) + }) - return it("should return the callback with true", function() { - return this.callback.calledWith(null, true).should.equal(true); - }); - }); - }); + return it('should return the callback with true', function() { + return this.callback.calledWith(null, true).should.equal(true) + }) + }) + }) + describe('tryLock', function() { + describe('when the lock is taken', function() { + beforeEach(function() { + this.rclient.set = sinon.stub().callsArgWith(5, null, null) + this.LockManager.randomLock = sinon + .stub() + .returns('locked-random-value') + return this.LockManager.tryLock(this.key, this.callback) + }) - describe("tryLock", function() { - describe("when the lock is taken", function() { - beforeEach(function() { - this.rclient.set = sinon.stub().callsArgWith(5, null, null); - this.LockManager.randomLock = sinon.stub().returns("locked-random-value"); - return this.LockManager.tryLock(this.key, this.callback); - }); + it('should check the lock in redis', function() { + return this.rclient.set + .calledWith( + this.key, + 'locked-random-value', + 'EX', + this.LockManager.LOCK_TTL, + 'NX' + ) + .should.equal(true) + }) - it("should check the lock in redis", function() { - return this.rclient.set - .calledWith(this.key, "locked-random-value", "EX", this.LockManager.LOCK_TTL, "NX") - .should.equal(true); - }); + return it('should return the callback with false', function() { + return this.callback.calledWith(null, false).should.equal(true) + }) + }) - return it("should return the callback with false", function() { - return this.callback.calledWith(null, false).should.equal(true); - }); - }); + return describe('when the lock is free', function() { + beforeEach(function() { + this.rclient.set = sinon.stub().callsArgWith(5, null, 'OK') + return this.LockManager.tryLock(this.key, this.callback) + }) - return describe("when the lock is free", function() { - beforeEach(function() { - this.rclient.set = sinon.stub().callsArgWith(5, null, "OK"); - return this.LockManager.tryLock(this.key, this.callback); - }); + return it('should return the callback with true', function() { + return this.callback.calledWith(null, true).should.equal(true) + }) + }) + }) - return it("should return the callback with true", function() { - return this.callback.calledWith(null, true).should.equal(true); - }); - }); - }); + describe('deleteLock', function() { + return beforeEach(function() { + beforeEach(function() { + this.rclient.del = sinon.stub().callsArg(1) + return this.LockManager.deleteLock(this.key, this.callback) + }) - describe("deleteLock", function() { return beforeEach(function() { - beforeEach(function() { - this.rclient.del = sinon.stub().callsArg(1); - return this.LockManager.deleteLock(this.key, this.callback); - }); + it('should delete the lock in redis', function() { + return this.rclient.del.calledWith(key).should.equal(true) + }) - it("should delete the lock in redis", function() { - return this.rclient.del - .calledWith(key) - .should.equal(true); - }); + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + }) - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); } - ); + describe('getLock', function() { + describe('when the lock is not taken', function() { + beforeEach(function(done) { + this.LockManager.tryLock = sinon.stub().callsArgWith(1, null, true) + return this.LockManager.getLock(this.key, (...args) => { + this.callback(...Array.from(args || [])) + return done() + }) + }) - describe("getLock", function() { - describe("when the lock is not taken", function() { - beforeEach(function(done) { - this.LockManager.tryLock = sinon.stub().callsArgWith(1, null, true); - return this.LockManager.getLock(this.key, (...args) => { - this.callback(...Array.from(args || [])); - return done(); - }); - }); + it('should try to get the lock', function() { + return this.LockManager.tryLock.calledWith(this.key).should.equal(true) + }) - it("should try to get the lock", function() { - return this.LockManager.tryLock - .calledWith(this.key) - .should.equal(true); - }); + it('should only need to try once', function() { + return this.LockManager.tryLock.callCount.should.equal(1) + }) - it("should only need to try once", function() { - return this.LockManager.tryLock.callCount.should.equal(1); - }); + return it('should return the callback', function() { + return this.callback.calledWith(null).should.equal(true) + }) + }) - return it("should return the callback", function() { - return this.callback.calledWith(null).should.equal(true); - }); - }); + describe('when the lock is initially set', function() { + beforeEach(function(done) { + const startTime = Date.now() + this.LockManager.LOCK_TEST_INTERVAL = 5 + this.LockManager.tryLock = function(doc_id, callback) { + if (callback == null) { + callback = function(error, isFree) {} + } + if (Date.now() - startTime < 100) { + return callback(null, false) + } else { + return callback(null, true) + } + } + sinon.spy(this.LockManager, 'tryLock') - describe("when the lock is initially set", function() { - beforeEach(function(done) { - const startTime = Date.now(); - this.LockManager.LOCK_TEST_INTERVAL = 5; - this.LockManager.tryLock = function(doc_id, callback) { - if (callback == null) { callback = function(error, isFree) {}; } - if ((Date.now() - startTime) < 100) { - return callback(null, false); - } else { - return callback(null, true); - } - }; - sinon.spy(this.LockManager, "tryLock"); + return this.LockManager.getLock(this.key, (...args) => { + this.callback(...Array.from(args || [])) + return done() + }) + }) - return this.LockManager.getLock(this.key, (...args) => { - this.callback(...Array.from(args || [])); - return done(); - }); - }); + it('should call tryLock multiple times until free', function() { + return (this.LockManager.tryLock.callCount > 1).should.equal(true) + }) - it("should call tryLock multiple times until free", function() { - return (this.LockManager.tryLock.callCount > 1).should.equal(true); - }); + return it('should return the callback', function() { + return this.callback.calledWith(null).should.equal(true) + }) + }) - return it("should return the callback", function() { - return this.callback.calledWith(null).should.equal(true); - }); - }); + return describe('when the lock times out', function() { + beforeEach(function(done) { + const time = Date.now() + this.LockManager.MAX_LOCK_WAIT_TIME = 5 + this.LockManager.tryLock = sinon.stub().callsArgWith(1, null, false) + return this.LockManager.getLock(this.key, (...args) => { + this.callback(...Array.from(args || [])) + return done() + }) + }) - return describe("when the lock times out", function() { - beforeEach(function(done) { - const time = Date.now(); - this.LockManager.MAX_LOCK_WAIT_TIME = 5; - this.LockManager.tryLock = sinon.stub().callsArgWith(1, null, false); - return this.LockManager.getLock(this.key, (...args) => { - this.callback(...Array.from(args || [])); - return done(); - }); - }); + return it('should return the callback with an error', function() { + return this.callback + .calledWith(sinon.match.instanceOf(Error)) + .should.equal(true) + }) + }) + }) - return it("should return the callback with an error", function() { - return this.callback.calledWith(sinon.match.instanceOf(Error)).should.equal(true); - }); - }); - }); + return describe('runWithLock', function() { + describe('with successful run', function() { + beforeEach(function() { + this.runner = function(releaseLock) { + if (releaseLock == null) { + releaseLock = function(error) {} + } + return releaseLock() + } + sinon.spy(this, 'runner') + this.LockManager.getLock = sinon.stub().callsArg(1) + this.LockManager.releaseLock = sinon.stub().callsArg(2) + return this.LockManager.runWithLock( + this.key, + this.runner, + this.callback + ) + }) - return describe("runWithLock", function() { - describe("with successful run", function() { - beforeEach(function() { - this.runner = function(releaseLock) { - if (releaseLock == null) { releaseLock = function(error) {}; } - return releaseLock(); - }; - sinon.spy(this, "runner"); - this.LockManager.getLock = sinon.stub().callsArg(1); - this.LockManager.releaseLock = sinon.stub().callsArg(2); - return this.LockManager.runWithLock(this.key, this.runner, this.callback); - }); + it('should get the lock', function() { + return this.LockManager.getLock.calledWith(this.key).should.equal(true) + }) - it("should get the lock", function() { - return this.LockManager.getLock - .calledWith(this.key) - .should.equal(true); - }); + it('should run the passed function', function() { + return this.runner.called.should.equal(true) + }) - it("should run the passed function", function() { - return this.runner.called.should.equal(true); - }); + it('should release the lock', function() { + return this.LockManager.releaseLock + .calledWith(this.key) + .should.equal(true) + }) - it("should release the lock", function() { - return this.LockManager.releaseLock - .calledWith(this.key) - .should.equal(true); - }); + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); + describe('when the runner function returns an error', function() { + beforeEach(function() { + this.error = new Error('oops') + this.runner = releaseLock => { + if (releaseLock == null) { + releaseLock = function(error) {} + } + return releaseLock(this.error) + } + sinon.spy(this, 'runner') + this.LockManager.getLock = sinon.stub().callsArg(1) + this.LockManager.releaseLock = sinon.stub().callsArg(2) + return this.LockManager.runWithLock( + this.key, + this.runner, + this.callback + ) + }) - describe("when the runner function returns an error", function() { - beforeEach(function() { - this.error = new Error("oops"); - this.runner = releaseLock => { - if (releaseLock == null) { releaseLock = function(error) {}; } - return releaseLock(this.error); - }; - sinon.spy(this, "runner"); - this.LockManager.getLock = sinon.stub().callsArg(1); - this.LockManager.releaseLock = sinon.stub().callsArg(2); - return this.LockManager.runWithLock(this.key, this.runner, this.callback); - }); + it('should release the lock', function() { + return this.LockManager.releaseLock + .calledWith(this.key) + .should.equal(true) + }) - it("should release the lock", function() { - return this.LockManager.releaseLock - .calledWith(this.key) - .should.equal(true); - }); + return it('should call the callback with the error', function() { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) - return it("should call the callback with the error", function() { - return this.callback.calledWith(this.error).should.equal(true); - }); - }); + return describe('releaseLock', function() { + describe('when the lock is current', function() { + beforeEach(function() { + this.rclient.eval = sinon.stub().yields(null, 1) + return this.LockManager.releaseLock( + this.key, + this.lockValue, + this.callback + ) + }) - return describe("releaseLock", function() { - describe("when the lock is current", function() { - beforeEach(function() { - this.rclient.eval = sinon.stub().yields(null, 1); - return this.LockManager.releaseLock(this.key, this.lockValue, this.callback); - }); + it('should clear the data from redis', function() { + return this.rclient.eval + .calledWith( + this.LockManager.unlockScript, + 1, + this.key, + this.lockValue + ) + .should.equal(true) + }) - it('should clear the data from redis', function() { - return this.rclient.eval.calledWith(this.LockManager.unlockScript, 1, this.key, this.lockValue).should.equal(true); - }); + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) - return it('should call the callback', function() { - return this.callback.called.should.equal(true); - }); - }); - - return describe("when the lock has expired", function() { - beforeEach(function() { - this.rclient.eval = sinon.stub().yields(null, 0); - return this.LockManager.releaseLock(this.key, this.lockValue, this.callback); - }); - - return it('should return an error if the lock has expired', function() { - return this.callback.calledWith(sinon.match.has('message', "tried to release timed out lock")).should.equal(true); - }); - }); - }); - }); -}); + return describe('when the lock has expired', function() { + beforeEach(function() { + this.rclient.eval = sinon.stub().yields(null, 0) + return this.LockManager.releaseLock( + this.key, + this.lockValue, + this.callback + ) + }) + return it('should return an error if the lock has expired', function() { + return this.callback + .calledWith( + sinon.match.has('message', 'tried to release timed out lock') + ) + .should.equal(true) + }) + }) + }) + }) +}) diff --git a/services/track-changes/test/unit/js/MongoManager/MongoManagerTests.js b/services/track-changes/test/unit/js/MongoManager/MongoManagerTests.js index a72ec1dec8..8adc8dd202 100644 --- a/services/track-changes/test/unit/js/MongoManager/MongoManagerTests.js +++ b/services/track-changes/test/unit/js/MongoManager/MongoManagerTests.js @@ -9,195 +9,237 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const sinon = require('sinon'); -const chai = require('chai'); -const should = chai.should(); -const { expect } = chai; -const modulePath = "../../../../app/js/MongoManager.js"; -const packModulePath = "../../../../app/js/PackManager.js"; -const SandboxedModule = require('sandboxed-module'); -const {ObjectId} = require("mongojs"); -const tk = require("timekeeper"); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/js/MongoManager.js' +const packModulePath = '../../../../app/js/PackManager.js' +const SandboxedModule = require('sandboxed-module') +const { ObjectId } = require('mongojs') +const tk = require('timekeeper') -describe("MongoManager", function() { - beforeEach(function() { - tk.freeze(new Date()); - this.MongoManager = SandboxedModule.require(modulePath, { requires: { - "./mongojs" : { db: (this.db = {}), ObjectId }, - "./PackManager" : (this.PackManager = {}), - 'metrics-sharelatex': {timeAsyncMethod(){}}, - 'logger-sharelatex': {log(){}} - } - }); - this.callback = sinon.stub(); - this.doc_id = ObjectId().toString(); - return this.project_id = ObjectId().toString(); - }); +describe('MongoManager', function() { + beforeEach(function() { + tk.freeze(new Date()) + this.MongoManager = SandboxedModule.require(modulePath, { + requires: { + './mongojs': { db: (this.db = {}), ObjectId }, + './PackManager': (this.PackManager = {}), + 'metrics-sharelatex': { timeAsyncMethod() {} }, + 'logger-sharelatex': { log() {} } + } + }) + this.callback = sinon.stub() + this.doc_id = ObjectId().toString() + return (this.project_id = ObjectId().toString()) + }) - afterEach(function() { return tk.reset(); }); + afterEach(function() { + return tk.reset() + }) - describe("getLastCompressedUpdate", function() { - beforeEach(function() { - this.update = "mock-update"; - this.db.docHistory = {}; - this.db.docHistory.find = sinon.stub().returns(this.db.docHistory); - this.db.docHistory.findOne = sinon.stub().returns(this.db.docHistory); - this.db.docHistory.sort = sinon.stub().returns(this.db.docHistory); - this.db.docHistory.limit = sinon.stub().returns(this.db.docHistory); - this.db.docHistory.toArray = sinon.stub().callsArgWith(0, null, [this.update]); + describe('getLastCompressedUpdate', function() { + beforeEach(function() { + this.update = 'mock-update' + this.db.docHistory = {} + this.db.docHistory.find = sinon.stub().returns(this.db.docHistory) + this.db.docHistory.findOne = sinon.stub().returns(this.db.docHistory) + this.db.docHistory.sort = sinon.stub().returns(this.db.docHistory) + this.db.docHistory.limit = sinon.stub().returns(this.db.docHistory) + this.db.docHistory.toArray = sinon + .stub() + .callsArgWith(0, null, [this.update]) - return this.MongoManager.getLastCompressedUpdate(this.doc_id, this.callback); - }); + return this.MongoManager.getLastCompressedUpdate( + this.doc_id, + this.callback + ) + }) - it("should find the updates for the doc", function() { - return this.db.docHistory.find - .calledWith({doc_id: ObjectId(this.doc_id)}) - .should.equal(true); - }); + it('should find the updates for the doc', function() { + return this.db.docHistory.find + .calledWith({ doc_id: ObjectId(this.doc_id) }) + .should.equal(true) + }) - it("should limit to one result", function() { - return this.db.docHistory.limit - .calledWith(1) - .should.equal(true); - }); + it('should limit to one result', function() { + return this.db.docHistory.limit.calledWith(1).should.equal(true) + }) - it("should sort in descending version order", function() { - return this.db.docHistory.sort - .calledWith({v: -1}) - .should.equal(true); - }); + it('should sort in descending version order', function() { + return this.db.docHistory.sort.calledWith({ v: -1 }).should.equal(true) + }) - return it("should call the call back with the update", function() { - return this.callback.calledWith(null, this.update).should.equal(true); - }); - }); + return it('should call the call back with the update', function() { + return this.callback.calledWith(null, this.update).should.equal(true) + }) + }) + describe('peekLastCompressedUpdate', function() { + describe('when there is no last update', function() { + beforeEach(function() { + this.PackManager.getLastPackFromIndex = sinon + .stub() + .callsArgWith(1, null, null) + this.MongoManager.getLastCompressedUpdate = sinon + .stub() + .callsArgWith(1, null, null) + return this.MongoManager.peekLastCompressedUpdate( + this.doc_id, + this.callback + ) + }) - describe("peekLastCompressedUpdate", function() { - describe("when there is no last update", function() { - beforeEach(function() { - this.PackManager.getLastPackFromIndex = sinon.stub().callsArgWith(1, null, null); - this.MongoManager.getLastCompressedUpdate = sinon.stub().callsArgWith(1, null, null); - return this.MongoManager.peekLastCompressedUpdate(this.doc_id, this.callback); - }); + it('should get the last update', function() { + return this.MongoManager.getLastCompressedUpdate + .calledWith(this.doc_id) + .should.equal(true) + }) - it("should get the last update", function() { - return this.MongoManager.getLastCompressedUpdate - .calledWith(this.doc_id) - .should.equal(true); - }); + return it('should call the callback with no update', function() { + return this.callback.calledWith(null, null).should.equal(true) + }) + }) - return it("should call the callback with no update", function() { - return this.callback.calledWith(null, null).should.equal(true); - }); - }); + describe('when there is an update', function() { + beforeEach(function() { + this.update = { _id: Object() } + this.MongoManager.getLastCompressedUpdate = sinon + .stub() + .callsArgWith(1, null, this.update) + return this.MongoManager.peekLastCompressedUpdate( + this.doc_id, + this.callback + ) + }) - describe("when there is an update", function() { - beforeEach(function() { - this.update = { _id: Object() }; - this.MongoManager.getLastCompressedUpdate = sinon.stub().callsArgWith(1, null, this.update); - return this.MongoManager.peekLastCompressedUpdate(this.doc_id, this.callback); - }); + it('should get the last update', function() { + return this.MongoManager.getLastCompressedUpdate + .calledWith(this.doc_id) + .should.equal(true) + }) - it("should get the last update", function() { - return this.MongoManager.getLastCompressedUpdate - .calledWith(this.doc_id) - .should.equal(true); - }); + return it('should call the callback with the update', function() { + return this.callback.calledWith(null, this.update).should.equal(true) + }) + }) - return it("should call the callback with the update", function() { - return this.callback.calledWith(null, this.update).should.equal(true); - }); - }); + return describe('when there is a last update in S3', function() { + beforeEach(function() { + this.update = { _id: Object(), v: 12345, v_end: 12345, inS3: true } + this.PackManager.getLastPackFromIndex = sinon + .stub() + .callsArgWith(1, null, this.update) + this.MongoManager.getLastCompressedUpdate = sinon + .stub() + .callsArgWith(1, null) + return this.MongoManager.peekLastCompressedUpdate( + this.doc_id, + this.callback + ) + }) - return describe("when there is a last update in S3", function() { - beforeEach(function() { - this.update = { _id: Object(), v: 12345, v_end: 12345, inS3:true}; - this.PackManager.getLastPackFromIndex = sinon.stub().callsArgWith(1, null, this.update); - this.MongoManager.getLastCompressedUpdate = sinon.stub().callsArgWith(1, null); - return this.MongoManager.peekLastCompressedUpdate(this.doc_id, this.callback); - }); + it('should get the last update', function() { + return this.MongoManager.getLastCompressedUpdate + .calledWith(this.doc_id) + .should.equal(true) + }) - it("should get the last update", function() { - return this.MongoManager.getLastCompressedUpdate - .calledWith(this.doc_id) - .should.equal(true); - }); + return it('should call the callback with a null update and the correct version', function() { + return this.callback + .calledWith(null, null, this.update.v_end) + .should.equal(true) + }) + }) + }) - return it("should call the callback with a null update and the correct version", function() { - return this.callback.calledWith(null, null, this.update.v_end).should.equal(true); - }); - }); - }); + describe('backportProjectId', function() { + beforeEach(function() { + this.db.docHistory = { update: sinon.stub().callsArg(3) } + return this.MongoManager.backportProjectId( + this.project_id, + this.doc_id, + this.callback + ) + }) + it("should insert the project_id into all entries for the doc_id which don't have it set", function() { + return this.db.docHistory.update + .calledWith( + { + doc_id: ObjectId(this.doc_id), + project_id: { $exists: false } + }, + { + $set: { project_id: ObjectId(this.project_id) } + }, + { + multi: true + } + ) + .should.equal(true) + }) - describe("backportProjectId", function() { - beforeEach(function() { - this.db.docHistory = - {update: sinon.stub().callsArg(3)}; - return this.MongoManager.backportProjectId(this.project_id, this.doc_id, this.callback); - }); + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) - it("should insert the project_id into all entries for the doc_id which don't have it set", function() { - return this.db.docHistory.update - .calledWith({ - doc_id: ObjectId(this.doc_id), - project_id: { $exists: false } - }, { - $set: { project_id: ObjectId(this.project_id) } - }, { - multi: true - }) - .should.equal(true); - }); + describe('getProjectMetaData', function() { + beforeEach(function() { + this.metadata = { mock: 'metadata' } + this.db.projectHistoryMetaData = { + find: sinon.stub().callsArgWith(1, null, [this.metadata]) + } + return this.MongoManager.getProjectMetaData( + this.project_id, + this.callback + ) + }) - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); + it('should look up the meta data in the db', function() { + return this.db.projectHistoryMetaData.find + .calledWith({ project_id: ObjectId(this.project_id) }) + .should.equal(true) + }) - describe("getProjectMetaData", function() { - beforeEach(function() { - this.metadata = { "mock": "metadata" }; - this.db.projectHistoryMetaData = - {find: sinon.stub().callsArgWith(1, null, [this.metadata])}; - return this.MongoManager.getProjectMetaData(this.project_id, this.callback); - }); + return it('should return the metadata', function() { + return this.callback.calledWith(null, this.metadata).should.equal(true) + }) + }) - it("should look up the meta data in the db", function() { - return this.db.projectHistoryMetaData.find - .calledWith({ project_id: ObjectId(this.project_id) }) - .should.equal(true); - }); + return describe('setProjectMetaData', function() { + beforeEach(function() { + this.metadata = { mock: 'metadata' } + this.db.projectHistoryMetaData = { + update: sinon.stub().callsArgWith(3, null, [this.metadata]) + } + return this.MongoManager.setProjectMetaData( + this.project_id, + this.metadata, + this.callback + ) + }) - return it("should return the metadata", function() { - return this.callback.calledWith(null, this.metadata).should.equal(true); - }); - }); - - return describe("setProjectMetaData", function() { - beforeEach(function() { - this.metadata = { "mock": "metadata" }; - this.db.projectHistoryMetaData = - {update: sinon.stub().callsArgWith(3, null, [this.metadata])}; - return this.MongoManager.setProjectMetaData(this.project_id, this.metadata, this.callback); - }); - - it("should upsert the metadata into the DB", function() { - return this.db.projectHistoryMetaData.update - .calledWith({ - project_id: ObjectId(this.project_id) - }, { - $set: this.metadata - }, { - upsert: true - }) - .should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); -}); + it('should upsert the metadata into the DB', function() { + return this.db.projectHistoryMetaData.update + .calledWith( + { + project_id: ObjectId(this.project_id) + }, + { + $set: this.metadata + }, + { + upsert: true + } + ) + .should.equal(true) + }) + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) +}) diff --git a/services/track-changes/test/unit/js/PackManager/PackManagerTests.js b/services/track-changes/test/unit/js/PackManager/PackManagerTests.js index dda7a5e6bb..843a010f8f 100644 --- a/services/track-changes/test/unit/js/PackManager/PackManagerTests.js +++ b/services/track-changes/test/unit/js/PackManager/PackManagerTests.js @@ -10,461 +10,701 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const sinon = require('sinon'); -const chai = require('chai'); -const { assert } = require('chai'); -const should = chai.should(); -const { expect } = chai; -const modulePath = "../../../../app/js/PackManager.js"; -const SandboxedModule = require('sandboxed-module'); -const {ObjectId} = require("mongojs"); -const bson = require("bson"); -const BSON = new bson.BSONPure(); -const _ = require("underscore"); - -const tk = require("timekeeper"); - -describe("PackManager", function() { - beforeEach(function() { - tk.freeze(new Date()); - this.PackManager = SandboxedModule.require(modulePath, { requires: { - "./mongojs" : { db: (this.db = {}), ObjectId, BSON }, - "./LockManager" : {}, - "./MongoAWS": {}, - "logger-sharelatex": { log: sinon.stub(), error: sinon.stub() }, - 'metrics-sharelatex': {inc(){}}, - "./ProjectIterator": require("../../../../app/js/ProjectIterator.js"), // Cache for speed - "settings-sharelatex": { - redis: {lock: {key_schema: {}}} - } - } - }); - this.callback = sinon.stub(); - this.doc_id = ObjectId().toString(); - this.project_id = ObjectId().toString(); - return this.PackManager.MAX_COUNT = 512; - }); - - afterEach(function() { return tk.reset(); }); - - describe("insertCompressedUpdates", function() { - beforeEach(function() { - this.lastUpdate = { - _id: "12345", - pack: [ - { op: "op-1", meta: "meta-1", v: 1}, - { op: "op-2", meta: "meta-2", v: 2} - ], - n : 2, - sz : 100 - }; - this.newUpdates = [ - { op: "op-3", meta: "meta-3", v: 3}, - { op: "op-4", meta: "meta-4", v: 4} - ]; - return this.db.docHistory = { - save: sinon.stub().callsArg(1), - insert: sinon.stub().callsArg(1), - findAndModify: sinon.stub().callsArg(1) - }; - }); - - describe("with no last update", function() { - beforeEach(function() { - this.PackManager.insertUpdatesIntoNewPack = sinon.stub().callsArg(4); - return this.PackManager.insertCompressedUpdates(this.project_id, this.doc_id, null, this.newUpdates, true, this.callback); - }); - - describe("for a small update", function() { - it("should insert the update into a new pack", function() { - return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates, true).should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - - return describe("for many small updates", function() { - beforeEach(function() { - this.newUpdates = (__range__(0, 2048, true).map((i) => ({ op: `op-${i}`, meta: `meta-${i}`, v: i}))); - return this.PackManager.insertCompressedUpdates(this.project_id, this.doc_id, null, this.newUpdates, false, this.callback); - }); - - it("should append the initial updates to the existing pack", function() { - return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates.slice(0, 512), false).should.equal(true); - }); - - it("should insert the first set remaining updates as a new pack", function() { - return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates.slice(512, 1024), false).should.equal(true); - }); - - it("should insert the second set of remaining updates as a new pack", function() { - return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates.slice(1024, 1536), false).should.equal(true); - }); - - it("should insert the third set of remaining updates as a new pack", function() { - return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates.slice(1536, 2048), false).should.equal(true); - }); - - it("should insert the final set of remaining updates as a new pack", function() { - return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates.slice(2048, 2049), false).should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - }); - - - - describe("with an existing pack as the last update", function() { - beforeEach(function() { - this.PackManager.appendUpdatesToExistingPack = sinon.stub().callsArg(5); - this.PackManager.insertUpdatesIntoNewPack = sinon.stub().callsArg(4); - return this.PackManager.insertCompressedUpdates(this.project_id, this.doc_id, this.lastUpdate, this.newUpdates, false, this.callback); - }); - - describe("for a small update", function() { - it("should append the update to the existing pack", function() { - return this.PackManager.appendUpdatesToExistingPack.calledWith(this.project_id, this.doc_id, this.lastUpdate, this.newUpdates, false).should.equal(true); - }); - it("should not insert any new packs", function() { - return this.PackManager.insertUpdatesIntoNewPack.called.should.equal(false); - }); - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - - describe("for many small updates", function() { - beforeEach(function() { - this.newUpdates = (__range__(0, 2048, true).map((i) => ({ op: `op-${i}`, meta: `meta-${i}`, v: i}))); - return this.PackManager.insertCompressedUpdates(this.project_id, this.doc_id, this.lastUpdate, this.newUpdates, false, this.callback); - }); - - it("should append the initial updates to the existing pack", function() { - return this.PackManager.appendUpdatesToExistingPack.calledWith(this.project_id, this.doc_id, this.lastUpdate, this.newUpdates.slice(0, 510), false).should.equal(true); - }); - - it("should insert the first set remaining updates as a new pack", function() { - return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates.slice(510, 1022), false).should.equal(true); - }); - - it("should insert the second set of remaining updates as a new pack", function() { - return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates.slice(1022, 1534), false).should.equal(true); - }); - - it("should insert the third set of remaining updates as a new pack", function() { - return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates.slice(1534, 2046), false).should.equal(true); - }); - - it("should insert the final set of remaining updates as a new pack", function() { - return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates.slice(2046, 2049), false).should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - - return describe("for many big updates", function() { - beforeEach(function() { - const longString = (__range__(0, (0.75*this.PackManager.MAX_SIZE), true).map((j) => "a")).join(""); - this.newUpdates = ([0, 1, 2, 3, 4].map((i) => ({ op: `op-${i}-${longString}`, meta: `meta-${i}`, v: i}))); - return this.PackManager.insertCompressedUpdates(this.project_id, this.doc_id, this.lastUpdate, this.newUpdates, false, this.callback); - }); - - it("should append the initial updates to the existing pack", function() { - return this.PackManager.appendUpdatesToExistingPack.calledWith(this.project_id, this.doc_id, this.lastUpdate, this.newUpdates.slice(0, 1), false).should.equal(true); - }); - - it("should insert the first set remaining updates as a new pack", function() { - return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates.slice(1, 2), false).should.equal(true); - }); - - it("should insert the second set of remaining updates as a new pack", function() { - return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates.slice(2, 3), false).should.equal(true); - }); - - it("should insert the third set of remaining updates as a new pack", function() { - return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates.slice(3, 4), false).should.equal(true); - }); - - it("should insert the final set of remaining updates as a new pack", function() { - return this.PackManager.insertUpdatesIntoNewPack.calledWith(this.project_id, this.doc_id, this.newUpdates.slice(4, 5), false).should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - }); - - describe("flushCompressedUpdates", function() { return describe("when there is no previous update", function() { - beforeEach(function() { - return this.PackManager.flushCompressedUpdates(this.project_id, this.doc_id, null, this.newUpdates, true, this.callback); - }); - - return describe("for a small update that will expire", function() { - it("should insert the update into mongo", function() { - return this.db.docHistory.save.calledWithMatch({ - pack: this.newUpdates, - project_id: ObjectId(this.project_id), - doc_id: ObjectId(this.doc_id), - n: this.newUpdates.length, - v: this.newUpdates[0].v, - v_end: this.newUpdates[this.newUpdates.length-1].v - }).should.equal(true); - }); - - it("should set an expiry time in the future", function() { - return this.db.docHistory.save.calledWithMatch({ - expiresAt: new Date(Date.now() + (7 * 24 * 3600 * 1000)) - }).should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - }); } - ); - - describe("when there is a recent previous update in mongo that expires", function() { - beforeEach(function() { - this.lastUpdate = { - _id: "12345", - pack: [ - { op: "op-1", meta: "meta-1", v: 1}, - { op: "op-2", meta: "meta-2", v: 2} - ], - n : 2, - sz : 100, - meta: {start_ts: Date.now() - (6 * 3600 * 1000)}, - expiresAt: new Date(Date.now()) - }; - - return this.PackManager.flushCompressedUpdates(this.project_id, this.doc_id, this.lastUpdate, this.newUpdates, true, this.callback); - }); - - return describe("for a small update that will expire", function() { - it("should append the update in mongo", function() { - return this.db.docHistory.findAndModify.calledWithMatch({ - query: {_id: this.lastUpdate._id}, - update: { $push: {"pack" : {$each: this.newUpdates}}, $set: {v_end: this.newUpdates[this.newUpdates.length-1].v}} - }).should.equal(true); - }); - - it("should set an expiry time in the future", function() { - return this.db.docHistory.findAndModify.calledWithMatch({ - update: {$set: {expiresAt: new Date(Date.now() + (7 * 24 * 3600 * 1000))}} - }).should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - }); - - - describe("when there is a recent previous update in mongo that expires", function() { - beforeEach(function() { - this.PackManager.updateIndex = sinon.stub().callsArg(2); - - this.lastUpdate = { - _id: "12345", - pack: [ - { op: "op-1", meta: "meta-1", v: 1}, - { op: "op-2", meta: "meta-2", v: 2} - ], - n : 2, - sz : 100, - meta: {start_ts: Date.now() - (6 * 3600 * 1000)}, - expiresAt: new Date(Date.now()) - }; - - return this.PackManager.flushCompressedUpdates(this.project_id, this.doc_id, this.lastUpdate, this.newUpdates, false, this.callback); - }); - - return describe("for a small update that will not expire", function() { - it("should insert the update into mongo", function() { - return this.db.docHistory.save.calledWithMatch({ - pack: this.newUpdates, - project_id: ObjectId(this.project_id), - doc_id: ObjectId(this.doc_id), - n: this.newUpdates.length, - v: this.newUpdates[0].v, - v_end: this.newUpdates[this.newUpdates.length-1].v - }).should.equal(true); - }); - - it("should not set any expiry time", function() { - return this.db.docHistory.save.neverCalledWithMatch(sinon.match.has("expiresAt")).should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - }); - - return describe("when there is an old previous update in mongo", function() { - beforeEach(function() { - this.lastUpdate = { - _id: "12345", - pack: [ - { op: "op-1", meta: "meta-1", v: 1}, - { op: "op-2", meta: "meta-2", v: 2} - ], - n : 2, - sz : 100, - meta: {start_ts: Date.now() - (30 * 24 * 3600 * 1000)}, - expiresAt: new Date(Date.now() - (30 * 24 * 3600 * 1000)) - }; - - return this.PackManager.flushCompressedUpdates(this.project_id, this.doc_id, this.lastUpdate, this.newUpdates, true, this.callback); - }); - - return describe("for a small update that will expire", function() { - it("should insert the update into mongo", function() { - return this.db.docHistory.save.calledWithMatch({ - pack: this.newUpdates, - project_id: ObjectId(this.project_id), - doc_id: ObjectId(this.doc_id), - n: this.newUpdates.length, - v: this.newUpdates[0].v, - v_end: this.newUpdates[this.newUpdates.length-1].v - }).should.equal(true); - }); - - it("should set an expiry time in the future", function() { - return this.db.docHistory.save.calledWithMatch({ - expiresAt: new Date(Date.now() + (7 * 24 * 3600 * 1000)) - }).should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - }); - }); - - - describe("getOpsByVersionRange", function() {}); - - describe("loadPacksByVersionRange", function() {}); - - describe("fetchPacksIfNeeded", function() {}); - - describe("makeProjectIterator", function() {}); - - describe("getPackById", function() {}); - - describe("increaseTTL", function() {}); - - describe("getIndex", function() {}); - - describe("getPackFromIndex", function() {}); -// getLastPackFromIndex: -// getIndexWithKeys -// initialiseIndex -// updateIndex -// findCompletedPacks -// findUnindexedPacks -// insertPacksIntoIndexWithLock -// _insertPacksIntoIndex -// archivePack -// checkArchivedPack -// processOldPack -// updateIndexIfNeeded -// findUnarchivedPacks - - return describe("checkArchiveNotInProgress", function() { - - describe("when an archive is in progress", function() { - beforeEach(function() { - this.db.docHistoryIndex = - {findOne: sinon.stub().callsArgWith(2, null, {inS3:false})}; - return this.PackManager.checkArchiveNotInProgress(this.project_id, this.doc_id, this.pack_id, this.callback); - }); - it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - return it("should return an error", function() { - return this.callback.calledWith(sinon.match.has('message')).should.equal(true); - }); - }); - - describe("when an archive is completed", function() { - beforeEach(function() { - this.db.docHistoryIndex = - {findOne: sinon.stub().callsArgWith(2, null, {inS3:true})}; - return this.PackManager.checkArchiveNotInProgress(this.project_id, this.doc_id, this.pack_id, this.callback); - }); - it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - return it("should return an error", function() { - return this.callback.calledWith(sinon.match.has('message')).should.equal(true); - }); - }); - - return describe("when the archive has not started or completed", function() { - beforeEach(function() { - this.db.docHistoryIndex = - {findOne: sinon.stub().callsArgWith(2, null, {})}; - return this.PackManager.checkArchiveNotInProgress(this.project_id, this.doc_id, this.pack_id, this.callback); - }); - it("should call the callback with no error", function() { - return this.callback.called.should.equal(true); - }); - return it("should return with no error", function() { - return (typeof this.callback.lastCall.args[0]).should.equal('undefined'); - }); - }); - }); -}); - - // describe "setTTLOnArchivedPack", -> - // beforeEach -> - // @pack_id = "somepackid" - // @onedayinms = 86400000 - // @db.docHistory = - // findAndModify : sinon.stub().callsArgWith(1) - - // it "should set expires to 1 day", (done)-> - // #@PackManager._getOneDayInFutureWithRandomDelay = sinon.stub().returns(@onedayinms) - // @PackManager.setTTLOnArchivedPack @project_id, @doc_id, @pack_id, => - // args = @db.docHistory.findAndModify.args[0][0] - // args.query._id.should.equal @pack_id - // args.update['$set'].expiresAt.should.equal @onedayinms - // done() - - - // describe "_getOneDayInFutureWithRandomDelay", -> - // beforeEach -> - // @onedayinms = 86400000 - // @thirtyMins = 1000 * 60 * 30 - - // it "should give 1 day + 30 mins random time", (done)-> - // loops = 10000 - // while --loops > 0 - // randomDelay = @PackManager._getOneDayInFutureWithRandomDelay() - new Date(Date.now() + @onedayinms) - // randomDelay.should.be.above(0) - // randomDelay.should.be.below(@thirtyMins + 1) - // done() - +const sinon = require('sinon') +const chai = require('chai') +const { assert } = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/js/PackManager.js' +const SandboxedModule = require('sandboxed-module') +const { ObjectId } = require('mongojs') +const bson = require('bson') +const BSON = new bson.BSONPure() +const _ = require('underscore') + +const tk = require('timekeeper') + +describe('PackManager', function() { + beforeEach(function() { + tk.freeze(new Date()) + this.PackManager = SandboxedModule.require(modulePath, { + requires: { + './mongojs': { db: (this.db = {}), ObjectId, BSON }, + './LockManager': {}, + './MongoAWS': {}, + 'logger-sharelatex': { log: sinon.stub(), error: sinon.stub() }, + 'metrics-sharelatex': { inc() {} }, + './ProjectIterator': require('../../../../app/js/ProjectIterator.js'), // Cache for speed + 'settings-sharelatex': { + redis: { lock: { key_schema: {} } } + } + } + }) + this.callback = sinon.stub() + this.doc_id = ObjectId().toString() + this.project_id = ObjectId().toString() + return (this.PackManager.MAX_COUNT = 512) + }) + + afterEach(function() { + return tk.reset() + }) + + describe('insertCompressedUpdates', function() { + beforeEach(function() { + this.lastUpdate = { + _id: '12345', + pack: [ + { op: 'op-1', meta: 'meta-1', v: 1 }, + { op: 'op-2', meta: 'meta-2', v: 2 } + ], + n: 2, + sz: 100 + } + this.newUpdates = [ + { op: 'op-3', meta: 'meta-3', v: 3 }, + { op: 'op-4', meta: 'meta-4', v: 4 } + ] + return (this.db.docHistory = { + save: sinon.stub().callsArg(1), + insert: sinon.stub().callsArg(1), + findAndModify: sinon.stub().callsArg(1) + }) + }) + + describe('with no last update', function() { + beforeEach(function() { + this.PackManager.insertUpdatesIntoNewPack = sinon.stub().callsArg(4) + return this.PackManager.insertCompressedUpdates( + this.project_id, + this.doc_id, + null, + this.newUpdates, + true, + this.callback + ) + }) + + describe('for a small update', function() { + it('should insert the update into a new pack', function() { + return this.PackManager.insertUpdatesIntoNewPack + .calledWith(this.project_id, this.doc_id, this.newUpdates, true) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + return describe('for many small updates', function() { + beforeEach(function() { + this.newUpdates = __range__(0, 2048, true).map(i => ({ + op: `op-${i}`, + meta: `meta-${i}`, + v: i + })) + return this.PackManager.insertCompressedUpdates( + this.project_id, + this.doc_id, + null, + this.newUpdates, + false, + this.callback + ) + }) + + it('should append the initial updates to the existing pack', function() { + return this.PackManager.insertUpdatesIntoNewPack + .calledWith( + this.project_id, + this.doc_id, + this.newUpdates.slice(0, 512), + false + ) + .should.equal(true) + }) + + it('should insert the first set remaining updates as a new pack', function() { + return this.PackManager.insertUpdatesIntoNewPack + .calledWith( + this.project_id, + this.doc_id, + this.newUpdates.slice(512, 1024), + false + ) + .should.equal(true) + }) + + it('should insert the second set of remaining updates as a new pack', function() { + return this.PackManager.insertUpdatesIntoNewPack + .calledWith( + this.project_id, + this.doc_id, + this.newUpdates.slice(1024, 1536), + false + ) + .should.equal(true) + }) + + it('should insert the third set of remaining updates as a new pack', function() { + return this.PackManager.insertUpdatesIntoNewPack + .calledWith( + this.project_id, + this.doc_id, + this.newUpdates.slice(1536, 2048), + false + ) + .should.equal(true) + }) + + it('should insert the final set of remaining updates as a new pack', function() { + return this.PackManager.insertUpdatesIntoNewPack + .calledWith( + this.project_id, + this.doc_id, + this.newUpdates.slice(2048, 2049), + false + ) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + }) + + describe('with an existing pack as the last update', function() { + beforeEach(function() { + this.PackManager.appendUpdatesToExistingPack = sinon.stub().callsArg(5) + this.PackManager.insertUpdatesIntoNewPack = sinon.stub().callsArg(4) + return this.PackManager.insertCompressedUpdates( + this.project_id, + this.doc_id, + this.lastUpdate, + this.newUpdates, + false, + this.callback + ) + }) + + describe('for a small update', function() { + it('should append the update to the existing pack', function() { + return this.PackManager.appendUpdatesToExistingPack + .calledWith( + this.project_id, + this.doc_id, + this.lastUpdate, + this.newUpdates, + false + ) + .should.equal(true) + }) + it('should not insert any new packs', function() { + return this.PackManager.insertUpdatesIntoNewPack.called.should.equal( + false + ) + }) + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + describe('for many small updates', function() { + beforeEach(function() { + this.newUpdates = __range__(0, 2048, true).map(i => ({ + op: `op-${i}`, + meta: `meta-${i}`, + v: i + })) + return this.PackManager.insertCompressedUpdates( + this.project_id, + this.doc_id, + this.lastUpdate, + this.newUpdates, + false, + this.callback + ) + }) + + it('should append the initial updates to the existing pack', function() { + return this.PackManager.appendUpdatesToExistingPack + .calledWith( + this.project_id, + this.doc_id, + this.lastUpdate, + this.newUpdates.slice(0, 510), + false + ) + .should.equal(true) + }) + + it('should insert the first set remaining updates as a new pack', function() { + return this.PackManager.insertUpdatesIntoNewPack + .calledWith( + this.project_id, + this.doc_id, + this.newUpdates.slice(510, 1022), + false + ) + .should.equal(true) + }) + + it('should insert the second set of remaining updates as a new pack', function() { + return this.PackManager.insertUpdatesIntoNewPack + .calledWith( + this.project_id, + this.doc_id, + this.newUpdates.slice(1022, 1534), + false + ) + .should.equal(true) + }) + + it('should insert the third set of remaining updates as a new pack', function() { + return this.PackManager.insertUpdatesIntoNewPack + .calledWith( + this.project_id, + this.doc_id, + this.newUpdates.slice(1534, 2046), + false + ) + .should.equal(true) + }) + + it('should insert the final set of remaining updates as a new pack', function() { + return this.PackManager.insertUpdatesIntoNewPack + .calledWith( + this.project_id, + this.doc_id, + this.newUpdates.slice(2046, 2049), + false + ) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + return describe('for many big updates', function() { + beforeEach(function() { + const longString = __range__( + 0, + 0.75 * this.PackManager.MAX_SIZE, + true + ) + .map(j => 'a') + .join('') + this.newUpdates = [0, 1, 2, 3, 4].map(i => ({ + op: `op-${i}-${longString}`, + meta: `meta-${i}`, + v: i + })) + return this.PackManager.insertCompressedUpdates( + this.project_id, + this.doc_id, + this.lastUpdate, + this.newUpdates, + false, + this.callback + ) + }) + + it('should append the initial updates to the existing pack', function() { + return this.PackManager.appendUpdatesToExistingPack + .calledWith( + this.project_id, + this.doc_id, + this.lastUpdate, + this.newUpdates.slice(0, 1), + false + ) + .should.equal(true) + }) + + it('should insert the first set remaining updates as a new pack', function() { + return this.PackManager.insertUpdatesIntoNewPack + .calledWith( + this.project_id, + this.doc_id, + this.newUpdates.slice(1, 2), + false + ) + .should.equal(true) + }) + + it('should insert the second set of remaining updates as a new pack', function() { + return this.PackManager.insertUpdatesIntoNewPack + .calledWith( + this.project_id, + this.doc_id, + this.newUpdates.slice(2, 3), + false + ) + .should.equal(true) + }) + + it('should insert the third set of remaining updates as a new pack', function() { + return this.PackManager.insertUpdatesIntoNewPack + .calledWith( + this.project_id, + this.doc_id, + this.newUpdates.slice(3, 4), + false + ) + .should.equal(true) + }) + + it('should insert the final set of remaining updates as a new pack', function() { + return this.PackManager.insertUpdatesIntoNewPack + .calledWith( + this.project_id, + this.doc_id, + this.newUpdates.slice(4, 5), + false + ) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + }) + + describe('flushCompressedUpdates', function() { + return describe('when there is no previous update', function() { + beforeEach(function() { + return this.PackManager.flushCompressedUpdates( + this.project_id, + this.doc_id, + null, + this.newUpdates, + true, + this.callback + ) + }) + + return describe('for a small update that will expire', function() { + it('should insert the update into mongo', function() { + return this.db.docHistory.save + .calledWithMatch({ + pack: this.newUpdates, + project_id: ObjectId(this.project_id), + doc_id: ObjectId(this.doc_id), + n: this.newUpdates.length, + v: this.newUpdates[0].v, + v_end: this.newUpdates[this.newUpdates.length - 1].v + }) + .should.equal(true) + }) + + it('should set an expiry time in the future', function() { + return this.db.docHistory.save + .calledWithMatch({ + expiresAt: new Date(Date.now() + 7 * 24 * 3600 * 1000) + }) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + }) + }) + + describe('when there is a recent previous update in mongo that expires', function() { + beforeEach(function() { + this.lastUpdate = { + _id: '12345', + pack: [ + { op: 'op-1', meta: 'meta-1', v: 1 }, + { op: 'op-2', meta: 'meta-2', v: 2 } + ], + n: 2, + sz: 100, + meta: { start_ts: Date.now() - 6 * 3600 * 1000 }, + expiresAt: new Date(Date.now()) + } + + return this.PackManager.flushCompressedUpdates( + this.project_id, + this.doc_id, + this.lastUpdate, + this.newUpdates, + true, + this.callback + ) + }) + + return describe('for a small update that will expire', function() { + it('should append the update in mongo', function() { + return this.db.docHistory.findAndModify + .calledWithMatch({ + query: { _id: this.lastUpdate._id }, + update: { + $push: { pack: { $each: this.newUpdates } }, + $set: { v_end: this.newUpdates[this.newUpdates.length - 1].v } + } + }) + .should.equal(true) + }) + + it('should set an expiry time in the future', function() { + return this.db.docHistory.findAndModify + .calledWithMatch({ + update: { + $set: { expiresAt: new Date(Date.now() + 7 * 24 * 3600 * 1000) } + } + }) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + }) + + describe('when there is a recent previous update in mongo that expires', function() { + beforeEach(function() { + this.PackManager.updateIndex = sinon.stub().callsArg(2) + + this.lastUpdate = { + _id: '12345', + pack: [ + { op: 'op-1', meta: 'meta-1', v: 1 }, + { op: 'op-2', meta: 'meta-2', v: 2 } + ], + n: 2, + sz: 100, + meta: { start_ts: Date.now() - 6 * 3600 * 1000 }, + expiresAt: new Date(Date.now()) + } + + return this.PackManager.flushCompressedUpdates( + this.project_id, + this.doc_id, + this.lastUpdate, + this.newUpdates, + false, + this.callback + ) + }) + + return describe('for a small update that will not expire', function() { + it('should insert the update into mongo', function() { + return this.db.docHistory.save + .calledWithMatch({ + pack: this.newUpdates, + project_id: ObjectId(this.project_id), + doc_id: ObjectId(this.doc_id), + n: this.newUpdates.length, + v: this.newUpdates[0].v, + v_end: this.newUpdates[this.newUpdates.length - 1].v + }) + .should.equal(true) + }) + + it('should not set any expiry time', function() { + return this.db.docHistory.save + .neverCalledWithMatch(sinon.match.has('expiresAt')) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + }) + + return describe('when there is an old previous update in mongo', function() { + beforeEach(function() { + this.lastUpdate = { + _id: '12345', + pack: [ + { op: 'op-1', meta: 'meta-1', v: 1 }, + { op: 'op-2', meta: 'meta-2', v: 2 } + ], + n: 2, + sz: 100, + meta: { start_ts: Date.now() - 30 * 24 * 3600 * 1000 }, + expiresAt: new Date(Date.now() - 30 * 24 * 3600 * 1000) + } + + return this.PackManager.flushCompressedUpdates( + this.project_id, + this.doc_id, + this.lastUpdate, + this.newUpdates, + true, + this.callback + ) + }) + + return describe('for a small update that will expire', function() { + it('should insert the update into mongo', function() { + return this.db.docHistory.save + .calledWithMatch({ + pack: this.newUpdates, + project_id: ObjectId(this.project_id), + doc_id: ObjectId(this.doc_id), + n: this.newUpdates.length, + v: this.newUpdates[0].v, + v_end: this.newUpdates[this.newUpdates.length - 1].v + }) + .should.equal(true) + }) + + it('should set an expiry time in the future', function() { + return this.db.docHistory.save + .calledWithMatch({ + expiresAt: new Date(Date.now() + 7 * 24 * 3600 * 1000) + }) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + }) + }) + + describe('getOpsByVersionRange', function() {}) + + describe('loadPacksByVersionRange', function() {}) + + describe('fetchPacksIfNeeded', function() {}) + + describe('makeProjectIterator', function() {}) + + describe('getPackById', function() {}) + + describe('increaseTTL', function() {}) + + describe('getIndex', function() {}) + + describe('getPackFromIndex', function() {}) + // getLastPackFromIndex: + // getIndexWithKeys + // initialiseIndex + // updateIndex + // findCompletedPacks + // findUnindexedPacks + // insertPacksIntoIndexWithLock + // _insertPacksIntoIndex + // archivePack + // checkArchivedPack + // processOldPack + // updateIndexIfNeeded + // findUnarchivedPacks + + return describe('checkArchiveNotInProgress', function() { + describe('when an archive is in progress', function() { + beforeEach(function() { + this.db.docHistoryIndex = { + findOne: sinon.stub().callsArgWith(2, null, { inS3: false }) + } + return this.PackManager.checkArchiveNotInProgress( + this.project_id, + this.doc_id, + this.pack_id, + this.callback + ) + }) + it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + return it('should return an error', function() { + return this.callback + .calledWith(sinon.match.has('message')) + .should.equal(true) + }) + }) + + describe('when an archive is completed', function() { + beforeEach(function() { + this.db.docHistoryIndex = { + findOne: sinon.stub().callsArgWith(2, null, { inS3: true }) + } + return this.PackManager.checkArchiveNotInProgress( + this.project_id, + this.doc_id, + this.pack_id, + this.callback + ) + }) + it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + return it('should return an error', function() { + return this.callback + .calledWith(sinon.match.has('message')) + .should.equal(true) + }) + }) + + return describe('when the archive has not started or completed', function() { + beforeEach(function() { + this.db.docHistoryIndex = { + findOne: sinon.stub().callsArgWith(2, null, {}) + } + return this.PackManager.checkArchiveNotInProgress( + this.project_id, + this.doc_id, + this.pack_id, + this.callback + ) + }) + it('should call the callback with no error', function() { + return this.callback.called.should.equal(true) + }) + return it('should return with no error', function() { + return (typeof this.callback.lastCall.args[0]).should.equal('undefined') + }) + }) + }) +}) + +// describe "setTTLOnArchivedPack", -> +// beforeEach -> +// @pack_id = "somepackid" +// @onedayinms = 86400000 +// @db.docHistory = +// findAndModify : sinon.stub().callsArgWith(1) + +// it "should set expires to 1 day", (done)-> +// #@PackManager._getOneDayInFutureWithRandomDelay = sinon.stub().returns(@onedayinms) +// @PackManager.setTTLOnArchivedPack @project_id, @doc_id, @pack_id, => +// args = @db.docHistory.findAndModify.args[0][0] +// args.query._id.should.equal @pack_id +// args.update['$set'].expiresAt.should.equal @onedayinms +// done() + +// describe "_getOneDayInFutureWithRandomDelay", -> +// beforeEach -> +// @onedayinms = 86400000 +// @thirtyMins = 1000 * 60 * 30 + +// it "should give 1 day + 30 mins random time", (done)-> +// loops = 10000 +// while --loops > 0 +// randomDelay = @PackManager._getOneDayInFutureWithRandomDelay() - new Date(Date.now() + @onedayinms) +// randomDelay.should.be.above(0) +// randomDelay.should.be.below(@thirtyMins + 1) +// done() function __range__(left, right, inclusive) { - const range = []; - const ascending = left < right; - const end = !inclusive ? right : ascending ? right + 1 : right - 1; + const range = [] + const ascending = left < right + const end = !inclusive ? right : ascending ? right + 1 : right - 1 for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) { - range.push(i); + range.push(i) } - return range; -} \ No newline at end of file + return range +} diff --git a/services/track-changes/test/unit/js/RedisManager/RedisManagerTests.js b/services/track-changes/test/unit/js/RedisManager/RedisManagerTests.js index 5637ed6135..d05bf50814 100644 --- a/services/track-changes/test/unit/js/RedisManager/RedisManagerTests.js +++ b/services/track-changes/test/unit/js/RedisManager/RedisManagerTests.js @@ -11,118 +11,156 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const sinon = require('sinon'); -const chai = require('chai'); -const should = chai.should(); -const { expect } = chai; -const modulePath = "../../../../app/js/RedisManager.js"; -const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/js/RedisManager.js' +const SandboxedModule = require('sandboxed-module') -describe("RedisManager", function() { - beforeEach(function() { - this.RedisManager = SandboxedModule.require(modulePath, { requires: { - "redis-sharelatex" : { - createClient: () => { return this.rclient = { - auth: sinon.stub(), - multi: () => this.rclient - }; } - }, - "settings-sharelatex": { - redis: { - history: { - key_schema: { - uncompressedHistoryOps({doc_id}) { return `UncompressedHistoryOps:${doc_id}`; }, - docsWithHistoryOps({project_id}) { return `DocsWithHistoryOps:${project_id}`; } - } - } - } - } - } - } - ); - this.doc_id = "doc-id-123"; - this.project_id = "project-id-123"; - this.batchSize = 100; - return this.callback = sinon.stub(); - }); +describe('RedisManager', function() { + beforeEach(function() { + this.RedisManager = SandboxedModule.require(modulePath, { + requires: { + 'redis-sharelatex': { + createClient: () => { + return (this.rclient = { + auth: sinon.stub(), + multi: () => this.rclient + }) + } + }, + 'settings-sharelatex': { + redis: { + history: { + key_schema: { + uncompressedHistoryOps({ doc_id }) { + return `UncompressedHistoryOps:${doc_id}` + }, + docsWithHistoryOps({ project_id }) { + return `DocsWithHistoryOps:${project_id}` + } + } + } + } + } + } + }) + this.doc_id = 'doc-id-123' + this.project_id = 'project-id-123' + this.batchSize = 100 + return (this.callback = sinon.stub()) + }) - describe("getOldestDocUpdates", function() { - beforeEach(function() { - this.rawUpdates = [ {v: 42, op: "mock-op-42"}, { v: 45, op: "mock-op-45" }]; - this.jsonUpdates = (Array.from(this.rawUpdates).map((update) => JSON.stringify(update))); - this.rclient.lrange = sinon.stub().callsArgWith(3, null, this.jsonUpdates); - return this.RedisManager.getOldestDocUpdates(this.doc_id, this.batchSize, this.callback); - }); + describe('getOldestDocUpdates', function() { + beforeEach(function() { + this.rawUpdates = [ + { v: 42, op: 'mock-op-42' }, + { v: 45, op: 'mock-op-45' } + ] + this.jsonUpdates = Array.from(this.rawUpdates).map(update => + JSON.stringify(update) + ) + this.rclient.lrange = sinon.stub().callsArgWith(3, null, this.jsonUpdates) + return this.RedisManager.getOldestDocUpdates( + this.doc_id, + this.batchSize, + this.callback + ) + }) - it("should read the updates from redis", function() { - return this.rclient.lrange - .calledWith(`UncompressedHistoryOps:${this.doc_id}`, 0, this.batchSize - 1) - .should.equal(true); - }); + it('should read the updates from redis', function() { + return this.rclient.lrange + .calledWith( + `UncompressedHistoryOps:${this.doc_id}`, + 0, + this.batchSize - 1 + ) + .should.equal(true) + }) - it("should call the callback with the unparsed ops", function() { - return this.callback.calledWith(null, this.jsonUpdates).should.equal(true); - }); + it('should call the callback with the unparsed ops', function() { + return this.callback.calledWith(null, this.jsonUpdates).should.equal(true) + }) + describe('expandDocUpdates', function() { + beforeEach(function() { + return this.RedisManager.expandDocUpdates( + this.jsonUpdates, + this.callback + ) + }) - describe("expandDocUpdates", function() { - beforeEach(function() { - return this.RedisManager.expandDocUpdates(this.jsonUpdates, this.callback); - }); + return it('should call the callback with the parsed ops', function() { + return this.callback + .calledWith(null, this.rawUpdates) + .should.equal(true) + }) + }) - return it("should call the callback with the parsed ops", function() { - return this.callback.calledWith(null, this.rawUpdates).should.equal(true); - }); - }); + return describe('deleteAppliedDocUpdates', function() { + beforeEach(function() { + this.rclient.lrem = sinon.stub() + this.rclient.srem = sinon.stub() + this.rclient.exec = sinon.stub().callsArgWith(0) + return this.RedisManager.deleteAppliedDocUpdates( + this.project_id, + this.doc_id, + this.jsonUpdates, + this.callback + ) + }) + it('should delete the first update from redis', function() { + return this.rclient.lrem + .calledWith( + `UncompressedHistoryOps:${this.doc_id}`, + 1, + this.jsonUpdates[0] + ) + .should.equal(true) + }) - return describe("deleteAppliedDocUpdates", function() { - beforeEach(function() { - this.rclient.lrem = sinon.stub(); - this.rclient.srem = sinon.stub(); - this.rclient.exec = sinon.stub().callsArgWith(0); - return this.RedisManager.deleteAppliedDocUpdates(this.project_id, this.doc_id, this.jsonUpdates, this.callback); - }); + it('should delete the second update from redis', function() { + return this.rclient.lrem + .calledWith( + `UncompressedHistoryOps:${this.doc_id}`, + 1, + this.jsonUpdates[1] + ) + .should.equal(true) + }) - it("should delete the first update from redis", function() { - return this.rclient.lrem - .calledWith(`UncompressedHistoryOps:${this.doc_id}`, 1, this.jsonUpdates[0]) - .should.equal(true); - }); + it('should delete the doc from the set of docs with history ops', function() { + return this.rclient.srem + .calledWith(`DocsWithHistoryOps:${this.project_id}`, this.doc_id) + .should.equal(true) + }) - it("should delete the second update from redis", function() { - return this.rclient.lrem - .calledWith(`UncompressedHistoryOps:${this.doc_id}`, 1, this.jsonUpdates[1]) - .should.equal(true); - }); + return it('should call the callback ', function() { + return this.callback.called.should.equal(true) + }) + }) + }) - it("should delete the doc from the set of docs with history ops", function() { - return this.rclient.srem - .calledWith(`DocsWithHistoryOps:${this.project_id}`, this.doc_id) - .should.equal(true); - }); + return describe('getDocIdsWithHistoryOps', function() { + beforeEach(function() { + this.doc_ids = ['mock-id-1', 'mock-id-2'] + this.rclient.smembers = sinon.stub().callsArgWith(1, null, this.doc_ids) + return this.RedisManager.getDocIdsWithHistoryOps( + this.project_id, + this.callback + ) + }) - return it("should call the callback ", function() { - return this.callback.called.should.equal(true); - }); - }); - }); + it('should read the doc_ids from redis', function() { + return this.rclient.smembers + .calledWith(`DocsWithHistoryOps:${this.project_id}`) + .should.equal(true) + }) - return describe("getDocIdsWithHistoryOps", function() { - beforeEach(function() { - this.doc_ids = ["mock-id-1", "mock-id-2"]; - this.rclient.smembers = sinon.stub().callsArgWith(1, null, this.doc_ids); - return this.RedisManager.getDocIdsWithHistoryOps(this.project_id, this.callback); - }); - - it("should read the doc_ids from redis", function() { - return this.rclient.smembers - .calledWith(`DocsWithHistoryOps:${this.project_id}`) - .should.equal(true); - }); - - return it("should call the callback with the doc_ids", function() { - return this.callback.calledWith(null, this.doc_ids).should.equal(true); - }); - }); -}); + return it('should call the callback with the doc_ids', function() { + return this.callback.calledWith(null, this.doc_ids).should.equal(true) + }) + }) +}) diff --git a/services/track-changes/test/unit/js/RestoreManager/RestoreManagerTests.js b/services/track-changes/test/unit/js/RestoreManager/RestoreManagerTests.js index dfa2c07f05..86d2f92f0c 100644 --- a/services/track-changes/test/unit/js/RestoreManager/RestoreManagerTests.js +++ b/services/track-changes/test/unit/js/RestoreManager/RestoreManagerTests.js @@ -9,51 +9,62 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const sinon = require('sinon'); -const chai = require('chai'); -const should = chai.should(); -const { expect } = chai; -const modulePath = "../../../../app/js/RestoreManager.js"; -const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/js/RestoreManager.js' +const SandboxedModule = require('sandboxed-module') -describe("RestoreManager", function() { - beforeEach(function() { - this.RestoreManager = SandboxedModule.require(modulePath, { requires: { - "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }), - "./DocumentUpdaterManager": (this.DocumentUpdaterManager = {}), - "./DiffManager": (this.DiffManager = {}) - } - }); - this.callback = sinon.stub(); - this.project_id = "mock-project-id"; - this.doc_id = "mock-doc-id"; - this.user_id = "mock-user-id"; - return this.version = 42; - }); +describe('RestoreManager', function() { + beforeEach(function() { + this.RestoreManager = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub() + }), + './DocumentUpdaterManager': (this.DocumentUpdaterManager = {}), + './DiffManager': (this.DiffManager = {}) + } + }) + this.callback = sinon.stub() + this.project_id = 'mock-project-id' + this.doc_id = 'mock-doc-id' + this.user_id = 'mock-user-id' + return (this.version = 42) + }) - return describe("restoreToBeforeVersion", function() { - beforeEach(function() { - this.content = "mock content"; - this.DocumentUpdaterManager.setDocument = sinon.stub().callsArg(4); - this.DiffManager.getDocumentBeforeVersion = sinon.stub().callsArgWith(3, null, this.content); - return this.RestoreManager.restoreToBeforeVersion(this.project_id, this.doc_id, this.version, this.user_id, this.callback); - }); + return describe('restoreToBeforeVersion', function() { + beforeEach(function() { + this.content = 'mock content' + this.DocumentUpdaterManager.setDocument = sinon.stub().callsArg(4) + this.DiffManager.getDocumentBeforeVersion = sinon + .stub() + .callsArgWith(3, null, this.content) + return this.RestoreManager.restoreToBeforeVersion( + this.project_id, + this.doc_id, + this.version, + this.user_id, + this.callback + ) + }) - it("should get the content before the requested version", function() { - return this.DiffManager.getDocumentBeforeVersion - .calledWith(this.project_id, this.doc_id, this.version) - .should.equal(true); - }); + it('should get the content before the requested version', function() { + return this.DiffManager.getDocumentBeforeVersion + .calledWith(this.project_id, this.doc_id, this.version) + .should.equal(true) + }) - it("should set the document in the document updater", function() { - return this.DocumentUpdaterManager.setDocument - .calledWith(this.project_id, this.doc_id, this.content, this.user_id) - .should.equal(true); - }); + it('should set the document in the document updater', function() { + return this.DocumentUpdaterManager.setDocument + .calledWith(this.project_id, this.doc_id, this.content, this.user_id) + .should.equal(true) + }) - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); -}); - \ No newline at end of file + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) +}) diff --git a/services/track-changes/test/unit/js/UpdateCompressor/UpdateCompressorTests.js b/services/track-changes/test/unit/js/UpdateCompressor/UpdateCompressorTests.js index 1a12ec78d2..9a88db6357 100644 --- a/services/track-changes/test/unit/js/UpdateCompressor/UpdateCompressorTests.js +++ b/services/track-changes/test/unit/js/UpdateCompressor/UpdateCompressorTests.js @@ -9,584 +9,842 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const sinon = require('sinon'); -const chai = require('chai'); -const should = chai.should(); -const { expect } = chai; -const modulePath = "../../../../app/js/UpdateCompressor.js"; -const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/js/UpdateCompressor.js' +const SandboxedModule = require('sandboxed-module') -const bigstring = (__range__(0, 2*1024*1024, true).map((i) => "a")).join(""); -const mediumstring = (__range__(0, 1024*1024, true).map((j) => "a")).join(""); +const bigstring = __range__(0, 2 * 1024 * 1024, true) + .map(i => 'a') + .join('') +const mediumstring = __range__(0, 1024 * 1024, true) + .map(j => 'a') + .join('') -describe("UpdateCompressor", function() { - beforeEach(function() { - this.UpdateCompressor = SandboxedModule.require(modulePath, { requires: { - "../lib/diff_match_patch": require("../../../../app/lib/diff_match_patch") - } - } - ); - this.user_id = "user-id-1"; - this.other_user_id = "user-id-2"; - this.ts1 = Date.now(); - return this.ts2 = Date.now() + 1000; - }); +describe('UpdateCompressor', function() { + beforeEach(function() { + this.UpdateCompressor = SandboxedModule.require(modulePath, { + requires: { + '../lib/diff_match_patch': require('../../../../app/lib/diff_match_patch') + } + }) + this.user_id = 'user-id-1' + this.other_user_id = 'user-id-2' + this.ts1 = Date.now() + return (this.ts2 = Date.now() + 1000) + }) - describe("convertToSingleOpUpdates", function() { - it("should split grouped updates into individual updates", function() { - return expect(this.UpdateCompressor.convertToSingleOpUpdates([{ - op: [ (this.op1 = { p: 0, i: "Foo" }), (this.op2 = { p: 6, i: "bar"}) ], - meta: { ts: this.ts1, user_id: this.user_id }, - v: 42 - }, { - op: [ (this.op3 = { p: 10, i: "baz" }) ], - meta: { ts: this.ts2, user_id: this.other_user_id }, - v: 43 - }])) - .to.deep.equal([{ - op: this.op1, - meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id }, - v: 42 - }, { - op: this.op2, - meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id }, - v: 42 - }, { - op: this.op3, - meta: { start_ts: this.ts2, end_ts: this.ts2, user_id: this.other_user_id }, - v: 43 - }]); - }); + describe('convertToSingleOpUpdates', function() { + it('should split grouped updates into individual updates', function() { + return expect( + this.UpdateCompressor.convertToSingleOpUpdates([ + { + op: [ + (this.op1 = { p: 0, i: 'Foo' }), + (this.op2 = { p: 6, i: 'bar' }) + ], + meta: { ts: this.ts1, user_id: this.user_id }, + v: 42 + }, + { + op: [(this.op3 = { p: 10, i: 'baz' })], + meta: { ts: this.ts2, user_id: this.other_user_id }, + v: 43 + } + ]) + ).to.deep.equal([ + { + op: this.op1, + meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id }, + v: 42 + }, + { + op: this.op2, + meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id }, + v: 42 + }, + { + op: this.op3, + meta: { + start_ts: this.ts2, + end_ts: this.ts2, + user_id: this.other_user_id + }, + v: 43 + } + ]) + }) - it("should return no-op updates when the op list is empty", function() { - return expect(this.UpdateCompressor.convertToSingleOpUpdates([{ - op: [], - meta: { ts: this.ts1, user_id: this.user_id }, - v: 42 - }])) - .to.deep.equal([{ - op: this.UpdateCompressor.NOOP, - meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id }, - v: 42 - }]); - }); + it('should return no-op updates when the op list is empty', function() { + return expect( + this.UpdateCompressor.convertToSingleOpUpdates([ + { + op: [], + meta: { ts: this.ts1, user_id: this.user_id }, + v: 42 + } + ]) + ).to.deep.equal([ + { + op: this.UpdateCompressor.NOOP, + meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id }, + v: 42 + } + ]) + }) - return it("should ignore comment ops", function() { - return expect(this.UpdateCompressor.convertToSingleOpUpdates([{ - op: [ (this.op1 = { p: 0, i: "Foo" }), (this.op2 = { p: 9, c: "baz"}), (this.op3 = { p: 6, i: "bar"}) ], - meta: { ts: this.ts1, user_id: this.user_id }, - v: 42 - }])) - .to.deep.equal([{ - op: this.op1, - meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id }, - v: 42 - }, { - op: this.op3, - meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id }, - v: 42 - }]); - }); -}); + return it('should ignore comment ops', function() { + return expect( + this.UpdateCompressor.convertToSingleOpUpdates([ + { + op: [ + (this.op1 = { p: 0, i: 'Foo' }), + (this.op2 = { p: 9, c: 'baz' }), + (this.op3 = { p: 6, i: 'bar' }) + ], + meta: { ts: this.ts1, user_id: this.user_id }, + v: 42 + } + ]) + ).to.deep.equal([ + { + op: this.op1, + meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id }, + v: 42 + }, + { + op: this.op3, + meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id }, + v: 42 + } + ]) + }) + }) - describe("concatUpdatesWithSameVersion", function() { - it("should concat updates with the same version", function() { - return expect(this.UpdateCompressor.concatUpdatesWithSameVersion([{ - op: (this.op1 = { p: 0, i: "Foo" }), - meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id }, - v: 42 - }, { - op: (this.op2 = { p: 6, i: "bar" }), - meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id }, - v: 42 - }, { - op: (this.op3 = { p: 10, i: "baz" }), - meta: { start_ts: this.ts2, end_ts: this.ts2, user_id: this.other_user_id }, - v: 43 - }])) - .to.deep.equal([{ - op: [ this.op1, this.op2 ], - meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id }, - v: 42 - }, { - op: [ this.op3 ], - meta: { start_ts: this.ts2, end_ts: this.ts2, user_id: this.other_user_id }, - v: 43 - }]); - }); + describe('concatUpdatesWithSameVersion', function() { + it('should concat updates with the same version', function() { + return expect( + this.UpdateCompressor.concatUpdatesWithSameVersion([ + { + op: (this.op1 = { p: 0, i: 'Foo' }), + meta: { + start_ts: this.ts1, + end_ts: this.ts1, + user_id: this.user_id + }, + v: 42 + }, + { + op: (this.op2 = { p: 6, i: 'bar' }), + meta: { + start_ts: this.ts1, + end_ts: this.ts1, + user_id: this.user_id + }, + v: 42 + }, + { + op: (this.op3 = { p: 10, i: 'baz' }), + meta: { + start_ts: this.ts2, + end_ts: this.ts2, + user_id: this.other_user_id + }, + v: 43 + } + ]) + ).to.deep.equal([ + { + op: [this.op1, this.op2], + meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id }, + v: 42 + }, + { + op: [this.op3], + meta: { + start_ts: this.ts2, + end_ts: this.ts2, + user_id: this.other_user_id + }, + v: 43 + } + ]) + }) - return it("should turn a noop into an empty op", function() { - return expect(this.UpdateCompressor.concatUpdatesWithSameVersion([{ - op: this.UpdateCompressor.NOOP, - meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id }, - v: 42 - }])) - .to.deep.equal([{ - op: [], - meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id }, - v: 42 - }]); - }); -}); + return it('should turn a noop into an empty op', function() { + return expect( + this.UpdateCompressor.concatUpdatesWithSameVersion([ + { + op: this.UpdateCompressor.NOOP, + meta: { + start_ts: this.ts1, + end_ts: this.ts1, + user_id: this.user_id + }, + v: 42 + } + ]) + ).to.deep.equal([ + { + op: [], + meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id }, + v: 42 + } + ]) + }) + }) - describe("compress", function() { - describe("insert - insert", function() { - it("should append one insert to the other", function() { - return expect(this.UpdateCompressor.compressUpdates([{ - op: { p: 3, i: "foo" }, - meta: { ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, { - op: { p: 6, i: "bar" }, - meta: { ts: this.ts2, user_id: this.user_id - }, - v: 43 - }])) - .to.deep.equal([{ - op: { p: 3, i: "foobar" }, - meta: { start_ts: this.ts1, end_ts: this.ts2, user_id: this.user_id - }, - v: 43 - }]); - }); + describe('compress', function() { + describe('insert - insert', function() { + it('should append one insert to the other', function() { + return expect( + this.UpdateCompressor.compressUpdates([ + { + op: { p: 3, i: 'foo' }, + meta: { ts: this.ts1, user_id: this.user_id }, + v: 42 + }, + { + op: { p: 6, i: 'bar' }, + meta: { ts: this.ts2, user_id: this.user_id }, + v: 43 + } + ]) + ).to.deep.equal([ + { + op: { p: 3, i: 'foobar' }, + meta: { + start_ts: this.ts1, + end_ts: this.ts2, + user_id: this.user_id + }, + v: 43 + } + ]) + }) - it("should insert one insert inside the other", function() { - return expect(this.UpdateCompressor.compressUpdates([{ - op: { p: 3, i: "foo" }, - meta: { ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, { - op: { p: 5, i: "bar" }, - meta: { ts: this.ts2, user_id: this.user_id - }, - v: 43 - }])) - .to.deep.equal([{ - op: { p: 3, i: "fobaro" }, - meta: { start_ts: this.ts1, end_ts: this.ts2, user_id: this.user_id - }, - v: 43 - }]); - }); + it('should insert one insert inside the other', function() { + return expect( + this.UpdateCompressor.compressUpdates([ + { + op: { p: 3, i: 'foo' }, + meta: { ts: this.ts1, user_id: this.user_id }, + v: 42 + }, + { + op: { p: 5, i: 'bar' }, + meta: { ts: this.ts2, user_id: this.user_id }, + v: 43 + } + ]) + ).to.deep.equal([ + { + op: { p: 3, i: 'fobaro' }, + meta: { + start_ts: this.ts1, + end_ts: this.ts2, + user_id: this.user_id + }, + v: 43 + } + ]) + }) - it("should not append separated inserts", function() { - return expect(this.UpdateCompressor.compressUpdates([{ - op: { p: 3, i: "foo" }, - meta: { ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, { - op: { p: 9, i: "bar" }, - meta: { ts: this.ts2, user_id: this.user_id - }, - v: 43 - }])) - .to.deep.equal([{ - op: { p: 3, i: "foo" }, - meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, { - op: { p: 9, i: "bar" }, - meta: { start_ts: this.ts2, end_ts: this.ts2, user_id: this.user_id - }, - v: 43 - }]); - }); + it('should not append separated inserts', function() { + return expect( + this.UpdateCompressor.compressUpdates([ + { + op: { p: 3, i: 'foo' }, + meta: { ts: this.ts1, user_id: this.user_id }, + v: 42 + }, + { + op: { p: 9, i: 'bar' }, + meta: { ts: this.ts2, user_id: this.user_id }, + v: 43 + } + ]) + ).to.deep.equal([ + { + op: { p: 3, i: 'foo' }, + meta: { + start_ts: this.ts1, + end_ts: this.ts1, + user_id: this.user_id + }, + v: 42 + }, + { + op: { p: 9, i: 'bar' }, + meta: { + start_ts: this.ts2, + end_ts: this.ts2, + user_id: this.user_id + }, + v: 43 + } + ]) + }) - it("should not append inserts that are too big (second op)", function() { - return expect(this.UpdateCompressor.compressUpdates([{ - op: { p: 3, i: "foo" }, - meta: { ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, { - op: { p: 6, i: bigstring }, - meta: { ts: this.ts2, user_id: this.user_id - }, - v: 43 - }])) - .to.deep.equal([{ - op: { p: 3, i: "foo" }, - meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, { - op: { p: 6, i: bigstring }, - meta: { start_ts: this.ts2, end_ts: this.ts2, user_id: this.user_id - }, - v: 43 - }]); - }); + it('should not append inserts that are too big (second op)', function() { + return expect( + this.UpdateCompressor.compressUpdates([ + { + op: { p: 3, i: 'foo' }, + meta: { ts: this.ts1, user_id: this.user_id }, + v: 42 + }, + { + op: { p: 6, i: bigstring }, + meta: { ts: this.ts2, user_id: this.user_id }, + v: 43 + } + ]) + ).to.deep.equal([ + { + op: { p: 3, i: 'foo' }, + meta: { + start_ts: this.ts1, + end_ts: this.ts1, + user_id: this.user_id + }, + v: 42 + }, + { + op: { p: 6, i: bigstring }, + meta: { + start_ts: this.ts2, + end_ts: this.ts2, + user_id: this.user_id + }, + v: 43 + } + ]) + }) - it("should not append inserts that are too big (first op)", function() { - return expect(this.UpdateCompressor.compressUpdates([{ - op: { p: 3, i: bigstring }, - meta: { ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, { - op: { p: 3 + bigstring.length, i: "bar" }, - meta: { ts: this.ts2, user_id: this.user_id - }, - v: 43 - }])) - .to.deep.equal([{ - op: { p: 3, i: bigstring }, - meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, { - op: { p: 3 + bigstring.length, i: "bar" }, - meta: { start_ts: this.ts2, end_ts: this.ts2, user_id: this.user_id - }, - v: 43 - }]); - }); + it('should not append inserts that are too big (first op)', function() { + return expect( + this.UpdateCompressor.compressUpdates([ + { + op: { p: 3, i: bigstring }, + meta: { ts: this.ts1, user_id: this.user_id }, + v: 42 + }, + { + op: { p: 3 + bigstring.length, i: 'bar' }, + meta: { ts: this.ts2, user_id: this.user_id }, + v: 43 + } + ]) + ).to.deep.equal([ + { + op: { p: 3, i: bigstring }, + meta: { + start_ts: this.ts1, + end_ts: this.ts1, + user_id: this.user_id + }, + v: 42 + }, + { + op: { p: 3 + bigstring.length, i: 'bar' }, + meta: { + start_ts: this.ts2, + end_ts: this.ts2, + user_id: this.user_id + }, + v: 43 + } + ]) + }) - return it("should not append inserts that are too big (first and second op)", function() { - return expect(this.UpdateCompressor.compressUpdates([{ - op: { p: 3, i: mediumstring }, - meta: { ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, { - op: { p: 3 + mediumstring.length, i: mediumstring }, - meta: { ts: this.ts2, user_id: this.user_id - }, - v: 43 - }])) - .to.deep.equal([{ - op: { p: 3, i: mediumstring }, - meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, { - op: { p: 3 + mediumstring.length, i: mediumstring }, - meta: { start_ts: this.ts2, end_ts: this.ts2, user_id: this.user_id - }, - v: 43 - }]); - }); - }); + return it('should not append inserts that are too big (first and second op)', function() { + return expect( + this.UpdateCompressor.compressUpdates([ + { + op: { p: 3, i: mediumstring }, + meta: { ts: this.ts1, user_id: this.user_id }, + v: 42 + }, + { + op: { p: 3 + mediumstring.length, i: mediumstring }, + meta: { ts: this.ts2, user_id: this.user_id }, + v: 43 + } + ]) + ).to.deep.equal([ + { + op: { p: 3, i: mediumstring }, + meta: { + start_ts: this.ts1, + end_ts: this.ts1, + user_id: this.user_id + }, + v: 42 + }, + { + op: { p: 3 + mediumstring.length, i: mediumstring }, + meta: { + start_ts: this.ts2, + end_ts: this.ts2, + user_id: this.user_id + }, + v: 43 + } + ]) + }) + }) + describe('delete - delete', function() { + it('should append one delete to the other', function() { + return expect( + this.UpdateCompressor.compressUpdates([ + { + op: { p: 3, d: 'foo' }, + meta: { ts: this.ts1, user_id: this.user_id }, + v: 42 + }, + { + op: { p: 3, d: 'bar' }, + meta: { ts: this.ts2, user_id: this.user_id }, + v: 43 + } + ]) + ).to.deep.equal([ + { + op: { p: 3, d: 'foobar' }, + meta: { + start_ts: this.ts1, + end_ts: this.ts2, + user_id: this.user_id + }, + v: 43 + } + ]) + }) - describe("delete - delete", function() { - it("should append one delete to the other", function() { - return expect(this.UpdateCompressor.compressUpdates([{ - op: { p: 3, d: "foo" }, - meta: { ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, { - op: { p: 3, d: "bar" }, - meta: { ts: this.ts2, user_id: this.user_id - }, - v: 43 - }])) - .to.deep.equal([{ - op: { p: 3, d: "foobar" }, - meta: { start_ts: this.ts1, end_ts: this.ts2, user_id: this.user_id - }, - v: 43 - }]); - }); - - it("should insert one delete inside the other", function() { - return expect(this.UpdateCompressor.compressUpdates([{ - op: { p: 3, d: "foo" }, - meta: { ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, { - op: { p: 1, d: "bar" }, - meta: { ts: this.ts2, user_id: this.user_id - }, - v: 43 - }])) - .to.deep.equal([{ - op: { p: 1, d: "bafoor" }, - meta: { start_ts: this.ts1, end_ts: this.ts2, user_id: this.user_id - }, - v: 43 - }]); - }); - - return it("should not append separated deletes", function() { - return expect(this.UpdateCompressor.compressUpdates([{ - op: { p: 3, d: "foo" }, - meta: { ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, { - op: { p: 9, d: "bar" }, - meta: { ts: this.ts2, user_id: this.user_id - }, - v: 43 - }])) - .to.deep.equal([{ - op: { p: 3, d: "foo" }, - meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, { - op: { p: 9, d: "bar" }, - meta: { start_ts: this.ts2, end_ts: this.ts2, user_id: this.user_id - }, - v: 43 - }]); - }); - }); + it('should insert one delete inside the other', function() { + return expect( + this.UpdateCompressor.compressUpdates([ + { + op: { p: 3, d: 'foo' }, + meta: { ts: this.ts1, user_id: this.user_id }, + v: 42 + }, + { + op: { p: 1, d: 'bar' }, + meta: { ts: this.ts2, user_id: this.user_id }, + v: 43 + } + ]) + ).to.deep.equal([ + { + op: { p: 1, d: 'bafoor' }, + meta: { + start_ts: this.ts1, + end_ts: this.ts2, + user_id: this.user_id + }, + v: 43 + } + ]) + }) - describe("insert - delete", function() { - it("should undo a previous insert", function() { - return expect(this.UpdateCompressor.compressUpdates([{ - op: { p: 3, i: "foo" }, - meta: { ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, { - op: { p: 5, d: "o" }, - meta: { ts: this.ts2, user_id: this.user_id - }, - v: 43 - }])) - .to.deep.equal([{ - op: { p: 3, i: "fo" }, - meta: { start_ts: this.ts1, end_ts: this.ts2, user_id: this.user_id - }, - v: 43 - }]); - }); - - it("should remove part of an insert from the middle", function() { - return expect(this.UpdateCompressor.compressUpdates([{ - op: { p: 3, i: "fobaro" }, - meta: { ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, { - op: { p: 5, d: "bar" }, - meta: { ts: this.ts2, user_id: this.user_id - }, - v: 43 - }])) - .to.deep.equal([{ - op: { p: 3, i: "foo" }, - meta: { start_ts: this.ts1, end_ts: this.ts2, user_id: this.user_id - }, - v: 43 - }]); - }); + return it('should not append separated deletes', function() { + return expect( + this.UpdateCompressor.compressUpdates([ + { + op: { p: 3, d: 'foo' }, + meta: { ts: this.ts1, user_id: this.user_id }, + v: 42 + }, + { + op: { p: 9, d: 'bar' }, + meta: { ts: this.ts2, user_id: this.user_id }, + v: 43 + } + ]) + ).to.deep.equal([ + { + op: { p: 3, d: 'foo' }, + meta: { + start_ts: this.ts1, + end_ts: this.ts1, + user_id: this.user_id + }, + v: 42 + }, + { + op: { p: 9, d: 'bar' }, + meta: { + start_ts: this.ts2, + end_ts: this.ts2, + user_id: this.user_id + }, + v: 43 + } + ]) + }) + }) - it("should cancel out two opposite updates", function() { - return expect(this.UpdateCompressor.compressUpdates([{ - op: { p: 3, i: "foo" }, - meta: { ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, { - op: { p: 3, d: "foo" }, - meta: { ts: this.ts2, user_id: this.user_id - }, - v: 43 - }])) - .to.deep.equal([{ - op: { p: 3, i: "" }, - meta: { start_ts: this.ts1, end_ts: this.ts2, user_id: this.user_id - }, - v: 43 - } - ]); - }); - - it("should not combine separated updates", function() { - return expect(this.UpdateCompressor.compressUpdates([{ - op: { p: 3, i: "foo" }, - meta: { ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, { - op: { p: 9, d: "bar" }, - meta: { ts: this.ts2, user_id: this.user_id - }, - v: 43 - }])) - .to.deep.equal([{ - op: { p: 3, i: "foo" }, - meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, { - op: { p: 9, d: "bar" }, - meta: { start_ts: this.ts2, end_ts: this.ts2, user_id: this.user_id - }, - v: 43 - }]); - }); + describe('insert - delete', function() { + it('should undo a previous insert', function() { + return expect( + this.UpdateCompressor.compressUpdates([ + { + op: { p: 3, i: 'foo' }, + meta: { ts: this.ts1, user_id: this.user_id }, + v: 42 + }, + { + op: { p: 5, d: 'o' }, + meta: { ts: this.ts2, user_id: this.user_id }, + v: 43 + } + ]) + ).to.deep.equal([ + { + op: { p: 3, i: 'fo' }, + meta: { + start_ts: this.ts1, + end_ts: this.ts2, + user_id: this.user_id + }, + v: 43 + } + ]) + }) - return it("should not combine updates with overlap beyond the end", function() { - return expect(this.UpdateCompressor.compressUpdates([{ - op: { p: 3, i: "foobar" }, - meta: { ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, { - op: { p: 6, d: "bardle" }, - meta: { ts: this.ts2, user_id: this.user_id - }, - v: 43 - }])) - .to.deep.equal([{ - op: { p: 3, i: "foobar" }, - meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, { - op: { p: 6, d: "bardle" }, - meta: { start_ts: this.ts2, end_ts: this.ts2, user_id: this.user_id - }, - v: 43 - }]); - }); - }); - - describe("delete - insert", function() { - it("should do a diff of the content", function() { - return expect(this.UpdateCompressor.compressUpdates([{ - op: { p: 3, d: "one two three four five six seven eight" }, - meta: { ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, { - op: { p: 3, i: "one 2 three four five six seven eight" }, - meta: { ts: this.ts2, user_id: this.user_id - }, - v: 43 - }])) - .to.deep.equal([{ - op: { p: 7, d: "two" }, - meta: { start_ts: this.ts1, end_ts: this.ts2, user_id: this.user_id - }, - v: 43 - }, { - op: { p: 7, i: "2" }, - meta: { start_ts: this.ts1, end_ts: this.ts2, user_id: this.user_id - }, - v: 43 - }]); - }); - - return it("should return a no-op if the delete and insert are the same", function() { - return expect(this.UpdateCompressor.compressUpdates([{ - op: { p: 3, d: "one two three four five six seven eight" }, - meta: { ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, { - op: { p: 3, i: "one two three four five six seven eight" }, - meta: { ts: this.ts2, user_id: this.user_id - }, - v: 43 - }])) - .to.deep.equal([{ - op: { p: 3, i: "" }, - meta: { start_ts: this.ts1, end_ts: this.ts2, user_id: this.user_id - }, - v: 43 - }]); - }); - }); + it('should remove part of an insert from the middle', function() { + return expect( + this.UpdateCompressor.compressUpdates([ + { + op: { p: 3, i: 'fobaro' }, + meta: { ts: this.ts1, user_id: this.user_id }, + v: 42 + }, + { + op: { p: 5, d: 'bar' }, + meta: { ts: this.ts2, user_id: this.user_id }, + v: 43 + } + ]) + ).to.deep.equal([ + { + op: { p: 3, i: 'foo' }, + meta: { + start_ts: this.ts1, + end_ts: this.ts2, + user_id: this.user_id + }, + v: 43 + } + ]) + }) - describe("noop - insert", function() { return it("should leave them untouched", function() { - return expect(this.UpdateCompressor.compressUpdates([{ - op: this.UpdateCompressor.NOOP, - meta: { ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, { - op: { p: 6, i: "bar" }, - meta: { ts: this.ts1, user_id: this.user_id - }, - v: 43 - }])) - .to.deep.equal([{ - op: this.UpdateCompressor.NOOP, - meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, { - op: { p: 6, i: "bar" }, - meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id - }, - v: 43 - }]); - }); } - ); + it('should cancel out two opposite updates', function() { + return expect( + this.UpdateCompressor.compressUpdates([ + { + op: { p: 3, i: 'foo' }, + meta: { ts: this.ts1, user_id: this.user_id }, + v: 42 + }, + { + op: { p: 3, d: 'foo' }, + meta: { ts: this.ts2, user_id: this.user_id }, + v: 43 + } + ]) + ).to.deep.equal([ + { + op: { p: 3, i: '' }, + meta: { + start_ts: this.ts1, + end_ts: this.ts2, + user_id: this.user_id + }, + v: 43 + } + ]) + }) - return describe("noop - delete", function() { return it("should leave them untouched", function() { - return expect(this.UpdateCompressor.compressUpdates([{ - op: this.UpdateCompressor.NOOP, - meta: { ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, { - op: { p: 6, d: "bar" }, - meta: { ts: this.ts1, user_id: this.user_id - }, - v: 43 - }])) - .to.deep.equal([{ - op: this.UpdateCompressor.NOOP, - meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, { - op: { p: 6, d: "bar" }, - meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id - }, - v: 43 - }]); - }); } - ); -}); + it('should not combine separated updates', function() { + return expect( + this.UpdateCompressor.compressUpdates([ + { + op: { p: 3, i: 'foo' }, + meta: { ts: this.ts1, user_id: this.user_id }, + v: 42 + }, + { + op: { p: 9, d: 'bar' }, + meta: { ts: this.ts2, user_id: this.user_id }, + v: 43 + } + ]) + ).to.deep.equal([ + { + op: { p: 3, i: 'foo' }, + meta: { + start_ts: this.ts1, + end_ts: this.ts1, + user_id: this.user_id + }, + v: 42 + }, + { + op: { p: 9, d: 'bar' }, + meta: { + start_ts: this.ts2, + end_ts: this.ts2, + user_id: this.user_id + }, + v: 43 + } + ]) + }) - return describe("compressRawUpdates", function() { return describe("merging in-place with an array op", function() { return it("should not change the existing last updates", function() { - return expect(this.UpdateCompressor.compressRawUpdates({ - op: [ {"p":1000,"d":"hello"}, {"p":1000,"i":"HELLO()"} ], - meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id - }, - v: 42 - }, [{ - op: [{ p: 1006, i: "WORLD" }], - meta: { ts: this.ts2, user_id: this.user_id - }, - v: 43 - }])) - .to.deep.equal([{ - op: [{"p":1000,"d":"hello"}, {"p":1000,"i":"HELLO()"} ], - meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id - }, - v: 42 - },{ - op: [{"p":1006,"i":"WORLD"}], - meta: { start_ts: this.ts2, end_ts: this.ts2, user_id: this.user_id - }, - v: 43 - }]); - }); } - ); } -); -}); + return it('should not combine updates with overlap beyond the end', function() { + return expect( + this.UpdateCompressor.compressUpdates([ + { + op: { p: 3, i: 'foobar' }, + meta: { ts: this.ts1, user_id: this.user_id }, + v: 42 + }, + { + op: { p: 6, d: 'bardle' }, + meta: { ts: this.ts2, user_id: this.user_id }, + v: 43 + } + ]) + ).to.deep.equal([ + { + op: { p: 3, i: 'foobar' }, + meta: { + start_ts: this.ts1, + end_ts: this.ts1, + user_id: this.user_id + }, + v: 42 + }, + { + op: { p: 6, d: 'bardle' }, + meta: { + start_ts: this.ts2, + end_ts: this.ts2, + user_id: this.user_id + }, + v: 43 + } + ]) + }) + }) + + describe('delete - insert', function() { + it('should do a diff of the content', function() { + return expect( + this.UpdateCompressor.compressUpdates([ + { + op: { p: 3, d: 'one two three four five six seven eight' }, + meta: { ts: this.ts1, user_id: this.user_id }, + v: 42 + }, + { + op: { p: 3, i: 'one 2 three four five six seven eight' }, + meta: { ts: this.ts2, user_id: this.user_id }, + v: 43 + } + ]) + ).to.deep.equal([ + { + op: { p: 7, d: 'two' }, + meta: { + start_ts: this.ts1, + end_ts: this.ts2, + user_id: this.user_id + }, + v: 43 + }, + { + op: { p: 7, i: '2' }, + meta: { + start_ts: this.ts1, + end_ts: this.ts2, + user_id: this.user_id + }, + v: 43 + } + ]) + }) + + return it('should return a no-op if the delete and insert are the same', function() { + return expect( + this.UpdateCompressor.compressUpdates([ + { + op: { p: 3, d: 'one two three four five six seven eight' }, + meta: { ts: this.ts1, user_id: this.user_id }, + v: 42 + }, + { + op: { p: 3, i: 'one two three four five six seven eight' }, + meta: { ts: this.ts2, user_id: this.user_id }, + v: 43 + } + ]) + ).to.deep.equal([ + { + op: { p: 3, i: '' }, + meta: { + start_ts: this.ts1, + end_ts: this.ts2, + user_id: this.user_id + }, + v: 43 + } + ]) + }) + }) + + describe('noop - insert', function() { + return it('should leave them untouched', function() { + return expect( + this.UpdateCompressor.compressUpdates([ + { + op: this.UpdateCompressor.NOOP, + meta: { ts: this.ts1, user_id: this.user_id }, + v: 42 + }, + { + op: { p: 6, i: 'bar' }, + meta: { ts: this.ts1, user_id: this.user_id }, + v: 43 + } + ]) + ).to.deep.equal([ + { + op: this.UpdateCompressor.NOOP, + meta: { + start_ts: this.ts1, + end_ts: this.ts1, + user_id: this.user_id + }, + v: 42 + }, + { + op: { p: 6, i: 'bar' }, + meta: { + start_ts: this.ts1, + end_ts: this.ts1, + user_id: this.user_id + }, + v: 43 + } + ]) + }) + }) + + return describe('noop - delete', function() { + return it('should leave them untouched', function() { + return expect( + this.UpdateCompressor.compressUpdates([ + { + op: this.UpdateCompressor.NOOP, + meta: { ts: this.ts1, user_id: this.user_id }, + v: 42 + }, + { + op: { p: 6, d: 'bar' }, + meta: { ts: this.ts1, user_id: this.user_id }, + v: 43 + } + ]) + ).to.deep.equal([ + { + op: this.UpdateCompressor.NOOP, + meta: { + start_ts: this.ts1, + end_ts: this.ts1, + user_id: this.user_id + }, + v: 42 + }, + { + op: { p: 6, d: 'bar' }, + meta: { + start_ts: this.ts1, + end_ts: this.ts1, + user_id: this.user_id + }, + v: 43 + } + ]) + }) + }) + }) + + return describe('compressRawUpdates', function() { + return describe('merging in-place with an array op', function() { + return it('should not change the existing last updates', function() { + return expect( + this.UpdateCompressor.compressRawUpdates( + { + op: [ + { p: 1000, d: 'hello' }, + { p: 1000, i: 'HELLO()' } + ], + meta: { + start_ts: this.ts1, + end_ts: this.ts1, + user_id: this.user_id + }, + v: 42 + }, + [ + { + op: [{ p: 1006, i: 'WORLD' }], + meta: { ts: this.ts2, user_id: this.user_id }, + v: 43 + } + ] + ) + ).to.deep.equal([ + { + op: [ + { p: 1000, d: 'hello' }, + { p: 1000, i: 'HELLO()' } + ], + meta: { + start_ts: this.ts1, + end_ts: this.ts1, + user_id: this.user_id + }, + v: 42 + }, + { + op: [{ p: 1006, i: 'WORLD' }], + meta: { + start_ts: this.ts2, + end_ts: this.ts2, + user_id: this.user_id + }, + v: 43 + } + ]) + }) + }) + }) +}) function __range__(left, right, inclusive) { - const range = []; - const ascending = left < right; - const end = !inclusive ? right : ascending ? right + 1 : right - 1; + const range = [] + const ascending = left < right + const end = !inclusive ? right : ascending ? right + 1 : right - 1 for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) { - range.push(i); + range.push(i) } - return range; -} \ No newline at end of file + return range +} diff --git a/services/track-changes/test/unit/js/UpdateTrimmer/UpdateTrimmerTests.js b/services/track-changes/test/unit/js/UpdateTrimmer/UpdateTrimmerTests.js index 83d1632ada..1a4e0f7861 100644 --- a/services/track-changes/test/unit/js/UpdateTrimmer/UpdateTrimmerTests.js +++ b/services/track-changes/test/unit/js/UpdateTrimmer/UpdateTrimmerTests.js @@ -9,157 +9,180 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const sinon = require('sinon'); -const chai = require('chai'); -const should = chai.should(); -const { expect } = chai; -const modulePath = "../../../../app/js/UpdateTrimmer.js"; -const SandboxedModule = require('sandboxed-module'); -const tk = require("timekeeper"); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/js/UpdateTrimmer.js' +const SandboxedModule = require('sandboxed-module') +const tk = require('timekeeper') -describe("UpdateTrimmer", function() { - beforeEach(function() { - this.now = new Date(); - tk.freeze(this.now); +describe('UpdateTrimmer', function() { + beforeEach(function() { + this.now = new Date() + tk.freeze(this.now) - this.UpdateTrimmer = SandboxedModule.require(modulePath, { requires: { - "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }), - "./WebApiManager": (this.WebApiManager = {}), - "./MongoManager": (this.MongoManager = {}) - } - }); + this.UpdateTrimmer = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub() + }), + './WebApiManager': (this.WebApiManager = {}), + './MongoManager': (this.MongoManager = {}) + } + }) - this.callback = sinon.stub(); - return this.project_id = "mock-project-id"; - }); + this.callback = sinon.stub() + return (this.project_id = 'mock-project-id') + }) - afterEach(function() { return tk.reset(); }); + afterEach(function() { + return tk.reset() + }) - return describe("shouldTrimUpdates", function() { - beforeEach(function() { - this.metadata = {}; - this.details = - {features: {}}; - this.MongoManager.getProjectMetaData = sinon.stub().callsArgWith(1, null, this.metadata); - this.MongoManager.setProjectMetaData = sinon.stub().callsArgWith(2); - this.MongoManager.upgradeHistory = sinon.stub().callsArgWith(1); - return this.WebApiManager.getProjectDetails = sinon.stub().callsArgWith(1, null, this.details); - }); + return describe('shouldTrimUpdates', function() { + beforeEach(function() { + this.metadata = {} + this.details = { features: {} } + this.MongoManager.getProjectMetaData = sinon + .stub() + .callsArgWith(1, null, this.metadata) + this.MongoManager.setProjectMetaData = sinon.stub().callsArgWith(2) + this.MongoManager.upgradeHistory = sinon.stub().callsArgWith(1) + return (this.WebApiManager.getProjectDetails = sinon + .stub() + .callsArgWith(1, null, this.details)) + }) - describe("with preserveHistory set in the project meta data", function() { - beforeEach(function() { - this.metadata.preserveHistory = true; - return this.UpdateTrimmer.shouldTrimUpdates(this.project_id, this.callback); - }); + describe('with preserveHistory set in the project meta data', function() { + beforeEach(function() { + this.metadata.preserveHistory = true + return this.UpdateTrimmer.shouldTrimUpdates( + this.project_id, + this.callback + ) + }) - it("should look up the meta data", function() { - return this.MongoManager.getProjectMetaData - .calledWith(this.project_id) - .should.equal(true); - }); + it('should look up the meta data', function() { + return this.MongoManager.getProjectMetaData + .calledWith(this.project_id) + .should.equal(true) + }) - it("should not look up the project details", function() { - return this.WebApiManager.getProjectDetails - .called - .should.equal(false); - }); + it('should not look up the project details', function() { + return this.WebApiManager.getProjectDetails.called.should.equal(false) + }) - return it("should return false", function() { - return this.callback.calledWith(null, false).should.equal(true); - }); - }); + return it('should return false', function() { + return this.callback.calledWith(null, false).should.equal(true) + }) + }) - describe("without preserveHistory set in the project meta data", function() { - beforeEach(function() { - return this.metadata.preserveHistory = false; - }); + describe('without preserveHistory set in the project meta data', function() { + beforeEach(function() { + return (this.metadata.preserveHistory = false) + }) - describe("when the project has the versioning feature", function() { - beforeEach(function() { - this.details.features.versioning = true; - return this.UpdateTrimmer.shouldTrimUpdates(this.project_id, this.callback); - }); + describe('when the project has the versioning feature', function() { + beforeEach(function() { + this.details.features.versioning = true + return this.UpdateTrimmer.shouldTrimUpdates( + this.project_id, + this.callback + ) + }) - it("should look up the meta data", function() { - return this.MongoManager.getProjectMetaData - .calledWith(this.project_id) - .should.equal(true); - }); + it('should look up the meta data', function() { + return this.MongoManager.getProjectMetaData + .calledWith(this.project_id) + .should.equal(true) + }) - it("should look up the project details", function() { - return this.WebApiManager.getProjectDetails - .calledWith(this.project_id) - .should.equal(true); - }); + it('should look up the project details', function() { + return this.WebApiManager.getProjectDetails + .calledWith(this.project_id) + .should.equal(true) + }) - it("should insert preserveHistory into the metadata", function() { - return this.MongoManager.setProjectMetaData - .calledWith(this.project_id, {preserveHistory: true}) - .should.equal(true); - }); + it('should insert preserveHistory into the metadata', function() { + return this.MongoManager.setProjectMetaData + .calledWith(this.project_id, { preserveHistory: true }) + .should.equal(true) + }) - it("should upgrade any existing history", function() { - return this.MongoManager.upgradeHistory - .calledWith(this.project_id) - .should.equal(true); - }); + it('should upgrade any existing history', function() { + return this.MongoManager.upgradeHistory + .calledWith(this.project_id) + .should.equal(true) + }) - return it("should return false", function() { - return this.callback.calledWith(null, false).should.equal(true); - }); - }); + return it('should return false', function() { + return this.callback.calledWith(null, false).should.equal(true) + }) + }) - return describe("when the project does not have the versioning feature", function() { - beforeEach(function() { - this.details.features.versioning = false; - return this.UpdateTrimmer.shouldTrimUpdates(this.project_id, this.callback); - }); + return describe('when the project does not have the versioning feature', function() { + beforeEach(function() { + this.details.features.versioning = false + return this.UpdateTrimmer.shouldTrimUpdates( + this.project_id, + this.callback + ) + }) - return it("should return true", function() { - return this.callback.calledWith(null, true).should.equal(true); - }); - }); - }); + return it('should return true', function() { + return this.callback.calledWith(null, true).should.equal(true) + }) + }) + }) - return describe("without any meta data", function() { - beforeEach(function() { - return this.MongoManager.getProjectMetaData = sinon.stub().callsArgWith(1, null, null); - }); + return describe('without any meta data', function() { + beforeEach(function() { + return (this.MongoManager.getProjectMetaData = sinon + .stub() + .callsArgWith(1, null, null)) + }) - describe("when the project has the versioning feature", function() { - beforeEach(function() { - this.details.features.versioning = true; - return this.UpdateTrimmer.shouldTrimUpdates(this.project_id, this.callback); - }); + describe('when the project has the versioning feature', function() { + beforeEach(function() { + this.details.features.versioning = true + return this.UpdateTrimmer.shouldTrimUpdates( + this.project_id, + this.callback + ) + }) - it("should insert preserveHistory into the metadata", function() { - return this.MongoManager.setProjectMetaData - .calledWith(this.project_id, {preserveHistory: true}) - .should.equal(true); - }); + it('should insert preserveHistory into the metadata', function() { + return this.MongoManager.setProjectMetaData + .calledWith(this.project_id, { preserveHistory: true }) + .should.equal(true) + }) - it("should upgrade any existing history", function() { - return this.MongoManager.upgradeHistory - .calledWith(this.project_id) - .should.equal(true); - }); + it('should upgrade any existing history', function() { + return this.MongoManager.upgradeHistory + .calledWith(this.project_id) + .should.equal(true) + }) - return it("should return false", function() { - return this.callback.calledWith(null, false).should.equal(true); - }); - }); + return it('should return false', function() { + return this.callback.calledWith(null, false).should.equal(true) + }) + }) - return describe("when the project does not have the versioning feature", function() { - beforeEach(function() { - this.details.features.versioning = false; - return this.UpdateTrimmer.shouldTrimUpdates(this.project_id, this.callback); - }); - - return it("should return true", function() { - return this.callback.calledWith(null, true).should.equal(true); - }); - }); - }); - }); -}); + return describe('when the project does not have the versioning feature', function() { + beforeEach(function() { + this.details.features.versioning = false + return this.UpdateTrimmer.shouldTrimUpdates( + this.project_id, + this.callback + ) + }) + return it('should return true', function() { + return this.callback.calledWith(null, true).should.equal(true) + }) + }) + }) + }) +}) diff --git a/services/track-changes/test/unit/js/UpdatesManager/UpdatesManagerTests.js b/services/track-changes/test/unit/js/UpdatesManager/UpdatesManagerTests.js index 2ac748c8f6..2891d1d4ee 100644 --- a/services/track-changes/test/unit/js/UpdatesManager/UpdatesManagerTests.js +++ b/services/track-changes/test/unit/js/UpdatesManager/UpdatesManagerTests.js @@ -13,1009 +13,1323 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const sinon = require('sinon'); -const chai = require('chai'); -const should = chai.should(); -const { expect } = chai; -const modulePath = "../../../../app/js/UpdatesManager.js"; -const SandboxedModule = require('sandboxed-module'); - -describe("UpdatesManager", function() { - beforeEach(function() { - this.UpdatesManager = SandboxedModule.require(modulePath, { requires: { - "./UpdateCompressor": (this.UpdateCompressor = {}), - "./MongoManager" : (this.MongoManager = {}), - "./PackManager" : (this.PackManager = {}), - "./RedisManager" : (this.RedisManager = {}), - "./LockManager" : (this.LockManager = {}), - "./WebApiManager": (this.WebApiManager = {}), - "./UpdateTrimmer": (this.UpdateTrimmer = {}), - "./DocArchiveManager": (this.DocArchiveManager = {}), - "logger-sharelatex": { log: sinon.stub(), error: sinon.stub() }, - "settings-sharelatex": { - redis: { lock: { key_schema: { - historyLock({doc_id}) { return `HistoryLock:${doc_id}`; } - } - } - } - } - } - } - ); - this.doc_id = "doc-id-123"; - this.project_id = "project-id-123"; - this.callback = sinon.stub(); - return this.temporary = "temp-mock"; - }); - - describe("compressAndSaveRawUpdates", function() { - describe("when there are no raw ops", function() { - beforeEach(function() { - this.MongoManager.peekLastCompressedUpdate = sinon.stub(); - return this.UpdatesManager.compressAndSaveRawUpdates(this.project_id, this.doc_id, [], this.temporary, this.callback); - }); - - it("should not need to access the database", function() { - return this.MongoManager.peekLastCompressedUpdate.called.should.equal(false); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - - describe("when there is no compressed history to begin with", function() { - beforeEach(function() { - this.rawUpdates = [{ v: 12, op: "mock-op-12" }, { v: 13, op: "mock-op-13" }]; - this.compressedUpdates = [ { v: 13, op: "compressed-op-12" } ]; - - this.MongoManager.peekLastCompressedUpdate = sinon.stub().callsArgWith(1, null, null); - this.PackManager.insertCompressedUpdates = sinon.stub().callsArg(5); - this.UpdateCompressor.compressRawUpdates = sinon.stub().returns(this.compressedUpdates); - return this.UpdatesManager.compressAndSaveRawUpdates(this.project_id, this.doc_id, this.rawUpdates, this.temporary, this.callback); - }); - - it("should look at the last compressed op", function() { - return this.MongoManager.peekLastCompressedUpdate - .calledWith(this.doc_id) - .should.equal(true); - }); - - it("should save the compressed ops as a pack", function() { - return this.PackManager.insertCompressedUpdates - .calledWith(this.project_id, this.doc_id, null, this.compressedUpdates, this.temporary) - .should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - - describe("when the raw ops need appending to existing history", function() { - beforeEach(function() { - this.lastCompressedUpdate = { v: 11, op: "compressed-op-11" }; - this.compressedUpdates = [ { v: 12, op: "compressed-op-11+12" }, { v: 13, op: "compressed-op-12" } ]; - - this.MongoManager.peekLastCompressedUpdate = sinon.stub().callsArgWith(1, null, this.lastCompressedUpdate, this.lastCompressedUpdate.v); - this.PackManager.insertCompressedUpdates = sinon.stub().callsArg(5); - return this.UpdateCompressor.compressRawUpdates = sinon.stub().returns(this.compressedUpdates); - }); - - describe("when the raw ops start where the existing history ends", function() { - beforeEach(function() { - this.rawUpdates = [{ v: 12, op: "mock-op-12" }, { v: 13, op: "mock-op-13" }]; - return this.UpdatesManager.compressAndSaveRawUpdates(this.project_id, this.doc_id, this.rawUpdates, this.temporary, this.callback); - }); - - it("should look at the last compressed op", function() { - return this.MongoManager.peekLastCompressedUpdate - .calledWith(this.doc_id) - .should.equal(true); - }); - - it("should compress the raw ops", function() { - return this.UpdateCompressor.compressRawUpdates - .calledWith(null, this.rawUpdates) - .should.equal(true); - }); - - it("should save the new compressed ops into a pack", function() { - return this.PackManager.insertCompressedUpdates - .calledWith(this.project_id, this.doc_id, this.lastCompressedUpdate, this.compressedUpdates, this.temporary) - .should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - - describe("when the raw ops start where the existing history ends and the history is in a pack", function() { - beforeEach(function() { - this.lastCompressedUpdate = {pack: [{ v: 11, op: "compressed-op-11" }], v:11}; - this.rawUpdates = [{ v: 12, op: "mock-op-12" }, { v: 13, op: "mock-op-13" }]; - this.MongoManager.peekLastCompressedUpdate = sinon.stub().callsArgWith(1, null, this.lastCompressedUpdate, this.lastCompressedUpdate.v); - return this.UpdatesManager.compressAndSaveRawUpdates(this.project_id, this.doc_id, this.rawUpdates, this.temporary, this.callback); - }); - - it("should look at the last compressed op", function() { - return this.MongoManager.peekLastCompressedUpdate - .calledWith(this.doc_id) - .should.equal(true); - }); - - it("should compress the raw ops", function() { - return this.UpdateCompressor.compressRawUpdates - .calledWith(null, this.rawUpdates) - .should.equal(true); - }); - - it("should save the new compressed ops into a pack", function() { - return this.PackManager.insertCompressedUpdates - .calledWith(this.project_id, this.doc_id, this.lastCompressedUpdate, this.compressedUpdates, this.temporary) - .should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - - describe("when some raw ops are passed that have already been compressed", function() { - beforeEach(function() { - this.rawUpdates = [{ v: 10, op: "mock-op-10" }, { v: 11, op: "mock-op-11"}, { v: 12, op: "mock-op-12" }, { v: 13, op: "mock-op-13" }]; - - return this.UpdatesManager.compressAndSaveRawUpdates(this.project_id, this.doc_id, this.rawUpdates, this.temporary, this.callback); - }); - - return it("should only compress the more recent raw ops", function() { - return this.UpdateCompressor.compressRawUpdates - .calledWith(null, this.rawUpdates.slice(-2)) - .should.equal(true); - }); - }); - - describe("when the raw ops do not follow from the last compressed op version", function() { - beforeEach(function() { - this.rawUpdates = [{ v: 13, op: "mock-op-13" }]; - return this.UpdatesManager.compressAndSaveRawUpdates(this.project_id, this.doc_id, this.rawUpdates, this.temporary, this.callback); - }); - - it("should call the callback with an error", function() { - return this.callback - .calledWith(sinon.match.has('message', "Tried to apply raw op at version 13 to last compressed update with version 11 from unknown time")) - .should.equal(true); - }); - - return it("should not insert any update into mongo", function() { - return this.PackManager.insertCompressedUpdates.called.should.equal(false); - }); - }); - - return describe("when the raw ops are out of order", function() { - beforeEach(function() { - this.rawUpdates = [{ v: 13, op: "mock-op-13" }, { v: 12, op: "mock-op-12" }]; - return this.UpdatesManager.compressAndSaveRawUpdates(this.project_id, this.doc_id, this.rawUpdates, this.temporary, this.callback); - }); - - it("should call the callback with an error", function() { - return this.callback - .calledWith(sinon.match.has('message')) - .should.equal(true); - }); - - return it("should not insert any update into mongo", function() { - return this.PackManager.insertCompressedUpdates.called.should.equal(false); - }); - }); - }); - - - return describe("when the raw ops need appending to existing history which is in S3", function() { - beforeEach(function() { - this.lastCompressedUpdate = null; - this.lastVersion = 11; - this.compressedUpdates = [ { v: 13, op: "compressed-op-12" } ]; - - this.MongoManager.peekLastCompressedUpdate = sinon.stub().callsArgWith(1, null, null, this.lastVersion); - this.PackManager.insertCompressedUpdates = sinon.stub().callsArg(5); - return this.UpdateCompressor.compressRawUpdates = sinon.stub().returns(this.compressedUpdates); - }); - - return describe("when the raw ops start where the existing history ends", function() { - beforeEach(function() { - this.rawUpdates = [{ v: 12, op: "mock-op-12" }, { v: 13, op: "mock-op-13" }]; - return this.UpdatesManager.compressAndSaveRawUpdates(this.project_id, this.doc_id, this.rawUpdates, this.temporary, this.callback); - }); - - it("should try to look at the last compressed op", function() { - return this.MongoManager.peekLastCompressedUpdate - .calledWith(this.doc_id) - .should.equal(true); - }); - - it("should compress the last compressed op and the raw ops", function() { - return this.UpdateCompressor.compressRawUpdates - .calledWith(this.lastCompressedUpdate, this.rawUpdates) - .should.equal(true); - }); - - it("should save the compressed ops", function() { - return this.PackManager.insertCompressedUpdates - .calledWith(this.project_id, this.doc_id, null, this.compressedUpdates, this.temporary) - .should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - }); - }); - - describe("processUncompressedUpdates", function() { - beforeEach(function() { - this.UpdatesManager.compressAndSaveRawUpdates = sinon.stub().callsArgWith(4); - this.RedisManager.deleteAppliedDocUpdates = sinon.stub().callsArg(3); - this.MongoManager.backportProjectId = sinon.stub().callsArg(2); - return this.UpdateTrimmer.shouldTrimUpdates = sinon.stub().callsArgWith(1, null, (this.temporary = "temp mock")); - }); - - describe("when there is fewer than one batch to send", function() { - beforeEach(function() { - this.updates = ["mock-update"]; - this.RedisManager.getOldestDocUpdates = sinon.stub().callsArgWith(2, null, this.updates); - this.RedisManager.expandDocUpdates = sinon.stub().callsArgWith(1, null, this.updates); - return this.UpdatesManager.processUncompressedUpdates(this.project_id, this.doc_id, this.temporary, this.callback); - }); - - it("should get the oldest updates", function() { - return this.RedisManager.getOldestDocUpdates - .calledWith(this.doc_id, this.UpdatesManager.REDIS_READ_BATCH_SIZE) - .should.equal(true); - }); - - it("should compress and save the updates", function() { - return this.UpdatesManager.compressAndSaveRawUpdates - .calledWith(this.project_id, this.doc_id, this.updates, this.temporary) - .should.equal(true); - }); - - it("should delete the batch of uncompressed updates that was just processed", function() { - return this.RedisManager.deleteAppliedDocUpdates - .calledWith(this.project_id, this.doc_id, this.updates) - .should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - - return describe("when there are multiple batches to send", function() { - beforeEach(function(done) { - this.UpdatesManager.REDIS_READ_BATCH_SIZE = 2; - this.updates = ["mock-update-0", "mock-update-1", "mock-update-2", "mock-update-3", "mock-update-4"]; - this.redisArray = this.updates.slice(); - this.RedisManager.getOldestDocUpdates = (doc_id, batchSize, callback) => { - if (callback == null) { callback = function(error, updates) {}; } - const updates = this.redisArray.slice(0, batchSize); - this.redisArray = this.redisArray.slice(batchSize); - return callback(null, updates); - }; - sinon.spy(this.RedisManager, "getOldestDocUpdates"); - this.RedisManager.expandDocUpdates = (jsonUpdates, callback) => { - return callback(null, jsonUpdates); - }; - sinon.spy(this.RedisManager, "expandDocUpdates"); - return this.UpdatesManager.processUncompressedUpdates(this.project_id, this.doc_id, this.temporary, (...args) => { - this.callback(...Array.from(args || [])); - return done(); - }); - }); - - it("should get the oldest updates in three batches ", function() { - return this.RedisManager.getOldestDocUpdates.callCount.should.equal(3); - }); - - it("should compress and save the updates in batches", function() { - this.UpdatesManager.compressAndSaveRawUpdates - .calledWith(this.project_id, this.doc_id, this.updates.slice(0,2), this.temporary) - .should.equal(true); - this.UpdatesManager.compressAndSaveRawUpdates - .calledWith(this.project_id, this.doc_id, this.updates.slice(2,4), this.temporary) - .should.equal(true); - return this.UpdatesManager.compressAndSaveRawUpdates - .calledWith(this.project_id, this.doc_id, this.updates.slice(4,5), this.temporary) - .should.equal(true); - }); - - it("should delete the batches of uncompressed updates", function() { - return this.RedisManager.deleteAppliedDocUpdates.callCount.should.equal(3); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - }); - - describe("processCompressedUpdatesWithLock", function() { - beforeEach(function() { - this.UpdateTrimmer.shouldTrimUpdates = sinon.stub().callsArgWith(1, null, (this.temporary = "temp mock")); - this.MongoManager.backportProjectId = sinon.stub().callsArg(2); - this.UpdatesManager._processUncompressedUpdates = sinon.stub().callsArg(3); - this.LockManager.runWithLock = sinon.stub().callsArg(2); - return this.UpdatesManager.processUncompressedUpdatesWithLock(this.project_id, this.doc_id, this.callback); - }); - - it("should check if the updates are temporary", function() { - return this.UpdateTrimmer.shouldTrimUpdates - .calledWith(this.project_id) - .should.equal(true); - }); - - it("should backport the project id", function() { - return this.MongoManager.backportProjectId - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); - - it("should run processUncompressedUpdates with the lock", function() { - return this.LockManager.runWithLock - .calledWith( - `HistoryLock:${this.doc_id}` - ) - .should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - - describe("getDocUpdates", function() { - beforeEach(function() { - this.updates = ["mock-updates"]; - this.options = { to: "mock-to", limit: "mock-limit" }; - this.PackManager.getOpsByVersionRange = sinon.stub().callsArgWith(4, null, this.updates); - this.UpdatesManager.processUncompressedUpdatesWithLock = sinon.stub().callsArg(2); - return this.UpdatesManager.getDocUpdates(this.project_id, this.doc_id, this.options, this.callback); - }); - - it("should process outstanding updates", function() { - return this.UpdatesManager.processUncompressedUpdatesWithLock - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); - - it("should get the updates from the database", function() { - return this.PackManager.getOpsByVersionRange - .calledWith(this.project_id, this.doc_id, this.options.from, this.options.to) - .should.equal(true); - }); - - return it("should return the updates", function() { - return this.callback - .calledWith(null, this.updates) - .should.equal(true); - }); - }); - - describe("getDocUpdatesWithUserInfo", function() { - beforeEach(function() { - this.updates = ["mock-updates"]; - this.options = { to: "mock-to", limit: "mock-limit" }; - this.updatesWithUserInfo = ["updates-with-user-info"]; - this.UpdatesManager.getDocUpdates = sinon.stub().callsArgWith(3, null, this.updates); - this.UpdatesManager.fillUserInfo = sinon.stub().callsArgWith(1, null, this.updatesWithUserInfo); - return this.UpdatesManager.getDocUpdatesWithUserInfo(this.project_id, this.doc_id, this.options, this.callback); - }); - - it("should get the updates", function() { - return this.UpdatesManager.getDocUpdates - .calledWith(this.project_id, this.doc_id, this.options) - .should.equal(true); - }); - - it("should file the updates with the user info", function() { - return this.UpdatesManager.fillUserInfo - .calledWith(this.updates) - .should.equal(true); - }); - - return it("should return the updates with the filled details", function() { - return this.callback.calledWith(null, this.updatesWithUserInfo).should.equal(true); - }); - }); - - describe("processUncompressedUpdatesForProject", function() { - beforeEach(function(done) { - this.doc_ids = ["mock-id-1", "mock-id-2"]; - this.UpdateTrimmer.shouldTrimUpdates = sinon.stub().callsArgWith(1, null, (this.temporary = "temp mock")); - this.MongoManager.backportProjectId = sinon.stub().callsArg(2); - this.UpdatesManager._processUncompressedUpdatesForDocWithLock = sinon.stub().callsArg(3); - this.RedisManager.getDocIdsWithHistoryOps = sinon.stub().callsArgWith(1, null, this.doc_ids); - return this.UpdatesManager.processUncompressedUpdatesForProject(this.project_id, () => { - this.callback(); - return done(); - }); - }); - - it("should get all the docs with history ops", function() { - return this.RedisManager.getDocIdsWithHistoryOps - .calledWith(this.project_id) - .should.equal(true); - }); - - it("should process the doc ops for the each doc_id", function() { - return Array.from(this.doc_ids).map((doc_id) => - this.UpdatesManager._processUncompressedUpdatesForDocWithLock - .calledWith(this.project_id, doc_id, this.temporary) - .should.equal(true)); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - - describe("getSummarizedProjectUpdates", function() { - beforeEach(function() { - this.updates = [{doc_id: 123, v:456, op: "mock-updates", meta: {user_id: 123, start_ts: 1233, end_ts:1234}}]; - this.options = { before: "mock-before", limit: "mock-limit" }; - this.summarizedUpdates = [ - {meta: {user_ids: [123], start_ts: 1233, end_ts:1234},docs:{"123":{fromV:456,toV:456}}} - ]; - this.updatesWithUserInfo = ["updates-with-user-info"]; - this.done_state = false; - this.iterator = { - next: cb => { - this.done_state = true; - return cb(null, this.updates); - }, - done: () => { - return this.done_state; - } - }; - this.PackManager.makeProjectIterator = sinon.stub().callsArgWith(2, null, this.iterator); - this.UpdatesManager.processUncompressedUpdatesForProject = sinon.stub().callsArg(1); - this.UpdatesManager.fillSummarizedUserInfo = sinon.stub().callsArgWith(1, null, this.updatesWithUserInfo); - return this.UpdatesManager.getSummarizedProjectUpdates(this.project_id, this.options, this.callback); - }); - - it("should process any outstanding updates", function() { - return this.UpdatesManager.processUncompressedUpdatesForProject - .calledWith(this.project_id) - .should.equal(true); - }); - - it("should get the updates", function() { - return this.PackManager.makeProjectIterator - .calledWith(this.project_id, this.options.before) - .should.equal(true); - }); - - it("should fill the updates with the user info", function() { - return this.UpdatesManager.fillSummarizedUserInfo - .calledWith(this.summarizedUpdates) - .should.equal(true); - }); - - return it("should return the updates with the filled details", function() { - return this.callback.calledWith(null, this.updatesWithUserInfo).should.equal(true); - }); - }); - - // describe "_extendBatchOfSummarizedUpdates", -> - // beforeEach -> - // @before = Date.now() - // @min_count = 2 - // @existingSummarizedUpdates = ["summarized-updates-3"] - // @summarizedUpdates = ["summarized-updates-3", "summarized-update-2", "summarized-update-1"] - - // describe "when there are updates to get", -> - // beforeEach -> - // @updates = [ - // {op: "mock-op-1", meta: end_ts: @before - 10}, - // {op: "mock-op-1", meta: end_ts: @nextBeforeTimestamp = @before - 20} - // ] - // @existingSummarizedUpdates = ["summarized-updates-3"] - // @summarizedUpdates = ["summarized-updates-3", "summarized-update-2", "summarized-update-1"] - // @UpdatesManager._summarizeUpdates = sinon.stub().returns(@summarizedUpdates) - // @UpdatesManager.getProjectUpdatesWithUserInfo = sinon.stub().callsArgWith(2, null, @updates) - // @UpdatesManager._extendBatchOfSummarizedUpdates @project_id, @existingSummarizedUpdates, @before, @min_count, @callback - - // it "should get the updates", -> - // @UpdatesManager.getProjectUpdatesWithUserInfo - // .calledWith(@project_id, { before: @before, limit: 3 * @min_count }) - // .should.equal true - - // it "should summarize the updates", -> - // @UpdatesManager._summarizeUpdates - // .calledWith(@updates, @existingSummarizedUpdates) - // .should.equal true - - // it "should call the callback with the summarized updates and the next before timestamp", -> - // @callback.calledWith(null, @summarizedUpdates, @nextBeforeTimestamp).should.equal true - - // describe "when there are no more updates", -> - // beforeEach -> - // @updates = [] - // @UpdatesManager._summarizeUpdates = sinon.stub().returns(@summarizedUpdates) - // @UpdatesManager.getProjectUpdatesWithUserInfo = sinon.stub().callsArgWith(2, null, @updates) - // @UpdatesManager._extendBatchOfSummarizedUpdates @project_id, @existingSummarizedUpdates, @before, @min_count, @callback - - // it "should call the callback with the summarized updates and null for nextBeforeTimestamp", -> - // @callback.calledWith(null, @summarizedUpdates, null).should.equal true - - // describe "getSummarizedProjectUpdates", -> - // describe "when one batch of updates is enough to meet the limit", -> - // beforeEach -> - // @before = Date.now() - // @min_count = 2 - // @updates = ["summarized-updates-3", "summarized-updates-2"] - // @nextBeforeTimestamp = @before - 100 - // @UpdatesManager._extendBatchOfSummarizedUpdates = sinon.stub().callsArgWith(4, null, @updates, @nextBeforeTimestamp) - // @UpdatesManager.getSummarizedProjectUpdates @project_id, { before: @before, min_count: @min_count }, @callback - - // it "should get the batch of summarized updates", -> - // @UpdatesManager._extendBatchOfSummarizedUpdates - // .calledWith(@project_id, [], @before, @min_count) - // .should.equal true - - // it "should call the callback with the updates", -> - // @callback.calledWith(null, @updates, @nextBeforeTimestamp).should.equal true - - // describe "when multiple batches are needed to meet the limit", -> - // beforeEach -> - // @before = Date.now() - // @min_count = 4 - // @firstBatch = [{ toV: 6, fromV: 6 }, { toV: 5, fromV: 5 }] - // @nextBeforeTimestamp = @before - 100 - // @secondBatch = [{ toV: 4, fromV: 4 }, { toV: 3, fromV: 3 }] - // @nextNextBeforeTimestamp = @before - 200 - // @UpdatesManager._extendBatchOfSummarizedUpdates = (project_id, existingUpdates, before, desiredLength, callback) => - // if existingUpdates.length == 0 - // callback null, @firstBatch, @nextBeforeTimestamp - // else - // callback null, @firstBatch.concat(@secondBatch), @nextNextBeforeTimestamp - // sinon.spy @UpdatesManager, "_extendBatchOfSummarizedUpdates" - // @UpdatesManager.getSummarizedProjectUpdates @project_id, { before: @before, min_count: @min_count }, @callback - - // it "should get the first batch of summarized updates", -> - // @UpdatesManager._extendBatchOfSummarizedUpdates - // .calledWith(@project_id, [], @before, @min_count) - // .should.equal true - - // it "should get the second batch of summarized updates", -> - // @UpdatesManager._extendBatchOfSummarizedUpdates - // .calledWith(@project_id, @firstBatch, @nextBeforeTimestamp, @min_count) - // .should.equal true - - // it "should call the callback with all the updates", -> - // @callback.calledWith(null, @firstBatch.concat(@secondBatch), @nextNextBeforeTimestamp).should.equal true - - // describe "when the end of the database is hit", -> - // beforeEach -> - // @before = Date.now() - // @min_count = 4 - // @updates = [{ toV: 6, fromV: 6 }, { toV: 5, fromV: 5 }] - // @UpdatesManager._extendBatchOfSummarizedUpdates = sinon.stub().callsArgWith(4, null, @updates, null) - // @UpdatesManager.getSummarizedProjectUpdates @project_id, { before: @before, min_count: @min_count }, @callback - - // it "should get the batch of summarized updates", -> - // @UpdatesManager._extendBatchOfSummarizedUpdates - // .calledWith(@project_id, [], @before, @min_count) - // .should.equal true - - // it "should call the callback with the updates", -> - // @callback.calledWith(null, @updates, null).should.equal true - - describe("fillUserInfo", function() { - describe("with valid users", function() { - beforeEach(function(done) { - const {ObjectId} = require("mongojs"); - this.user_id_1 = ObjectId().toString(); - this.user_id_2 = ObjectId().toString(); - this.updates = [{ - meta: { - user_id: this.user_id_1 - }, - op: "mock-op-1" - }, { - meta: { - user_id: this.user_id_1 - }, - op: "mock-op-2" - }, { - meta: { - user_id: this.user_id_2 - }, - op: "mock-op-3" - }]; - this.user_info = {}; - this.user_info[this.user_id_1] = {email: "user1@sharelatex.com"}; - this.user_info[this.user_id_2] = {email: "user2@sharelatex.com"}; - - this.WebApiManager.getUserInfo = (user_id, callback) => { - if (callback == null) { callback = function(error, userInfo) {}; } - return callback(null, this.user_info[user_id]); - }; - sinon.spy(this.WebApiManager, "getUserInfo"); - - return this.UpdatesManager.fillUserInfo(this.updates, (error, results) => { - this.results = results; - return done(); - }); - }); - - it("should only call getUserInfo once for each user_id", function() { - this.WebApiManager.getUserInfo.calledTwice.should.equal(true); - this.WebApiManager.getUserInfo - .calledWith(this.user_id_1) - .should.equal(true); - return this.WebApiManager.getUserInfo - .calledWith(this.user_id_2) - .should.equal(true); - }); - - return it("should return the updates with the user info filled", function() { - return expect(this.results).to.deep.equal([{ - meta: { - user: { - email: "user1@sharelatex.com" - } - }, - op: "mock-op-1" - }, { - meta: { - user: { - email: "user1@sharelatex.com" - } - }, - op: "mock-op-2" - }, { - meta: { - user: { - email: "user2@sharelatex.com" - } - }, - op: "mock-op-3" - }]); - }); - }); - - - return describe("with invalid user ids", function() { - beforeEach(function(done) { - this.updates = [{ - meta: { - user_id: null - }, - op: "mock-op-1" - }, { - meta: { - user_id: "anonymous-user" - }, - op: "mock-op-2" - }]; - this.WebApiManager.getUserInfo = (user_id, callback) => { - if (callback == null) { callback = function(error, userInfo) {}; } - return callback(null, this.user_info[user_id]); - }; - sinon.spy(this.WebApiManager, "getUserInfo"); - - return this.UpdatesManager.fillUserInfo(this.updates, (error, results) => { - this.results = results; - return done(); - }); - }); - - it("should not call getUserInfo", function() { - return this.WebApiManager.getUserInfo.called.should.equal(false); - }); - - return it("should return the updates without the user info filled", function() { - return expect(this.results).to.deep.equal([{ - meta: {}, - op: "mock-op-1" - }, { - meta: {}, - op: "mock-op-2" - }]); - }); - }); -}); - - return describe("_summarizeUpdates", function() { - beforeEach(function() { - this.now = Date.now(); - this.user_1 = { id: "mock-user-1" }; - return this.user_2 = { id: "mock-user-2" };}); - - it("should concat updates that are close in time", function() { - const result = this.UpdatesManager._summarizeUpdates([{ - doc_id: "doc-id-1", - meta: { - user_id: this.user_1.id, - start_ts: this.now + 20, - end_ts: this.now + 30 - }, - v: 5 - }, { - doc_id: "doc-id-1", - meta: { - user_id: this.user_2.id, - start_ts: this.now, - end_ts: this.now + 10 - }, - v: 4 - }]); - - return expect(result).to.deep.equal([{ - docs: { - "doc-id-1": { - fromV: 4, - toV: 5 - } - }, - meta: { - user_ids: [this.user_1.id, this.user_2.id], - start_ts: this.now, - end_ts: this.now + 30 - } - }]); - }); - - it("should leave updates that are far apart in time", function() { - const oneDay = 1000 * 60 * 60 * 24; - const result = this.UpdatesManager._summarizeUpdates([{ - doc_id: "doc-id-1", - meta: { - user_id: this.user_2.id, - start_ts: this.now + oneDay, - end_ts: this.now + oneDay + 10 - }, - v: 5 - }, { - doc_id: "doc-id-1", - meta: { - user_id: this.user_1.id, - start_ts: this.now, - end_ts: this.now + 10 - }, - v: 4 - }]); - return expect(result).to.deep.equal([{ - docs: { - "doc-id-1": { - fromV: 5, - toV: 5 - } - }, - meta: { - user_ids: [this.user_2.id], - start_ts: this.now + oneDay, - end_ts: this.now + oneDay + 10 - } - }, { - docs: { - "doc-id-1": { - fromV: 4, - toV: 4 - } - }, - meta: { - user_ids: [this.user_1.id], - start_ts: this.now, - end_ts: this.now + 10 - } - }]); - }); - - it("should concat onto existing summarized updates", function() { - const result = this.UpdatesManager._summarizeUpdates([{ - doc_id: "doc-id-2", - meta: { - user_id: this.user_1.id, - start_ts: this.now + 20, - end_ts: this.now + 30 - }, - v: 5 - }, { - doc_id: "doc-id-2", - meta: { - user_id: this.user_2.id, - start_ts: this.now, - end_ts: this.now + 10 - }, - v: 4 - }], [{ - docs: { - "doc-id-1": { - fromV: 6, - toV: 8 - } - }, - meta: { - user_ids: [this.user_1.id], - start_ts: this.now + 40, - end_ts: this.now + 50 - } - }]); - return expect(result).to.deep.equal([{ - docs: { - "doc-id-1": { - toV: 8, - fromV: 6 - }, - "doc-id-2": { - toV: 5, - fromV: 4 - } - }, - meta: { - user_ids: [this.user_1.id, this.user_2.id], - start_ts: this.now, - end_ts: this.now + 50 - } - }]); - }); - - it("should include null user values", function() { - const result = this.UpdatesManager._summarizeUpdates([{ - doc_id: "doc-id-1", - meta: { - user_id: this.user_1.id, - start_ts: this.now + 20, - end_ts: this.now + 30 - }, - v: 5 - }, { - doc_id: "doc-id-1", - meta: { - user_id: null, - start_ts: this.now, - end_ts: this.now + 10 - }, - v: 4 - }]); - return expect(result).to.deep.equal([{ - docs: { - "doc-id-1": { - fromV: 4, - toV: 5 - } - }, - meta: { - user_ids: [this.user_1.id, null], - start_ts: this.now, - end_ts: this.now + 30 - } - }]); - }); - - it("should include null user values, when the null is earlier in the updates list", function() { - const result = this.UpdatesManager._summarizeUpdates([{ - doc_id: "doc-id-1", - meta: { - user_id: null, - start_ts: this.now, - end_ts: this.now + 10 - }, - v: 4 - }, { - doc_id: "doc-id-1", - meta: { - user_id: this.user_1.id, - start_ts: this.now + 20, - end_ts: this.now + 30 - }, - v: 5 - }]); - return expect(result).to.deep.equal([{ - docs: { - "doc-id-1": { - fromV: 4, - toV: 5 - } - }, - meta: { - user_ids: [null, this.user_1.id], - start_ts: this.now, - end_ts: this.now + 30 - } - }]); - }); - - it("should roll several null user values into one", function() { - const result = this.UpdatesManager._summarizeUpdates([{ - doc_id: "doc-id-1", - meta: { - user_id: this.user_1.id, - start_ts: this.now + 20, - end_ts: this.now + 30 - }, - v: 5 - }, { - doc_id: "doc-id-1", - meta: { - user_id: null, - start_ts: this.now, - end_ts: this.now + 10 - }, - v: 4 - }, { - doc_id: "doc-id-1", - meta: { - user_id: null, - start_ts: this.now + 2, - end_ts: this.now + 4 - }, - v: 4 - }]); - return expect(result).to.deep.equal([{ - docs: { - "doc-id-1": { - fromV: 4, - toV: 5 - } - }, - meta: { - user_ids: [this.user_1.id, null], - start_ts: this.now, - end_ts: this.now + 30 - } - }]); - }); - - return it("should split updates before a big delete", function() { - const result = this.UpdatesManager._summarizeUpdates([{ - doc_id: "doc-id-1", - op: [{ d: "this is a long long long long long delete", p: 34 }], - meta: { - user_id: this.user_1.id, - start_ts: this.now + 20, - end_ts: this.now + 30 - }, - v: 5 - }, { - doc_id: "doc-id-1", - meta: { - user_id: this.user_2.id, - start_ts: this.now, - end_ts: this.now + 10 - }, - v: 4 - }]); - - return expect(result).to.deep.equal([{ - docs: { - "doc-id-1": { - fromV: 5, - toV: 5 - } - }, - meta: { - user_ids: [this.user_1.id], - start_ts: this.now + 20, - end_ts: this.now + 30 - } - }, { - docs: { - "doc-id-1": { - fromV: 4, - toV: 4 - } - }, - meta: { - user_ids: [this.user_2.id], - start_ts: this.now, - end_ts: this.now + 10 - } - }]); - }); -}); -}); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/js/UpdatesManager.js' +const SandboxedModule = require('sandboxed-module') + +describe('UpdatesManager', function() { + beforeEach(function() { + this.UpdatesManager = SandboxedModule.require(modulePath, { + requires: { + './UpdateCompressor': (this.UpdateCompressor = {}), + './MongoManager': (this.MongoManager = {}), + './PackManager': (this.PackManager = {}), + './RedisManager': (this.RedisManager = {}), + './LockManager': (this.LockManager = {}), + './WebApiManager': (this.WebApiManager = {}), + './UpdateTrimmer': (this.UpdateTrimmer = {}), + './DocArchiveManager': (this.DocArchiveManager = {}), + 'logger-sharelatex': { log: sinon.stub(), error: sinon.stub() }, + 'settings-sharelatex': { + redis: { + lock: { + key_schema: { + historyLock({ doc_id }) { + return `HistoryLock:${doc_id}` + } + } + } + } + } + } + }) + this.doc_id = 'doc-id-123' + this.project_id = 'project-id-123' + this.callback = sinon.stub() + return (this.temporary = 'temp-mock') + }) + + describe('compressAndSaveRawUpdates', function() { + describe('when there are no raw ops', function() { + beforeEach(function() { + this.MongoManager.peekLastCompressedUpdate = sinon.stub() + return this.UpdatesManager.compressAndSaveRawUpdates( + this.project_id, + this.doc_id, + [], + this.temporary, + this.callback + ) + }) + + it('should not need to access the database', function() { + return this.MongoManager.peekLastCompressedUpdate.called.should.equal( + false + ) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + describe('when there is no compressed history to begin with', function() { + beforeEach(function() { + this.rawUpdates = [ + { v: 12, op: 'mock-op-12' }, + { v: 13, op: 'mock-op-13' } + ] + this.compressedUpdates = [{ v: 13, op: 'compressed-op-12' }] + + this.MongoManager.peekLastCompressedUpdate = sinon + .stub() + .callsArgWith(1, null, null) + this.PackManager.insertCompressedUpdates = sinon.stub().callsArg(5) + this.UpdateCompressor.compressRawUpdates = sinon + .stub() + .returns(this.compressedUpdates) + return this.UpdatesManager.compressAndSaveRawUpdates( + this.project_id, + this.doc_id, + this.rawUpdates, + this.temporary, + this.callback + ) + }) + + it('should look at the last compressed op', function() { + return this.MongoManager.peekLastCompressedUpdate + .calledWith(this.doc_id) + .should.equal(true) + }) + + it('should save the compressed ops as a pack', function() { + return this.PackManager.insertCompressedUpdates + .calledWith( + this.project_id, + this.doc_id, + null, + this.compressedUpdates, + this.temporary + ) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + describe('when the raw ops need appending to existing history', function() { + beforeEach(function() { + this.lastCompressedUpdate = { v: 11, op: 'compressed-op-11' } + this.compressedUpdates = [ + { v: 12, op: 'compressed-op-11+12' }, + { v: 13, op: 'compressed-op-12' } + ] + + this.MongoManager.peekLastCompressedUpdate = sinon + .stub() + .callsArgWith( + 1, + null, + this.lastCompressedUpdate, + this.lastCompressedUpdate.v + ) + this.PackManager.insertCompressedUpdates = sinon.stub().callsArg(5) + return (this.UpdateCompressor.compressRawUpdates = sinon + .stub() + .returns(this.compressedUpdates)) + }) + + describe('when the raw ops start where the existing history ends', function() { + beforeEach(function() { + this.rawUpdates = [ + { v: 12, op: 'mock-op-12' }, + { v: 13, op: 'mock-op-13' } + ] + return this.UpdatesManager.compressAndSaveRawUpdates( + this.project_id, + this.doc_id, + this.rawUpdates, + this.temporary, + this.callback + ) + }) + + it('should look at the last compressed op', function() { + return this.MongoManager.peekLastCompressedUpdate + .calledWith(this.doc_id) + .should.equal(true) + }) + + it('should compress the raw ops', function() { + return this.UpdateCompressor.compressRawUpdates + .calledWith(null, this.rawUpdates) + .should.equal(true) + }) + + it('should save the new compressed ops into a pack', function() { + return this.PackManager.insertCompressedUpdates + .calledWith( + this.project_id, + this.doc_id, + this.lastCompressedUpdate, + this.compressedUpdates, + this.temporary + ) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + describe('when the raw ops start where the existing history ends and the history is in a pack', function() { + beforeEach(function() { + this.lastCompressedUpdate = { + pack: [{ v: 11, op: 'compressed-op-11' }], + v: 11 + } + this.rawUpdates = [ + { v: 12, op: 'mock-op-12' }, + { v: 13, op: 'mock-op-13' } + ] + this.MongoManager.peekLastCompressedUpdate = sinon + .stub() + .callsArgWith( + 1, + null, + this.lastCompressedUpdate, + this.lastCompressedUpdate.v + ) + return this.UpdatesManager.compressAndSaveRawUpdates( + this.project_id, + this.doc_id, + this.rawUpdates, + this.temporary, + this.callback + ) + }) + + it('should look at the last compressed op', function() { + return this.MongoManager.peekLastCompressedUpdate + .calledWith(this.doc_id) + .should.equal(true) + }) + + it('should compress the raw ops', function() { + return this.UpdateCompressor.compressRawUpdates + .calledWith(null, this.rawUpdates) + .should.equal(true) + }) + + it('should save the new compressed ops into a pack', function() { + return this.PackManager.insertCompressedUpdates + .calledWith( + this.project_id, + this.doc_id, + this.lastCompressedUpdate, + this.compressedUpdates, + this.temporary + ) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + describe('when some raw ops are passed that have already been compressed', function() { + beforeEach(function() { + this.rawUpdates = [ + { v: 10, op: 'mock-op-10' }, + { v: 11, op: 'mock-op-11' }, + { v: 12, op: 'mock-op-12' }, + { v: 13, op: 'mock-op-13' } + ] + + return this.UpdatesManager.compressAndSaveRawUpdates( + this.project_id, + this.doc_id, + this.rawUpdates, + this.temporary, + this.callback + ) + }) + + return it('should only compress the more recent raw ops', function() { + return this.UpdateCompressor.compressRawUpdates + .calledWith(null, this.rawUpdates.slice(-2)) + .should.equal(true) + }) + }) + + describe('when the raw ops do not follow from the last compressed op version', function() { + beforeEach(function() { + this.rawUpdates = [{ v: 13, op: 'mock-op-13' }] + return this.UpdatesManager.compressAndSaveRawUpdates( + this.project_id, + this.doc_id, + this.rawUpdates, + this.temporary, + this.callback + ) + }) + + it('should call the callback with an error', function() { + return this.callback + .calledWith( + sinon.match.has( + 'message', + 'Tried to apply raw op at version 13 to last compressed update with version 11 from unknown time' + ) + ) + .should.equal(true) + }) + + return it('should not insert any update into mongo', function() { + return this.PackManager.insertCompressedUpdates.called.should.equal( + false + ) + }) + }) + + return describe('when the raw ops are out of order', function() { + beforeEach(function() { + this.rawUpdates = [ + { v: 13, op: 'mock-op-13' }, + { v: 12, op: 'mock-op-12' } + ] + return this.UpdatesManager.compressAndSaveRawUpdates( + this.project_id, + this.doc_id, + this.rawUpdates, + this.temporary, + this.callback + ) + }) + + it('should call the callback with an error', function() { + return this.callback + .calledWith(sinon.match.has('message')) + .should.equal(true) + }) + + return it('should not insert any update into mongo', function() { + return this.PackManager.insertCompressedUpdates.called.should.equal( + false + ) + }) + }) + }) + + return describe('when the raw ops need appending to existing history which is in S3', function() { + beforeEach(function() { + this.lastCompressedUpdate = null + this.lastVersion = 11 + this.compressedUpdates = [{ v: 13, op: 'compressed-op-12' }] + + this.MongoManager.peekLastCompressedUpdate = sinon + .stub() + .callsArgWith(1, null, null, this.lastVersion) + this.PackManager.insertCompressedUpdates = sinon.stub().callsArg(5) + return (this.UpdateCompressor.compressRawUpdates = sinon + .stub() + .returns(this.compressedUpdates)) + }) + + return describe('when the raw ops start where the existing history ends', function() { + beforeEach(function() { + this.rawUpdates = [ + { v: 12, op: 'mock-op-12' }, + { v: 13, op: 'mock-op-13' } + ] + return this.UpdatesManager.compressAndSaveRawUpdates( + this.project_id, + this.doc_id, + this.rawUpdates, + this.temporary, + this.callback + ) + }) + + it('should try to look at the last compressed op', function() { + return this.MongoManager.peekLastCompressedUpdate + .calledWith(this.doc_id) + .should.equal(true) + }) + + it('should compress the last compressed op and the raw ops', function() { + return this.UpdateCompressor.compressRawUpdates + .calledWith(this.lastCompressedUpdate, this.rawUpdates) + .should.equal(true) + }) + + it('should save the compressed ops', function() { + return this.PackManager.insertCompressedUpdates + .calledWith( + this.project_id, + this.doc_id, + null, + this.compressedUpdates, + this.temporary + ) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + }) + }) + + describe('processUncompressedUpdates', function() { + beforeEach(function() { + this.UpdatesManager.compressAndSaveRawUpdates = sinon + .stub() + .callsArgWith(4) + this.RedisManager.deleteAppliedDocUpdates = sinon.stub().callsArg(3) + this.MongoManager.backportProjectId = sinon.stub().callsArg(2) + return (this.UpdateTrimmer.shouldTrimUpdates = sinon + .stub() + .callsArgWith(1, null, (this.temporary = 'temp mock'))) + }) + + describe('when there is fewer than one batch to send', function() { + beforeEach(function() { + this.updates = ['mock-update'] + this.RedisManager.getOldestDocUpdates = sinon + .stub() + .callsArgWith(2, null, this.updates) + this.RedisManager.expandDocUpdates = sinon + .stub() + .callsArgWith(1, null, this.updates) + return this.UpdatesManager.processUncompressedUpdates( + this.project_id, + this.doc_id, + this.temporary, + this.callback + ) + }) + + it('should get the oldest updates', function() { + return this.RedisManager.getOldestDocUpdates + .calledWith(this.doc_id, this.UpdatesManager.REDIS_READ_BATCH_SIZE) + .should.equal(true) + }) + + it('should compress and save the updates', function() { + return this.UpdatesManager.compressAndSaveRawUpdates + .calledWith( + this.project_id, + this.doc_id, + this.updates, + this.temporary + ) + .should.equal(true) + }) + + it('should delete the batch of uncompressed updates that was just processed', function() { + return this.RedisManager.deleteAppliedDocUpdates + .calledWith(this.project_id, this.doc_id, this.updates) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + return describe('when there are multiple batches to send', function() { + beforeEach(function(done) { + this.UpdatesManager.REDIS_READ_BATCH_SIZE = 2 + this.updates = [ + 'mock-update-0', + 'mock-update-1', + 'mock-update-2', + 'mock-update-3', + 'mock-update-4' + ] + this.redisArray = this.updates.slice() + this.RedisManager.getOldestDocUpdates = ( + doc_id, + batchSize, + callback + ) => { + if (callback == null) { + callback = function(error, updates) {} + } + const updates = this.redisArray.slice(0, batchSize) + this.redisArray = this.redisArray.slice(batchSize) + return callback(null, updates) + } + sinon.spy(this.RedisManager, 'getOldestDocUpdates') + this.RedisManager.expandDocUpdates = (jsonUpdates, callback) => { + return callback(null, jsonUpdates) + } + sinon.spy(this.RedisManager, 'expandDocUpdates') + return this.UpdatesManager.processUncompressedUpdates( + this.project_id, + this.doc_id, + this.temporary, + (...args) => { + this.callback(...Array.from(args || [])) + return done() + } + ) + }) + + it('should get the oldest updates in three batches ', function() { + return this.RedisManager.getOldestDocUpdates.callCount.should.equal(3) + }) + + it('should compress and save the updates in batches', function() { + this.UpdatesManager.compressAndSaveRawUpdates + .calledWith( + this.project_id, + this.doc_id, + this.updates.slice(0, 2), + this.temporary + ) + .should.equal(true) + this.UpdatesManager.compressAndSaveRawUpdates + .calledWith( + this.project_id, + this.doc_id, + this.updates.slice(2, 4), + this.temporary + ) + .should.equal(true) + return this.UpdatesManager.compressAndSaveRawUpdates + .calledWith( + this.project_id, + this.doc_id, + this.updates.slice(4, 5), + this.temporary + ) + .should.equal(true) + }) + + it('should delete the batches of uncompressed updates', function() { + return this.RedisManager.deleteAppliedDocUpdates.callCount.should.equal( + 3 + ) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + }) + + describe('processCompressedUpdatesWithLock', function() { + beforeEach(function() { + this.UpdateTrimmer.shouldTrimUpdates = sinon + .stub() + .callsArgWith(1, null, (this.temporary = 'temp mock')) + this.MongoManager.backportProjectId = sinon.stub().callsArg(2) + this.UpdatesManager._processUncompressedUpdates = sinon.stub().callsArg(3) + this.LockManager.runWithLock = sinon.stub().callsArg(2) + return this.UpdatesManager.processUncompressedUpdatesWithLock( + this.project_id, + this.doc_id, + this.callback + ) + }) + + it('should check if the updates are temporary', function() { + return this.UpdateTrimmer.shouldTrimUpdates + .calledWith(this.project_id) + .should.equal(true) + }) + + it('should backport the project id', function() { + return this.MongoManager.backportProjectId + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) + + it('should run processUncompressedUpdates with the lock', function() { + return this.LockManager.runWithLock + .calledWith(`HistoryLock:${this.doc_id}`) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + describe('getDocUpdates', function() { + beforeEach(function() { + this.updates = ['mock-updates'] + this.options = { to: 'mock-to', limit: 'mock-limit' } + this.PackManager.getOpsByVersionRange = sinon + .stub() + .callsArgWith(4, null, this.updates) + this.UpdatesManager.processUncompressedUpdatesWithLock = sinon + .stub() + .callsArg(2) + return this.UpdatesManager.getDocUpdates( + this.project_id, + this.doc_id, + this.options, + this.callback + ) + }) + + it('should process outstanding updates', function() { + return this.UpdatesManager.processUncompressedUpdatesWithLock + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) + + it('should get the updates from the database', function() { + return this.PackManager.getOpsByVersionRange + .calledWith( + this.project_id, + this.doc_id, + this.options.from, + this.options.to + ) + .should.equal(true) + }) + + return it('should return the updates', function() { + return this.callback.calledWith(null, this.updates).should.equal(true) + }) + }) + + describe('getDocUpdatesWithUserInfo', function() { + beforeEach(function() { + this.updates = ['mock-updates'] + this.options = { to: 'mock-to', limit: 'mock-limit' } + this.updatesWithUserInfo = ['updates-with-user-info'] + this.UpdatesManager.getDocUpdates = sinon + .stub() + .callsArgWith(3, null, this.updates) + this.UpdatesManager.fillUserInfo = sinon + .stub() + .callsArgWith(1, null, this.updatesWithUserInfo) + return this.UpdatesManager.getDocUpdatesWithUserInfo( + this.project_id, + this.doc_id, + this.options, + this.callback + ) + }) + + it('should get the updates', function() { + return this.UpdatesManager.getDocUpdates + .calledWith(this.project_id, this.doc_id, this.options) + .should.equal(true) + }) + + it('should file the updates with the user info', function() { + return this.UpdatesManager.fillUserInfo + .calledWith(this.updates) + .should.equal(true) + }) + + return it('should return the updates with the filled details', function() { + return this.callback + .calledWith(null, this.updatesWithUserInfo) + .should.equal(true) + }) + }) + + describe('processUncompressedUpdatesForProject', function() { + beforeEach(function(done) { + this.doc_ids = ['mock-id-1', 'mock-id-2'] + this.UpdateTrimmer.shouldTrimUpdates = sinon + .stub() + .callsArgWith(1, null, (this.temporary = 'temp mock')) + this.MongoManager.backportProjectId = sinon.stub().callsArg(2) + this.UpdatesManager._processUncompressedUpdatesForDocWithLock = sinon + .stub() + .callsArg(3) + this.RedisManager.getDocIdsWithHistoryOps = sinon + .stub() + .callsArgWith(1, null, this.doc_ids) + return this.UpdatesManager.processUncompressedUpdatesForProject( + this.project_id, + () => { + this.callback() + return done() + } + ) + }) + + it('should get all the docs with history ops', function() { + return this.RedisManager.getDocIdsWithHistoryOps + .calledWith(this.project_id) + .should.equal(true) + }) + + it('should process the doc ops for the each doc_id', function() { + return Array.from(this.doc_ids).map(doc_id => + this.UpdatesManager._processUncompressedUpdatesForDocWithLock + .calledWith(this.project_id, doc_id, this.temporary) + .should.equal(true) + ) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + describe('getSummarizedProjectUpdates', function() { + beforeEach(function() { + this.updates = [ + { + doc_id: 123, + v: 456, + op: 'mock-updates', + meta: { user_id: 123, start_ts: 1233, end_ts: 1234 } + } + ] + this.options = { before: 'mock-before', limit: 'mock-limit' } + this.summarizedUpdates = [ + { + meta: { user_ids: [123], start_ts: 1233, end_ts: 1234 }, + docs: { '123': { fromV: 456, toV: 456 } } + } + ] + this.updatesWithUserInfo = ['updates-with-user-info'] + this.done_state = false + this.iterator = { + next: cb => { + this.done_state = true + return cb(null, this.updates) + }, + done: () => { + return this.done_state + } + } + this.PackManager.makeProjectIterator = sinon + .stub() + .callsArgWith(2, null, this.iterator) + this.UpdatesManager.processUncompressedUpdatesForProject = sinon + .stub() + .callsArg(1) + this.UpdatesManager.fillSummarizedUserInfo = sinon + .stub() + .callsArgWith(1, null, this.updatesWithUserInfo) + return this.UpdatesManager.getSummarizedProjectUpdates( + this.project_id, + this.options, + this.callback + ) + }) + + it('should process any outstanding updates', function() { + return this.UpdatesManager.processUncompressedUpdatesForProject + .calledWith(this.project_id) + .should.equal(true) + }) + + it('should get the updates', function() { + return this.PackManager.makeProjectIterator + .calledWith(this.project_id, this.options.before) + .should.equal(true) + }) + + it('should fill the updates with the user info', function() { + return this.UpdatesManager.fillSummarizedUserInfo + .calledWith(this.summarizedUpdates) + .should.equal(true) + }) + + return it('should return the updates with the filled details', function() { + return this.callback + .calledWith(null, this.updatesWithUserInfo) + .should.equal(true) + }) + }) + + // describe "_extendBatchOfSummarizedUpdates", -> + // beforeEach -> + // @before = Date.now() + // @min_count = 2 + // @existingSummarizedUpdates = ["summarized-updates-3"] + // @summarizedUpdates = ["summarized-updates-3", "summarized-update-2", "summarized-update-1"] + + // describe "when there are updates to get", -> + // beforeEach -> + // @updates = [ + // {op: "mock-op-1", meta: end_ts: @before - 10}, + // {op: "mock-op-1", meta: end_ts: @nextBeforeTimestamp = @before - 20} + // ] + // @existingSummarizedUpdates = ["summarized-updates-3"] + // @summarizedUpdates = ["summarized-updates-3", "summarized-update-2", "summarized-update-1"] + // @UpdatesManager._summarizeUpdates = sinon.stub().returns(@summarizedUpdates) + // @UpdatesManager.getProjectUpdatesWithUserInfo = sinon.stub().callsArgWith(2, null, @updates) + // @UpdatesManager._extendBatchOfSummarizedUpdates @project_id, @existingSummarizedUpdates, @before, @min_count, @callback + + // it "should get the updates", -> + // @UpdatesManager.getProjectUpdatesWithUserInfo + // .calledWith(@project_id, { before: @before, limit: 3 * @min_count }) + // .should.equal true + + // it "should summarize the updates", -> + // @UpdatesManager._summarizeUpdates + // .calledWith(@updates, @existingSummarizedUpdates) + // .should.equal true + + // it "should call the callback with the summarized updates and the next before timestamp", -> + // @callback.calledWith(null, @summarizedUpdates, @nextBeforeTimestamp).should.equal true + + // describe "when there are no more updates", -> + // beforeEach -> + // @updates = [] + // @UpdatesManager._summarizeUpdates = sinon.stub().returns(@summarizedUpdates) + // @UpdatesManager.getProjectUpdatesWithUserInfo = sinon.stub().callsArgWith(2, null, @updates) + // @UpdatesManager._extendBatchOfSummarizedUpdates @project_id, @existingSummarizedUpdates, @before, @min_count, @callback + + // it "should call the callback with the summarized updates and null for nextBeforeTimestamp", -> + // @callback.calledWith(null, @summarizedUpdates, null).should.equal true + + // describe "getSummarizedProjectUpdates", -> + // describe "when one batch of updates is enough to meet the limit", -> + // beforeEach -> + // @before = Date.now() + // @min_count = 2 + // @updates = ["summarized-updates-3", "summarized-updates-2"] + // @nextBeforeTimestamp = @before - 100 + // @UpdatesManager._extendBatchOfSummarizedUpdates = sinon.stub().callsArgWith(4, null, @updates, @nextBeforeTimestamp) + // @UpdatesManager.getSummarizedProjectUpdates @project_id, { before: @before, min_count: @min_count }, @callback + + // it "should get the batch of summarized updates", -> + // @UpdatesManager._extendBatchOfSummarizedUpdates + // .calledWith(@project_id, [], @before, @min_count) + // .should.equal true + + // it "should call the callback with the updates", -> + // @callback.calledWith(null, @updates, @nextBeforeTimestamp).should.equal true + + // describe "when multiple batches are needed to meet the limit", -> + // beforeEach -> + // @before = Date.now() + // @min_count = 4 + // @firstBatch = [{ toV: 6, fromV: 6 }, { toV: 5, fromV: 5 }] + // @nextBeforeTimestamp = @before - 100 + // @secondBatch = [{ toV: 4, fromV: 4 }, { toV: 3, fromV: 3 }] + // @nextNextBeforeTimestamp = @before - 200 + // @UpdatesManager._extendBatchOfSummarizedUpdates = (project_id, existingUpdates, before, desiredLength, callback) => + // if existingUpdates.length == 0 + // callback null, @firstBatch, @nextBeforeTimestamp + // else + // callback null, @firstBatch.concat(@secondBatch), @nextNextBeforeTimestamp + // sinon.spy @UpdatesManager, "_extendBatchOfSummarizedUpdates" + // @UpdatesManager.getSummarizedProjectUpdates @project_id, { before: @before, min_count: @min_count }, @callback + + // it "should get the first batch of summarized updates", -> + // @UpdatesManager._extendBatchOfSummarizedUpdates + // .calledWith(@project_id, [], @before, @min_count) + // .should.equal true + + // it "should get the second batch of summarized updates", -> + // @UpdatesManager._extendBatchOfSummarizedUpdates + // .calledWith(@project_id, @firstBatch, @nextBeforeTimestamp, @min_count) + // .should.equal true + + // it "should call the callback with all the updates", -> + // @callback.calledWith(null, @firstBatch.concat(@secondBatch), @nextNextBeforeTimestamp).should.equal true + + // describe "when the end of the database is hit", -> + // beforeEach -> + // @before = Date.now() + // @min_count = 4 + // @updates = [{ toV: 6, fromV: 6 }, { toV: 5, fromV: 5 }] + // @UpdatesManager._extendBatchOfSummarizedUpdates = sinon.stub().callsArgWith(4, null, @updates, null) + // @UpdatesManager.getSummarizedProjectUpdates @project_id, { before: @before, min_count: @min_count }, @callback + + // it "should get the batch of summarized updates", -> + // @UpdatesManager._extendBatchOfSummarizedUpdates + // .calledWith(@project_id, [], @before, @min_count) + // .should.equal true + + // it "should call the callback with the updates", -> + // @callback.calledWith(null, @updates, null).should.equal true + + describe('fillUserInfo', function() { + describe('with valid users', function() { + beforeEach(function(done) { + const { ObjectId } = require('mongojs') + this.user_id_1 = ObjectId().toString() + this.user_id_2 = ObjectId().toString() + this.updates = [ + { + meta: { + user_id: this.user_id_1 + }, + op: 'mock-op-1' + }, + { + meta: { + user_id: this.user_id_1 + }, + op: 'mock-op-2' + }, + { + meta: { + user_id: this.user_id_2 + }, + op: 'mock-op-3' + } + ] + this.user_info = {} + this.user_info[this.user_id_1] = { email: 'user1@sharelatex.com' } + this.user_info[this.user_id_2] = { email: 'user2@sharelatex.com' } + + this.WebApiManager.getUserInfo = (user_id, callback) => { + if (callback == null) { + callback = function(error, userInfo) {} + } + return callback(null, this.user_info[user_id]) + } + sinon.spy(this.WebApiManager, 'getUserInfo') + + return this.UpdatesManager.fillUserInfo( + this.updates, + (error, results) => { + this.results = results + return done() + } + ) + }) + + it('should only call getUserInfo once for each user_id', function() { + this.WebApiManager.getUserInfo.calledTwice.should.equal(true) + this.WebApiManager.getUserInfo + .calledWith(this.user_id_1) + .should.equal(true) + return this.WebApiManager.getUserInfo + .calledWith(this.user_id_2) + .should.equal(true) + }) + + return it('should return the updates with the user info filled', function() { + return expect(this.results).to.deep.equal([ + { + meta: { + user: { + email: 'user1@sharelatex.com' + } + }, + op: 'mock-op-1' + }, + { + meta: { + user: { + email: 'user1@sharelatex.com' + } + }, + op: 'mock-op-2' + }, + { + meta: { + user: { + email: 'user2@sharelatex.com' + } + }, + op: 'mock-op-3' + } + ]) + }) + }) + + return describe('with invalid user ids', function() { + beforeEach(function(done) { + this.updates = [ + { + meta: { + user_id: null + }, + op: 'mock-op-1' + }, + { + meta: { + user_id: 'anonymous-user' + }, + op: 'mock-op-2' + } + ] + this.WebApiManager.getUserInfo = (user_id, callback) => { + if (callback == null) { + callback = function(error, userInfo) {} + } + return callback(null, this.user_info[user_id]) + } + sinon.spy(this.WebApiManager, 'getUserInfo') + + return this.UpdatesManager.fillUserInfo( + this.updates, + (error, results) => { + this.results = results + return done() + } + ) + }) + + it('should not call getUserInfo', function() { + return this.WebApiManager.getUserInfo.called.should.equal(false) + }) + + return it('should return the updates without the user info filled', function() { + return expect(this.results).to.deep.equal([ + { + meta: {}, + op: 'mock-op-1' + }, + { + meta: {}, + op: 'mock-op-2' + } + ]) + }) + }) + }) + + return describe('_summarizeUpdates', function() { + beforeEach(function() { + this.now = Date.now() + this.user_1 = { id: 'mock-user-1' } + return (this.user_2 = { id: 'mock-user-2' }) + }) + + it('should concat updates that are close in time', function() { + const result = this.UpdatesManager._summarizeUpdates([ + { + doc_id: 'doc-id-1', + meta: { + user_id: this.user_1.id, + start_ts: this.now + 20, + end_ts: this.now + 30 + }, + v: 5 + }, + { + doc_id: 'doc-id-1', + meta: { + user_id: this.user_2.id, + start_ts: this.now, + end_ts: this.now + 10 + }, + v: 4 + } + ]) + + return expect(result).to.deep.equal([ + { + docs: { + 'doc-id-1': { + fromV: 4, + toV: 5 + } + }, + meta: { + user_ids: [this.user_1.id, this.user_2.id], + start_ts: this.now, + end_ts: this.now + 30 + } + } + ]) + }) + + it('should leave updates that are far apart in time', function() { + const oneDay = 1000 * 60 * 60 * 24 + const result = this.UpdatesManager._summarizeUpdates([ + { + doc_id: 'doc-id-1', + meta: { + user_id: this.user_2.id, + start_ts: this.now + oneDay, + end_ts: this.now + oneDay + 10 + }, + v: 5 + }, + { + doc_id: 'doc-id-1', + meta: { + user_id: this.user_1.id, + start_ts: this.now, + end_ts: this.now + 10 + }, + v: 4 + } + ]) + return expect(result).to.deep.equal([ + { + docs: { + 'doc-id-1': { + fromV: 5, + toV: 5 + } + }, + meta: { + user_ids: [this.user_2.id], + start_ts: this.now + oneDay, + end_ts: this.now + oneDay + 10 + } + }, + { + docs: { + 'doc-id-1': { + fromV: 4, + toV: 4 + } + }, + meta: { + user_ids: [this.user_1.id], + start_ts: this.now, + end_ts: this.now + 10 + } + } + ]) + }) + + it('should concat onto existing summarized updates', function() { + const result = this.UpdatesManager._summarizeUpdates( + [ + { + doc_id: 'doc-id-2', + meta: { + user_id: this.user_1.id, + start_ts: this.now + 20, + end_ts: this.now + 30 + }, + v: 5 + }, + { + doc_id: 'doc-id-2', + meta: { + user_id: this.user_2.id, + start_ts: this.now, + end_ts: this.now + 10 + }, + v: 4 + } + ], + [ + { + docs: { + 'doc-id-1': { + fromV: 6, + toV: 8 + } + }, + meta: { + user_ids: [this.user_1.id], + start_ts: this.now + 40, + end_ts: this.now + 50 + } + } + ] + ) + return expect(result).to.deep.equal([ + { + docs: { + 'doc-id-1': { + toV: 8, + fromV: 6 + }, + 'doc-id-2': { + toV: 5, + fromV: 4 + } + }, + meta: { + user_ids: [this.user_1.id, this.user_2.id], + start_ts: this.now, + end_ts: this.now + 50 + } + } + ]) + }) + + it('should include null user values', function() { + const result = this.UpdatesManager._summarizeUpdates([ + { + doc_id: 'doc-id-1', + meta: { + user_id: this.user_1.id, + start_ts: this.now + 20, + end_ts: this.now + 30 + }, + v: 5 + }, + { + doc_id: 'doc-id-1', + meta: { + user_id: null, + start_ts: this.now, + end_ts: this.now + 10 + }, + v: 4 + } + ]) + return expect(result).to.deep.equal([ + { + docs: { + 'doc-id-1': { + fromV: 4, + toV: 5 + } + }, + meta: { + user_ids: [this.user_1.id, null], + start_ts: this.now, + end_ts: this.now + 30 + } + } + ]) + }) + + it('should include null user values, when the null is earlier in the updates list', function() { + const result = this.UpdatesManager._summarizeUpdates([ + { + doc_id: 'doc-id-1', + meta: { + user_id: null, + start_ts: this.now, + end_ts: this.now + 10 + }, + v: 4 + }, + { + doc_id: 'doc-id-1', + meta: { + user_id: this.user_1.id, + start_ts: this.now + 20, + end_ts: this.now + 30 + }, + v: 5 + } + ]) + return expect(result).to.deep.equal([ + { + docs: { + 'doc-id-1': { + fromV: 4, + toV: 5 + } + }, + meta: { + user_ids: [null, this.user_1.id], + start_ts: this.now, + end_ts: this.now + 30 + } + } + ]) + }) + + it('should roll several null user values into one', function() { + const result = this.UpdatesManager._summarizeUpdates([ + { + doc_id: 'doc-id-1', + meta: { + user_id: this.user_1.id, + start_ts: this.now + 20, + end_ts: this.now + 30 + }, + v: 5 + }, + { + doc_id: 'doc-id-1', + meta: { + user_id: null, + start_ts: this.now, + end_ts: this.now + 10 + }, + v: 4 + }, + { + doc_id: 'doc-id-1', + meta: { + user_id: null, + start_ts: this.now + 2, + end_ts: this.now + 4 + }, + v: 4 + } + ]) + return expect(result).to.deep.equal([ + { + docs: { + 'doc-id-1': { + fromV: 4, + toV: 5 + } + }, + meta: { + user_ids: [this.user_1.id, null], + start_ts: this.now, + end_ts: this.now + 30 + } + } + ]) + }) + + return it('should split updates before a big delete', function() { + const result = this.UpdatesManager._summarizeUpdates([ + { + doc_id: 'doc-id-1', + op: [{ d: 'this is a long long long long long delete', p: 34 }], + meta: { + user_id: this.user_1.id, + start_ts: this.now + 20, + end_ts: this.now + 30 + }, + v: 5 + }, + { + doc_id: 'doc-id-1', + meta: { + user_id: this.user_2.id, + start_ts: this.now, + end_ts: this.now + 10 + }, + v: 4 + } + ]) + + return expect(result).to.deep.equal([ + { + docs: { + 'doc-id-1': { + fromV: 5, + toV: 5 + } + }, + meta: { + user_ids: [this.user_1.id], + start_ts: this.now + 20, + end_ts: this.now + 30 + } + }, + { + docs: { + 'doc-id-1': { + fromV: 4, + toV: 4 + } + }, + meta: { + user_ids: [this.user_2.id], + start_ts: this.now, + end_ts: this.now + 10 + } + } + ]) + }) + }) +}) diff --git a/services/track-changes/test/unit/js/WebApiManager/WebApiManagerTests.js b/services/track-changes/test/unit/js/WebApiManager/WebApiManagerTests.js index 984493afc4..d62466b0b9 100644 --- a/services/track-changes/test/unit/js/WebApiManager/WebApiManagerTests.js +++ b/services/track-changes/test/unit/js/WebApiManager/WebApiManagerTests.js @@ -9,162 +9,206 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const sinon = require('sinon'); -const chai = require('chai'); -const should = chai.should(); -const { expect } = chai; -const modulePath = "../../../../app/js/WebApiManager.js"; -const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/js/WebApiManager.js' +const SandboxedModule = require('sandboxed-module') -describe("WebApiManager", function() { - beforeEach(function() { - this.WebApiManager = SandboxedModule.require(modulePath, { requires: { - "requestretry": (this.request = {}), - "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }), - 'settings-sharelatex': (this.settings = { - apis: { - web: { - url: "http://example.com", - user: "sharelatex", - pass: "password" - } - } - }) - } - } - ); - this.callback = sinon.stub(); - this.user_id = "mock-user-id"; - this.project_id = "mock-project-id"; - this.user_info = { - email: "leo@sharelatex.com", - id: this.user_id, - first_name: "Leo", - last_nane: "Lion", - extra_param: "blah" - }; - return this.project = - {features: "mock-features"}; - }); +describe('WebApiManager', function() { + beforeEach(function() { + this.WebApiManager = SandboxedModule.require(modulePath, { + requires: { + requestretry: (this.request = {}), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub() + }), + 'settings-sharelatex': (this.settings = { + apis: { + web: { + url: 'http://example.com', + user: 'sharelatex', + pass: 'password' + } + } + }) + } + }) + this.callback = sinon.stub() + this.user_id = 'mock-user-id' + this.project_id = 'mock-project-id' + this.user_info = { + email: 'leo@sharelatex.com', + id: this.user_id, + first_name: 'Leo', + last_nane: 'Lion', + extra_param: 'blah' + } + return (this.project = { features: 'mock-features' }) + }) - describe("getUserInfo", function() { - describe("successfully", function() { - beforeEach(function() { - this.body = JSON.stringify(this.user_info); - this.request.get = sinon.stub().callsArgWith(1, null, {statusCode: 200}, this.body); - return this.WebApiManager.getUserInfo(this.user_id, this.callback); - }); + describe('getUserInfo', function() { + describe('successfully', function() { + beforeEach(function() { + this.body = JSON.stringify(this.user_info) + this.request.get = sinon + .stub() + .callsArgWith(1, null, { statusCode: 200 }, this.body) + return this.WebApiManager.getUserInfo(this.user_id, this.callback) + }) - it('should get the user from the web api', function() { - return this.request.get - .calledWithMatch({ - url: `${this.settings.apis.web.url}/user/${this.user_id}/personal_info`, - auth: { - user: this.settings.apis.web.user, - pass: this.settings.apis.web.pass, - sendImmediately: true - } - }) - .should.equal(true); - }); + it('should get the user from the web api', function() { + return this.request.get + .calledWithMatch({ + url: `${this.settings.apis.web.url}/user/${this.user_id}/personal_info`, + auth: { + user: this.settings.apis.web.user, + pass: this.settings.apis.web.pass, + sendImmediately: true + } + }) + .should.equal(true) + }) - return it("should call the callback with only the email, id and names", function() { - return this.callback.calledWith(null, { - id: this.user_id, - email: this.user_info.email, - first_name: this.user_info.first_name, - last_name: this.user_info.last_name - }).should.equal(true); - }); - }); + return it('should call the callback with only the email, id and names', function() { + return this.callback + .calledWith(null, { + id: this.user_id, + email: this.user_info.email, + first_name: this.user_info.first_name, + last_name: this.user_info.last_name + }) + .should.equal(true) + }) + }) - describe("when the web API returns an error", function() { - beforeEach(function() { - this.request.get = sinon.stub().callsArgWith(1, (this.error = new Error("something went wrong")), null, null); - return this.WebApiManager.getUserInfo(this.user_id, this.callback); - }); + describe('when the web API returns an error', function() { + beforeEach(function() { + this.request.get = sinon + .stub() + .callsArgWith( + 1, + (this.error = new Error('something went wrong')), + null, + null + ) + return this.WebApiManager.getUserInfo(this.user_id, this.callback) + }) - return it("should return an error to the callback", function() { - return this.callback.calledWith(this.error).should.equal(true); - }); - }); + return it('should return an error to the callback', function() { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) - describe("when the web returns a failure error code", function() { - beforeEach(function() { - this.request.get = sinon.stub().callsArgWith(1, null, { statusCode: 500, attempts: 42}, ""); - return this.WebApiManager.getUserInfo(this.user_id, this.callback); - }); + describe('when the web returns a failure error code', function() { + beforeEach(function() { + this.request.get = sinon + .stub() + .callsArgWith(1, null, { statusCode: 500, attempts: 42 }, '') + return this.WebApiManager.getUserInfo(this.user_id, this.callback) + }) - return it("should return the callback with an error", function() { - return this.callback - .calledWith(sinon.match.has('message', "web returned a non-success status code: 500 (attempts: 42)")) - .should.equal(true); - }); - }); + return it('should return the callback with an error', function() { + return this.callback + .calledWith( + sinon.match.has( + 'message', + 'web returned a non-success status code: 500 (attempts: 42)' + ) + ) + .should.equal(true) + }) + }) - return describe("when the user cannot be found", function() { - beforeEach(function() { - this.request.get = sinon.stub().callsArgWith(1, null, {statusCode: 404}, "nothing"); - return this.WebApiManager.getUserInfo(this.user_id, this.callback); - }); + return describe('when the user cannot be found', function() { + beforeEach(function() { + this.request.get = sinon + .stub() + .callsArgWith(1, null, { statusCode: 404 }, 'nothing') + return this.WebApiManager.getUserInfo(this.user_id, this.callback) + }) - return it("should return a null value", function() { - return this.callback - .calledWith(null, null) - .should.equal(true); - }); - }); - }); + return it('should return a null value', function() { + return this.callback.calledWith(null, null).should.equal(true) + }) + }) + }) + return describe('getProjectDetails', function() { + describe('successfully', function() { + beforeEach(function() { + this.body = JSON.stringify(this.project) + this.request.get = sinon + .stub() + .callsArgWith(1, null, { statusCode: 200 }, this.body) + return this.WebApiManager.getProjectDetails( + this.project_id, + this.callback + ) + }) - return describe("getProjectDetails", function() { - describe("successfully", function() { - beforeEach(function() { - this.body = JSON.stringify(this.project); - this.request.get = sinon.stub().callsArgWith(1, null, {statusCode: 200}, this.body); - return this.WebApiManager.getProjectDetails(this.project_id, this.callback); - }); + it('should get the project from the web api', function() { + return this.request.get + .calledWithMatch({ + url: `${this.settings.apis.web.url}/project/${this.project_id}/details`, + auth: { + user: this.settings.apis.web.user, + pass: this.settings.apis.web.pass, + sendImmediately: true + } + }) + .should.equal(true) + }) - it('should get the project from the web api', function() { - return this.request.get - .calledWithMatch({ - url: `${this.settings.apis.web.url}/project/${this.project_id}/details`, - auth: { - user: this.settings.apis.web.user, - pass: this.settings.apis.web.pass, - sendImmediately: true - } - }) - .should.equal(true); - }); + return it('should call the callback with the project', function() { + return this.callback.calledWith(null, this.project).should.equal(true) + }) + }) - return it("should call the callback with the project", function() { - return this.callback.calledWith(null, this.project).should.equal(true); - }); - }); + describe('when the web API returns an error', function() { + beforeEach(function() { + this.request.get = sinon + .stub() + .callsArgWith( + 1, + (this.error = new Error('something went wrong')), + null, + null + ) + return this.WebApiManager.getProjectDetails( + this.project_id, + this.callback + ) + }) - describe("when the web API returns an error", function() { - beforeEach(function() { - this.request.get = sinon.stub().callsArgWith(1, (this.error = new Error("something went wrong")), null, null); - return this.WebApiManager.getProjectDetails(this.project_id, this.callback); - }); + return it('should return an error to the callback', function() { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) - return it("should return an error to the callback", function() { - return this.callback.calledWith(this.error).should.equal(true); - }); - }); + return describe('when the web returns a failure error code', function() { + beforeEach(function() { + this.request.get = sinon + .stub() + .callsArgWith(1, null, { statusCode: 500, attempts: 42 }, '') + return this.WebApiManager.getProjectDetails( + this.project_id, + this.callback + ) + }) - return describe("when the web returns a failure error code", function() { - beforeEach(function() { - this.request.get = sinon.stub().callsArgWith(1, null, { statusCode: 500, attempts: 42 }, ""); - return this.WebApiManager.getProjectDetails(this.project_id, this.callback); - }); - - return it("should return the callback with an error", function() { - return this.callback - .calledWith(sinon.match.has('message', "web returned a non-success status code: 500 (attempts: 42)")) - .should.equal(true); - }); - }); - }); -}); + return it('should return the callback with an error', function() { + return this.callback + .calledWith( + sinon.match.has( + 'message', + 'web returned a non-success status code: 500 (attempts: 42)' + ) + ) + .should.equal(true) + }) + }) + }) +})