From 9a2f8ecbd48aa3cfa51638d1b26d5f865aabba4d Mon Sep 17 00:00:00 2001 From: Tim Alby Date: Wed, 6 May 2020 12:11:36 +0200 Subject: [PATCH] prettier: convert test/unit decaffeinated files to Prettier format --- .../test/unit/js/DiffCodec/DiffCodecTests.js | 144 +- .../DispatchManager/DispatchManagerTests.js | 274 +- .../DocumentManager/DocumentManagerTests.js | 1786 +++++++----- .../js/HistoryManager/HistoryManagerTests.js | 624 +++-- .../HistoryRedisManagerTests.js | 158 +- .../js/HttpController/HttpControllerTests.js | 1743 ++++++------ .../unit/js/LockManager/CheckingTheLock.js | 102 +- .../unit/js/LockManager/ReleasingTheLock.js | 151 +- .../test/unit/js/LockManager/getLockTests.js | 202 +- .../test/unit/js/LockManager/tryLockTests.js | 235 +- .../PersistenceManagerTests.js | 638 +++-- .../ProjectHistoryRedisManagerTests.js | 300 ++- .../flushAndDeleteProjectTests.js | 235 +- .../js/ProjectManager/flushProjectTests.js | 218 +- .../js/ProjectManager/getProjectDocsTests.js | 333 ++- .../js/ProjectManager/updateProjectTests.js | 574 ++-- .../js/RangesManager/RangesManagerTests.js | 874 +++--- .../js/RateLimitManager/RateLimitManager.js | 220 +- .../RealTimeRedisManagerTests.js | 253 +- .../unit/js/RedisManager/RedisManagerTests.js | 2399 ++++++++++------- .../unit/js/ShareJS/TextTransformTests.js | 706 ++--- .../test/unit/js/ShareJsDB/ShareJsDBTests.js | 230 +- .../ShareJsUpdateManagerTests.js | 367 +-- .../js/UpdateManager/UpdateManagerTests.js | 1115 ++++---- 24 files changed, 7979 insertions(+), 5902 deletions(-) diff --git a/services/document-updater/test/unit/js/DiffCodec/DiffCodecTests.js b/services/document-updater/test/unit/js/DiffCodec/DiffCodecTests.js index 85bb8c6a1b..f208c17bd6 100644 --- a/services/document-updater/test/unit/js/DiffCodec/DiffCodecTests.js +++ b/services/document-updater/test/unit/js/DiffCodec/DiffCodecTests.js @@ -10,74 +10,86 @@ * 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/DiffCodec.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/DiffCodec.js' +const SandboxedModule = require('sandboxed-module') -describe("DiffCodec", function() { - beforeEach(function() { - this.callback = sinon.stub(); - return this.DiffCodec = SandboxedModule.require(modulePath); - }); +describe('DiffCodec', function () { + beforeEach(function () { + this.callback = sinon.stub() + return (this.DiffCodec = SandboxedModule.require(modulePath)) + }) - return describe("diffAsShareJsOps", function() { - it("should insert new text correctly", function(done) { - this.before = ["hello world"]; - this.after = ["hello beautiful world"]; - return this.DiffCodec.diffAsShareJsOp(this.before, this.after, (error, ops) => { - expect(ops).to.deep.equal([{ - i: "beautiful ", - p: 6 - } - ]); - return done(); - }); - }); + return describe('diffAsShareJsOps', function () { + it('should insert new text correctly', function (done) { + this.before = ['hello world'] + this.after = ['hello beautiful world'] + return this.DiffCodec.diffAsShareJsOp( + this.before, + this.after, + (error, ops) => { + expect(ops).to.deep.equal([ + { + i: 'beautiful ', + p: 6 + } + ]) + return done() + } + ) + }) - it("should shift later inserts by previous inserts", function(done) { - this.before = ["the boy played with the ball"]; - this.after = ["the tall boy played with the red ball"]; - return this.DiffCodec.diffAsShareJsOp(this.before, this.after, (error, ops) => { - expect(ops).to.deep.equal([ - { i: "tall ", p: 4 }, - { i: "red ", p: 29 } - ]); - return done(); - }); - }); + it('should shift later inserts by previous inserts', function (done) { + this.before = ['the boy played with the ball'] + this.after = ['the tall boy played with the red ball'] + return this.DiffCodec.diffAsShareJsOp( + this.before, + this.after, + (error, ops) => { + expect(ops).to.deep.equal([ + { i: 'tall ', p: 4 }, + { i: 'red ', p: 29 } + ]) + return done() + } + ) + }) - it("should delete text correctly", function(done) { - this.before = ["hello beautiful world"]; - this.after = ["hello world"]; - return this.DiffCodec.diffAsShareJsOp(this.before, this.after, (error, ops) => { - expect(ops).to.deep.equal([{ - d: "beautiful ", - p: 6 - } - ]); - return done(); - }); - }); - - - return it("should shift later deletes by the first deletes", function(done) { - this.before = ["the tall boy played with the red ball"]; - this.after = ["the boy played with the ball"]; - return this.DiffCodec.diffAsShareJsOp(this.before, this.after, (error, ops) => { - expect(ops).to.deep.equal([ - { d: "tall ", p: 4 }, - { d: "red ", p: 24 } - ]); - return done(); - }); - }); - }); -}); - - + it('should delete text correctly', function (done) { + this.before = ['hello beautiful world'] + this.after = ['hello world'] + return this.DiffCodec.diffAsShareJsOp( + this.before, + this.after, + (error, ops) => { + expect(ops).to.deep.equal([ + { + d: 'beautiful ', + p: 6 + } + ]) + return done() + } + ) + }) + return it('should shift later deletes by the first deletes', function (done) { + this.before = ['the tall boy played with the red ball'] + this.after = ['the boy played with the ball'] + return this.DiffCodec.diffAsShareJsOp( + this.before, + this.after, + (error, ops) => { + expect(ops).to.deep.equal([ + { d: 'tall ', p: 4 }, + { d: 'red ', p: 24 } + ]) + return done() + } + ) + }) + }) +}) diff --git a/services/document-updater/test/unit/js/DispatchManager/DispatchManagerTests.js b/services/document-updater/test/unit/js/DispatchManager/DispatchManagerTests.js index 8b0c9afcf9..48eb2fbb92 100644 --- a/services/document-updater/test/unit/js/DispatchManager/DispatchManagerTests.js +++ b/services/document-updater/test/unit/js/DispatchManager/DispatchManagerTests.js @@ -11,144 +11,166 @@ * 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 modulePath = "../../../../app/js/DispatchManager.js"; -const SandboxedModule = require('sandboxed-module'); -const Errors = require("../../../../app/js/Errors.js"); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const modulePath = '../../../../app/js/DispatchManager.js' +const SandboxedModule = require('sandboxed-module') +const Errors = require('../../../../app/js/Errors.js') -describe("DispatchManager", function() { - beforeEach(function() { - this.timeout(3000); - this.DispatchManager = SandboxedModule.require(modulePath, { requires: { - "./UpdateManager" : (this.UpdateManager = {}), - "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() }), - "settings-sharelatex": (this.settings = { - redis: { - documentupdater: {} - } - }), - "redis-sharelatex": (this.redis = {}), - "./RateLimitManager": {}, - "./Errors": Errors, - "./Metrics": { - Timer() { - return {done() {}}; - } - } - } - } - ); - this.callback = sinon.stub(); - return this.RateLimiter = { run(task,cb) { return task(cb); } };}); // run task without rate limit +describe('DispatchManager', function () { + beforeEach(function () { + this.timeout(3000) + this.DispatchManager = SandboxedModule.require(modulePath, { + requires: { + './UpdateManager': (this.UpdateManager = {}), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub(), + warn: sinon.stub() + }), + 'settings-sharelatex': (this.settings = { + redis: { + documentupdater: {} + } + }), + 'redis-sharelatex': (this.redis = {}), + './RateLimitManager': {}, + './Errors': Errors, + './Metrics': { + Timer() { + return { done() {} } + } + } + } + }) + this.callback = sinon.stub() + return (this.RateLimiter = { + run(task, cb) { + return task(cb) + } + }) + }) // run task without rate limit - return describe("each worker", function() { - beforeEach(function() { - this.client = - {auth: sinon.stub()}; - this.redis.createClient = sinon.stub().returns(this.client); - return this.worker = this.DispatchManager.createDispatcher(this.RateLimiter); - }); - - it("should create a new redis client", function() { - return this.redis.createClient.called.should.equal(true); - }); - - describe("_waitForUpdateThenDispatchWorker", function() { - beforeEach(function() { - this.project_id = "project-id-123"; - this.doc_id = "doc-id-123"; - this.doc_key = `${this.project_id}:${this.doc_id}`; - return this.client.blpop = sinon.stub().callsArgWith(2, null, ["pending-updates-list", this.doc_key]); - }); + return describe('each worker', function () { + beforeEach(function () { + this.client = { auth: sinon.stub() } + this.redis.createClient = sinon.stub().returns(this.client) + return (this.worker = this.DispatchManager.createDispatcher( + this.RateLimiter + )) + }) - describe("in the normal case", function() { - beforeEach(function() { - this.UpdateManager.processOutstandingUpdatesWithLock = sinon.stub().callsArg(2); - return this.worker._waitForUpdateThenDispatchWorker(this.callback); - }); + it('should create a new redis client', function () { + return this.redis.createClient.called.should.equal(true) + }) - it("should call redis with BLPOP", function() { - return this.client.blpop - .calledWith("pending-updates-list", 0) - .should.equal(true); - }); - - it("should call processOutstandingUpdatesWithLock", function() { - return this.UpdateManager.processOutstandingUpdatesWithLock - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); + describe('_waitForUpdateThenDispatchWorker', function () { + beforeEach(function () { + this.project_id = 'project-id-123' + this.doc_id = 'doc-id-123' + this.doc_key = `${this.project_id}:${this.doc_id}` + return (this.client.blpop = sinon + .stub() + .callsArgWith(2, null, ['pending-updates-list', this.doc_key])) + }) - it("should not log any errors", function() { - this.logger.error.called.should.equal(false); - return this.logger.warn.called.should.equal(false); - }); + describe('in the normal case', function () { + beforeEach(function () { + this.UpdateManager.processOutstandingUpdatesWithLock = sinon + .stub() + .callsArg(2) + return this.worker._waitForUpdateThenDispatchWorker(this.callback) + }) - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); + it('should call redis with BLPOP', function () { + return this.client.blpop + .calledWith('pending-updates-list', 0) + .should.equal(true) + }) - describe("with an error", function() { - beforeEach(function() { - this.UpdateManager.processOutstandingUpdatesWithLock = sinon.stub().callsArgWith(2, new Error("a generic error")); - return this.worker._waitForUpdateThenDispatchWorker(this.callback); - }); + it('should call processOutstandingUpdatesWithLock', function () { + return this.UpdateManager.processOutstandingUpdatesWithLock + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) - it("should log an error", function() { - return this.logger.error.called.should.equal(true); - }); + it('should not log any errors', function () { + this.logger.error.called.should.equal(false) + return this.logger.warn.called.should.equal(false) + }) - 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("with a 'Delete component' error", function() { - beforeEach(function() { - this.UpdateManager.processOutstandingUpdatesWithLock = sinon.stub().callsArgWith(2, new Errors.DeleteMismatchError()); - return this.worker._waitForUpdateThenDispatchWorker(this.callback); - }); + describe('with an error', function () { + beforeEach(function () { + this.UpdateManager.processOutstandingUpdatesWithLock = sinon + .stub() + .callsArgWith(2, new Error('a generic error')) + return this.worker._waitForUpdateThenDispatchWorker(this.callback) + }) - it("should log a warning", function() { - return this.logger.warn.called.should.equal(true); - }); + it('should log an error', function () { + return this.logger.error.called.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("run", function() { return it("should call _waitForUpdateThenDispatchWorker until shutting down", function(done) { - let callCount = 0; - this.worker._waitForUpdateThenDispatchWorker = callback => { - if (callback == null) { callback = function(error) {}; } - callCount++; - if (callCount === 3) { - this.settings.shuttingDown = true; - } - return setTimeout(() => callback() - , 10); - }; - sinon.spy(this.worker, "_waitForUpdateThenDispatchWorker"); - - - this.worker.run(); + return describe("with a 'Delete component' error", function () { + beforeEach(function () { + this.UpdateManager.processOutstandingUpdatesWithLock = sinon + .stub() + .callsArgWith(2, new Errors.DeleteMismatchError()) + return this.worker._waitForUpdateThenDispatchWorker(this.callback) + }) - var checkStatus = () => { - if (!this.settings.shuttingDown) { // retry until shutdown - setTimeout(checkStatus, 100); - - } else { - this.worker._waitForUpdateThenDispatchWorker.callCount.should.equal(3); - return done(); - } - }; + it('should log a warning', function () { + return this.logger.warn.called.should.equal(true) + }) - return checkStatus(); - }); }); - }); -}); + return it('should call the callback', function () { + return this.callback.called.should.equal(true) + }) + }) + }) + + return describe('run', function () { + return it('should call _waitForUpdateThenDispatchWorker until shutting down', function (done) { + let callCount = 0 + this.worker._waitForUpdateThenDispatchWorker = (callback) => { + if (callback == null) { + callback = function (error) {} + } + callCount++ + if (callCount === 3) { + this.settings.shuttingDown = true + } + return setTimeout(() => callback(), 10) + } + sinon.spy(this.worker, '_waitForUpdateThenDispatchWorker') + + this.worker.run() + + var checkStatus = () => { + if (!this.settings.shuttingDown) { + // retry until shutdown + setTimeout(checkStatus, 100) + } else { + this.worker._waitForUpdateThenDispatchWorker.callCount.should.equal( + 3 + ) + return done() + } + } + + return checkStatus() + }) + }) + }) +}) diff --git a/services/document-updater/test/unit/js/DocumentManager/DocumentManagerTests.js b/services/document-updater/test/unit/js/DocumentManager/DocumentManagerTests.js index cc13bbeb7a..4baa5cee8d 100644 --- a/services/document-updater/test/unit/js/DocumentManager/DocumentManagerTests.js +++ b/services/document-updater/test/unit/js/DocumentManager/DocumentManagerTests.js @@ -12,690 +12,1102 @@ * 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 modulePath = "../../../../app/js/DocumentManager.js"; -const SandboxedModule = require('sandboxed-module'); -const Errors = require("../../../../app/js/Errors"); -const tk = require("timekeeper"); - -describe("DocumentManager", function() { - beforeEach(function() { - let Timer; - tk.freeze(new Date()); - this.DocumentManager = SandboxedModule.require(modulePath, { requires: { - "./RedisManager": (this.RedisManager = {}), - "./ProjectHistoryRedisManager": (this.ProjectHistoryRedisManager = {}), - "./PersistenceManager": (this.PersistenceManager = {}), - "./HistoryManager": (this.HistoryManager = { - flushDocChangesAsync: sinon.stub(), - flushProjectChangesAsync: sinon.stub() - }), - "logger-sharelatex": (this.logger = {log: sinon.stub(), warn: sinon.stub()}), - "./DocOpsManager": (this.DocOpsManager = {}), - "./Metrics": (this.Metrics = { - Timer: (Timer = (function() { - Timer = class Timer { - static initClass() { - this.prototype.done = sinon.stub(); - } - }; - Timer.initClass(); - return Timer; - })()) - }), - "./RealTimeRedisManager": (this.RealTimeRedisManager = {}), - "./DiffCodec": (this.DiffCodec = {}), - "./UpdateManager": (this.UpdateManager = {}), - "./RangesManager": (this.RangesManager = {}) - } - }); - this.project_id = "project-id-123"; - this.projectHistoryId = "history-id-123"; - this.projectHistoryType = "project-history"; - this.doc_id = "doc-id-123"; - this.user_id = 1234; - this.callback = sinon.stub(); - this.lines = ["one", "two", "three"]; - this.version = 42; - this.ranges = { comments: "mock", entries: "mock" }; - this.pathname = '/a/b/c.tex'; - this.unflushedTime = Date.now(); - this.lastUpdatedAt = Date.now(); - return this.lastUpdatedBy = 'last-author-id'; - }); - - afterEach(function() { return tk.reset(); }); - - describe("flushAndDeleteDoc", function() { - describe("successfully", function() { - beforeEach(function() { - this.RedisManager.removeDocFromMemory = sinon.stub().callsArg(2); - this.DocumentManager.flushDocIfLoaded = sinon.stub().callsArgWith(2); - return this.DocumentManager.flushAndDeleteDoc(this.project_id, this.doc_id, {}, this.callback); - }); - - it("should flush the doc", function() { - return this.DocumentManager.flushDocIfLoaded - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); - - it("should remove the doc from redis", function() { - return this.RedisManager.removeDocFromMemory - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); - - it("should call the callback without error", function() { - return this.callback.calledWith(null).should.equal(true); - }); - - it("should time the execution", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - - return it("should flush to the history api", function() { - return this.HistoryManager.flushDocChangesAsync - .calledWithExactly(this.project_id, this.doc_id) - .should.equal(true); - }); - }); - - return describe("when a flush error occurs", function() { - beforeEach(function() { - this.DocumentManager.flushDocIfLoaded = sinon.stub().callsArgWith(2, new Error("boom!")); - return this.RedisManager.removeDocFromMemory = sinon.stub().callsArg(2); - }); - - it("should not remove the doc from redis", function(done) { - return this.DocumentManager.flushAndDeleteDoc(this.project_id, this.doc_id, {}, error => { - error.should.exist; - this.RedisManager.removeDocFromMemory.called.should.equal(false); - return done(); - }); - }); - - return describe("when ignoring flush errors", function() { return it("should remove the doc from redis", function(done) { - return this.DocumentManager.flushAndDeleteDoc(this.project_id, this.doc_id, { ignoreFlushErrors: true }, error => { - if (error != null) { - return done(error); - } - this.RedisManager.removeDocFromMemory.called.should.equal(true); - return done(); - }); - }); }); - }); - }); - - describe("flushDocIfLoaded", function() { - describe("when the doc is in Redis", function() { - beforeEach(function() { - this.RedisManager.getDoc = sinon.stub().callsArgWith(2, null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId, this.unflushedTime, this.lastUpdatedAt, this.lastUpdatedBy); - this.RedisManager.clearUnflushedTime = sinon.stub().callsArgWith(1, null); - this.PersistenceManager.setDoc = sinon.stub().yields(); - return this.DocumentManager.flushDocIfLoaded(this.project_id, this.doc_id, this.callback); - }); - - it("should get the doc from redis", function() { - return this.RedisManager.getDoc - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); - - it("should write the doc lines to the persistence layer", function() { - return this.PersistenceManager.setDoc - .calledWith(this.project_id, this.doc_id, this.lines, this.version, this.ranges, this.lastUpdatedAt, this.lastUpdatedBy) - .should.equal(true); - }); - - it("should call the callback without error", function() { - return this.callback.calledWith(null).should.equal(true); - }); - - return it("should time the execution", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - }); - - return describe("when the document is not in Redis", function() { - beforeEach(function() { - this.RedisManager.getDoc = sinon.stub().callsArgWith(2, null, null, null, null); - this.PersistenceManager.setDoc = sinon.stub().yields(); - this.DocOpsManager.flushDocOpsToMongo = sinon.stub().callsArgWith(2); - return this.DocumentManager.flushDocIfLoaded(this.project_id, this.doc_id, this.callback); - }); - - it("should get the doc from redis", function() { - return this.RedisManager.getDoc - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); - - it("should not write anything to the persistence layer", function() { - this.PersistenceManager.setDoc.called.should.equal(false); - return this.DocOpsManager.flushDocOpsToMongo.called.should.equal(false); - }); - - it("should call the callback without error", function() { - return this.callback.calledWith(null).should.equal(true); - }); - - return it("should time the execution", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - }); - }); - - describe("getDocAndRecentOps", function() { - describe("with a previous version specified", function() { - beforeEach(function() { - this.DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId); - this.RedisManager.getPreviousDocOps = sinon.stub().callsArgWith(3, null, this.ops); - return this.DocumentManager.getDocAndRecentOps(this.project_id, this.doc_id, this.fromVersion, this.callback); - }); - - it("should get the doc", function() { - return this.DocumentManager.getDoc - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); - - it("should get the doc ops", function() { - return this.RedisManager.getPreviousDocOps - .calledWith(this.doc_id, this.fromVersion, this.version) - .should.equal(true); - }); - - it("should call the callback with the doc info", function() { - return this.callback.calledWith(null, this.lines, this.version, this.ops, this.ranges, this.pathname, this.projectHistoryId).should.equal(true); - }); - - return it("should time the execution", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - }); - - return describe("with no previous version specified", function() { - beforeEach(function() { - this.DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId); - this.RedisManager.getPreviousDocOps = sinon.stub().callsArgWith(3, null, this.ops); - return this.DocumentManager.getDocAndRecentOps(this.project_id, this.doc_id, -1, this.callback); - }); - - it("should get the doc", function() { - return this.DocumentManager.getDoc - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); - - it("should not need to get the doc ops", function() { - return this.RedisManager.getPreviousDocOps.called.should.equal(false); - }); - - it("should call the callback with the doc info", function() { - return this.callback.calledWith(null, this.lines, this.version, [], this.ranges, this.pathname, this.projectHistoryId).should.equal(true); - }); - - return it("should time the execution", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - }); - }); - - describe("getDoc", function() { - describe("when the doc exists in Redis", function() { - beforeEach(function() { - this.RedisManager.getDoc = sinon.stub().callsArgWith(2, null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId, this.unflushedTime); - return this.DocumentManager.getDoc(this.project_id, this.doc_id, this.callback); - }); - - it("should get the doc from Redis", function() { - return this.RedisManager.getDoc - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); - - it("should call the callback with the doc info", function() { - return this.callback.calledWith(null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId, this.unflushedTime, true).should.equal(true); - }); - - return it("should time the execution", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - }); - - return describe("when the doc does not exist in Redis", function() { - beforeEach(function() { - this.RedisManager.getDoc = sinon.stub().callsArgWith(2, null, null, null, null, null, null); - this.PersistenceManager.getDoc = sinon.stub().callsArgWith(2, null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId, this.projectHistoryType); - this.RedisManager.putDocInMemory = sinon.stub().yields(); - this.RedisManager.setHistoryType = sinon.stub().yields(); - return this.DocumentManager.getDoc(this.project_id, this.doc_id, this.callback); - }); - - it("should try to get the doc from Redis", function() { - return this.RedisManager.getDoc - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); - - it("should get the doc from the PersistenceManager", function() { - return this.PersistenceManager.getDoc - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); - - it("should set the doc in Redis", function() { - return this.RedisManager.putDocInMemory - .calledWith(this.project_id, this.doc_id, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId) - .should.equal(true); - }); - - it("should set the history type in Redis", function() { - return this.RedisManager.setHistoryType - .calledWith(this.doc_id, this.projectHistoryType) - .should.equal(true); - }); - - it("should call the callback with the doc info", function() { - return this.callback.calledWith(null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId, null, false).should.equal(true); - }); - - return it("should time the execution", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - }); - }); - - describe("setDoc", function() { return describe("with plain tex lines", function() { - beforeEach(function() { - this.beforeLines = ["before", "lines"]; - this.afterLines = ["after", "lines"]; - this.ops = [{ i: "foo", p: 4 }, { d: "bar", p: 42 }]; - this.DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, this.beforeLines, this.version, this.ranges, this.pathname, this.projectHistoryId, this.unflushedTime, true); - this.DiffCodec.diffAsShareJsOp = sinon.stub().callsArgWith(2, null, this.ops); - this.UpdateManager.applyUpdate = sinon.stub().callsArgWith(3, null); - this.DocumentManager.flushDocIfLoaded = sinon.stub().callsArg(2); - return this.DocumentManager.flushAndDeleteDoc = sinon.stub().callsArg(3); - }); - - describe("when already loaded", function() { - beforeEach(function() { - return this.DocumentManager.setDoc(this.project_id, this.doc_id, this.afterLines, this.source, this.user_id, false, this.callback); - }); - - it("should get the current doc lines", function() { - return this.DocumentManager.getDoc - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); - - it("should return a diff of the old and new lines", function() { - return this.DiffCodec.diffAsShareJsOp - .calledWith(this.beforeLines, this.afterLines) - .should.equal(true); - }); - - it("should apply the diff as a ShareJS op", function() { - return this.UpdateManager.applyUpdate - .calledWith( - this.project_id, - this.doc_id, - { - doc: this.doc_id, - v: this.version, - op: this.ops, - meta: { - type: "external", - source: this.source, - user_id: this.user_id - } - } - ) - .should.equal(true); - }); - - it("should flush the doc to Mongo", function() { - return this.DocumentManager.flushDocIfLoaded - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); - - it("should not flush the project history", function() { - return this.HistoryManager.flushProjectChangesAsync - .called.should.equal(false); - }); - - it("should call the callback", function() { - return this.callback.calledWith(null).should.equal(true); - }); - - return it("should time the execution", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - }); - - describe("when not already loaded", function() { - beforeEach(function() { - this.DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, this.beforeLines, this.version, this.pathname, null, false); - return this.DocumentManager.setDoc(this.project_id, this.doc_id, this.afterLines, this.source, this.user_id, false, this.callback); - }); - - it("should flush and delete the doc from the doc updater", function() { - return this.DocumentManager.flushAndDeleteDoc - .calledWith(this.project_id, this.doc_id, {}) - .should.equal(true); - }); - - return it("should not flush the project history", function() { - return this.HistoryManager.flushProjectChangesAsync - .calledWithExactly(this.project_id) - .should.equal(true); - }); - }); - - describe("without new lines", function() { - beforeEach(function() { - return this.DocumentManager.setDoc(this.project_id, this.doc_id, null, this.source, this.user_id, false, this.callback); - }); - - it("should return the callback with an error", function() { - return this.callback.calledWith(new Error("No lines were passed to setDoc")); - }); - - return it("should not try to get the doc lines", function() { - return this.DocumentManager.getDoc.called.should.equal(false); - }); - }); - - return describe("with the undoing flag", function() { - beforeEach(function() { - // Copy ops so we don't interfere with other tests - this.ops = [{ i: "foo", p: 4 }, { d: "bar", p: 42 }]; - this.DiffCodec.diffAsShareJsOp = sinon.stub().callsArgWith(2, null, this.ops); - return this.DocumentManager.setDoc(this.project_id, this.doc_id, this.afterLines, this.source, this.user_id, true, this.callback); - }); - - return it("should set the undo flag on each op", function() { - return Array.from(this.ops).map((op) => - op.u.should.equal(true)); - }); - }); - }); }); - - describe("acceptChanges", function() { - beforeEach(function() { - this.change_id = "mock-change-id"; - this.change_ids = [ "mock-change-id-1", "mock-change-id-2", "mock-change-id-3", "mock-change-id-4" ]; - this.version = 34; - this.lines = ["original", "lines"]; - this.ranges = { entries: "mock", comments: "mock" }; - this.updated_ranges = { entries: "updated", comments: "updated" }; - this.DocumentManager.getDoc = sinon.stub().yields(null, this.lines, this.version, this.ranges); - this.RangesManager.acceptChanges = sinon.stub().yields(null, this.updated_ranges); - return this.RedisManager.updateDocument = sinon.stub().yields(); - }); - - describe("successfully with a single change", function() { - beforeEach(function() { - return this.DocumentManager.acceptChanges(this.project_id, this.doc_id, [ this.change_id ], this.callback); - }); - - it("should get the document's current ranges", function() { - return this.DocumentManager.getDoc - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); - - it("should apply the accept change to the ranges", function() { - return this.RangesManager.acceptChanges - .calledWith([ this.change_id ], this.ranges) - .should.equal(true); - }); - - it("should save the updated ranges", function() { - return this.RedisManager.updateDocument - .calledWith(this.project_id, this.doc_id, this.lines, this.version, [], this.updated_ranges, {}) - .should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - - describe("successfully with multiple changes", function() { - beforeEach(function() { - return this.DocumentManager.acceptChanges(this.project_id, this.doc_id, this.change_ids, this.callback); - }); - - return it("should apply the accept change to the ranges", function() { - return this.RangesManager.acceptChanges - .calledWith(this.change_ids, this.ranges) - .should.equal(true); - }); - }); - - return describe("when the doc is not found", function() { - beforeEach(function() { - this.DocumentManager.getDoc = sinon.stub().yields(null, null, null, null); - return this.DocumentManager.acceptChanges(this.project_id, this.doc_id, [ this.change_id ], this.callback); - }); - - it("should not save anything", function() { - return this.RedisManager.updateDocument.called.should.equal(false); - }); - - return it("should call the callback with a not found error", function() { - const error = new Errors.NotFoundError(`document not found: ${this.doc_id}`); - return this.callback.calledWith(error).should.equal(true); - }); - }); - }); - - describe("deleteComment", function() { - beforeEach(function() { - this.comment_id = "mock-comment-id"; - this.version = 34; - this.lines = ["original", "lines"]; - this.ranges = { comments: ["one", "two", "three"] }; - this.updated_ranges = { comments: ["one", "three"] }; - this.DocumentManager.getDoc = sinon.stub().yields(null, this.lines, this.version, this.ranges); - this.RangesManager.deleteComment = sinon.stub().yields(null, this.updated_ranges); - return this.RedisManager.updateDocument = sinon.stub().yields(); - }); - - describe("successfully", function() { - beforeEach(function() { - return this.DocumentManager.deleteComment(this.project_id, this.doc_id, this.comment_id, this.callback); - }); - - it("should get the document's current ranges", function() { - return this.DocumentManager.getDoc - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); - - it("should delete the comment from the ranges", function() { - return this.RangesManager.deleteComment - .calledWith(this.comment_id, this.ranges) - .should.equal(true); - }); - - it("should save the updated ranges", function() { - return this.RedisManager.updateDocument - .calledWith(this.project_id, this.doc_id, this.lines, this.version, [], this.updated_ranges, {}) - .should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - - return describe("when the doc is not found", function() { - beforeEach(function() { - this.DocumentManager.getDoc = sinon.stub().yields(null, null, null, null); - return this.DocumentManager.acceptChanges(this.project_id, this.doc_id, [ this.comment_id ], this.callback); - }); - - it("should not save anything", function() { - return this.RedisManager.updateDocument.called.should.equal(false); - }); - - return it("should call the callback with a not found error", function() { - const error = new Errors.NotFoundError(`document not found: ${this.doc_id}`); - return this.callback.calledWith(error).should.equal(true); - }); - }); - }); - - describe("getDocAndFlushIfOld", function() { - beforeEach(function() { - return this.DocumentManager.flushDocIfLoaded = sinon.stub().callsArg(2); - }); - - describe("when the doc is in Redis", function() { - describe("and has changes to be flushed", function() { - beforeEach(function() { - this.DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, this.lines, this.version, this.ranges, this.projectHistoryId, this.pathname, Date.now() - 1e9, true); - return this.DocumentManager.getDocAndFlushIfOld(this.project_id, this.doc_id, this.callback); - }); - - it("should get the doc", function() { - return this.DocumentManager.getDoc - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); - - it("should flush the doc", function() { - return this.DocumentManager.flushDocIfLoaded - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); - - return it("should call the callback with the lines and versions", function() { - return this.callback.calledWith(null, this.lines, this.version).should.equal(true); - }); - }); - - return describe("and has only changes that don't need to be flushed", function() { - beforeEach(function() { - this.DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, this.lines, this.version, this.ranges, this.pathname, Date.now() - 100, true); - return this.DocumentManager.getDocAndFlushIfOld(this.project_id, this.doc_id, this.callback); - }); - - it("should get the doc", function() { - return this.DocumentManager.getDoc - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); - - it("should not flush the doc", function() { - return this.DocumentManager.flushDocIfLoaded - .called.should.equal(false); - }); - - return it("should call the callback with the lines and versions", function() { - return this.callback.calledWith(null, this.lines, this.version).should.equal(true); - }); - }); - }); - - return describe("when the doc is not in Redis", function() { - beforeEach(function() { - this.DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, this.lines, this.version, this.ranges, null, false); - return this.DocumentManager.getDocAndFlushIfOld(this.project_id, this.doc_id, this.callback); - }); - - it("should get the doc", function() { - return this.DocumentManager.getDoc - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); - - it("should not flush the doc", function() { - return this.DocumentManager.flushDocIfLoaded - .called.should.equal(false); - }); - - return it("should call the callback with the lines and versions", function() { - return this.callback.calledWith(null, this.lines, this.version).should.equal(true); - }); - }); - }); - - describe("renameDoc", function() { - beforeEach(function() { - this.update = 'some-update'; - return this.RedisManager.renameDoc = sinon.stub().yields(); - }); - - return describe("successfully", function() { - beforeEach(function() { - return this.DocumentManager.renameDoc(this.project_id, this.doc_id, this.user_id, this.update, this.projectHistoryId, this.callback); - }); - - it("should rename the document", function() { - return this.RedisManager.renameDoc - .calledWith(this.project_id, this.doc_id, this.user_id, this.update, this.projectHistoryId) - .should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - }); - - return describe("resyncDocContents", function() { - describe("when doc is loaded in redis", function() { - beforeEach(function() { - this.RedisManager.getDoc = sinon.stub().callsArgWith(2, null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId); - this.ProjectHistoryRedisManager.queueResyncDocContent = sinon.stub(); - return this.DocumentManager.resyncDocContents(this.project_id, this.doc_id, this.callback); - }); - - it("gets the doc contents from redis", function() { - return this.RedisManager.getDoc - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); - - return it("queues a resync doc content update", function() { - return this.ProjectHistoryRedisManager.queueResyncDocContent - .calledWith(this.project_id, this.projectHistoryId, this.doc_id, this.lines, this.version, this.pathname, this.callback) - .should.equal(true); - }); - }); - - return describe("when doc is not loaded in redis", function() { - beforeEach(function() { - this.RedisManager.getDoc = sinon.stub().callsArgWith(2, null); - this.PersistenceManager.getDoc = sinon.stub().callsArgWith(2, null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId); - this.ProjectHistoryRedisManager.queueResyncDocContent = sinon.stub(); - return this.DocumentManager.resyncDocContents(this.project_id, this.doc_id, this.callback); - }); - - it("tries to get the doc contents from redis", function() { - return this.RedisManager.getDoc - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); - - it("gets the doc contents from web", function() { - return this.PersistenceManager.getDoc - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); - - return it("queues a resync doc content update", function() { - return this.ProjectHistoryRedisManager.queueResyncDocContent - .calledWith(this.project_id, this.projectHistoryId, this.doc_id, this.lines, this.version, this.pathname, this.callback) - .should.equal(true); - }); - }); - }); -}); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const modulePath = '../../../../app/js/DocumentManager.js' +const SandboxedModule = require('sandboxed-module') +const Errors = require('../../../../app/js/Errors') +const tk = require('timekeeper') + +describe('DocumentManager', function () { + beforeEach(function () { + let Timer + tk.freeze(new Date()) + this.DocumentManager = SandboxedModule.require(modulePath, { + requires: { + './RedisManager': (this.RedisManager = {}), + './ProjectHistoryRedisManager': (this.ProjectHistoryRedisManager = {}), + './PersistenceManager': (this.PersistenceManager = {}), + './HistoryManager': (this.HistoryManager = { + flushDocChangesAsync: sinon.stub(), + flushProjectChangesAsync: sinon.stub() + }), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + warn: sinon.stub() + }), + './DocOpsManager': (this.DocOpsManager = {}), + './Metrics': (this.Metrics = { + Timer: (Timer = (function () { + Timer = class Timer { + static initClass() { + this.prototype.done = sinon.stub() + } + } + Timer.initClass() + return Timer + })()) + }), + './RealTimeRedisManager': (this.RealTimeRedisManager = {}), + './DiffCodec': (this.DiffCodec = {}), + './UpdateManager': (this.UpdateManager = {}), + './RangesManager': (this.RangesManager = {}) + } + }) + this.project_id = 'project-id-123' + this.projectHistoryId = 'history-id-123' + this.projectHistoryType = 'project-history' + this.doc_id = 'doc-id-123' + this.user_id = 1234 + this.callback = sinon.stub() + this.lines = ['one', 'two', 'three'] + this.version = 42 + this.ranges = { comments: 'mock', entries: 'mock' } + this.pathname = '/a/b/c.tex' + this.unflushedTime = Date.now() + this.lastUpdatedAt = Date.now() + return (this.lastUpdatedBy = 'last-author-id') + }) + + afterEach(function () { + return tk.reset() + }) + + describe('flushAndDeleteDoc', function () { + describe('successfully', function () { + beforeEach(function () { + this.RedisManager.removeDocFromMemory = sinon.stub().callsArg(2) + this.DocumentManager.flushDocIfLoaded = sinon.stub().callsArgWith(2) + return this.DocumentManager.flushAndDeleteDoc( + this.project_id, + this.doc_id, + {}, + this.callback + ) + }) + + it('should flush the doc', function () { + return this.DocumentManager.flushDocIfLoaded + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) + + it('should remove the doc from redis', function () { + return this.RedisManager.removeDocFromMemory + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) + + it('should call the callback without error', function () { + return this.callback.calledWith(null).should.equal(true) + }) + + it('should time the execution', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + + return it('should flush to the history api', function () { + return this.HistoryManager.flushDocChangesAsync + .calledWithExactly(this.project_id, this.doc_id) + .should.equal(true) + }) + }) + + return describe('when a flush error occurs', function () { + beforeEach(function () { + this.DocumentManager.flushDocIfLoaded = sinon + .stub() + .callsArgWith(2, new Error('boom!')) + return (this.RedisManager.removeDocFromMemory = sinon + .stub() + .callsArg(2)) + }) + + it('should not remove the doc from redis', function (done) { + return this.DocumentManager.flushAndDeleteDoc( + this.project_id, + this.doc_id, + {}, + (error) => { + error.should.exist + this.RedisManager.removeDocFromMemory.called.should.equal(false) + return done() + } + ) + }) + + return describe('when ignoring flush errors', function () { + return it('should remove the doc from redis', function (done) { + return this.DocumentManager.flushAndDeleteDoc( + this.project_id, + this.doc_id, + { ignoreFlushErrors: true }, + (error) => { + if (error != null) { + return done(error) + } + this.RedisManager.removeDocFromMemory.called.should.equal(true) + return done() + } + ) + }) + }) + }) + }) + + describe('flushDocIfLoaded', function () { + describe('when the doc is in Redis', function () { + beforeEach(function () { + this.RedisManager.getDoc = sinon + .stub() + .callsArgWith( + 2, + null, + this.lines, + this.version, + this.ranges, + this.pathname, + this.projectHistoryId, + this.unflushedTime, + this.lastUpdatedAt, + this.lastUpdatedBy + ) + this.RedisManager.clearUnflushedTime = sinon + .stub() + .callsArgWith(1, null) + this.PersistenceManager.setDoc = sinon.stub().yields() + return this.DocumentManager.flushDocIfLoaded( + this.project_id, + this.doc_id, + this.callback + ) + }) + + it('should get the doc from redis', function () { + return this.RedisManager.getDoc + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) + + it('should write the doc lines to the persistence layer', function () { + return this.PersistenceManager.setDoc + .calledWith( + this.project_id, + this.doc_id, + this.lines, + this.version, + this.ranges, + this.lastUpdatedAt, + this.lastUpdatedBy + ) + .should.equal(true) + }) + + it('should call the callback without error', function () { + return this.callback.calledWith(null).should.equal(true) + }) + + return it('should time the execution', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) + + return describe('when the document is not in Redis', function () { + beforeEach(function () { + this.RedisManager.getDoc = sinon + .stub() + .callsArgWith(2, null, null, null, null) + this.PersistenceManager.setDoc = sinon.stub().yields() + this.DocOpsManager.flushDocOpsToMongo = sinon.stub().callsArgWith(2) + return this.DocumentManager.flushDocIfLoaded( + this.project_id, + this.doc_id, + this.callback + ) + }) + + it('should get the doc from redis', function () { + return this.RedisManager.getDoc + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) + + it('should not write anything to the persistence layer', function () { + this.PersistenceManager.setDoc.called.should.equal(false) + return this.DocOpsManager.flushDocOpsToMongo.called.should.equal(false) + }) + + it('should call the callback without error', function () { + return this.callback.calledWith(null).should.equal(true) + }) + + return it('should time the execution', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) + }) + + describe('getDocAndRecentOps', function () { + describe('with a previous version specified', function () { + beforeEach(function () { + this.DocumentManager.getDoc = sinon + .stub() + .callsArgWith( + 2, + null, + this.lines, + this.version, + this.ranges, + this.pathname, + this.projectHistoryId + ) + this.RedisManager.getPreviousDocOps = sinon + .stub() + .callsArgWith(3, null, this.ops) + return this.DocumentManager.getDocAndRecentOps( + this.project_id, + this.doc_id, + this.fromVersion, + this.callback + ) + }) + + it('should get the doc', function () { + return this.DocumentManager.getDoc + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) + + it('should get the doc ops', function () { + return this.RedisManager.getPreviousDocOps + .calledWith(this.doc_id, this.fromVersion, this.version) + .should.equal(true) + }) + + it('should call the callback with the doc info', function () { + return this.callback + .calledWith( + null, + this.lines, + this.version, + this.ops, + this.ranges, + this.pathname, + this.projectHistoryId + ) + .should.equal(true) + }) + + return it('should time the execution', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) + + return describe('with no previous version specified', function () { + beforeEach(function () { + this.DocumentManager.getDoc = sinon + .stub() + .callsArgWith( + 2, + null, + this.lines, + this.version, + this.ranges, + this.pathname, + this.projectHistoryId + ) + this.RedisManager.getPreviousDocOps = sinon + .stub() + .callsArgWith(3, null, this.ops) + return this.DocumentManager.getDocAndRecentOps( + this.project_id, + this.doc_id, + -1, + this.callback + ) + }) + + it('should get the doc', function () { + return this.DocumentManager.getDoc + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) + + it('should not need to get the doc ops', function () { + return this.RedisManager.getPreviousDocOps.called.should.equal(false) + }) + + it('should call the callback with the doc info', function () { + return this.callback + .calledWith( + null, + this.lines, + this.version, + [], + this.ranges, + this.pathname, + this.projectHistoryId + ) + .should.equal(true) + }) + + return it('should time the execution', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) + }) + + describe('getDoc', function () { + describe('when the doc exists in Redis', function () { + beforeEach(function () { + this.RedisManager.getDoc = sinon + .stub() + .callsArgWith( + 2, + null, + this.lines, + this.version, + this.ranges, + this.pathname, + this.projectHistoryId, + this.unflushedTime + ) + return this.DocumentManager.getDoc( + this.project_id, + this.doc_id, + this.callback + ) + }) + + it('should get the doc from Redis', function () { + return this.RedisManager.getDoc + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) + + it('should call the callback with the doc info', function () { + return this.callback + .calledWith( + null, + this.lines, + this.version, + this.ranges, + this.pathname, + this.projectHistoryId, + this.unflushedTime, + true + ) + .should.equal(true) + }) + + return it('should time the execution', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) + + return describe('when the doc does not exist in Redis', function () { + beforeEach(function () { + this.RedisManager.getDoc = sinon + .stub() + .callsArgWith(2, null, null, null, null, null, null) + this.PersistenceManager.getDoc = sinon + .stub() + .callsArgWith( + 2, + null, + this.lines, + this.version, + this.ranges, + this.pathname, + this.projectHistoryId, + this.projectHistoryType + ) + this.RedisManager.putDocInMemory = sinon.stub().yields() + this.RedisManager.setHistoryType = sinon.stub().yields() + return this.DocumentManager.getDoc( + this.project_id, + this.doc_id, + this.callback + ) + }) + + it('should try to get the doc from Redis', function () { + return this.RedisManager.getDoc + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) + + it('should get the doc from the PersistenceManager', function () { + return this.PersistenceManager.getDoc + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) + + it('should set the doc in Redis', function () { + return this.RedisManager.putDocInMemory + .calledWith( + this.project_id, + this.doc_id, + this.lines, + this.version, + this.ranges, + this.pathname, + this.projectHistoryId + ) + .should.equal(true) + }) + + it('should set the history type in Redis', function () { + return this.RedisManager.setHistoryType + .calledWith(this.doc_id, this.projectHistoryType) + .should.equal(true) + }) + + it('should call the callback with the doc info', function () { + return this.callback + .calledWith( + null, + this.lines, + this.version, + this.ranges, + this.pathname, + this.projectHistoryId, + null, + false + ) + .should.equal(true) + }) + + return it('should time the execution', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) + }) + + describe('setDoc', function () { + return describe('with plain tex lines', function () { + beforeEach(function () { + this.beforeLines = ['before', 'lines'] + this.afterLines = ['after', 'lines'] + this.ops = [ + { i: 'foo', p: 4 }, + { d: 'bar', p: 42 } + ] + this.DocumentManager.getDoc = sinon + .stub() + .callsArgWith( + 2, + null, + this.beforeLines, + this.version, + this.ranges, + this.pathname, + this.projectHistoryId, + this.unflushedTime, + true + ) + this.DiffCodec.diffAsShareJsOp = sinon + .stub() + .callsArgWith(2, null, this.ops) + this.UpdateManager.applyUpdate = sinon.stub().callsArgWith(3, null) + this.DocumentManager.flushDocIfLoaded = sinon.stub().callsArg(2) + return (this.DocumentManager.flushAndDeleteDoc = sinon + .stub() + .callsArg(3)) + }) + + describe('when already loaded', function () { + beforeEach(function () { + return this.DocumentManager.setDoc( + this.project_id, + this.doc_id, + this.afterLines, + this.source, + this.user_id, + false, + this.callback + ) + }) + + it('should get the current doc lines', function () { + return this.DocumentManager.getDoc + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) + + it('should return a diff of the old and new lines', function () { + return this.DiffCodec.diffAsShareJsOp + .calledWith(this.beforeLines, this.afterLines) + .should.equal(true) + }) + + it('should apply the diff as a ShareJS op', function () { + return this.UpdateManager.applyUpdate + .calledWith(this.project_id, this.doc_id, { + doc: this.doc_id, + v: this.version, + op: this.ops, + meta: { + type: 'external', + source: this.source, + user_id: this.user_id + } + }) + .should.equal(true) + }) + + it('should flush the doc to Mongo', function () { + return this.DocumentManager.flushDocIfLoaded + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) + + it('should not flush the project history', function () { + return this.HistoryManager.flushProjectChangesAsync.called.should.equal( + false + ) + }) + + it('should call the callback', function () { + return this.callback.calledWith(null).should.equal(true) + }) + + return it('should time the execution', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) + + describe('when not already loaded', function () { + beforeEach(function () { + this.DocumentManager.getDoc = sinon + .stub() + .callsArgWith( + 2, + null, + this.beforeLines, + this.version, + this.pathname, + null, + false + ) + return this.DocumentManager.setDoc( + this.project_id, + this.doc_id, + this.afterLines, + this.source, + this.user_id, + false, + this.callback + ) + }) + + it('should flush and delete the doc from the doc updater', function () { + return this.DocumentManager.flushAndDeleteDoc + .calledWith(this.project_id, this.doc_id, {}) + .should.equal(true) + }) + + return it('should not flush the project history', function () { + return this.HistoryManager.flushProjectChangesAsync + .calledWithExactly(this.project_id) + .should.equal(true) + }) + }) + + describe('without new lines', function () { + beforeEach(function () { + return this.DocumentManager.setDoc( + this.project_id, + this.doc_id, + null, + this.source, + this.user_id, + false, + this.callback + ) + }) + + it('should return the callback with an error', function () { + return this.callback.calledWith( + new Error('No lines were passed to setDoc') + ) + }) + + return it('should not try to get the doc lines', function () { + return this.DocumentManager.getDoc.called.should.equal(false) + }) + }) + + return describe('with the undoing flag', function () { + beforeEach(function () { + // Copy ops so we don't interfere with other tests + this.ops = [ + { i: 'foo', p: 4 }, + { d: 'bar', p: 42 } + ] + this.DiffCodec.diffAsShareJsOp = sinon + .stub() + .callsArgWith(2, null, this.ops) + return this.DocumentManager.setDoc( + this.project_id, + this.doc_id, + this.afterLines, + this.source, + this.user_id, + true, + this.callback + ) + }) + + return it('should set the undo flag on each op', function () { + return Array.from(this.ops).map((op) => op.u.should.equal(true)) + }) + }) + }) + }) + + describe('acceptChanges', function () { + beforeEach(function () { + this.change_id = 'mock-change-id' + this.change_ids = [ + 'mock-change-id-1', + 'mock-change-id-2', + 'mock-change-id-3', + 'mock-change-id-4' + ] + this.version = 34 + this.lines = ['original', 'lines'] + this.ranges = { entries: 'mock', comments: 'mock' } + this.updated_ranges = { entries: 'updated', comments: 'updated' } + this.DocumentManager.getDoc = sinon + .stub() + .yields(null, this.lines, this.version, this.ranges) + this.RangesManager.acceptChanges = sinon + .stub() + .yields(null, this.updated_ranges) + return (this.RedisManager.updateDocument = sinon.stub().yields()) + }) + + describe('successfully with a single change', function () { + beforeEach(function () { + return this.DocumentManager.acceptChanges( + this.project_id, + this.doc_id, + [this.change_id], + this.callback + ) + }) + + it("should get the document's current ranges", function () { + return this.DocumentManager.getDoc + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) + + it('should apply the accept change to the ranges', function () { + return this.RangesManager.acceptChanges + .calledWith([this.change_id], this.ranges) + .should.equal(true) + }) + + it('should save the updated ranges', function () { + return this.RedisManager.updateDocument + .calledWith( + this.project_id, + this.doc_id, + this.lines, + this.version, + [], + this.updated_ranges, + {} + ) + .should.equal(true) + }) + + return it('should call the callback', function () { + return this.callback.called.should.equal(true) + }) + }) + + describe('successfully with multiple changes', function () { + beforeEach(function () { + return this.DocumentManager.acceptChanges( + this.project_id, + this.doc_id, + this.change_ids, + this.callback + ) + }) + + return it('should apply the accept change to the ranges', function () { + return this.RangesManager.acceptChanges + .calledWith(this.change_ids, this.ranges) + .should.equal(true) + }) + }) + + return describe('when the doc is not found', function () { + beforeEach(function () { + this.DocumentManager.getDoc = sinon + .stub() + .yields(null, null, null, null) + return this.DocumentManager.acceptChanges( + this.project_id, + this.doc_id, + [this.change_id], + this.callback + ) + }) + + it('should not save anything', function () { + return this.RedisManager.updateDocument.called.should.equal(false) + }) + + return it('should call the callback with a not found error', function () { + const error = new Errors.NotFoundError( + `document not found: ${this.doc_id}` + ) + return this.callback.calledWith(error).should.equal(true) + }) + }) + }) + + describe('deleteComment', function () { + beforeEach(function () { + this.comment_id = 'mock-comment-id' + this.version = 34 + this.lines = ['original', 'lines'] + this.ranges = { comments: ['one', 'two', 'three'] } + this.updated_ranges = { comments: ['one', 'three'] } + this.DocumentManager.getDoc = sinon + .stub() + .yields(null, this.lines, this.version, this.ranges) + this.RangesManager.deleteComment = sinon + .stub() + .yields(null, this.updated_ranges) + return (this.RedisManager.updateDocument = sinon.stub().yields()) + }) + + describe('successfully', function () { + beforeEach(function () { + return this.DocumentManager.deleteComment( + this.project_id, + this.doc_id, + this.comment_id, + this.callback + ) + }) + + it("should get the document's current ranges", function () { + return this.DocumentManager.getDoc + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) + + it('should delete the comment from the ranges', function () { + return this.RangesManager.deleteComment + .calledWith(this.comment_id, this.ranges) + .should.equal(true) + }) + + it('should save the updated ranges', function () { + return this.RedisManager.updateDocument + .calledWith( + this.project_id, + this.doc_id, + this.lines, + this.version, + [], + this.updated_ranges, + {} + ) + .should.equal(true) + }) + + return it('should call the callback', function () { + return this.callback.called.should.equal(true) + }) + }) + + return describe('when the doc is not found', function () { + beforeEach(function () { + this.DocumentManager.getDoc = sinon + .stub() + .yields(null, null, null, null) + return this.DocumentManager.acceptChanges( + this.project_id, + this.doc_id, + [this.comment_id], + this.callback + ) + }) + + it('should not save anything', function () { + return this.RedisManager.updateDocument.called.should.equal(false) + }) + + return it('should call the callback with a not found error', function () { + const error = new Errors.NotFoundError( + `document not found: ${this.doc_id}` + ) + return this.callback.calledWith(error).should.equal(true) + }) + }) + }) + + describe('getDocAndFlushIfOld', function () { + beforeEach(function () { + return (this.DocumentManager.flushDocIfLoaded = sinon.stub().callsArg(2)) + }) + + describe('when the doc is in Redis', function () { + describe('and has changes to be flushed', function () { + beforeEach(function () { + this.DocumentManager.getDoc = sinon + .stub() + .callsArgWith( + 2, + null, + this.lines, + this.version, + this.ranges, + this.projectHistoryId, + this.pathname, + Date.now() - 1e9, + true + ) + return this.DocumentManager.getDocAndFlushIfOld( + this.project_id, + this.doc_id, + this.callback + ) + }) + + it('should get the doc', function () { + return this.DocumentManager.getDoc + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) + + it('should flush the doc', function () { + return this.DocumentManager.flushDocIfLoaded + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) + + return it('should call the callback with the lines and versions', function () { + return this.callback + .calledWith(null, this.lines, this.version) + .should.equal(true) + }) + }) + + return describe("and has only changes that don't need to be flushed", function () { + beforeEach(function () { + this.DocumentManager.getDoc = sinon + .stub() + .callsArgWith( + 2, + null, + this.lines, + this.version, + this.ranges, + this.pathname, + Date.now() - 100, + true + ) + return this.DocumentManager.getDocAndFlushIfOld( + this.project_id, + this.doc_id, + this.callback + ) + }) + + it('should get the doc', function () { + return this.DocumentManager.getDoc + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) + + it('should not flush the doc', function () { + return this.DocumentManager.flushDocIfLoaded.called.should.equal( + false + ) + }) + + return it('should call the callback with the lines and versions', function () { + return this.callback + .calledWith(null, this.lines, this.version) + .should.equal(true) + }) + }) + }) + + return describe('when the doc is not in Redis', function () { + beforeEach(function () { + this.DocumentManager.getDoc = sinon + .stub() + .callsArgWith( + 2, + null, + this.lines, + this.version, + this.ranges, + null, + false + ) + return this.DocumentManager.getDocAndFlushIfOld( + this.project_id, + this.doc_id, + this.callback + ) + }) + + it('should get the doc', function () { + return this.DocumentManager.getDoc + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) + + it('should not flush the doc', function () { + return this.DocumentManager.flushDocIfLoaded.called.should.equal(false) + }) + + return it('should call the callback with the lines and versions', function () { + return this.callback + .calledWith(null, this.lines, this.version) + .should.equal(true) + }) + }) + }) + + describe('renameDoc', function () { + beforeEach(function () { + this.update = 'some-update' + return (this.RedisManager.renameDoc = sinon.stub().yields()) + }) + + return describe('successfully', function () { + beforeEach(function () { + return this.DocumentManager.renameDoc( + this.project_id, + this.doc_id, + this.user_id, + this.update, + this.projectHistoryId, + this.callback + ) + }) + + it('should rename the document', function () { + return this.RedisManager.renameDoc + .calledWith( + this.project_id, + this.doc_id, + this.user_id, + this.update, + this.projectHistoryId + ) + .should.equal(true) + }) + + return it('should call the callback', function () { + return this.callback.called.should.equal(true) + }) + }) + }) + + return describe('resyncDocContents', function () { + describe('when doc is loaded in redis', function () { + beforeEach(function () { + this.RedisManager.getDoc = sinon + .stub() + .callsArgWith( + 2, + null, + this.lines, + this.version, + this.ranges, + this.pathname, + this.projectHistoryId + ) + this.ProjectHistoryRedisManager.queueResyncDocContent = sinon.stub() + return this.DocumentManager.resyncDocContents( + this.project_id, + this.doc_id, + this.callback + ) + }) + + it('gets the doc contents from redis', function () { + return this.RedisManager.getDoc + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) + + return it('queues a resync doc content update', function () { + return this.ProjectHistoryRedisManager.queueResyncDocContent + .calledWith( + this.project_id, + this.projectHistoryId, + this.doc_id, + this.lines, + this.version, + this.pathname, + this.callback + ) + .should.equal(true) + }) + }) + + return describe('when doc is not loaded in redis', function () { + beforeEach(function () { + this.RedisManager.getDoc = sinon.stub().callsArgWith(2, null) + this.PersistenceManager.getDoc = sinon + .stub() + .callsArgWith( + 2, + null, + this.lines, + this.version, + this.ranges, + this.pathname, + this.projectHistoryId + ) + this.ProjectHistoryRedisManager.queueResyncDocContent = sinon.stub() + return this.DocumentManager.resyncDocContents( + this.project_id, + this.doc_id, + this.callback + ) + }) + + it('tries to get the doc contents from redis', function () { + return this.RedisManager.getDoc + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) + + it('gets the doc contents from web', function () { + return this.PersistenceManager.getDoc + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) + + return it('queues a resync doc content update', function () { + return this.ProjectHistoryRedisManager.queueResyncDocContent + .calledWith( + this.project_id, + this.projectHistoryId, + this.doc_id, + this.lines, + this.version, + this.pathname, + this.callback + ) + .should.equal(true) + }) + }) + }) +}) diff --git a/services/document-updater/test/unit/js/HistoryManager/HistoryManagerTests.js b/services/document-updater/test/unit/js/HistoryManager/HistoryManagerTests.js index 64a07b06ae..263f1cd094 100644 --- a/services/document-updater/test/unit/js/HistoryManager/HistoryManagerTests.js +++ b/services/document-updater/test/unit/js/HistoryManager/HistoryManagerTests.js @@ -9,305 +9,417 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const SandboxedModule = require('sandboxed-module'); -const sinon = require('sinon'); -require('chai').should(); -const modulePath = require('path').join(__dirname, '../../../../app/js/HistoryManager'); +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const modulePath = require('path').join( + __dirname, + '../../../../app/js/HistoryManager' +) -describe("HistoryManager", function() { - beforeEach(function() { - this.HistoryManager = SandboxedModule.require(modulePath, { requires: { - "request": (this.request = {}), - "settings-sharelatex": (this.Settings = { - apis: { - project_history: { - enabled: true, - url: "http://project_history.example.com" - }, - trackchanges: { - url: "http://trackchanges.example.com" - } - } - }), - "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub(), debug: sinon.stub() }), - "./DocumentManager": (this.DocumentManager = {}), - "./HistoryRedisManager": (this.HistoryRedisManager = {}), - "./RedisManager": (this.RedisManager = {}), - "./ProjectHistoryRedisManager": (this.ProjectHistoryRedisManager = {}), - "./Metrics": (this.metrics = {inc: sinon.stub()}) - } - }); - this.project_id = "mock-project-id"; - this.doc_id = "mock-doc-id"; - return this.callback = sinon.stub(); - }); +describe('HistoryManager', function () { + beforeEach(function () { + this.HistoryManager = SandboxedModule.require(modulePath, { + requires: { + request: (this.request = {}), + 'settings-sharelatex': (this.Settings = { + apis: { + project_history: { + enabled: true, + url: 'http://project_history.example.com' + }, + trackchanges: { + url: 'http://trackchanges.example.com' + } + } + }), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub(), + debug: sinon.stub() + }), + './DocumentManager': (this.DocumentManager = {}), + './HistoryRedisManager': (this.HistoryRedisManager = {}), + './RedisManager': (this.RedisManager = {}), + './ProjectHistoryRedisManager': (this.ProjectHistoryRedisManager = {}), + './Metrics': (this.metrics = { inc: sinon.stub() }) + } + }) + this.project_id = 'mock-project-id' + this.doc_id = 'mock-doc-id' + return (this.callback = sinon.stub()) + }) - describe("flushDocChangesAsync", function() { - beforeEach(function() { - return this.request.post = sinon.stub().callsArgWith(1, null, {statusCode: 204}); - }); + describe('flushDocChangesAsync', function () { + beforeEach(function () { + return (this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 204 })) + }) - describe("when the project uses track changes", function() { - beforeEach(function() { - this.RedisManager.getHistoryType = sinon.stub().yields(null, 'track-changes'); - return this.HistoryManager.flushDocChangesAsync(this.project_id, this.doc_id); - }); + describe('when the project uses track changes', function () { + beforeEach(function () { + this.RedisManager.getHistoryType = sinon + .stub() + .yields(null, 'track-changes') + return this.HistoryManager.flushDocChangesAsync( + this.project_id, + this.doc_id + ) + }) - return it("should send a request to the track changes api", function() { - return this.request.post - .calledWith(`${this.Settings.apis.trackchanges.url}/project/${this.project_id}/doc/${this.doc_id}/flush`) - .should.equal(true); - }); - }); + return it('should send a request to the track changes api', function () { + return this.request.post + .calledWith( + `${this.Settings.apis.trackchanges.url}/project/${this.project_id}/doc/${this.doc_id}/flush` + ) + .should.equal(true) + }) + }) - describe("when the project uses project history and double flush is not disabled", function() { - beforeEach(function() { - this.RedisManager.getHistoryType = sinon.stub().yields(null, 'project-history'); - return this.HistoryManager.flushDocChangesAsync(this.project_id, this.doc_id); - }); + describe('when the project uses project history and double flush is not disabled', function () { + beforeEach(function () { + this.RedisManager.getHistoryType = sinon + .stub() + .yields(null, 'project-history') + return this.HistoryManager.flushDocChangesAsync( + this.project_id, + this.doc_id + ) + }) - return it("should send a request to the track changes api", function() { - return this.request.post - .called - .should.equal(true); - }); - }); + return it('should send a request to the track changes api', function () { + return this.request.post.called.should.equal(true) + }) + }) - return describe("when the project uses project history and double flush is disabled", function() { - beforeEach(function() { - this.Settings.disableDoubleFlush = true; - this.RedisManager.getHistoryType = sinon.stub().yields(null, 'project-history'); - return this.HistoryManager.flushDocChangesAsync(this.project_id, this.doc_id); - }); + return describe('when the project uses project history and double flush is disabled', function () { + beforeEach(function () { + this.Settings.disableDoubleFlush = true + this.RedisManager.getHistoryType = sinon + .stub() + .yields(null, 'project-history') + return this.HistoryManager.flushDocChangesAsync( + this.project_id, + this.doc_id + ) + }) - return it("should not send a request to the track changes api", function() { - return this.request.post - .called - .should.equal(false); - }); - }); - }); + return it('should not send a request to the track changes api', function () { + return this.request.post.called.should.equal(false) + }) + }) + }) + describe('flushProjectChangesAsync', function () { + beforeEach(function () { + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 204 }) - describe("flushProjectChangesAsync", function() { - beforeEach(function() { - this.request.post = sinon.stub().callsArgWith(1, null, {statusCode: 204}); + return this.HistoryManager.flushProjectChangesAsync(this.project_id) + }) - return this.HistoryManager.flushProjectChangesAsync(this.project_id); - }); + return it('should send a request to the project history api', function () { + return this.request.post + .calledWith({ + url: `${this.Settings.apis.project_history.url}/project/${this.project_id}/flush`, + qs: { background: true } + }) + .should.equal(true) + }) + }) - return it("should send a request to the project history api", function() { - return this.request.post - .calledWith({url: `${this.Settings.apis.project_history.url}/project/${this.project_id}/flush`, qs:{background:true}}) - .should.equal(true); - }); - }); + describe('flushProjectChanges', function () { + describe('in the normal case', function () { + beforeEach(function () { + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 204 }) + return this.HistoryManager.flushProjectChanges(this.project_id, { + background: true + }) + }) - describe("flushProjectChanges", function() { + return it('should send a request to the project history api', function () { + return this.request.post + .calledWith({ + url: `${this.Settings.apis.project_history.url}/project/${this.project_id}/flush`, + qs: { background: true } + }) + .should.equal(true) + }) + }) - describe("in the normal case", function() { - beforeEach(function() { - this.request.post = sinon.stub().callsArgWith(1, null, {statusCode: 204}); - return this.HistoryManager.flushProjectChanges(this.project_id, {background:true});}); + return describe('with the skip_history_flush option', function () { + beforeEach(function () { + this.request.post = sinon.stub() + return this.HistoryManager.flushProjectChanges(this.project_id, { + skip_history_flush: true + }) + }) - return it("should send a request to the project history api", function() { - return this.request.post - .calledWith({url: `${this.Settings.apis.project_history.url}/project/${this.project_id}/flush`, qs:{background:true}}) - .should.equal(true); - }); - }); + return it('should not send a request to the project history api', function () { + return this.request.post.called.should.equal(false) + }) + }) + }) - return describe("with the skip_history_flush option", function() { - beforeEach(function() { - this.request.post = sinon.stub(); - return this.HistoryManager.flushProjectChanges(this.project_id, {skip_history_flush:true});}); + describe('recordAndFlushHistoryOps', function () { + beforeEach(function () { + this.ops = ['mock-ops'] + this.project_ops_length = 10 + this.doc_ops_length = 5 - return it("should not send a request to the project history api", function() { - return this.request.post - .called - .should.equal(false); - }); - }); - }); + this.HistoryManager.flushProjectChangesAsync = sinon.stub() + this.HistoryRedisManager.recordDocHasHistoryOps = sinon.stub().callsArg(3) + return (this.HistoryManager.flushDocChangesAsync = sinon.stub()) + }) - describe("recordAndFlushHistoryOps", function() { - beforeEach(function() { - this.ops = [ 'mock-ops' ]; - this.project_ops_length = 10; - this.doc_ops_length = 5; + describe('with no ops', function () { + beforeEach(function () { + return this.HistoryManager.recordAndFlushHistoryOps( + this.project_id, + this.doc_id, + [], + this.doc_ops_length, + this.project_ops_length, + this.callback + ) + }) - this.HistoryManager.flushProjectChangesAsync = sinon.stub(); - this.HistoryRedisManager.recordDocHasHistoryOps = sinon.stub().callsArg(3); - return this.HistoryManager.flushDocChangesAsync = sinon.stub(); - }); + it('should not flush project changes', function () { + return this.HistoryManager.flushProjectChangesAsync.called.should.equal( + false + ) + }) - describe("with no ops", function() { - beforeEach(function() { - return this.HistoryManager.recordAndFlushHistoryOps( - this.project_id, this.doc_id, [], this.doc_ops_length, this.project_ops_length, this.callback - ); - }); + it('should not record doc has history ops', function () { + return this.HistoryRedisManager.recordDocHasHistoryOps.called.should.equal( + false + ) + }) - it("should not flush project changes", function() { - return this.HistoryManager.flushProjectChangesAsync.called.should.equal(false); - }); + it('should not flush doc changes', function () { + return this.HistoryManager.flushDocChangesAsync.called.should.equal( + false + ) + }) - it("should not record doc has history ops", function() { - return this.HistoryRedisManager.recordDocHasHistoryOps.called.should.equal(false); - }); + return it('should call the callback', function () { + return this.callback.called.should.equal(true) + }) + }) - it("should not flush doc changes", function() { - return this.HistoryManager.flushDocChangesAsync.called.should.equal(false); - }); + describe('with enough ops to flush project changes', function () { + beforeEach(function () { + this.HistoryManager.shouldFlushHistoryOps = sinon.stub() + this.HistoryManager.shouldFlushHistoryOps + .withArgs(this.project_ops_length) + .returns(true) + this.HistoryManager.shouldFlushHistoryOps + .withArgs(this.doc_ops_length) + .returns(false) - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); + return this.HistoryManager.recordAndFlushHistoryOps( + this.project_id, + this.doc_id, + this.ops, + this.doc_ops_length, + this.project_ops_length, + this.callback + ) + }) - describe("with enough ops to flush project changes", function() { - beforeEach(function() { - this.HistoryManager.shouldFlushHistoryOps = sinon.stub(); - this.HistoryManager.shouldFlushHistoryOps.withArgs(this.project_ops_length).returns(true); - this.HistoryManager.shouldFlushHistoryOps.withArgs(this.doc_ops_length).returns(false); + it('should flush project changes', function () { + return this.HistoryManager.flushProjectChangesAsync + .calledWith(this.project_id) + .should.equal(true) + }) - return this.HistoryManager.recordAndFlushHistoryOps( - this.project_id, this.doc_id, this.ops, this.doc_ops_length, this.project_ops_length, this.callback - ); - }); + it('should record doc has history ops', function () { + return this.HistoryRedisManager.recordDocHasHistoryOps.calledWith( + this.project_id, + this.doc_id, + this.ops + ) + }) - it("should flush project changes", function() { - return this.HistoryManager.flushProjectChangesAsync - .calledWith(this.project_id) - .should.equal(true); - }); + it('should not flush doc changes', function () { + return this.HistoryManager.flushDocChangesAsync.called.should.equal( + false + ) + }) - it("should record doc has history ops", function() { - return this.HistoryRedisManager.recordDocHasHistoryOps - .calledWith(this.project_id, this.doc_id, this.ops); - }); + return it('should call the callback', function () { + return this.callback.called.should.equal(true) + }) + }) - it("should not flush doc changes", function() { - return this.HistoryManager.flushDocChangesAsync.called.should.equal(false); - }); + describe('with enough ops to flush doc changes', function () { + beforeEach(function () { + this.HistoryManager.shouldFlushHistoryOps = sinon.stub() + this.HistoryManager.shouldFlushHistoryOps + .withArgs(this.project_ops_length) + .returns(false) + this.HistoryManager.shouldFlushHistoryOps + .withArgs(this.doc_ops_length) + .returns(true) - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); + return this.HistoryManager.recordAndFlushHistoryOps( + this.project_id, + this.doc_id, + this.ops, + this.doc_ops_length, + this.project_ops_length, + this.callback + ) + }) - describe("with enough ops to flush doc changes", function() { - beforeEach(function() { - this.HistoryManager.shouldFlushHistoryOps = sinon.stub(); - this.HistoryManager.shouldFlushHistoryOps.withArgs(this.project_ops_length).returns(false); - this.HistoryManager.shouldFlushHistoryOps.withArgs(this.doc_ops_length).returns(true); + it('should not flush project changes', function () { + return this.HistoryManager.flushProjectChangesAsync.called.should.equal( + false + ) + }) - return this.HistoryManager.recordAndFlushHistoryOps( - this.project_id, this.doc_id, this.ops, this.doc_ops_length, this.project_ops_length, this.callback - ); - }); + it('should record doc has history ops', function () { + return this.HistoryRedisManager.recordDocHasHistoryOps.calledWith( + this.project_id, + this.doc_id, + this.ops + ) + }) - it("should not flush project changes", function() { - return this.HistoryManager.flushProjectChangesAsync.called.should.equal(false); - }); + it('should flush doc changes', function () { + return this.HistoryManager.flushDocChangesAsync + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) - it("should record doc has history ops", function() { - return this.HistoryRedisManager.recordDocHasHistoryOps - .calledWith(this.project_id, this.doc_id, this.ops); - }); + return it('should call the callback', function () { + return this.callback.called.should.equal(true) + }) + }) - it("should flush doc changes", function() { - return this.HistoryManager.flushDocChangesAsync - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); + describe('when recording doc has history ops errors', function () { + beforeEach(function () { + this.error = new Error('error') + this.HistoryRedisManager.recordDocHasHistoryOps = sinon + .stub() + .callsArgWith(3, this.error) - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); + return this.HistoryManager.recordAndFlushHistoryOps( + this.project_id, + this.doc_id, + this.ops, + this.doc_ops_length, + this.project_ops_length, + this.callback + ) + }) - describe("when recording doc has history ops errors", function() { - beforeEach(function() { - this.error = new Error("error"); - this.HistoryRedisManager.recordDocHasHistoryOps = - sinon.stub().callsArgWith(3, this.error); + it('should not flush doc changes', function () { + return this.HistoryManager.flushDocChangesAsync.called.should.equal( + false + ) + }) - return this.HistoryManager.recordAndFlushHistoryOps( - this.project_id, this.doc_id, this.ops, this.doc_ops_length, this.project_ops_length, this.callback - ); - }); + return it('should call the callback with the error', function () { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) - it("should not flush doc changes", function() { - return this.HistoryManager.flushDocChangesAsync.called.should.equal(false); - }); + return describe('shouldFlushHistoryOps', function () { + it('should return false if the number of ops is not known', function () { + return this.HistoryManager.shouldFlushHistoryOps( + null, + ['a', 'b', 'c'].length, + 1 + ).should.equal(false) + }) - return it("should call the callback with the error", function() { - return this.callback.calledWith(this.error).should.equal(true); - }); - }); + it("should return false if the updates didn't take us past the threshold", function () { + // Currently there are 14 ops + // Previously we were on 11 ops + // We didn't pass over a multiple of 5 + this.HistoryManager.shouldFlushHistoryOps( + 14, + ['a', 'b', 'c'].length, + 5 + ).should.equal(false) - return describe("shouldFlushHistoryOps", function() { - it("should return false if the number of ops is not known", function() { - return this.HistoryManager.shouldFlushHistoryOps(null, ['a', 'b', 'c'].length, 1).should.equal(false); - }); + it('should return true if the updates took to the threshold', function () {}) + // Currently there are 15 ops + // Previously we were on 12 ops + // We've reached a new multiple of 5 + return this.HistoryManager.shouldFlushHistoryOps( + 15, + ['a', 'b', 'c'].length, + 5 + ).should.equal(true) + }) - it("should return false if the updates didn't take us past the threshold", function() { - // Currently there are 14 ops - // Previously we were on 11 ops - // We didn't pass over a multiple of 5 - this.HistoryManager.shouldFlushHistoryOps(14, ['a', 'b', 'c'].length, 5).should.equal(false); + return it('should return true if the updates took past the threshold', function () { + // Currently there are 19 ops + // Previously we were on 16 ops + // We didn't pass over a multiple of 5 + return this.HistoryManager.shouldFlushHistoryOps( + 17, + ['a', 'b', 'c'].length, + 5 + ).should.equal(true) + }) + }) + }) - it("should return true if the updates took to the threshold", function() {}); - // Currently there are 15 ops - // Previously we were on 12 ops - // We've reached a new multiple of 5 - return this.HistoryManager.shouldFlushHistoryOps(15, ['a', 'b', 'c'].length, 5).should.equal(true); - }); + return describe('resyncProjectHistory', function () { + beforeEach(function () { + this.projectHistoryId = 'history-id-1234' + this.docs = [ + { + doc: this.doc_id, + path: 'main.tex' + } + ] + this.files = [ + { + file: 'mock-file-id', + path: 'universe.png', + url: `www.filestore.test/${this.project_id}/mock-file-id` + } + ] + this.ProjectHistoryRedisManager.queueResyncProjectStructure = sinon + .stub() + .yields() + this.DocumentManager.resyncDocContentsWithLock = sinon.stub().yields() + return this.HistoryManager.resyncProjectHistory( + this.project_id, + this.projectHistoryId, + this.docs, + this.files, + this.callback + ) + }) - return it("should return true if the updates took past the threshold", function() { - // Currently there are 19 ops - // Previously we were on 16 ops - // We didn't pass over a multiple of 5 - return this.HistoryManager.shouldFlushHistoryOps(17, ['a', 'b', 'c'].length, 5).should.equal(true); - }); - }); - }); + it('should queue a project structure reync', function () { + return this.ProjectHistoryRedisManager.queueResyncProjectStructure + .calledWith( + this.project_id, + this.projectHistoryId, + this.docs, + this.files + ) + .should.equal(true) + }) - return describe("resyncProjectHistory", function() { - beforeEach(function() { - this.projectHistoryId = 'history-id-1234'; - this.docs = [{ - doc: this.doc_id, - path: 'main.tex' - } - ]; - this.files = [{ - file: 'mock-file-id', - path: 'universe.png', - url: `www.filestore.test/${this.project_id}/mock-file-id` - } - ]; - this.ProjectHistoryRedisManager.queueResyncProjectStructure = sinon.stub().yields(); - this.DocumentManager.resyncDocContentsWithLock = sinon.stub().yields(); - return this.HistoryManager.resyncProjectHistory(this.project_id, this.projectHistoryId, this.docs, this.files, this.callback); - }); + it('should queue doc content reyncs', function () { + return this.DocumentManager.resyncDocContentsWithLock + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) - it("should queue a project structure reync", function() { - return this.ProjectHistoryRedisManager.queueResyncProjectStructure - .calledWith(this.project_id, this.projectHistoryId, this.docs, this.files) - .should.equal(true); - }); - - it("should queue doc content reyncs", function() { - return this.DocumentManager - .resyncDocContentsWithLock - .calledWith(this.project_id, this.doc_id) - .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) + }) + }) +}) diff --git a/services/document-updater/test/unit/js/HistoryRedisManager/HistoryRedisManagerTests.js b/services/document-updater/test/unit/js/HistoryRedisManager/HistoryRedisManagerTests.js index b5184a11e6..68321533b3 100644 --- a/services/document-updater/test/unit/js/HistoryRedisManager/HistoryRedisManagerTests.js +++ b/services/document-updater/test/unit/js/HistoryRedisManager/HistoryRedisManagerTests.js @@ -11,79 +11,93 @@ * 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 modulePath = "../../../../app/js/HistoryRedisManager.js"; -const SandboxedModule = require('sandboxed-module'); -const Errors = require("../../../../app/js/Errors"); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const modulePath = '../../../../app/js/HistoryRedisManager.js' +const SandboxedModule = require('sandboxed-module') +const Errors = require('../../../../app/js/Errors') -describe("HistoryRedisManager", function() { - beforeEach(function() { - this.rclient = { - auth() {}, - exec: sinon.stub() - }; - this.rclient.multi = () => this.rclient; - this.HistoryRedisManager = SandboxedModule.require(modulePath, { requires: { - "redis-sharelatex": { createClient: () => this.rclient - }, - "settings-sharelatex": { - redis: { - history: (this.settings = { - key_schema: { - uncompressedHistoryOps({doc_id}) { return `UncompressedHistoryOps:${doc_id}`; }, - docsWithHistoryOps({project_id}) { return `DocsWithHistoryOps:${project_id}`; } - } - }) - } - }, - "logger-sharelatex": { log() {} } - } - }); - this.doc_id = "doc-id-123"; - this.project_id = "project-id-123"; - return this.callback = sinon.stub(); - }); +describe('HistoryRedisManager', function () { + beforeEach(function () { + this.rclient = { + auth() {}, + exec: sinon.stub() + } + this.rclient.multi = () => this.rclient + this.HistoryRedisManager = SandboxedModule.require(modulePath, { + requires: { + 'redis-sharelatex': { createClient: () => this.rclient }, + 'settings-sharelatex': { + redis: { + history: (this.settings = { + key_schema: { + uncompressedHistoryOps({ doc_id }) { + return `UncompressedHistoryOps:${doc_id}` + }, + docsWithHistoryOps({ project_id }) { + return `DocsWithHistoryOps:${project_id}` + } + } + }) + } + }, + 'logger-sharelatex': { log() {} } + } + }) + this.doc_id = 'doc-id-123' + this.project_id = 'project-id-123' + return (this.callback = sinon.stub()) + }) - return describe("recordDocHasHistoryOps", function() { - beforeEach(function() { - this.ops = [{ op: [{ i: "foo", p: 4 }] },{ op: [{ i: "bar", p: 56 }] }]; - return this.rclient.sadd = sinon.stub().yields(); - }); - - describe("with ops", function() { - beforeEach(function(done) { - return this.HistoryRedisManager.recordDocHasHistoryOps(this.project_id, this.doc_id, this.ops, (...args) => { - this.callback(...Array.from(args || [])); - return done(); - }); - }); - - return it("should add the doc_id to the set of which records the project docs", function() { - return this.rclient.sadd - .calledWith(`DocsWithHistoryOps:${this.project_id}`, this.doc_id) - .should.equal(true); - }); - }); + return describe('recordDocHasHistoryOps', function () { + beforeEach(function () { + this.ops = [{ op: [{ i: 'foo', p: 4 }] }, { op: [{ i: 'bar', p: 56 }] }] + return (this.rclient.sadd = sinon.stub().yields()) + }) - return describe("with no ops", function() { - beforeEach(function(done) { - return this.HistoryRedisManager.recordDocHasHistoryOps(this.project_id, this.doc_id, [], (...args) => { - this.callback(...Array.from(args || [])); - return done(); - }); - }); - - it("should not add the doc_id to the set of which records the project docs", function() { - return this.rclient.sadd - .called - .should.equal(false); - }); + describe('with ops', function () { + beforeEach(function (done) { + return this.HistoryRedisManager.recordDocHasHistoryOps( + this.project_id, + this.doc_id, + this.ops, + (...args) => { + this.callback(...Array.from(args || [])) + return done() + } + ) + }) - return it("should call the callback with an error", function() { - return this.callback.calledWith(new Error("cannot push no ops")).should.equal(true); - }); - }); - }); -}); + return it('should add the doc_id to the set of which records the project docs', function () { + return this.rclient.sadd + .calledWith(`DocsWithHistoryOps:${this.project_id}`, this.doc_id) + .should.equal(true) + }) + }) + + return describe('with no ops', function () { + beforeEach(function (done) { + return this.HistoryRedisManager.recordDocHasHistoryOps( + this.project_id, + this.doc_id, + [], + (...args) => { + this.callback(...Array.from(args || [])) + return done() + } + ) + }) + + it('should not add the doc_id to the set of which records the project docs', function () { + return this.rclient.sadd.called.should.equal(false) + }) + + return it('should call the callback with an error', function () { + return this.callback + .calledWith(new Error('cannot push no ops')) + .should.equal(true) + }) + }) + }) +}) diff --git a/services/document-updater/test/unit/js/HttpController/HttpControllerTests.js b/services/document-updater/test/unit/js/HttpController/HttpControllerTests.js index 5189f3515d..8f4125fcfa 100644 --- a/services/document-updater/test/unit/js/HttpController/HttpControllerTests.js +++ b/services/document-updater/test/unit/js/HttpController/HttpControllerTests.js @@ -10,806 +10,943 @@ * DS206: Consider reworking classes to avoid initClass * 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 modulePath = "../../../../app/js/HttpController.js"; -const SandboxedModule = require('sandboxed-module'); -const Errors = require("../../../../app/js/Errors.js"); - -describe("HttpController", function() { - beforeEach(function() { - let Timer; - this.HttpController = SandboxedModule.require(modulePath, { requires: { - "./DocumentManager": (this.DocumentManager = {}), - "./HistoryManager": (this.HistoryManager = - {flushProjectChangesAsync: sinon.stub()}), - "./ProjectManager": (this.ProjectManager = {}), - "logger-sharelatex" : (this.logger = { log: sinon.stub() }), - "./ProjectFlusher": {flushAllProjects() {}}, - "./DeleteQueueManager": (this.DeleteQueueManager = {}), - "./Metrics": (this.Metrics = {}), - "./Errors" : Errors - } - } - ); - this.Metrics.Timer = (Timer = (function() { - Timer = class Timer { - static initClass() { - this.prototype.done = sinon.stub(); - } - }; - Timer.initClass(); - return Timer; - })()); - this.project_id = "project-id-123"; - this.doc_id = "doc-id-123"; - this.next = sinon.stub(); - return this.res = { - send: sinon.stub(), - sendStatus: sinon.stub(), - json: sinon.stub() - }; - }); - - describe("getDoc", function() { - beforeEach(function() { - this.lines = ["one", "two", "three"]; - this.ops = ["mock-op-1", "mock-op-2"]; - this.version = 42; - this.fromVersion = 42; - this.ranges = { changes: "mock", comments: "mock" }; - this.pathname = '/a/b/c'; - return this.req = { - params: { - project_id: this.project_id, - doc_id: this.doc_id - } - }; - }); - - describe("when the document exists and no recent ops are requested", function() { - beforeEach(function() { - this.DocumentManager.getDocAndRecentOpsWithLock = sinon.stub().callsArgWith(3, null, this.lines, this.version, [], this.ranges, this.pathname); - return this.HttpController.getDoc(this.req, this.res, this.next); - }); - - it("should get the doc", function() { - return this.DocumentManager.getDocAndRecentOpsWithLock - .calledWith(this.project_id, this.doc_id, -1) - .should.equal(true); - }); - - it("should return the doc as JSON", function() { - return this.res.json - .calledWith({ - id: this.doc_id, - lines: this.lines, - version: this.version, - ops: [], - ranges: this.ranges, - pathname: this.pathname - }) - .should.equal(true); - }); - - it("should log the request", function() { - return this.logger.log - .calledWith({doc_id: this.doc_id, project_id: this.project_id}, "getting doc via http") - .should.equal(true); - }); - - return it("should time the request", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - }); - - describe("when recent ops are requested", function() { - beforeEach(function() { - this.DocumentManager.getDocAndRecentOpsWithLock = sinon.stub().callsArgWith(3, null, this.lines, this.version, this.ops, this.ranges, this.pathname); - this.req.query = {fromVersion: `${this.fromVersion}`}; - return this.HttpController.getDoc(this.req, this.res, this.next); - }); - - it("should get the doc", function() { - return this.DocumentManager.getDocAndRecentOpsWithLock - .calledWith(this.project_id, this.doc_id, this.fromVersion) - .should.equal(true); - }); - - it("should return the doc as JSON", function() { - return this.res.json - .calledWith({ - id: this.doc_id, - lines: this.lines, - version: this.version, - ops: this.ops, - ranges: this.ranges, - pathname: this.pathname - }) - .should.equal(true); - }); - - it("should log the request", function() { - return this.logger.log - .calledWith({doc_id: this.doc_id, project_id: this.project_id}, "getting doc via http") - .should.equal(true); - }); - - return it("should time the request", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - }); - - describe("when the document does not exist", function() { - beforeEach(function() { - this.DocumentManager.getDocAndRecentOpsWithLock = sinon.stub().callsArgWith(3, null, null, null); - return this.HttpController.getDoc(this.req, this.res, this.next); - }); - - return it("should call next with NotFoundError", function() { - return this.next - .calledWith(new Errors.NotFoundError("not found")) - .should.equal(true); - }); - }); - - return describe("when an errors occurs", function() { - beforeEach(function() { - this.DocumentManager.getDocAndRecentOpsWithLock = sinon.stub().callsArgWith(3, new Error("oops"), null, null); - return this.HttpController.getDoc(this.req, this.res, this.next); - }); - - return it("should call next with the error", function() { - return this.next - .calledWith(new Error("oops")) - .should.equal(true); - }); - }); - }); - - describe("setDoc", function() { - beforeEach(function() { - this.lines = ["one", "two", "three"]; - this.source = "dropbox"; - this.user_id = "user-id-123"; - return this.req = { - headers: {}, - params: { - project_id: this.project_id, - doc_id: this.doc_id - }, - body: { - lines: this.lines, - source: this.source, - user_id: this.user_id, - undoing: (this.undoing = true) - } - }; - }); - - describe("successfully", function() { - beforeEach(function() { - this.DocumentManager.setDocWithLock = sinon.stub().callsArgWith(6); - return this.HttpController.setDoc(this.req, this.res, this.next); - }); - - it("should set the doc", function() { - return this.DocumentManager.setDocWithLock - .calledWith(this.project_id, this.doc_id, this.lines, this.source, this.user_id, this.undoing) - .should.equal(true); - }); - - it("should return a successful No Content response", function() { - return this.res.sendStatus - .calledWith(204) - .should.equal(true); - }); - - it("should log the request", function() { - return this.logger.log - .calledWith({doc_id: this.doc_id, project_id: this.project_id, lines: this.lines, source: this.source, user_id: this.user_id, undoing: this.undoing}, "setting doc via http") - .should.equal(true); - }); - - return it("should time the request", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - }); - - describe("when an errors occurs", function() { - beforeEach(function() { - this.DocumentManager.setDocWithLock = sinon.stub().callsArgWith(6, new Error("oops")); - return this.HttpController.setDoc(this.req, this.res, this.next); - }); - - return it("should call next with the error", function() { - return this.next - .calledWith(new Error("oops")) - .should.equal(true); - }); - }); - - return describe("when the payload is too large", function() { - beforeEach(function() { - const lines = []; - for (let _ = 0; _ <= 200000; _++) { - lines.push("test test test"); - } - this.req.body.lines = lines; - this.DocumentManager.setDocWithLock = sinon.stub().callsArgWith(6); - return this.HttpController.setDoc(this.req, this.res, this.next); - }); - - it('should send back a 406 response', function() { - return this.res.sendStatus.calledWith(406).should.equal(true); - }); - - return it('should not call setDocWithLock', function() { - return this.DocumentManager.setDocWithLock.callCount.should.equal(0); - }); - }); - }); - - describe("flushProject", function() { - beforeEach(function() { - return this.req = { - params: { - project_id: this.project_id - } - }; - }); - - describe("successfully", function() { - beforeEach(function() { - this.ProjectManager.flushProjectWithLocks = sinon.stub().callsArgWith(1); - return this.HttpController.flushProject(this.req, this.res, this.next); - }); - - it("should flush the project", function() { - return this.ProjectManager.flushProjectWithLocks - .calledWith(this.project_id) - .should.equal(true); - }); - - it("should return a successful No Content response", function() { - return this.res.sendStatus - .calledWith(204) - .should.equal(true); - }); - - it("should log the request", function() { - return this.logger.log - .calledWith({project_id: this.project_id}, "flushing project via http") - .should.equal(true); - }); - - return it("should time the request", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - }); - - return describe("when an errors occurs", function() { - beforeEach(function() { - this.ProjectManager.flushProjectWithLocks = sinon.stub().callsArgWith(1, new Error("oops")); - return this.HttpController.flushProject(this.req, this.res, this.next); - }); - - return it("should call next with the error", function() { - return this.next - .calledWith(new Error("oops")) - .should.equal(true); - }); - }); - }); - - describe("flushDocIfLoaded", function() { - beforeEach(function() { - this.lines = ["one", "two", "three"]; - this.version = 42; - return this.req = { - params: { - project_id: this.project_id, - doc_id: this.doc_id - } - }; - }); - - describe("successfully", function() { - beforeEach(function() { - this.DocumentManager.flushDocIfLoadedWithLock = sinon.stub().callsArgWith(2); - return this.HttpController.flushDocIfLoaded(this.req, this.res, this.next); - }); - - it("should flush the doc", function() { - return this.DocumentManager.flushDocIfLoadedWithLock - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); - - it("should return a successful No Content response", function() { - return this.res.sendStatus - .calledWith(204) - .should.equal(true); - }); - - it("should log the request", function() { - return this.logger.log - .calledWith({doc_id: this.doc_id, project_id: this.project_id}, "flushing doc via http") - .should.equal(true); - }); - - return it("should time the request", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - }); - - return describe("when an errors occurs", function() { - beforeEach(function() { - this.DocumentManager.flushDocIfLoadedWithLock = sinon.stub().callsArgWith(2, new Error("oops")); - return this.HttpController.flushDocIfLoaded(this.req, this.res, this.next); - }); - - return it("should call next with the error", function() { - return this.next - .calledWith(new Error("oops")) - .should.equal(true); - }); - }); - }); - - describe("deleteDoc", function() { - beforeEach(function() { - return this.req = { - params: { - project_id: this.project_id, - doc_id: this.doc_id - }, - query: {} - };}); - - describe("successfully", function() { - beforeEach(function() { - this.DocumentManager.flushAndDeleteDocWithLock = sinon.stub().callsArgWith(3); - return this.HttpController.deleteDoc(this.req, this.res, this.next); - }); - - it("should flush and delete the doc", function() { - return this.DocumentManager.flushAndDeleteDocWithLock - .calledWith(this.project_id, this.doc_id, { ignoreFlushErrors: false }) - .should.equal(true); - }); - - it("should flush project history", function() { - return this.HistoryManager.flushProjectChangesAsync - .calledWithExactly(this.project_id) - .should.equal(true); - }); - - it("should return a successful No Content response", function() { - return this.res.sendStatus - .calledWith(204) - .should.equal(true); - }); - - it("should log the request", function() { - return this.logger.log - .calledWith({doc_id: this.doc_id, project_id: this.project_id}, "deleting doc via http") - .should.equal(true); - }); - - return it("should time the request", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - }); - - describe("ignoring errors", function() { - beforeEach(function() { - this.req.query.ignore_flush_errors = 'true'; - this.DocumentManager.flushAndDeleteDocWithLock = sinon.stub().yields(); - return this.HttpController.deleteDoc(this.req, this.res, this.next); - }); - - it("should delete the doc", function() { - return this.DocumentManager.flushAndDeleteDocWithLock - .calledWith(this.project_id, this.doc_id, { ignoreFlushErrors: true }) - .should.equal(true); - }); - - return it("should return a successful No Content response", function() { - return this.res.sendStatus.calledWith(204).should.equal(true); - }); - }); - - return describe("when an errors occurs", function() { - beforeEach(function() { - this.DocumentManager.flushAndDeleteDocWithLock = sinon.stub().callsArgWith(3, new Error("oops")); - return this.HttpController.deleteDoc(this.req, this.res, this.next); - }); - - it("should flush project history", function() { - return this.HistoryManager.flushProjectChangesAsync - .calledWithExactly(this.project_id) - .should.equal(true); - }); - - return it("should call next with the error", function() { - return this.next - .calledWith(new Error("oops")) - .should.equal(true); - }); - }); - }); - - describe("deleteProject", function() { - beforeEach(function() { - return this.req = { - params: { - project_id: this.project_id - } - }; - }); - - describe("successfully", function() { - beforeEach(function() { - this.ProjectManager.flushAndDeleteProjectWithLocks = sinon.stub().callsArgWith(2); - return this.HttpController.deleteProject(this.req, this.res, this.next); - }); - - it("should delete the project", function() { - return this.ProjectManager.flushAndDeleteProjectWithLocks - .calledWith(this.project_id) - .should.equal(true); - }); - - it("should return a successful No Content response", function() { - return this.res.sendStatus - .calledWith(204) - .should.equal(true); - }); - - it("should log the request", function() { - return this.logger.log - .calledWith({project_id: this.project_id}, "deleting project via http") - .should.equal(true); - }); - - return it("should time the request", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - }); - - describe("with the background=true option from realtime", function() { - beforeEach(function() { - this.ProjectManager.queueFlushAndDeleteProject = sinon.stub().callsArgWith(1); - this.req.query = {background:true, shutdown:true}; - return this.HttpController.deleteProject(this.req, this.res, this.next); - }); - - return it("should queue the flush and delete", function() { - return this.ProjectManager.queueFlushAndDeleteProject - .calledWith(this.project_id) - .should.equal(true); - }); - }); - - return describe("when an errors occurs", function() { - beforeEach(function() { - this.ProjectManager.flushAndDeleteProjectWithLocks = sinon.stub().callsArgWith(2, new Error("oops")); - return this.HttpController.deleteProject(this.req, this.res, this.next); - }); - - return it("should call next with the error", function() { - return this.next - .calledWith(new Error("oops")) - .should.equal(true); - }); - }); - }); - - describe("acceptChanges", function() { - beforeEach(function() { - return this.req = { - params: { - project_id: this.project_id, - doc_id: this.doc_id, - change_id: (this.change_id = "mock-change-od-1") - } - }; - }); - - describe("successfully with a single change", function() { - beforeEach(function() { - this.DocumentManager.acceptChangesWithLock = sinon.stub().callsArgWith(3); - return this.HttpController.acceptChanges(this.req, this.res, this.next); - }); - - it("should accept the change", function() { - return this.DocumentManager.acceptChangesWithLock - .calledWith(this.project_id, this.doc_id, [ this.change_id ]) - .should.equal(true); - }); - - it("should return a successful No Content response", function() { - return this.res.sendStatus - .calledWith(204) - .should.equal(true); - }); - - it("should log the request", function() { - return this.logger.log - .calledWith({project_id: this.project_id, doc_id: this.doc_id}, "accepting 1 changes via http") - .should.equal(true); - }); - - return it("should time the request", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - }); - - describe("succesfully with with multiple changes", function() { - beforeEach(function() { - this.change_ids = [ "mock-change-od-1", "mock-change-od-2", "mock-change-od-3", "mock-change-od-4" ]; - this.req.body = - {change_ids: this.change_ids}; - this.DocumentManager.acceptChangesWithLock = sinon.stub().callsArgWith(3); - return this.HttpController.acceptChanges(this.req, this.res, this.next); - }); - - it("should accept the changes in the body payload", function() { - return this.DocumentManager.acceptChangesWithLock - .calledWith(this.project_id, this.doc_id, this.change_ids) - .should.equal(true); - }); - - return it("should log the request with the correct number of changes", function() { - return this.logger.log - .calledWith({project_id: this.project_id, doc_id: this.doc_id}, `accepting ${ this.change_ids.length } changes via http`) - .should.equal(true); - }); - }); - - return describe("when an errors occurs", function() { - beforeEach(function() { - this.DocumentManager.acceptChangesWithLock = sinon.stub().callsArgWith(3, new Error("oops")); - return this.HttpController.acceptChanges(this.req, this.res, this.next); - }); - - return it("should call next with the error", function() { - return this.next - .calledWith(new Error("oops")) - .should.equal(true); - }); - }); - }); - - describe("deleteComment", function() { - beforeEach(function() { - return this.req = { - params: { - project_id: this.project_id, - doc_id: this.doc_id, - comment_id: (this.comment_id = "mock-comment-id") - } - }; - }); - - describe("successfully", function() { - beforeEach(function() { - this.DocumentManager.deleteCommentWithLock = sinon.stub().callsArgWith(3); - return this.HttpController.deleteComment(this.req, this.res, this.next); - }); - - it("should accept the change", function() { - return this.DocumentManager.deleteCommentWithLock - .calledWith(this.project_id, this.doc_id, this.comment_id) - .should.equal(true); - }); - - it("should return a successful No Content response", function() { - return this.res.sendStatus - .calledWith(204) - .should.equal(true); - }); - - it("should log the request", function() { - return this.logger.log - .calledWith({project_id: this.project_id, doc_id: this.doc_id, comment_id: this.comment_id}, "deleting comment via http") - .should.equal(true); - }); - - return it("should time the request", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - }); - - return describe("when an errors occurs", function() { - beforeEach(function() { - this.DocumentManager.deleteCommentWithLock = sinon.stub().callsArgWith(3, new Error("oops")); - return this.HttpController.deleteComment(this.req, this.res, this.next); - }); - - return it("should call next with the error", function() { - return this.next - .calledWith(new Error("oops")) - .should.equal(true); - }); - }); - }); - - describe("getProjectDocsAndFlushIfOld", function() { - beforeEach(function() { - this.state = "01234567890abcdef"; - this.docs = [{_id: "1234", lines: "hello", v: 23}, {_id: "4567", lines: "world", v: 45}]; - return this.req = { - params: { - project_id: this.project_id - }, - query: { - state: this.state - } - }; - }); - - describe("successfully", function() { - beforeEach(function() { - this.ProjectManager.getProjectDocsAndFlushIfOld = sinon.stub().callsArgWith(3,null, this.docs); - return this.HttpController.getProjectDocsAndFlushIfOld(this.req, this.res, this.next); - }); - - it("should get docs from the project manager", function() { - return this.ProjectManager.getProjectDocsAndFlushIfOld - .calledWith(this.project_id, this.state, {}) - .should.equal(true); - }); - - it("should return a successful response", function() { - return this.res.send - .calledWith(this.docs) - .should.equal(true); - }); - - it("should log the request", function() { - return this.logger.log - .calledWith({project_id: this.project_id, exclude: []}, "getting docs via http") - .should.equal(true); - }); - - it("should log the response", function() { - return this.logger.log - .calledWith({project_id: this.project_id, result: ["1234:23", "4567:45"]}, "got docs via http") - .should.equal(true); - }); - - return it("should time the request", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - }); - - describe("when there is a conflict", function() { - beforeEach(function() { - this.ProjectManager.getProjectDocsAndFlushIfOld = sinon.stub().callsArgWith(3, new Errors.ProjectStateChangedError("project state changed")); - return this.HttpController.getProjectDocsAndFlushIfOld(this.req, this.res, this.next); - }); - - return it("should return an HTTP 409 Conflict response", function() { - return this.res.sendStatus - .calledWith(409) - .should.equal(true); - }); - }); - - return describe("when an error occurs", function() { - beforeEach(function() { - this.ProjectManager.getProjectDocsAndFlushIfOld = sinon.stub().callsArgWith(3, new Error("oops")); - return this.HttpController.getProjectDocsAndFlushIfOld(this.req, this.res, this.next); - }); - - return it("should call next with the error", function() { - return this.next - .calledWith(new Error("oops")) - .should.equal(true); - }); - }); - }); - - describe("updateProject", function() { - beforeEach(function() { - this.projectHistoryId = "history-id-123"; - this.userId = "user-id-123"; - this.docUpdates = sinon.stub(); - this.fileUpdates = sinon.stub(); - this.version = 1234567; - return this.req = { - body: {projectHistoryId: this.projectHistoryId, userId: this.userId, docUpdates: this.docUpdates, fileUpdates: this.fileUpdates, version: this.version}, - params: { - project_id: this.project_id - } - }; - }); - - describe("successfully", function() { - beforeEach(function() { - this.ProjectManager.updateProjectWithLocks = sinon.stub().callsArgWith(6); - return this.HttpController.updateProject(this.req, this.res, this.next); - }); - - it("should accept the change", function() { - return this.ProjectManager.updateProjectWithLocks - .calledWith(this.project_id, this.projectHistoryId, this.userId, this.docUpdates, this.fileUpdates, this.version) - .should.equal(true); - }); - - it("should return a successful No Content response", function() { - return this.res.sendStatus - .calledWith(204) - .should.equal(true); - }); - - return it("should time the request", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - }); - - return describe("when an errors occurs", function() { - beforeEach(function() { - this.ProjectManager.updateProjectWithLocks = sinon.stub().callsArgWith(6, new Error("oops")); - return this.HttpController.updateProject(this.req, this.res, this.next); - }); - - return it("should call next with the error", function() { - return this.next - .calledWith(new Error("oops")) - .should.equal(true); - }); - }); - }); - - return describe("resyncProjectHistory", function() { - beforeEach(function() { - this.projectHistoryId = "history-id-123"; - this.docs = sinon.stub(); - this.files = sinon.stub(); - this.fileUpdates = sinon.stub(); - return this.req = { - body: - {projectHistoryId: this.projectHistoryId, docs: this.docs, files: this.files}, - params: { - project_id: this.project_id - } - }; - }); - - describe("successfully", function() { - beforeEach(function() { - this.HistoryManager.resyncProjectHistory = sinon.stub().callsArgWith(4); - return this.HttpController.resyncProjectHistory(this.req, this.res, this.next); - }); - - it("should accept the change", function() { - return this.HistoryManager.resyncProjectHistory - .calledWith(this.project_id, this.projectHistoryId, this.docs, this.files) - .should.equal(true); - }); - - return it("should return a successful No Content response", function() { - return this.res.sendStatus - .calledWith(204) - .should.equal(true); - }); - }); - - return describe("when an errors occurs", function() { - beforeEach(function() { - this.HistoryManager.resyncProjectHistory = sinon.stub().callsArgWith(4, new Error("oops")); - return this.HttpController.resyncProjectHistory(this.req, this.res, this.next); - }); - - return it("should call next with the error", function() { - return this.next - .calledWith(new Error("oops")) - .should.equal(true); - }); - }); - }); -}); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const modulePath = '../../../../app/js/HttpController.js' +const SandboxedModule = require('sandboxed-module') +const Errors = require('../../../../app/js/Errors.js') + +describe('HttpController', function () { + beforeEach(function () { + let Timer + this.HttpController = SandboxedModule.require(modulePath, { + requires: { + './DocumentManager': (this.DocumentManager = {}), + './HistoryManager': (this.HistoryManager = { + flushProjectChangesAsync: sinon.stub() + }), + './ProjectManager': (this.ProjectManager = {}), + 'logger-sharelatex': (this.logger = { log: sinon.stub() }), + './ProjectFlusher': { flushAllProjects() {} }, + './DeleteQueueManager': (this.DeleteQueueManager = {}), + './Metrics': (this.Metrics = {}), + './Errors': Errors + } + }) + this.Metrics.Timer = Timer = (function () { + Timer = class Timer { + static initClass() { + this.prototype.done = sinon.stub() + } + } + Timer.initClass() + return Timer + })() + this.project_id = 'project-id-123' + this.doc_id = 'doc-id-123' + this.next = sinon.stub() + return (this.res = { + send: sinon.stub(), + sendStatus: sinon.stub(), + json: sinon.stub() + }) + }) + + describe('getDoc', function () { + beforeEach(function () { + this.lines = ['one', 'two', 'three'] + this.ops = ['mock-op-1', 'mock-op-2'] + this.version = 42 + this.fromVersion = 42 + this.ranges = { changes: 'mock', comments: 'mock' } + this.pathname = '/a/b/c' + return (this.req = { + params: { + project_id: this.project_id, + doc_id: this.doc_id + } + }) + }) + + describe('when the document exists and no recent ops are requested', function () { + beforeEach(function () { + this.DocumentManager.getDocAndRecentOpsWithLock = sinon + .stub() + .callsArgWith( + 3, + null, + this.lines, + this.version, + [], + this.ranges, + this.pathname + ) + return this.HttpController.getDoc(this.req, this.res, this.next) + }) + + it('should get the doc', function () { + return this.DocumentManager.getDocAndRecentOpsWithLock + .calledWith(this.project_id, this.doc_id, -1) + .should.equal(true) + }) + + it('should return the doc as JSON', function () { + return this.res.json + .calledWith({ + id: this.doc_id, + lines: this.lines, + version: this.version, + ops: [], + ranges: this.ranges, + pathname: this.pathname + }) + .should.equal(true) + }) + + it('should log the request', function () { + return this.logger.log + .calledWith( + { doc_id: this.doc_id, project_id: this.project_id }, + 'getting doc via http' + ) + .should.equal(true) + }) + + return it('should time the request', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) + + describe('when recent ops are requested', function () { + beforeEach(function () { + this.DocumentManager.getDocAndRecentOpsWithLock = sinon + .stub() + .callsArgWith( + 3, + null, + this.lines, + this.version, + this.ops, + this.ranges, + this.pathname + ) + this.req.query = { fromVersion: `${this.fromVersion}` } + return this.HttpController.getDoc(this.req, this.res, this.next) + }) + + it('should get the doc', function () { + return this.DocumentManager.getDocAndRecentOpsWithLock + .calledWith(this.project_id, this.doc_id, this.fromVersion) + .should.equal(true) + }) + + it('should return the doc as JSON', function () { + return this.res.json + .calledWith({ + id: this.doc_id, + lines: this.lines, + version: this.version, + ops: this.ops, + ranges: this.ranges, + pathname: this.pathname + }) + .should.equal(true) + }) + + it('should log the request', function () { + return this.logger.log + .calledWith( + { doc_id: this.doc_id, project_id: this.project_id }, + 'getting doc via http' + ) + .should.equal(true) + }) + + return it('should time the request', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) + + describe('when the document does not exist', function () { + beforeEach(function () { + this.DocumentManager.getDocAndRecentOpsWithLock = sinon + .stub() + .callsArgWith(3, null, null, null) + return this.HttpController.getDoc(this.req, this.res, this.next) + }) + + return it('should call next with NotFoundError', function () { + return this.next + .calledWith(new Errors.NotFoundError('not found')) + .should.equal(true) + }) + }) + + return describe('when an errors occurs', function () { + beforeEach(function () { + this.DocumentManager.getDocAndRecentOpsWithLock = sinon + .stub() + .callsArgWith(3, new Error('oops'), null, null) + return this.HttpController.getDoc(this.req, this.res, this.next) + }) + + return it('should call next with the error', function () { + return this.next.calledWith(new Error('oops')).should.equal(true) + }) + }) + }) + + describe('setDoc', function () { + beforeEach(function () { + this.lines = ['one', 'two', 'three'] + this.source = 'dropbox' + this.user_id = 'user-id-123' + return (this.req = { + headers: {}, + params: { + project_id: this.project_id, + doc_id: this.doc_id + }, + body: { + lines: this.lines, + source: this.source, + user_id: this.user_id, + undoing: (this.undoing = true) + } + }) + }) + + describe('successfully', function () { + beforeEach(function () { + this.DocumentManager.setDocWithLock = sinon.stub().callsArgWith(6) + return this.HttpController.setDoc(this.req, this.res, this.next) + }) + + it('should set the doc', function () { + return this.DocumentManager.setDocWithLock + .calledWith( + this.project_id, + this.doc_id, + this.lines, + this.source, + this.user_id, + this.undoing + ) + .should.equal(true) + }) + + it('should return a successful No Content response', function () { + return this.res.sendStatus.calledWith(204).should.equal(true) + }) + + it('should log the request', function () { + return this.logger.log + .calledWith( + { + doc_id: this.doc_id, + project_id: this.project_id, + lines: this.lines, + source: this.source, + user_id: this.user_id, + undoing: this.undoing + }, + 'setting doc via http' + ) + .should.equal(true) + }) + + return it('should time the request', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) + + describe('when an errors occurs', function () { + beforeEach(function () { + this.DocumentManager.setDocWithLock = sinon + .stub() + .callsArgWith(6, new Error('oops')) + return this.HttpController.setDoc(this.req, this.res, this.next) + }) + + return it('should call next with the error', function () { + return this.next.calledWith(new Error('oops')).should.equal(true) + }) + }) + + return describe('when the payload is too large', function () { + beforeEach(function () { + const lines = [] + for (let _ = 0; _ <= 200000; _++) { + lines.push('test test test') + } + this.req.body.lines = lines + this.DocumentManager.setDocWithLock = sinon.stub().callsArgWith(6) + return this.HttpController.setDoc(this.req, this.res, this.next) + }) + + it('should send back a 406 response', function () { + return this.res.sendStatus.calledWith(406).should.equal(true) + }) + + return it('should not call setDocWithLock', function () { + return this.DocumentManager.setDocWithLock.callCount.should.equal(0) + }) + }) + }) + + describe('flushProject', function () { + beforeEach(function () { + return (this.req = { + params: { + project_id: this.project_id + } + }) + }) + + describe('successfully', function () { + beforeEach(function () { + this.ProjectManager.flushProjectWithLocks = sinon.stub().callsArgWith(1) + return this.HttpController.flushProject(this.req, this.res, this.next) + }) + + it('should flush the project', function () { + return this.ProjectManager.flushProjectWithLocks + .calledWith(this.project_id) + .should.equal(true) + }) + + it('should return a successful No Content response', function () { + return this.res.sendStatus.calledWith(204).should.equal(true) + }) + + it('should log the request', function () { + return this.logger.log + .calledWith( + { project_id: this.project_id }, + 'flushing project via http' + ) + .should.equal(true) + }) + + return it('should time the request', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) + + return describe('when an errors occurs', function () { + beforeEach(function () { + this.ProjectManager.flushProjectWithLocks = sinon + .stub() + .callsArgWith(1, new Error('oops')) + return this.HttpController.flushProject(this.req, this.res, this.next) + }) + + return it('should call next with the error', function () { + return this.next.calledWith(new Error('oops')).should.equal(true) + }) + }) + }) + + describe('flushDocIfLoaded', function () { + beforeEach(function () { + this.lines = ['one', 'two', 'three'] + this.version = 42 + return (this.req = { + params: { + project_id: this.project_id, + doc_id: this.doc_id + } + }) + }) + + describe('successfully', function () { + beforeEach(function () { + this.DocumentManager.flushDocIfLoadedWithLock = sinon + .stub() + .callsArgWith(2) + return this.HttpController.flushDocIfLoaded( + this.req, + this.res, + this.next + ) + }) + + it('should flush the doc', function () { + return this.DocumentManager.flushDocIfLoadedWithLock + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) + + it('should return a successful No Content response', function () { + return this.res.sendStatus.calledWith(204).should.equal(true) + }) + + it('should log the request', function () { + return this.logger.log + .calledWith( + { doc_id: this.doc_id, project_id: this.project_id }, + 'flushing doc via http' + ) + .should.equal(true) + }) + + return it('should time the request', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) + + return describe('when an errors occurs', function () { + beforeEach(function () { + this.DocumentManager.flushDocIfLoadedWithLock = sinon + .stub() + .callsArgWith(2, new Error('oops')) + return this.HttpController.flushDocIfLoaded( + this.req, + this.res, + this.next + ) + }) + + return it('should call next with the error', function () { + return this.next.calledWith(new Error('oops')).should.equal(true) + }) + }) + }) + + describe('deleteDoc', function () { + beforeEach(function () { + return (this.req = { + params: { + project_id: this.project_id, + doc_id: this.doc_id + }, + query: {} + }) + }) + + describe('successfully', function () { + beforeEach(function () { + this.DocumentManager.flushAndDeleteDocWithLock = sinon + .stub() + .callsArgWith(3) + return this.HttpController.deleteDoc(this.req, this.res, this.next) + }) + + it('should flush and delete the doc', function () { + return this.DocumentManager.flushAndDeleteDocWithLock + .calledWith(this.project_id, this.doc_id, { + ignoreFlushErrors: false + }) + .should.equal(true) + }) + + it('should flush project history', function () { + return this.HistoryManager.flushProjectChangesAsync + .calledWithExactly(this.project_id) + .should.equal(true) + }) + + it('should return a successful No Content response', function () { + return this.res.sendStatus.calledWith(204).should.equal(true) + }) + + it('should log the request', function () { + return this.logger.log + .calledWith( + { doc_id: this.doc_id, project_id: this.project_id }, + 'deleting doc via http' + ) + .should.equal(true) + }) + + return it('should time the request', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) + + describe('ignoring errors', function () { + beforeEach(function () { + this.req.query.ignore_flush_errors = 'true' + this.DocumentManager.flushAndDeleteDocWithLock = sinon.stub().yields() + return this.HttpController.deleteDoc(this.req, this.res, this.next) + }) + + it('should delete the doc', function () { + return this.DocumentManager.flushAndDeleteDocWithLock + .calledWith(this.project_id, this.doc_id, { ignoreFlushErrors: true }) + .should.equal(true) + }) + + return it('should return a successful No Content response', function () { + return this.res.sendStatus.calledWith(204).should.equal(true) + }) + }) + + return describe('when an errors occurs', function () { + beforeEach(function () { + this.DocumentManager.flushAndDeleteDocWithLock = sinon + .stub() + .callsArgWith(3, new Error('oops')) + return this.HttpController.deleteDoc(this.req, this.res, this.next) + }) + + it('should flush project history', function () { + return this.HistoryManager.flushProjectChangesAsync + .calledWithExactly(this.project_id) + .should.equal(true) + }) + + return it('should call next with the error', function () { + return this.next.calledWith(new Error('oops')).should.equal(true) + }) + }) + }) + + describe('deleteProject', function () { + beforeEach(function () { + return (this.req = { + params: { + project_id: this.project_id + } + }) + }) + + describe('successfully', function () { + beforeEach(function () { + this.ProjectManager.flushAndDeleteProjectWithLocks = sinon + .stub() + .callsArgWith(2) + return this.HttpController.deleteProject(this.req, this.res, this.next) + }) + + it('should delete the project', function () { + return this.ProjectManager.flushAndDeleteProjectWithLocks + .calledWith(this.project_id) + .should.equal(true) + }) + + it('should return a successful No Content response', function () { + return this.res.sendStatus.calledWith(204).should.equal(true) + }) + + it('should log the request', function () { + return this.logger.log + .calledWith( + { project_id: this.project_id }, + 'deleting project via http' + ) + .should.equal(true) + }) + + return it('should time the request', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) + + describe('with the background=true option from realtime', function () { + beforeEach(function () { + this.ProjectManager.queueFlushAndDeleteProject = sinon + .stub() + .callsArgWith(1) + this.req.query = { background: true, shutdown: true } + return this.HttpController.deleteProject(this.req, this.res, this.next) + }) + + return it('should queue the flush and delete', function () { + return this.ProjectManager.queueFlushAndDeleteProject + .calledWith(this.project_id) + .should.equal(true) + }) + }) + + return describe('when an errors occurs', function () { + beforeEach(function () { + this.ProjectManager.flushAndDeleteProjectWithLocks = sinon + .stub() + .callsArgWith(2, new Error('oops')) + return this.HttpController.deleteProject(this.req, this.res, this.next) + }) + + return it('should call next with the error', function () { + return this.next.calledWith(new Error('oops')).should.equal(true) + }) + }) + }) + + describe('acceptChanges', function () { + beforeEach(function () { + return (this.req = { + params: { + project_id: this.project_id, + doc_id: this.doc_id, + change_id: (this.change_id = 'mock-change-od-1') + } + }) + }) + + describe('successfully with a single change', function () { + beforeEach(function () { + this.DocumentManager.acceptChangesWithLock = sinon + .stub() + .callsArgWith(3) + return this.HttpController.acceptChanges(this.req, this.res, this.next) + }) + + it('should accept the change', function () { + return this.DocumentManager.acceptChangesWithLock + .calledWith(this.project_id, this.doc_id, [this.change_id]) + .should.equal(true) + }) + + it('should return a successful No Content response', function () { + return this.res.sendStatus.calledWith(204).should.equal(true) + }) + + it('should log the request', function () { + return this.logger.log + .calledWith( + { project_id: this.project_id, doc_id: this.doc_id }, + 'accepting 1 changes via http' + ) + .should.equal(true) + }) + + return it('should time the request', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) + + describe('succesfully with with multiple changes', function () { + beforeEach(function () { + this.change_ids = [ + 'mock-change-od-1', + 'mock-change-od-2', + 'mock-change-od-3', + 'mock-change-od-4' + ] + this.req.body = { change_ids: this.change_ids } + this.DocumentManager.acceptChangesWithLock = sinon + .stub() + .callsArgWith(3) + return this.HttpController.acceptChanges(this.req, this.res, this.next) + }) + + it('should accept the changes in the body payload', function () { + return this.DocumentManager.acceptChangesWithLock + .calledWith(this.project_id, this.doc_id, this.change_ids) + .should.equal(true) + }) + + return it('should log the request with the correct number of changes', function () { + return this.logger.log + .calledWith( + { project_id: this.project_id, doc_id: this.doc_id }, + `accepting ${this.change_ids.length} changes via http` + ) + .should.equal(true) + }) + }) + + return describe('when an errors occurs', function () { + beforeEach(function () { + this.DocumentManager.acceptChangesWithLock = sinon + .stub() + .callsArgWith(3, new Error('oops')) + return this.HttpController.acceptChanges(this.req, this.res, this.next) + }) + + return it('should call next with the error', function () { + return this.next.calledWith(new Error('oops')).should.equal(true) + }) + }) + }) + + describe('deleteComment', function () { + beforeEach(function () { + return (this.req = { + params: { + project_id: this.project_id, + doc_id: this.doc_id, + comment_id: (this.comment_id = 'mock-comment-id') + } + }) + }) + + describe('successfully', function () { + beforeEach(function () { + this.DocumentManager.deleteCommentWithLock = sinon + .stub() + .callsArgWith(3) + return this.HttpController.deleteComment(this.req, this.res, this.next) + }) + + it('should accept the change', function () { + return this.DocumentManager.deleteCommentWithLock + .calledWith(this.project_id, this.doc_id, this.comment_id) + .should.equal(true) + }) + + it('should return a successful No Content response', function () { + return this.res.sendStatus.calledWith(204).should.equal(true) + }) + + it('should log the request', function () { + return this.logger.log + .calledWith( + { + project_id: this.project_id, + doc_id: this.doc_id, + comment_id: this.comment_id + }, + 'deleting comment via http' + ) + .should.equal(true) + }) + + return it('should time the request', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) + + return describe('when an errors occurs', function () { + beforeEach(function () { + this.DocumentManager.deleteCommentWithLock = sinon + .stub() + .callsArgWith(3, new Error('oops')) + return this.HttpController.deleteComment(this.req, this.res, this.next) + }) + + return it('should call next with the error', function () { + return this.next.calledWith(new Error('oops')).should.equal(true) + }) + }) + }) + + describe('getProjectDocsAndFlushIfOld', function () { + beforeEach(function () { + this.state = '01234567890abcdef' + this.docs = [ + { _id: '1234', lines: 'hello', v: 23 }, + { _id: '4567', lines: 'world', v: 45 } + ] + return (this.req = { + params: { + project_id: this.project_id + }, + query: { + state: this.state + } + }) + }) + + describe('successfully', function () { + beforeEach(function () { + this.ProjectManager.getProjectDocsAndFlushIfOld = sinon + .stub() + .callsArgWith(3, null, this.docs) + return this.HttpController.getProjectDocsAndFlushIfOld( + this.req, + this.res, + this.next + ) + }) + + it('should get docs from the project manager', function () { + return this.ProjectManager.getProjectDocsAndFlushIfOld + .calledWith(this.project_id, this.state, {}) + .should.equal(true) + }) + + it('should return a successful response', function () { + return this.res.send.calledWith(this.docs).should.equal(true) + }) + + it('should log the request', function () { + return this.logger.log + .calledWith( + { project_id: this.project_id, exclude: [] }, + 'getting docs via http' + ) + .should.equal(true) + }) + + it('should log the response', function () { + return this.logger.log + .calledWith( + { project_id: this.project_id, result: ['1234:23', '4567:45'] }, + 'got docs via http' + ) + .should.equal(true) + }) + + return it('should time the request', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) + + describe('when there is a conflict', function () { + beforeEach(function () { + this.ProjectManager.getProjectDocsAndFlushIfOld = sinon + .stub() + .callsArgWith( + 3, + new Errors.ProjectStateChangedError('project state changed') + ) + return this.HttpController.getProjectDocsAndFlushIfOld( + this.req, + this.res, + this.next + ) + }) + + return it('should return an HTTP 409 Conflict response', function () { + return this.res.sendStatus.calledWith(409).should.equal(true) + }) + }) + + return describe('when an error occurs', function () { + beforeEach(function () { + this.ProjectManager.getProjectDocsAndFlushIfOld = sinon + .stub() + .callsArgWith(3, new Error('oops')) + return this.HttpController.getProjectDocsAndFlushIfOld( + this.req, + this.res, + this.next + ) + }) + + return it('should call next with the error', function () { + return this.next.calledWith(new Error('oops')).should.equal(true) + }) + }) + }) + + describe('updateProject', function () { + beforeEach(function () { + this.projectHistoryId = 'history-id-123' + this.userId = 'user-id-123' + this.docUpdates = sinon.stub() + this.fileUpdates = sinon.stub() + this.version = 1234567 + return (this.req = { + body: { + projectHistoryId: this.projectHistoryId, + userId: this.userId, + docUpdates: this.docUpdates, + fileUpdates: this.fileUpdates, + version: this.version + }, + params: { + project_id: this.project_id + } + }) + }) + + describe('successfully', function () { + beforeEach(function () { + this.ProjectManager.updateProjectWithLocks = sinon + .stub() + .callsArgWith(6) + return this.HttpController.updateProject(this.req, this.res, this.next) + }) + + it('should accept the change', function () { + return this.ProjectManager.updateProjectWithLocks + .calledWith( + this.project_id, + this.projectHistoryId, + this.userId, + this.docUpdates, + this.fileUpdates, + this.version + ) + .should.equal(true) + }) + + it('should return a successful No Content response', function () { + return this.res.sendStatus.calledWith(204).should.equal(true) + }) + + return it('should time the request', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) + + return describe('when an errors occurs', function () { + beforeEach(function () { + this.ProjectManager.updateProjectWithLocks = sinon + .stub() + .callsArgWith(6, new Error('oops')) + return this.HttpController.updateProject(this.req, this.res, this.next) + }) + + return it('should call next with the error', function () { + return this.next.calledWith(new Error('oops')).should.equal(true) + }) + }) + }) + + return describe('resyncProjectHistory', function () { + beforeEach(function () { + this.projectHistoryId = 'history-id-123' + this.docs = sinon.stub() + this.files = sinon.stub() + this.fileUpdates = sinon.stub() + return (this.req = { + body: { + projectHistoryId: this.projectHistoryId, + docs: this.docs, + files: this.files + }, + params: { + project_id: this.project_id + } + }) + }) + + describe('successfully', function () { + beforeEach(function () { + this.HistoryManager.resyncProjectHistory = sinon.stub().callsArgWith(4) + return this.HttpController.resyncProjectHistory( + this.req, + this.res, + this.next + ) + }) + + it('should accept the change', function () { + return this.HistoryManager.resyncProjectHistory + .calledWith( + this.project_id, + this.projectHistoryId, + this.docs, + this.files + ) + .should.equal(true) + }) + + return it('should return a successful No Content response', function () { + return this.res.sendStatus.calledWith(204).should.equal(true) + }) + }) + + return describe('when an errors occurs', function () { + beforeEach(function () { + this.HistoryManager.resyncProjectHistory = sinon + .stub() + .callsArgWith(4, new Error('oops')) + return this.HttpController.resyncProjectHistory( + this.req, + this.res, + this.next + ) + }) + + return it('should call next with the error', function () { + return this.next.calledWith(new Error('oops')).should.equal(true) + }) + }) + }) +}) diff --git a/services/document-updater/test/unit/js/LockManager/CheckingTheLock.js b/services/document-updater/test/unit/js/LockManager/CheckingTheLock.js index 58392480d0..d91b25b64c 100644 --- a/services/document-updater/test/unit/js/LockManager/CheckingTheLock.js +++ b/services/document-updater/test/unit/js/LockManager/CheckingTheLock.js @@ -11,59 +11,57 @@ * DS206: Consider reworking classes to avoid initClass * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -require('coffee-script'); -const sinon = require('sinon'); -const assert = require('assert'); -const path = require('path'); -const modulePath = path.join(__dirname, '../../../../app/js/LockManager.js'); -const project_id = 1234; -const doc_id = 5678; -const blockingKey = `Blocking:${doc_id}`; -const SandboxedModule = require('sandboxed-module'); +require('coffee-script') +const sinon = require('sinon') +const assert = require('assert') +const path = require('path') +const modulePath = path.join(__dirname, '../../../../app/js/LockManager.js') +const project_id = 1234 +const doc_id = 5678 +const blockingKey = `Blocking:${doc_id}` +const SandboxedModule = require('sandboxed-module') -describe('LockManager - checking the lock', function(){ +describe('LockManager - checking the lock', function () { + let Profiler + const existsStub = sinon.stub() - let Profiler; - const existsStub = sinon.stub(); - - const mocks = { - "logger-sharelatex": { log() {} - }, - "redis-sharelatex": { - createClient(){ - return { - auth() {}, - exists: existsStub - }; - } - }, - "./Metrics": {inc() {}}, - "./Profiler": (Profiler = (function() { - Profiler = class Profiler { - static initClass() { - this.prototype.log = sinon.stub().returns({ end: sinon.stub() }); - this.prototype.end = sinon.stub(); - } - }; - Profiler.initClass(); - return Profiler; - })()) - }; - const LockManager = SandboxedModule.require(modulePath, {requires: mocks}); + const mocks = { + 'logger-sharelatex': { log() {} }, + 'redis-sharelatex': { + createClient() { + return { + auth() {}, + exists: existsStub + } + } + }, + './Metrics': { inc() {} }, + './Profiler': (Profiler = (function () { + Profiler = class Profiler { + static initClass() { + this.prototype.log = sinon.stub().returns({ end: sinon.stub() }) + this.prototype.end = sinon.stub() + } + } + Profiler.initClass() + return Profiler + })()) + } + const LockManager = SandboxedModule.require(modulePath, { requires: mocks }) - it('should return true if the key does not exists', function(done){ - existsStub.yields(null, "0"); - return LockManager.checkLock(doc_id, (err, free) => { - free.should.equal(true); - return done(); - }); - }); + it('should return true if the key does not exists', function (done) { + existsStub.yields(null, '0') + return LockManager.checkLock(doc_id, (err, free) => { + free.should.equal(true) + return done() + }) + }) - return it('should return false if the key does exists', function(done){ - existsStub.yields(null, "1"); - return LockManager.checkLock(doc_id, (err, free) => { - free.should.equal(false); - return done(); - }); - }); -}); + return it('should return false if the key does exists', function (done) { + existsStub.yields(null, '1') + return LockManager.checkLock(doc_id, (err, free) => { + free.should.equal(false) + return done() + }) + }) +}) diff --git a/services/document-updater/test/unit/js/LockManager/ReleasingTheLock.js b/services/document-updater/test/unit/js/LockManager/ReleasingTheLock.js index 0cf4ef88b1..9c5d70e1e0 100644 --- a/services/document-updater/test/unit/js/LockManager/ReleasingTheLock.js +++ b/services/document-updater/test/unit/js/LockManager/ReleasingTheLock.js @@ -11,79 +11,90 @@ * DS206: Consider reworking classes to avoid initClass * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -require('coffee-script'); -const sinon = require('sinon'); -const assert = require('assert'); -const path = require('path'); -const modulePath = path.join(__dirname, '../../../../app/js/LockManager.js'); -const project_id = 1234; -const doc_id = 5678; -const SandboxedModule = require('sandboxed-module'); +require('coffee-script') +const sinon = require('sinon') +const assert = require('assert') +const path = require('path') +const modulePath = path.join(__dirname, '../../../../app/js/LockManager.js') +const project_id = 1234 +const doc_id = 5678 +const SandboxedModule = require('sandboxed-module') -describe('LockManager - releasing the lock', function(){ - beforeEach(function() { - let Profiler; - this.client = { - auth() {}, - eval: sinon.stub() - }; - const mocks = { - "logger-sharelatex": { - log() {}, - error() {} - }, - "redis-sharelatex": { - createClient : () => this.client - }, - "settings-sharelatex": { - redis: { - lock: { - key_schema: { - blockingKey({doc_id}) { return `Blocking:${doc_id}`; } - } - } - } - }, - "./Metrics": {inc() {}}, - "./Profiler": (Profiler = (function() { - Profiler = class Profiler { - static initClass() { - this.prototype.log = sinon.stub().returns({ end: sinon.stub() }); - this.prototype.end = sinon.stub(); - } - }; - Profiler.initClass(); - return Profiler; - })()) - }; - this.LockManager = SandboxedModule.require(modulePath, {requires: mocks}); - this.lockValue = "lock-value-stub"; - return this.callback = sinon.stub(); - }); +describe('LockManager - releasing the lock', function () { + beforeEach(function () { + let Profiler + this.client = { + auth() {}, + eval: sinon.stub() + } + const mocks = { + 'logger-sharelatex': { + log() {}, + error() {} + }, + 'redis-sharelatex': { + createClient: () => this.client + }, + 'settings-sharelatex': { + redis: { + lock: { + key_schema: { + blockingKey({ doc_id }) { + return `Blocking:${doc_id}` + } + } + } + } + }, + './Metrics': { inc() {} }, + './Profiler': (Profiler = (function () { + Profiler = class Profiler { + static initClass() { + this.prototype.log = sinon.stub().returns({ end: sinon.stub() }) + this.prototype.end = sinon.stub() + } + } + Profiler.initClass() + return Profiler + })()) + } + this.LockManager = SandboxedModule.require(modulePath, { requires: mocks }) + this.lockValue = 'lock-value-stub' + return (this.callback = sinon.stub()) + }) - describe("when the lock is current", function() { - beforeEach(function() { - this.client.eval = sinon.stub().yields(null, 1); - return this.LockManager.releaseLock(doc_id, this.lockValue, this.callback); - }); + describe('when the lock is current', function () { + beforeEach(function () { + this.client.eval = sinon.stub().yields(null, 1) + return this.LockManager.releaseLock(doc_id, this.lockValue, this.callback) + }) - it('should clear the data from redis', function() { - return this.client.eval.calledWith(this.LockManager.unlockScript, 1, `Blocking:${doc_id}`, this.lockValue).should.equal(true); - }); + it('should clear the data from redis', function () { + return this.client.eval + .calledWith( + this.LockManager.unlockScript, + 1, + `Blocking:${doc_id}`, + 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.client.eval = sinon.stub().yields(null, 0); - return this.LockManager.releaseLock(doc_id, this.lockValue, this.callback); - }); + return describe('when the lock has expired', function () { + beforeEach(function () { + this.client.eval = sinon.stub().yields(null, 0) + return this.LockManager.releaseLock(doc_id, this.lockValue, this.callback) + }) - return it('should return an error if the lock has expired', function() { - return this.callback.calledWith(new Error("tried to release timed out lock")).should.equal(true); - }); - }); -}); + return it('should return an error if the lock has expired', function () { + return this.callback + .calledWith(new Error('tried to release timed out lock')) + .should.equal(true) + }) + }) +}) diff --git a/services/document-updater/test/unit/js/LockManager/getLockTests.js b/services/document-updater/test/unit/js/LockManager/getLockTests.js index 0d99618e59..1c41f2b0b4 100644 --- a/services/document-updater/test/unit/js/LockManager/getLockTests.js +++ b/services/document-updater/test/unit/js/LockManager/getLockTests.js @@ -14,116 +14,114 @@ * 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 modulePath = "../../../../app/js/LockManager.js"; -const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const modulePath = '../../../../app/js/LockManager.js' +const SandboxedModule = require('sandboxed-module') -describe('LockManager - getting the lock', function() { - beforeEach(function() { - let Profiler; - this.LockManager = SandboxedModule.require(modulePath, { requires: { - "logger-sharelatex": { log() {} - }, - "redis-sharelatex": { - createClient : () => { - return {auth() {}}; - } - }, - "./Metrics": {inc() {}}, - "./Profiler": (Profiler = (function() { - Profiler = class Profiler { - static initClass() { - this.prototype.log = sinon.stub().returns({ end: sinon.stub() }); - this.prototype.end = sinon.stub(); - } - }; - Profiler.initClass(); - return Profiler; - })()) - } - } - ); - this.callback = sinon.stub(); - return this.doc_id = "doc-id-123"; - }); - - describe("when the lock is not set", function() { - beforeEach(function(done) { - this.lockValue = "mock-lock-value"; - this.LockManager.tryLock = sinon.stub().callsArgWith(1, null, true, this.lockValue); - return this.LockManager.getLock(this.doc_id, (...args) => { - this.callback(...Array.from(args || [])); - return done(); - }); - }); +describe('LockManager - getting the lock', function () { + beforeEach(function () { + let Profiler + this.LockManager = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': { log() {} }, + 'redis-sharelatex': { + createClient: () => { + return { auth() {} } + } + }, + './Metrics': { inc() {} }, + './Profiler': (Profiler = (function () { + Profiler = class Profiler { + static initClass() { + this.prototype.log = sinon.stub().returns({ end: sinon.stub() }) + this.prototype.end = sinon.stub() + } + } + Profiler.initClass() + return Profiler + })()) + } + }) + this.callback = sinon.stub() + return (this.doc_id = 'doc-id-123') + }) - it("should try to get the lock", function() { - return this.LockManager.tryLock - .calledWith(this.doc_id) - .should.equal(true); - }); + describe('when the lock is not set', function () { + beforeEach(function (done) { + this.lockValue = 'mock-lock-value' + this.LockManager.tryLock = sinon + .stub() + .callsArgWith(1, null, true, this.lockValue) + return this.LockManager.getLock(this.doc_id, (...args) => { + this.callback(...Array.from(args || [])) + return done() + }) + }) - it("should only need to try once", function() { - return this.LockManager.tryLock.callCount.should.equal(1); - }); + it('should try to get the lock', function () { + return this.LockManager.tryLock.calledWith(this.doc_id).should.equal(true) + }) - return it("should return the callback with the lock value", function() { - return this.callback.calledWith(null, this.lockValue).should.equal(true); - }); - }); + it('should only need to try once', function () { + return this.LockManager.tryLock.callCount.should.equal(1) + }) - describe("when the lock is initially set", function() { - beforeEach(function(done) { - this.lockValue = "mock-lock-value"; - const startTime = Date.now(); - let tries = 0; - this.LockManager.LOCK_TEST_INTERVAL = 5; - this.LockManager.tryLock = (doc_id, callback) => { - if (callback == null) { callback = function(error, isFree) {}; } - if (((Date.now() - startTime) < 20) || (tries < 2)) { - tries = tries + 1; - return callback(null, false); - } else { - return callback(null, true, this.lockValue); - } - }; - sinon.spy(this.LockManager, "tryLock"); + return it('should return the callback with the lock value', function () { + return this.callback.calledWith(null, this.lockValue).should.equal(true) + }) + }) - return this.LockManager.getLock(this.doc_id, (...args) => { - this.callback(...Array.from(args || [])); - return done(); - }); - }); + describe('when the lock is initially set', function () { + beforeEach(function (done) { + this.lockValue = 'mock-lock-value' + const startTime = Date.now() + let tries = 0 + this.LockManager.LOCK_TEST_INTERVAL = 5 + this.LockManager.tryLock = (doc_id, callback) => { + if (callback == null) { + callback = function (error, isFree) {} + } + if (Date.now() - startTime < 20 || tries < 2) { + tries = tries + 1 + return callback(null, false) + } else { + return callback(null, true, this.lockValue) + } + } + sinon.spy(this.LockManager, 'tryLock') - it("should call tryLock multiple times until free", function() { - return (this.LockManager.tryLock.callCount > 1).should.equal(true); - }); + return this.LockManager.getLock(this.doc_id, (...args) => { + this.callback(...Array.from(args || [])) + return done() + }) + }) - return it("should return the callback with the lock value", function() { - return this.callback.calledWith(null, this.lockValue).should.equal(true); - }); - }); + it('should call tryLock multiple times until free', function () { + return (this.LockManager.tryLock.callCount > 1).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.doc_id, (...args) => { - this.callback(...Array.from(args || [])); - return done(); - }); - }); - - return it("should return the callback with an error", function() { - const e = new Error("Timeout"); - e.doc_id = this.doc_id; - return this.callback.calledWith(e).should.equal(true); - }); - }); -}); - + return it('should return the callback with the lock value', function () { + return this.callback.calledWith(null, this.lockValue).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.doc_id, (...args) => { + this.callback(...Array.from(args || [])) + return done() + }) + }) + return it('should return the callback with an error', function () { + const e = new Error('Timeout') + e.doc_id = this.doc_id + return this.callback.calledWith(e).should.equal(true) + }) + }) +}) diff --git a/services/document-updater/test/unit/js/LockManager/tryLockTests.js b/services/document-updater/test/unit/js/LockManager/tryLockTests.js index 9f6acf0002..77aecd87d1 100644 --- a/services/document-updater/test/unit/js/LockManager/tryLockTests.js +++ b/services/document-updater/test/unit/js/LockManager/tryLockTests.js @@ -11,129 +11,140 @@ * DS206: Consider reworking classes to avoid initClass * 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 modulePath = "../../../../app/js/LockManager.js"; -const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const modulePath = '../../../../app/js/LockManager.js' +const SandboxedModule = require('sandboxed-module') -describe('LockManager - trying the lock', function() { - beforeEach(function() { - let Profiler; - this.LockManager = SandboxedModule.require(modulePath, { requires: { - "logger-sharelatex": { log() {} - }, - "redis-sharelatex": { - createClient : () => { - return { - auth() {}, - set: (this.set = sinon.stub()) - }; - } - }, - "./Metrics": {inc() {}}, - "settings-sharelatex": { - redis: { - lock: { - key_schema: { - blockingKey({doc_id}) { return `Blocking:${doc_id}`; } - } - } - } - }, - "./Profiler": (this.Profiler = (Profiler = (function() { - Profiler = class Profiler { - static initClass() { - this.prototype.log = sinon.stub().returns({ end: sinon.stub() }); - this.prototype.end = sinon.stub(); - } - }; - Profiler.initClass(); - return Profiler; - })())) - } - } - ); +describe('LockManager - trying the lock', function () { + beforeEach(function () { + let Profiler + this.LockManager = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': { log() {} }, + 'redis-sharelatex': { + createClient: () => { + return { + auth() {}, + set: (this.set = sinon.stub()) + } + } + }, + './Metrics': { inc() {} }, + 'settings-sharelatex': { + redis: { + lock: { + key_schema: { + blockingKey({ doc_id }) { + return `Blocking:${doc_id}` + } + } + } + } + }, + './Profiler': (this.Profiler = Profiler = (function () { + Profiler = class Profiler { + static initClass() { + this.prototype.log = sinon.stub().returns({ end: sinon.stub() }) + this.prototype.end = sinon.stub() + } + } + Profiler.initClass() + return Profiler + })()) + } + }) - this.callback = sinon.stub(); - return this.doc_id = "doc-id-123"; - }); - - describe("when the lock is not set", function() { - beforeEach(function() { - this.lockValue = "mock-lock-value"; - this.LockManager.randomLock = sinon.stub().returns(this.lockValue); - this.set.callsArgWith(5, null, "OK"); - return this.LockManager.tryLock(this.doc_id, this.callback); - }); + this.callback = sinon.stub() + return (this.doc_id = 'doc-id-123') + }) - it("should set the lock key with an expiry if it is not set", function() { - return this.set.calledWith(`Blocking:${this.doc_id}`, this.lockValue, "EX", 30, "NX") - .should.equal(true); - }); + describe('when the lock is not set', function () { + beforeEach(function () { + this.lockValue = 'mock-lock-value' + this.LockManager.randomLock = sinon.stub().returns(this.lockValue) + this.set.callsArgWith(5, null, 'OK') + return this.LockManager.tryLock(this.doc_id, this.callback) + }) - return it("should return the callback with true and the lock value", function() { - return this.callback.calledWith(null, true, this.lockValue).should.equal(true); - }); - }); + it('should set the lock key with an expiry if it is not set', function () { + return this.set + .calledWith(`Blocking:${this.doc_id}`, this.lockValue, 'EX', 30, 'NX') + .should.equal(true) + }) - describe("when the lock is already set", function() { - beforeEach(function() { - this.set.callsArgWith(5, null, null); - return this.LockManager.tryLock(this.doc_id, this.callback); - }); + return it('should return the callback with true and the lock value', function () { + return this.callback + .calledWith(null, true, this.lockValue) + .should.equal(true) + }) + }) - return it("should return the callback with false", function() { - return this.callback.calledWith(null, false).should.equal(true); - }); - }); + describe('when the lock is already set', function () { + beforeEach(function () { + this.set.callsArgWith(5, null, null) + return this.LockManager.tryLock(this.doc_id, this.callback) + }) - return describe("when it takes a long time for redis to set the lock", function() { - beforeEach(function() { - this.Profiler.prototype.end = () => 7000; // take a long time - this.Profiler.prototype.log = sinon.stub().returns({ end: this.Profiler.prototype.end }); - this.lockValue = "mock-lock-value"; - this.LockManager.randomLock = sinon.stub().returns(this.lockValue); - this.LockManager.releaseLock = sinon.stub().callsArgWith(2,null); - return this.set.callsArgWith(5, null, "OK"); - }); + return it('should return the callback with false', function () { + return this.callback.calledWith(null, false).should.equal(true) + }) + }) - describe("in all cases", function() { - beforeEach(function() { - return this.LockManager.tryLock(this.doc_id, this.callback); - }); + return describe('when it takes a long time for redis to set the lock', function () { + beforeEach(function () { + this.Profiler.prototype.end = () => 7000 // take a long time + this.Profiler.prototype.log = sinon + .stub() + .returns({ end: this.Profiler.prototype.end }) + this.lockValue = 'mock-lock-value' + this.LockManager.randomLock = sinon.stub().returns(this.lockValue) + this.LockManager.releaseLock = sinon.stub().callsArgWith(2, null) + return this.set.callsArgWith(5, null, 'OK') + }) - it("should set the lock key with an expiry if it is not set", function() { - return this.set.calledWith(`Blocking:${this.doc_id}`, this.lockValue, "EX", 30, "NX") - .should.equal(true); - }); + describe('in all cases', function () { + beforeEach(function () { + return this.LockManager.tryLock(this.doc_id, this.callback) + }) - return it("should try to release the lock", function() { - return this.LockManager.releaseLock.calledWith(this.doc_id, this.lockValue).should.equal(true); - }); - }); + it('should set the lock key with an expiry if it is not set', function () { + return this.set + .calledWith(`Blocking:${this.doc_id}`, this.lockValue, 'EX', 30, 'NX') + .should.equal(true) + }) - describe("if the lock is released successfully", function() { - beforeEach(function() { - this.LockManager.releaseLock = sinon.stub().callsArgWith(2,null); - return this.LockManager.tryLock(this.doc_id, this.callback); - }); + return it('should try to release the lock', function () { + return this.LockManager.releaseLock + .calledWith(this.doc_id, this.lockValue) + .should.equal(true) + }) + }) - return it("should return the callback with false", function() { - return this.callback.calledWith(null, false).should.equal(true); - }); - }); + describe('if the lock is released successfully', function () { + beforeEach(function () { + this.LockManager.releaseLock = sinon.stub().callsArgWith(2, null) + return this.LockManager.tryLock(this.doc_id, this.callback) + }) - return describe("if the lock has already timed out", function() { - beforeEach(function() { - this.LockManager.releaseLock = sinon.stub().callsArgWith(2, new Error("tried to release timed out lock")); - return this.LockManager.tryLock(this.doc_id, this.callback); - }); + 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 an error", function() { - const e = new Error("tried to release timed out lock"); - return this.callback.calledWith(e).should.equal(true); - }); - }); - }); -}); + return describe('if the lock has already timed out', function () { + beforeEach(function () { + this.LockManager.releaseLock = sinon + .stub() + .callsArgWith(2, new Error('tried to release timed out lock')) + return this.LockManager.tryLock(this.doc_id, this.callback) + }) + + return it('should return the callback with an error', function () { + const e = new Error('tried to release timed out lock') + return this.callback.calledWith(e).should.equal(true) + }) + }) + }) +}) diff --git a/services/document-updater/test/unit/js/PersistenceManager/PersistenceManagerTests.js b/services/document-updater/test/unit/js/PersistenceManager/PersistenceManagerTests.js index 68bedb4b2c..98039d7823 100644 --- a/services/document-updater/test/unit/js/PersistenceManager/PersistenceManagerTests.js +++ b/services/document-updater/test/unit/js/PersistenceManager/PersistenceManagerTests.js @@ -10,301 +10,427 @@ * DS206: Consider reworking classes to avoid initClass * 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 modulePath = "../../../../app/js/PersistenceManager.js"; -const SandboxedModule = require('sandboxed-module'); -const Errors = require("../../../../app/js/Errors"); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const modulePath = '../../../../app/js/PersistenceManager.js' +const SandboxedModule = require('sandboxed-module') +const Errors = require('../../../../app/js/Errors') -describe("PersistenceManager", function() { - beforeEach(function() { - let Timer; - this.request = sinon.stub(); - this.request.defaults = () => this.request; - this.PersistenceManager = SandboxedModule.require(modulePath, { requires: { - "requestretry": this.request, - "settings-sharelatex": (this.Settings = {}), - "./Metrics": (this.Metrics = { - Timer: (Timer = (function() { - Timer = class Timer { - static initClass() { - this.prototype.done = sinon.stub(); - } - }; - Timer.initClass(); - return Timer; - })()), - inc: sinon.stub() - }), - "logger-sharelatex": (this.logger = {log: sinon.stub(), err: sinon.stub()}) - } - }); - this.project_id = "project-id-123"; - this.projectHistoryId = "history-id-123"; - this.doc_id = "doc-id-123"; - this.lines = ["one", "two", "three"]; - this.version = 42; - this.callback = sinon.stub(); - this.ranges = { comments: "mock", entries: "mock" }; - this.pathname = '/a/b/c.tex'; - this.lastUpdatedAt = Date.now(); - this.lastUpdatedBy = 'last-author-id'; - return this.Settings.apis = { - web: { - url: (this.url = "www.example.com"), - user: (this.user = "sharelatex"), - pass: (this.pass = "password") - } - }; - }); +describe('PersistenceManager', function () { + beforeEach(function () { + let Timer + this.request = sinon.stub() + this.request.defaults = () => this.request + this.PersistenceManager = SandboxedModule.require(modulePath, { + requires: { + requestretry: this.request, + 'settings-sharelatex': (this.Settings = {}), + './Metrics': (this.Metrics = { + Timer: (Timer = (function () { + Timer = class Timer { + static initClass() { + this.prototype.done = sinon.stub() + } + } + Timer.initClass() + return Timer + })()), + inc: sinon.stub() + }), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + err: sinon.stub() + }) + } + }) + this.project_id = 'project-id-123' + this.projectHistoryId = 'history-id-123' + this.doc_id = 'doc-id-123' + this.lines = ['one', 'two', 'three'] + this.version = 42 + this.callback = sinon.stub() + this.ranges = { comments: 'mock', entries: 'mock' } + this.pathname = '/a/b/c.tex' + this.lastUpdatedAt = Date.now() + this.lastUpdatedBy = 'last-author-id' + return (this.Settings.apis = { + web: { + url: (this.url = 'www.example.com'), + user: (this.user = 'sharelatex'), + pass: (this.pass = 'password') + } + }) + }) - describe("getDoc", function() { - beforeEach(function() { - return this.webResponse = { - lines: this.lines, - version: this.version, - ranges: this.ranges, - pathname: this.pathname, - projectHistoryId: this.projectHistoryId - };}); + describe('getDoc', function () { + beforeEach(function () { + return (this.webResponse = { + lines: this.lines, + version: this.version, + ranges: this.ranges, + pathname: this.pathname, + projectHistoryId: this.projectHistoryId + }) + }) - describe("with a successful response from the web api", function() { - beforeEach(function() { - this.request.callsArgWith(1, null, {statusCode: 200}, JSON.stringify(this.webResponse)); - return this.PersistenceManager.getDoc(this.project_id, this.doc_id, this.callback); - }); + describe('with a successful response from the web api', function () { + beforeEach(function () { + this.request.callsArgWith( + 1, + null, + { statusCode: 200 }, + JSON.stringify(this.webResponse) + ) + return this.PersistenceManager.getDoc( + this.project_id, + this.doc_id, + this.callback + ) + }) - it("should call the web api", function() { - return this.request - .calledWith({ - url: `${this.url}/project/${this.project_id}/doc/${this.doc_id}`, - method: "GET", - headers: { - "accept": "application/json" - }, - auth: { - user: this.user, - pass: this.pass, - sendImmediately: true - }, - jar: false, - timeout: 5000 - }) - .should.equal(true); - }); + it('should call the web api', function () { + return this.request + .calledWith({ + url: `${this.url}/project/${this.project_id}/doc/${this.doc_id}`, + method: 'GET', + headers: { + accept: 'application/json' + }, + auth: { + user: this.user, + pass: this.pass, + sendImmediately: true + }, + jar: false, + timeout: 5000 + }) + .should.equal(true) + }) - it("should call the callback with the doc lines, version and ranges", function() { - return this.callback - .calledWith(null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId) - .should.equal(true); - }); + it('should call the callback with the doc lines, version and ranges', function () { + return this.callback + .calledWith( + null, + this.lines, + this.version, + this.ranges, + this.pathname, + this.projectHistoryId + ) + .should.equal(true) + }) - it("should time the execution", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); + it('should time the execution', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) - return it("should increment the metric", function() { - return this.Metrics.inc.calledWith("getDoc", 1, {status: 200}).should.equal(true); - }); - }); + return it('should increment the metric', function () { + return this.Metrics.inc + .calledWith('getDoc', 1, { status: 200 }) + .should.equal(true) + }) + }) - describe("when request returns an error", function() { - beforeEach(function() { - this.error = new Error("oops"); - this.error.code = "EOOPS"; - this.request.callsArgWith(1, this.error, null, null); - return this.PersistenceManager.getDoc(this.project_id, this.doc_id, this.callback); - }); + describe('when request returns an error', function () { + beforeEach(function () { + this.error = new Error('oops') + this.error.code = 'EOOPS' + this.request.callsArgWith(1, this.error, null, null) + return this.PersistenceManager.getDoc( + this.project_id, + this.doc_id, + this.callback + ) + }) - it("should return the error", function() { - return this.callback.calledWith(this.error).should.equal(true); - }); + it('should return the error', function () { + return this.callback.calledWith(this.error).should.equal(true) + }) - it("should time the execution", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); + it('should time the execution', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) - return it("should increment the metric", function() { - return this.Metrics.inc.calledWith("getDoc", 1, {status: "EOOPS"}).should.equal(true); - }); - }); + return it('should increment the metric', function () { + return this.Metrics.inc + .calledWith('getDoc', 1, { status: 'EOOPS' }) + .should.equal(true) + }) + }) - describe("when the request returns 404", function() { - beforeEach(function() { - this.request.callsArgWith(1, null, {statusCode: 404}, ""); - return this.PersistenceManager.getDoc(this.project_id, this.doc_id, this.callback); - }); + describe('when the request returns 404', function () { + beforeEach(function () { + this.request.callsArgWith(1, null, { statusCode: 404 }, '') + return this.PersistenceManager.getDoc( + this.project_id, + this.doc_id, + this.callback + ) + }) - it("should return a NotFoundError", function() { - return this.callback.calledWith(new Errors.NotFoundError("not found")).should.equal(true); - }); + it('should return a NotFoundError', function () { + return this.callback + .calledWith(new Errors.NotFoundError('not found')) + .should.equal(true) + }) - it("should time the execution", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); + it('should time the execution', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) - return it("should increment the metric", function() { - return this.Metrics.inc.calledWith("getDoc", 1, {status: 404}).should.equal(true); - }); - }); + return it('should increment the metric', function () { + return this.Metrics.inc + .calledWith('getDoc', 1, { status: 404 }) + .should.equal(true) + }) + }) - describe("when the request returns an error status code", function() { - beforeEach(function() { - this.request.callsArgWith(1, null, {statusCode: 500}, ""); - return this.PersistenceManager.getDoc(this.project_id, this.doc_id, this.callback); - }); + describe('when the request returns an error status code', function () { + beforeEach(function () { + this.request.callsArgWith(1, null, { statusCode: 500 }, '') + return this.PersistenceManager.getDoc( + this.project_id, + this.doc_id, + this.callback + ) + }) - it("should return an error", function() { - return this.callback.calledWith(new Error("web api error")).should.equal(true); - }); + it('should return an error', function () { + return this.callback + .calledWith(new Error('web api error')) + .should.equal(true) + }) - it("should time the execution", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); + it('should time the execution', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) - return it("should increment the metric", function() { - return this.Metrics.inc.calledWith("getDoc", 1, {status: 500}).should.equal(true); - }); - }); + return it('should increment the metric', function () { + return this.Metrics.inc + .calledWith('getDoc', 1, { status: 500 }) + .should.equal(true) + }) + }) - describe("when request returns an doc without lines", function() { - beforeEach(function() { - delete this.webResponse.lines; - this.request.callsArgWith(1, null, {statusCode: 200}, JSON.stringify(this.webResponse)); - return this.PersistenceManager.getDoc(this.project_id, this.doc_id, this.callback); - }); + describe('when request returns an doc without lines', function () { + beforeEach(function () { + delete this.webResponse.lines + this.request.callsArgWith( + 1, + null, + { statusCode: 200 }, + JSON.stringify(this.webResponse) + ) + return this.PersistenceManager.getDoc( + this.project_id, + this.doc_id, + this.callback + ) + }) - return it("should return and error", function() { - return this.callback.calledWith(new Error("web API response had no doc lines")).should.equal(true); - }); - }); + return it('should return and error', function () { + return this.callback + .calledWith(new Error('web API response had no doc lines')) + .should.equal(true) + }) + }) - describe("when request returns an doc without a version", function() { - beforeEach(function() { - delete this.webResponse.version; - this.request.callsArgWith(1, null, {statusCode: 200}, JSON.stringify(this.webResponse)); - return this.PersistenceManager.getDoc(this.project_id, this.doc_id, this.callback); - }); + describe('when request returns an doc without a version', function () { + beforeEach(function () { + delete this.webResponse.version + this.request.callsArgWith( + 1, + null, + { statusCode: 200 }, + JSON.stringify(this.webResponse) + ) + return this.PersistenceManager.getDoc( + this.project_id, + this.doc_id, + this.callback + ) + }) - return it("should return and error", function() { - return this.callback.calledWith(new Error("web API response had no valid doc version")).should.equal(true); - }); - }); + return it('should return and error', function () { + return this.callback + .calledWith(new Error('web API response had no valid doc version')) + .should.equal(true) + }) + }) - return describe("when request returns an doc without a pathname", function() { - beforeEach(function() { - delete this.webResponse.pathname; - this.request.callsArgWith(1, null, {statusCode: 200}, JSON.stringify(this.webResponse)); - return this.PersistenceManager.getDoc(this.project_id, this.doc_id, this.callback); - }); + return describe('when request returns an doc without a pathname', function () { + beforeEach(function () { + delete this.webResponse.pathname + this.request.callsArgWith( + 1, + null, + { statusCode: 200 }, + JSON.stringify(this.webResponse) + ) + return this.PersistenceManager.getDoc( + this.project_id, + this.doc_id, + this.callback + ) + }) - return it("should return and error", function() { - return this.callback.calledWith(new Error("web API response had no valid doc pathname")).should.equal(true); - }); - }); - }); + return it('should return and error', function () { + return this.callback + .calledWith(new Error('web API response had no valid doc pathname')) + .should.equal(true) + }) + }) + }) - return describe("setDoc", function() { - describe("with a successful response from the web api", function() { - beforeEach(function() { - this.request.callsArgWith(1, null, {statusCode: 200}); - return this.PersistenceManager.setDoc(this.project_id, this.doc_id, this.lines, this.version, this.ranges, this.lastUpdatedAt, this.lastUpdatedBy, this.callback); - }); + return describe('setDoc', function () { + describe('with a successful response from the web api', function () { + beforeEach(function () { + this.request.callsArgWith(1, null, { statusCode: 200 }) + return this.PersistenceManager.setDoc( + this.project_id, + this.doc_id, + this.lines, + this.version, + this.ranges, + this.lastUpdatedAt, + this.lastUpdatedBy, + this.callback + ) + }) - it("should call the web api", function() { - return this.request - .calledWith({ - url: `${this.url}/project/${this.project_id}/doc/${this.doc_id}`, - json: { - lines: this.lines, - version: this.version, - ranges: this.ranges, - lastUpdatedAt: this.lastUpdatedAt, - lastUpdatedBy: this.lastUpdatedBy - }, - method: "POST", - auth: { - user: this.user, - pass: this.pass, - sendImmediately: true - }, - jar: false, - timeout: 5000 - }) - .should.equal(true); - }); + it('should call the web api', function () { + return this.request + .calledWith({ + url: `${this.url}/project/${this.project_id}/doc/${this.doc_id}`, + json: { + lines: this.lines, + version: this.version, + ranges: this.ranges, + lastUpdatedAt: this.lastUpdatedAt, + lastUpdatedBy: this.lastUpdatedBy + }, + method: 'POST', + auth: { + user: this.user, + pass: this.pass, + sendImmediately: true + }, + jar: false, + timeout: 5000 + }) + .should.equal(true) + }) - it("should call the callback without error", function() { - return this.callback.calledWith(null).should.equal(true); - }); + it('should call the callback without error', function () { + return this.callback.calledWith(null).should.equal(true) + }) - it("should time the execution", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); + it('should time the execution', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) - return it("should increment the metric", function() { - return this.Metrics.inc.calledWith("setDoc", 1, {status: 200}).should.equal(true); - }); - }); + return it('should increment the metric', function () { + return this.Metrics.inc + .calledWith('setDoc', 1, { status: 200 }) + .should.equal(true) + }) + }) - describe("when request returns an error", function() { - beforeEach(function() { - this.error = new Error("oops"); - this.error.code = "EOOPS"; - this.request.callsArgWith(1, this.error, null, null); - return this.PersistenceManager.setDoc(this.project_id, this.doc_id, this.lines, this.version, this.ranges, this.lastUpdatedAt, this.lastUpdatedBy, this.callback); - }); + describe('when request returns an error', function () { + beforeEach(function () { + this.error = new Error('oops') + this.error.code = 'EOOPS' + this.request.callsArgWith(1, this.error, null, null) + return this.PersistenceManager.setDoc( + this.project_id, + this.doc_id, + this.lines, + this.version, + this.ranges, + this.lastUpdatedAt, + this.lastUpdatedBy, + this.callback + ) + }) - it("should return the error", function() { - return this.callback.calledWith(this.error).should.equal(true); - }); + it('should return the error', function () { + return this.callback.calledWith(this.error).should.equal(true) + }) - it("should time the execution", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); + it('should time the execution', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) - return it("should increment the metric", function() { - return this.Metrics.inc.calledWith("setDoc", 1, {status: "EOOPS"}).should.equal(true); - }); - }); + return it('should increment the metric', function () { + return this.Metrics.inc + .calledWith('setDoc', 1, { status: 'EOOPS' }) + .should.equal(true) + }) + }) - describe("when the request returns 404", function() { - beforeEach(function() { - this.request.callsArgWith(1, null, {statusCode: 404}, ""); - return this.PersistenceManager.setDoc(this.project_id, this.doc_id, this.lines, this.version, this.ranges, this.lastUpdatedAt, this.lastUpdatedBy, this.callback); - }); + describe('when the request returns 404', function () { + beforeEach(function () { + this.request.callsArgWith(1, null, { statusCode: 404 }, '') + return this.PersistenceManager.setDoc( + this.project_id, + this.doc_id, + this.lines, + this.version, + this.ranges, + this.lastUpdatedAt, + this.lastUpdatedBy, + this.callback + ) + }) - it("should return a NotFoundError", function() { - return this.callback.calledWith(new Errors.NotFoundError("not found")).should.equal(true); - }); + it('should return a NotFoundError', function () { + return this.callback + .calledWith(new Errors.NotFoundError('not found')) + .should.equal(true) + }) - it("should time the execution", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); + it('should time the execution', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) - return it("should increment the metric", function() { - return this.Metrics.inc.calledWith("setDoc", 1, {status: 404}).should.equal(true); - }); - }); + return it('should increment the metric', function () { + return this.Metrics.inc + .calledWith('setDoc', 1, { status: 404 }) + .should.equal(true) + }) + }) - return describe("when the request returns an error status code", function() { - beforeEach(function() { - this.request.callsArgWith(1, null, {statusCode: 500}, ""); - return this.PersistenceManager.setDoc(this.project_id, this.doc_id, this.lines, this.version, this.ranges, this.lastUpdatedAt, this.lastUpdatedBy, this.callback); - }); + return describe('when the request returns an error status code', function () { + beforeEach(function () { + this.request.callsArgWith(1, null, { statusCode: 500 }, '') + return this.PersistenceManager.setDoc( + this.project_id, + this.doc_id, + this.lines, + this.version, + this.ranges, + this.lastUpdatedAt, + this.lastUpdatedBy, + this.callback + ) + }) - it("should return an error", function() { - return this.callback.calledWith(new Error("web api error")).should.equal(true); - }); + it('should return an error', function () { + return this.callback + .calledWith(new Error('web api error')) + .should.equal(true) + }) - it("should time the execution", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); + it('should time the execution', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) - return it("should increment the metric", function() { - return this.Metrics.inc.calledWith("setDoc", 1, {status: 500}).should.equal(true); - }); - }); - }); -}); + return it('should increment the metric', function () { + return this.Metrics.inc + .calledWith('setDoc', 1, { status: 500 }) + .should.equal(true) + }) + }) + }) +}) diff --git a/services/document-updater/test/unit/js/ProjectHistoryRedisManager/ProjectHistoryRedisManagerTests.js b/services/document-updater/test/unit/js/ProjectHistoryRedisManager/ProjectHistoryRedisManagerTests.js index d3bba8f42f..c4ec5b1c12 100644 --- a/services/document-updater/test/unit/js/ProjectHistoryRedisManager/ProjectHistoryRedisManagerTests.js +++ b/services/document-updater/test/unit/js/ProjectHistoryRedisManager/ProjectHistoryRedisManagerTests.js @@ -11,149 +11,191 @@ * 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 modulePath = "../../../../app/js/ProjectHistoryRedisManager.js"; -const SandboxedModule = require('sandboxed-module'); -const tk = require("timekeeper"); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const modulePath = '../../../../app/js/ProjectHistoryRedisManager.js' +const SandboxedModule = require('sandboxed-module') +const tk = require('timekeeper') -describe("ProjectHistoryRedisManager", function() { - beforeEach(function() { - this.project_id = "project-id-123"; - this.projectHistoryId = "history-id-123"; - this.user_id = "user-id-123"; - this.callback = sinon.stub(); - this.rclient = {}; - tk.freeze(new Date()); - return this.ProjectHistoryRedisManager = SandboxedModule.require(modulePath, { - requires: { - "settings-sharelatex": (this.settings = { - redis: { - project_history: { - key_schema: { - projectHistoryOps({project_id}) { return `ProjectHistory:Ops:${project_id}`; }, - projectHistoryFirstOpTimestamp({project_id}) { return `ProjectHistory:FirstOpTimestamp:${project_id}`; } - } - } - } - }), - "redis-sharelatex": { - createClient: () => this.rclient - }, - "logger-sharelatex": { - log() {} - }, - "./Metrics": (this.metrics = { summary: sinon.stub()}) - }, - globals: { - JSON: (this.JSON = JSON) - } - } - ); - }); +describe('ProjectHistoryRedisManager', function () { + beforeEach(function () { + this.project_id = 'project-id-123' + this.projectHistoryId = 'history-id-123' + this.user_id = 'user-id-123' + this.callback = sinon.stub() + this.rclient = {} + tk.freeze(new Date()) + return (this.ProjectHistoryRedisManager = SandboxedModule.require( + modulePath, + { + requires: { + 'settings-sharelatex': (this.settings = { + redis: { + project_history: { + key_schema: { + projectHistoryOps({ project_id }) { + return `ProjectHistory:Ops:${project_id}` + }, + projectHistoryFirstOpTimestamp({ project_id }) { + return `ProjectHistory:FirstOpTimestamp:${project_id}` + } + } + } + } + }), + 'redis-sharelatex': { + createClient: () => this.rclient + }, + 'logger-sharelatex': { + log() {} + }, + './Metrics': (this.metrics = { summary: sinon.stub() }) + }, + globals: { + JSON: (this.JSON = JSON) + } + } + )) + }) - afterEach(function() { return tk.reset(); }); + afterEach(function () { + return tk.reset() + }) - describe("queueOps", function() { - beforeEach(function() { - this.ops = ["mock-op-1", "mock-op-2"]; - this.multi = {exec: sinon.stub()}; - this.multi.rpush = sinon.stub(); - this.multi.setnx = sinon.stub(); - this.rclient.multi = () => this.multi; - // @rclient = multi: () => @multi - return this.ProjectHistoryRedisManager.queueOps(this.project_id, ...Array.from(this.ops), this.callback); - }); + describe('queueOps', function () { + beforeEach(function () { + this.ops = ['mock-op-1', 'mock-op-2'] + this.multi = { exec: sinon.stub() } + this.multi.rpush = sinon.stub() + this.multi.setnx = sinon.stub() + this.rclient.multi = () => this.multi + // @rclient = multi: () => @multi + return this.ProjectHistoryRedisManager.queueOps( + this.project_id, + ...Array.from(this.ops), + this.callback + ) + }) - it("should queue an update", function() { - return this.multi.rpush - .calledWithExactly( - `ProjectHistory:Ops:${this.project_id}`, - this.ops[0], - this.ops[1] - ).should.equal(true); - }); + it('should queue an update', function () { + return this.multi.rpush + .calledWithExactly( + `ProjectHistory:Ops:${this.project_id}`, + this.ops[0], + this.ops[1] + ) + .should.equal(true) + }) - return it("should set the queue timestamp if not present", function() { - return this.multi.setnx - .calledWithExactly( - `ProjectHistory:FirstOpTimestamp:${this.project_id}`, - Date.now() - ).should.equal(true); - }); - }); + return it('should set the queue timestamp if not present', function () { + return this.multi.setnx + .calledWithExactly( + `ProjectHistory:FirstOpTimestamp:${this.project_id}`, + Date.now() + ) + .should.equal(true) + }) + }) - describe("queueRenameEntity", function() { - beforeEach(function() { - this.file_id = 1234; + describe('queueRenameEntity', function () { + beforeEach(function () { + this.file_id = 1234 - this.rawUpdate = { - pathname: (this.pathname = '/old'), - newPathname: (this.newPathname = '/new'), - version: (this.version = 2) - }; + this.rawUpdate = { + pathname: (this.pathname = '/old'), + newPathname: (this.newPathname = '/new'), + version: (this.version = 2) + } - this.ProjectHistoryRedisManager.queueOps = sinon.stub(); - return this.ProjectHistoryRedisManager.queueRenameEntity(this.project_id, this.projectHistoryId, 'file', this.file_id, this.user_id, this.rawUpdate, this.callback); - }); + this.ProjectHistoryRedisManager.queueOps = sinon.stub() + return this.ProjectHistoryRedisManager.queueRenameEntity( + this.project_id, + this.projectHistoryId, + 'file', + this.file_id, + this.user_id, + this.rawUpdate, + this.callback + ) + }) - return it("should queue an update", function() { - const update = { - pathname: this.pathname, - new_pathname: this.newPathname, - meta: { - user_id: this.user_id, - ts: new Date() - }, - version: this.version, - projectHistoryId: this.projectHistoryId, - file: this.file_id - }; + return it('should queue an update', function () { + const update = { + pathname: this.pathname, + new_pathname: this.newPathname, + meta: { + user_id: this.user_id, + ts: new Date() + }, + version: this.version, + projectHistoryId: this.projectHistoryId, + file: this.file_id + } - return this.ProjectHistoryRedisManager.queueOps - .calledWithExactly(this.project_id, this.JSON.stringify(update), this.callback) - .should.equal(true); - }); - }); + return this.ProjectHistoryRedisManager.queueOps + .calledWithExactly( + this.project_id, + this.JSON.stringify(update), + this.callback + ) + .should.equal(true) + }) + }) - return describe("queueAddEntity", function() { - beforeEach(function() { - this.rclient.rpush = sinon.stub().yields(); - this.doc_id = 1234; + return describe('queueAddEntity', function () { + beforeEach(function () { + this.rclient.rpush = sinon.stub().yields() + this.doc_id = 1234 - this.rawUpdate = { - pathname: (this.pathname = '/old'), - docLines: (this.docLines = 'a\nb'), - version: (this.version = 2), - url: (this.url = 'filestore.example.com') - }; + this.rawUpdate = { + pathname: (this.pathname = '/old'), + docLines: (this.docLines = 'a\nb'), + version: (this.version = 2), + url: (this.url = 'filestore.example.com') + } - this.ProjectHistoryRedisManager.queueOps = sinon.stub(); - return this.ProjectHistoryRedisManager.queueAddEntity(this.project_id, this.projectHistoryId, 'doc', this.doc_id, this.user_id, this.rawUpdate, this.callback); - }); + this.ProjectHistoryRedisManager.queueOps = sinon.stub() + return this.ProjectHistoryRedisManager.queueAddEntity( + this.project_id, + this.projectHistoryId, + 'doc', + this.doc_id, + this.user_id, + this.rawUpdate, + this.callback + ) + }) - it("should queue an update", function() { - const update = { - pathname: this.pathname, - docLines: this.docLines, - url: this.url, - meta: { - user_id: this.user_id, - ts: new Date() - }, - version: this.version, - projectHistoryId: this.projectHistoryId, - doc: this.doc_id - }; + it('should queue an update', function () { + const update = { + pathname: this.pathname, + docLines: this.docLines, + url: this.url, + meta: { + user_id: this.user_id, + ts: new Date() + }, + version: this.version, + projectHistoryId: this.projectHistoryId, + doc: this.doc_id + } - return this.ProjectHistoryRedisManager.queueOps - .calledWithExactly(this.project_id, this.JSON.stringify(update), this.callback) - .should.equal(true); - }); + return this.ProjectHistoryRedisManager.queueOps + .calledWithExactly( + this.project_id, + this.JSON.stringify(update), + this.callback + ) + .should.equal(true) + }) - describe("queueResyncProjectStructure", function() { return it("should queue an update", function() {}); }); + describe('queueResyncProjectStructure', function () { + return it('should queue an update', function () {}) + }) - return describe("queueResyncDocContent", function() { return it("should queue an update", function() {}); }); - }); -}); + return describe('queueResyncDocContent', function () { + return it('should queue an update', function () {}) + }) + }) +}) diff --git a/services/document-updater/test/unit/js/ProjectManager/flushAndDeleteProjectTests.js b/services/document-updater/test/unit/js/ProjectManager/flushAndDeleteProjectTests.js index ebebf64740..7afa407e7c 100644 --- a/services/document-updater/test/unit/js/ProjectManager/flushAndDeleteProjectTests.js +++ b/services/document-updater/test/unit/js/ProjectManager/flushAndDeleteProjectTests.js @@ -12,121 +12,146 @@ * DS206: Consider reworking classes to avoid initClass * 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 modulePath = "../../../../app/js/ProjectManager.js"; -const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const modulePath = '../../../../app/js/ProjectManager.js' +const SandboxedModule = require('sandboxed-module') -describe("ProjectManager - flushAndDeleteProject", function() { - beforeEach(function() { - let Timer; - this.ProjectManager = SandboxedModule.require(modulePath, { requires: { - "./RedisManager": (this.RedisManager = {}), - "./ProjectHistoryRedisManager": (this.ProjectHistoryRedisManager = {}), - "./DocumentManager": (this.DocumentManager = {}), - "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }), - "./HistoryManager": (this.HistoryManager = - {flushProjectChanges: sinon.stub().callsArg(2)}), - "./Metrics": (this.Metrics = { - Timer: (Timer = (function() { - Timer = class Timer { - static initClass() { - this.prototype.done = sinon.stub(); - } - }; - Timer.initClass(); - return Timer; - })()) - }) - } - } - ); - this.project_id = "project-id-123"; - return this.callback = sinon.stub(); - }); +describe('ProjectManager - flushAndDeleteProject', function () { + beforeEach(function () { + let Timer + this.ProjectManager = SandboxedModule.require(modulePath, { + requires: { + './RedisManager': (this.RedisManager = {}), + './ProjectHistoryRedisManager': (this.ProjectHistoryRedisManager = {}), + './DocumentManager': (this.DocumentManager = {}), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub() + }), + './HistoryManager': (this.HistoryManager = { + flushProjectChanges: sinon.stub().callsArg(2) + }), + './Metrics': (this.Metrics = { + Timer: (Timer = (function () { + Timer = class Timer { + static initClass() { + this.prototype.done = sinon.stub() + } + } + Timer.initClass() + return Timer + })()) + }) + } + }) + this.project_id = 'project-id-123' + return (this.callback = sinon.stub()) + }) - describe("successfully", function() { - beforeEach(function(done) { - this.doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"]; - this.RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, this.doc_ids); - this.DocumentManager.flushAndDeleteDocWithLock = sinon.stub().callsArg(3); - return this.ProjectManager.flushAndDeleteProjectWithLocks(this.project_id, {}, error => { - this.callback(error); - return done(); - }); - }); + describe('successfully', function () { + beforeEach(function (done) { + this.doc_ids = ['doc-id-1', 'doc-id-2', 'doc-id-3'] + this.RedisManager.getDocIdsInProject = sinon + .stub() + .callsArgWith(1, null, this.doc_ids) + this.DocumentManager.flushAndDeleteDocWithLock = sinon.stub().callsArg(3) + return this.ProjectManager.flushAndDeleteProjectWithLocks( + this.project_id, + {}, + (error) => { + this.callback(error) + return done() + } + ) + }) - it("should get the doc ids in the project", function() { - return this.RedisManager.getDocIdsInProject - .calledWith(this.project_id) - .should.equal(true); - }); + it('should get the doc ids in the project', function () { + return this.RedisManager.getDocIdsInProject + .calledWith(this.project_id) + .should.equal(true) + }) - it("should delete each doc in the project", function() { - return Array.from(this.doc_ids).map((doc_id) => - this.DocumentManager.flushAndDeleteDocWithLock - .calledWith(this.project_id, doc_id, {}) - .should.equal(true)); - }); + it('should delete each doc in the project', function () { + return Array.from(this.doc_ids).map((doc_id) => + this.DocumentManager.flushAndDeleteDocWithLock + .calledWith(this.project_id, doc_id, {}) + .should.equal(true) + ) + }) - it("should flush project history", function() { - return this.HistoryManager.flushProjectChanges - .calledWith(this.project_id, {}) - .should.equal(true); - }); + it('should flush project history', function () { + return this.HistoryManager.flushProjectChanges + .calledWith(this.project_id, {}) + .should.equal(true) + }) - it("should call the callback without error", function() { - return this.callback.calledWith(null).should.equal(true); - }); + it('should call the callback without error', function () { + return this.callback.calledWith(null).should.equal(true) + }) - return it("should time the execution", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - }); + return it('should time the execution', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) - return describe("when a doc errors", function() { - beforeEach(function(done) { - this.doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"]; - this.RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, this.doc_ids); - this.DocumentManager.flushAndDeleteDocWithLock = sinon.spy((project_id, doc_id, options, callback) => { - if (doc_id === "doc-id-1") { - return callback(this.error = new Error("oops, something went wrong")); - } else { - return callback(); - } - }); - return this.ProjectManager.flushAndDeleteProjectWithLocks(this.project_id, {}, error => { - this.callback(error); - return done(); - }); - }); + return describe('when a doc errors', function () { + beforeEach(function (done) { + this.doc_ids = ['doc-id-1', 'doc-id-2', 'doc-id-3'] + this.RedisManager.getDocIdsInProject = sinon + .stub() + .callsArgWith(1, null, this.doc_ids) + this.DocumentManager.flushAndDeleteDocWithLock = sinon.spy( + (project_id, doc_id, options, callback) => { + if (doc_id === 'doc-id-1') { + return callback( + (this.error = new Error('oops, something went wrong')) + ) + } else { + return callback() + } + } + ) + return this.ProjectManager.flushAndDeleteProjectWithLocks( + this.project_id, + {}, + (error) => { + this.callback(error) + return done() + } + ) + }) - it("should still flush each doc in the project", function() { - return Array.from(this.doc_ids).map((doc_id) => - this.DocumentManager.flushAndDeleteDocWithLock - .calledWith(this.project_id, doc_id, {}) - .should.equal(true)); - }); + it('should still flush each doc in the project', function () { + return Array.from(this.doc_ids).map((doc_id) => + this.DocumentManager.flushAndDeleteDocWithLock + .calledWith(this.project_id, doc_id, {}) + .should.equal(true) + ) + }) - it("should still flush project history", function() { - return this.HistoryManager.flushProjectChanges - .calledWith(this.project_id, {}) - .should.equal(true); - }); + it('should still flush project history', function () { + return this.HistoryManager.flushProjectChanges + .calledWith(this.project_id, {}) + .should.equal(true) + }) - it("should record the error", function() { - return this.logger.error - .calledWith({err: this.error, project_id: this.project_id, doc_id: "doc-id-1"}, "error deleting doc") - .should.equal(true); - }); + it('should record the error', function () { + return this.logger.error + .calledWith( + { err: this.error, project_id: this.project_id, doc_id: 'doc-id-1' }, + 'error deleting doc' + ) + .should.equal(true) + }) - it("should call the callback with an error", function() { - return this.callback.calledWith(new Error()).should.equal(true); - }); + it('should call the callback with an error', function () { + return this.callback.calledWith(new Error()).should.equal(true) + }) - return it("should time the execution", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - }); -}); + return it('should time the execution', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) +}) diff --git a/services/document-updater/test/unit/js/ProjectManager/flushProjectTests.js b/services/document-updater/test/unit/js/ProjectManager/flushProjectTests.js index 7d74c61507..110a827e64 100644 --- a/services/document-updater/test/unit/js/ProjectManager/flushProjectTests.js +++ b/services/document-updater/test/unit/js/ProjectManager/flushProjectTests.js @@ -14,109 +14,133 @@ * 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 modulePath = "../../../../app/js/ProjectManager.js"; -const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const modulePath = '../../../../app/js/ProjectManager.js' +const SandboxedModule = require('sandboxed-module') -describe("ProjectManager - flushProject", function() { - beforeEach(function() { - let Timer; - this.ProjectManager = SandboxedModule.require(modulePath, { requires: { - "./RedisManager": (this.RedisManager = {}), - "./ProjectHistoryRedisManager": (this.ProjectHistoryRedisManager = {}), - "./DocumentManager": (this.DocumentManager = {}), - "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }), - "./HistoryManager": (this.HistoryManager = {}), - "./Metrics": (this.Metrics = { - Timer: (Timer = (function() { - Timer = class Timer { - static initClass() { - this.prototype.done = sinon.stub(); - } - }; - Timer.initClass(); - return Timer; - })()) - }) - } - } - ); - this.project_id = "project-id-123"; - return this.callback = sinon.stub(); - }); +describe('ProjectManager - flushProject', function () { + beforeEach(function () { + let Timer + this.ProjectManager = SandboxedModule.require(modulePath, { + requires: { + './RedisManager': (this.RedisManager = {}), + './ProjectHistoryRedisManager': (this.ProjectHistoryRedisManager = {}), + './DocumentManager': (this.DocumentManager = {}), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub() + }), + './HistoryManager': (this.HistoryManager = {}), + './Metrics': (this.Metrics = { + Timer: (Timer = (function () { + Timer = class Timer { + static initClass() { + this.prototype.done = sinon.stub() + } + } + Timer.initClass() + return Timer + })()) + }) + } + }) + this.project_id = 'project-id-123' + return (this.callback = sinon.stub()) + }) - describe("successfully", function() { - beforeEach(function(done) { - this.doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"]; - this.RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, this.doc_ids); - this.DocumentManager.flushDocIfLoadedWithLock = sinon.stub().callsArg(2); - return this.ProjectManager.flushProjectWithLocks(this.project_id, error => { - this.callback(error); - return done(); - }); - }); + describe('successfully', function () { + beforeEach(function (done) { + this.doc_ids = ['doc-id-1', 'doc-id-2', 'doc-id-3'] + this.RedisManager.getDocIdsInProject = sinon + .stub() + .callsArgWith(1, null, this.doc_ids) + this.DocumentManager.flushDocIfLoadedWithLock = sinon.stub().callsArg(2) + return this.ProjectManager.flushProjectWithLocks( + this.project_id, + (error) => { + this.callback(error) + return done() + } + ) + }) - it("should get the doc ids in the project", function() { - return this.RedisManager.getDocIdsInProject - .calledWith(this.project_id) - .should.equal(true); - }); - - it("should flush each doc in the project", function() { - return Array.from(this.doc_ids).map((doc_id) => - this.DocumentManager.flushDocIfLoadedWithLock - .calledWith(this.project_id, doc_id) - .should.equal(true)); - }); + it('should get the doc ids in the project', function () { + return this.RedisManager.getDocIdsInProject + .calledWith(this.project_id) + .should.equal(true) + }) - it("should call the callback without error", function() { - return this.callback.calledWith(null).should.equal(true); - }); + it('should flush each doc in the project', function () { + return Array.from(this.doc_ids).map((doc_id) => + this.DocumentManager.flushDocIfLoadedWithLock + .calledWith(this.project_id, doc_id) + .should.equal(true) + ) + }) - return it("should time the execution", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - }); + it('should call the callback without error', function () { + return this.callback.calledWith(null).should.equal(true) + }) - return describe("when a doc errors", function() { - beforeEach(function(done) { - this.doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"]; - this.RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, this.doc_ids); - this.DocumentManager.flushDocIfLoadedWithLock = sinon.spy((project_id, doc_id, callback) => { - if (callback == null) { callback = function(error) {}; } - if (doc_id === "doc-id-1") { - return callback(this.error = new Error("oops, something went wrong")); - } else { - return callback(); - } - }); - return this.ProjectManager.flushProjectWithLocks(this.project_id, error => { - this.callback(error); - return done(); - }); - }); - - it("should still flush each doc in the project", function() { - return Array.from(this.doc_ids).map((doc_id) => - this.DocumentManager.flushDocIfLoadedWithLock - .calledWith(this.project_id, doc_id) - .should.equal(true)); - }); + return it('should time the execution', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) - it("should record the error", function() { - return this.logger.error - .calledWith({err: this.error, project_id: this.project_id, doc_id: "doc-id-1"}, "error flushing doc") - .should.equal(true); - }); + return describe('when a doc errors', function () { + beforeEach(function (done) { + this.doc_ids = ['doc-id-1', 'doc-id-2', 'doc-id-3'] + this.RedisManager.getDocIdsInProject = sinon + .stub() + .callsArgWith(1, null, this.doc_ids) + this.DocumentManager.flushDocIfLoadedWithLock = sinon.spy( + (project_id, doc_id, callback) => { + if (callback == null) { + callback = function (error) {} + } + if (doc_id === 'doc-id-1') { + return callback( + (this.error = new Error('oops, something went wrong')) + ) + } else { + return callback() + } + } + ) + return this.ProjectManager.flushProjectWithLocks( + this.project_id, + (error) => { + this.callback(error) + return done() + } + ) + }) - it("should call the callback with an error", function() { - return this.callback.calledWith(new Error()).should.equal(true); - }); + it('should still flush each doc in the project', function () { + return Array.from(this.doc_ids).map((doc_id) => + this.DocumentManager.flushDocIfLoadedWithLock + .calledWith(this.project_id, doc_id) + .should.equal(true) + ) + }) - return it("should time the execution", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - }); -}); + it('should record the error', function () { + return this.logger.error + .calledWith( + { err: this.error, project_id: this.project_id, doc_id: 'doc-id-1' }, + 'error flushing doc' + ) + .should.equal(true) + }) + + it('should call the callback with an error', function () { + return this.callback.calledWith(new Error()).should.equal(true) + }) + + return it('should time the execution', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) +}) diff --git a/services/document-updater/test/unit/js/ProjectManager/getProjectDocsTests.js b/services/document-updater/test/unit/js/ProjectManager/getProjectDocsTests.js index bb700a2a21..72ffe39ec1 100644 --- a/services/document-updater/test/unit/js/ProjectManager/getProjectDocsTests.js +++ b/services/document-updater/test/unit/js/ProjectManager/getProjectDocsTests.js @@ -10,158 +10,215 @@ * DS206: Consider reworking classes to avoid initClass * 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 modulePath = "../../../../app/js/ProjectManager.js"; -const SandboxedModule = require('sandboxed-module'); -const Errors = require("../../../../app/js/Errors.js"); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const modulePath = '../../../../app/js/ProjectManager.js' +const SandboxedModule = require('sandboxed-module') +const Errors = require('../../../../app/js/Errors.js') -describe("ProjectManager - getProjectDocsAndFlushIfOld", function() { - beforeEach(function() { - let Timer; - this.ProjectManager = SandboxedModule.require(modulePath, { requires: { - "./RedisManager": (this.RedisManager = {}), - "./ProjectHistoryRedisManager": (this.ProjectHistoryRedisManager = {}), - "./DocumentManager": (this.DocumentManager = {}), - "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }), - "./HistoryManager": (this.HistoryManager = {}), - "./Metrics": (this.Metrics = { - Timer: (Timer = (function() { - Timer = class Timer { - static initClass() { - this.prototype.done = sinon.stub(); - } - }; - Timer.initClass(); - return Timer; - })()) - }) - } - } - ); - this.project_id = "project-id-123"; - this.callback = sinon.stub(); - return this.doc_versions = [111, 222, 333];}); +describe('ProjectManager - getProjectDocsAndFlushIfOld', function () { + beforeEach(function () { + let Timer + this.ProjectManager = SandboxedModule.require(modulePath, { + requires: { + './RedisManager': (this.RedisManager = {}), + './ProjectHistoryRedisManager': (this.ProjectHistoryRedisManager = {}), + './DocumentManager': (this.DocumentManager = {}), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub() + }), + './HistoryManager': (this.HistoryManager = {}), + './Metrics': (this.Metrics = { + Timer: (Timer = (function () { + Timer = class Timer { + static initClass() { + this.prototype.done = sinon.stub() + } + } + Timer.initClass() + return Timer + })()) + }) + } + }) + this.project_id = 'project-id-123' + this.callback = sinon.stub() + return (this.doc_versions = [111, 222, 333]) + }) - describe("successfully", function() { - beforeEach(function(done) { - this.doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"]; - this.doc_lines = [["aaa","aaa"],["bbb","bbb"],["ccc","ccc"]]; - this.docs = [ - {_id: this.doc_ids[0], lines: this.doc_lines[0], v: this.doc_versions[0]}, - {_id: this.doc_ids[1], lines: this.doc_lines[1], v: this.doc_versions[1]}, - {_id: this.doc_ids[2], lines: this.doc_lines[2], v: this.doc_versions[2]} - ]; - this.RedisManager.checkOrSetProjectState = sinon.stub().callsArgWith(2, null); - this.RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, this.doc_ids); - this.DocumentManager.getDocAndFlushIfOldWithLock = sinon.stub(); - this.DocumentManager.getDocAndFlushIfOldWithLock.withArgs(this.project_id, this.doc_ids[0]) - .callsArgWith(2, null, this.doc_lines[0], this.doc_versions[0]); - this.DocumentManager.getDocAndFlushIfOldWithLock.withArgs(this.project_id, this.doc_ids[1]) - .callsArgWith(2, null, this.doc_lines[1], this.doc_versions[1]); - this.DocumentManager.getDocAndFlushIfOldWithLock.withArgs(this.project_id, this.doc_ids[2]) - .callsArgWith(2, null, this.doc_lines[2], this.doc_versions[2]); - return this.ProjectManager.getProjectDocsAndFlushIfOld(this.project_id, this.projectStateHash, this.excludeVersions, (error, docs) => { - this.callback(error, docs); - return done(); - }); - }); + describe('successfully', function () { + beforeEach(function (done) { + this.doc_ids = ['doc-id-1', 'doc-id-2', 'doc-id-3'] + this.doc_lines = [ + ['aaa', 'aaa'], + ['bbb', 'bbb'], + ['ccc', 'ccc'] + ] + this.docs = [ + { + _id: this.doc_ids[0], + lines: this.doc_lines[0], + v: this.doc_versions[0] + }, + { + _id: this.doc_ids[1], + lines: this.doc_lines[1], + v: this.doc_versions[1] + }, + { + _id: this.doc_ids[2], + lines: this.doc_lines[2], + v: this.doc_versions[2] + } + ] + this.RedisManager.checkOrSetProjectState = sinon + .stub() + .callsArgWith(2, null) + this.RedisManager.getDocIdsInProject = sinon + .stub() + .callsArgWith(1, null, this.doc_ids) + this.DocumentManager.getDocAndFlushIfOldWithLock = sinon.stub() + this.DocumentManager.getDocAndFlushIfOldWithLock + .withArgs(this.project_id, this.doc_ids[0]) + .callsArgWith(2, null, this.doc_lines[0], this.doc_versions[0]) + this.DocumentManager.getDocAndFlushIfOldWithLock + .withArgs(this.project_id, this.doc_ids[1]) + .callsArgWith(2, null, this.doc_lines[1], this.doc_versions[1]) + this.DocumentManager.getDocAndFlushIfOldWithLock + .withArgs(this.project_id, this.doc_ids[2]) + .callsArgWith(2, null, this.doc_lines[2], this.doc_versions[2]) + return this.ProjectManager.getProjectDocsAndFlushIfOld( + this.project_id, + this.projectStateHash, + this.excludeVersions, + (error, docs) => { + this.callback(error, docs) + return done() + } + ) + }) - it("should check the project state", function() { - return this.RedisManager.checkOrSetProjectState - .calledWith(this.project_id, this.projectStateHash) - .should.equal(true); - }); + it('should check the project state', function () { + return this.RedisManager.checkOrSetProjectState + .calledWith(this.project_id, this.projectStateHash) + .should.equal(true) + }) - it("should get the doc ids in the project", function() { - return this.RedisManager.getDocIdsInProject - .calledWith(this.project_id) - .should.equal(true); - }); + it('should get the doc ids in the project', function () { + return this.RedisManager.getDocIdsInProject + .calledWith(this.project_id) + .should.equal(true) + }) - it("should call the callback without error", function() { - return this.callback.calledWith(null, this.docs).should.equal(true); - }); + it('should call the callback without error', function () { + return this.callback.calledWith(null, this.docs).should.equal(true) + }) - return it("should time the execution", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - }); + return it('should time the execution', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) - describe("when the state does not match", function() { - beforeEach(function(done) { - this.doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"]; - this.RedisManager.checkOrSetProjectState = sinon.stub().callsArgWith(2, null, true); - return this.ProjectManager.getProjectDocsAndFlushIfOld(this.project_id, this.projectStateHash, this.excludeVersions, (error, docs) => { - this.callback(error, docs); - return done(); - }); - }); + describe('when the state does not match', function () { + beforeEach(function (done) { + this.doc_ids = ['doc-id-1', 'doc-id-2', 'doc-id-3'] + this.RedisManager.checkOrSetProjectState = sinon + .stub() + .callsArgWith(2, null, true) + return this.ProjectManager.getProjectDocsAndFlushIfOld( + this.project_id, + this.projectStateHash, + this.excludeVersions, + (error, docs) => { + this.callback(error, docs) + return done() + } + ) + }) - it("should check the project state", function() { - return this.RedisManager.checkOrSetProjectState - .calledWith(this.project_id, this.projectStateHash) - .should.equal(true); - }); + it('should check the project state', function () { + return this.RedisManager.checkOrSetProjectState + .calledWith(this.project_id, this.projectStateHash) + .should.equal(true) + }) - it("should call the callback with an error", function() { - return this.callback.calledWith(new Errors.ProjectStateChangedError("project state changed")).should.equal(true); - }); + it('should call the callback with an error', function () { + return this.callback + .calledWith( + new Errors.ProjectStateChangedError('project state changed') + ) + .should.equal(true) + }) - return it("should time the execution", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - }); + return it('should time the execution', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) - describe("when a doc errors", function() { - beforeEach(function(done) { - this.doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"]; - this.RedisManager.checkOrSetProjectState = sinon.stub().callsArgWith(2, null); - this.RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, this.doc_ids); - this.DocumentManager.getDocAndFlushIfOldWithLock = sinon.stub(); - this.DocumentManager.getDocAndFlushIfOldWithLock.withArgs(this.project_id, "doc-id-1") - .callsArgWith(2, null, ["test doc content"], this.doc_versions[1]); - this.DocumentManager.getDocAndFlushIfOldWithLock.withArgs(this.project_id, "doc-id-2") - .callsArgWith(2, (this.error = new Error("oops"))); // trigger an error - return this.ProjectManager.getProjectDocsAndFlushIfOld(this.project_id, this.projectStateHash, this.excludeVersions, (error, docs) => { - this.callback(error); - return done(); - }); - }); + describe('when a doc errors', function () { + beforeEach(function (done) { + this.doc_ids = ['doc-id-1', 'doc-id-2', 'doc-id-3'] + this.RedisManager.checkOrSetProjectState = sinon + .stub() + .callsArgWith(2, null) + this.RedisManager.getDocIdsInProject = sinon + .stub() + .callsArgWith(1, null, this.doc_ids) + this.DocumentManager.getDocAndFlushIfOldWithLock = sinon.stub() + this.DocumentManager.getDocAndFlushIfOldWithLock + .withArgs(this.project_id, 'doc-id-1') + .callsArgWith(2, null, ['test doc content'], this.doc_versions[1]) + this.DocumentManager.getDocAndFlushIfOldWithLock + .withArgs(this.project_id, 'doc-id-2') + .callsArgWith(2, (this.error = new Error('oops'))) // trigger an error + return this.ProjectManager.getProjectDocsAndFlushIfOld( + this.project_id, + this.projectStateHash, + this.excludeVersions, + (error, docs) => { + this.callback(error) + return done() + } + ) + }) - it("should record the error", function() { - return this.logger.error - .calledWith({err: this.error, project_id: this.project_id, doc_id: "doc-id-2"}, "error getting project doc lines in getProjectDocsAndFlushIfOld") - .should.equal(true); - }); + it('should record the error', function () { + return this.logger.error + .calledWith( + { err: this.error, project_id: this.project_id, doc_id: 'doc-id-2' }, + 'error getting project doc lines in getProjectDocsAndFlushIfOld' + ) + .should.equal(true) + }) - it("should call the callback with an error", function() { - return this.callback.calledWith(new Error("oops")).should.equal(true); - }); + it('should call the callback with an error', function () { + return this.callback.calledWith(new Error('oops')).should.equal(true) + }) - return it("should time the execution", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - }); + return it('should time the execution', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) - return describe("clearing the project state with clearProjectState", function() { - beforeEach(function(done) { - this.RedisManager.clearProjectState = sinon.stub().callsArg(1); - return this.ProjectManager.clearProjectState(this.project_id, error => { - this.callback(error); - return done(); - }); - }); + return describe('clearing the project state with clearProjectState', function () { + beforeEach(function (done) { + this.RedisManager.clearProjectState = sinon.stub().callsArg(1) + return this.ProjectManager.clearProjectState(this.project_id, (error) => { + this.callback(error) + return done() + }) + }) - it("should clear the project state", function() { - return this.RedisManager.clearProjectState - .calledWith(this.project_id) - .should.equal(true); - }); + it('should clear the project state', function () { + return this.RedisManager.clearProjectState + .calledWith(this.project_id) + .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) + }) + }) +}) diff --git a/services/document-updater/test/unit/js/ProjectManager/updateProjectTests.js b/services/document-updater/test/unit/js/ProjectManager/updateProjectTests.js index ffe8c49e97..5b439ef839 100644 --- a/services/document-updater/test/unit/js/ProjectManager/updateProjectTests.js +++ b/services/document-updater/test/unit/js/ProjectManager/updateProjectTests.js @@ -10,239 +10,393 @@ * DS206: Consider reworking classes to avoid initClass * 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 modulePath = "../../../../app/js/ProjectManager.js"; -const SandboxedModule = require('sandboxed-module'); -const _ = require('lodash'); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const modulePath = '../../../../app/js/ProjectManager.js' +const SandboxedModule = require('sandboxed-module') +const _ = require('lodash') -describe("ProjectManager", function() { - beforeEach(function() { - let Timer; - this.ProjectManager = SandboxedModule.require(modulePath, { requires: { - "./RedisManager": (this.RedisManager = {}), - "./ProjectHistoryRedisManager": (this.ProjectHistoryRedisManager = {}), - "./DocumentManager": (this.DocumentManager = {}), - "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }), - "./HistoryManager": (this.HistoryManager = {}), - "./Metrics": (this.Metrics = { - Timer: (Timer = (function() { - Timer = class Timer { - static initClass() { - this.prototype.done = sinon.stub(); - } - }; - Timer.initClass(); - return Timer; - })()) - }) - } - } - ); +describe('ProjectManager', function () { + beforeEach(function () { + let Timer + this.ProjectManager = SandboxedModule.require(modulePath, { + requires: { + './RedisManager': (this.RedisManager = {}), + './ProjectHistoryRedisManager': (this.ProjectHistoryRedisManager = {}), + './DocumentManager': (this.DocumentManager = {}), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub() + }), + './HistoryManager': (this.HistoryManager = {}), + './Metrics': (this.Metrics = { + Timer: (Timer = (function () { + Timer = class Timer { + static initClass() { + this.prototype.done = sinon.stub() + } + } + Timer.initClass() + return Timer + })()) + }) + } + }) - this.project_id = "project-id-123"; - this.projectHistoryId = 'history-id-123'; - this.user_id = "user-id-123"; - this.version = 1234567; - this.HistoryManager.shouldFlushHistoryOps = sinon.stub().returns(false); - this.HistoryManager.flushProjectChangesAsync = sinon.stub(); - return this.callback = sinon.stub(); - }); + this.project_id = 'project-id-123' + this.projectHistoryId = 'history-id-123' + this.user_id = 'user-id-123' + this.version = 1234567 + this.HistoryManager.shouldFlushHistoryOps = sinon.stub().returns(false) + this.HistoryManager.flushProjectChangesAsync = sinon.stub() + return (this.callback = sinon.stub()) + }) - return describe("updateProjectWithLocks", function() { - describe("rename operations", function() { - beforeEach(function() { - this.firstDocUpdate = { - id: 1, - pathname: 'foo', - newPathname: 'foo' - }; - this.secondDocUpdate = { - id: 2, - pathname: 'bar', - newPathname: 'bar2' - }; - this.docUpdates = [ this.firstDocUpdate, this.secondDocUpdate ]; - this.firstFileUpdate = { - id: 2, - pathname: 'bar', - newPathname: 'bar2' - }; - this.fileUpdates = [ this.firstFileUpdate ]; - this.DocumentManager.renameDocWithLock = sinon.stub().yields(); - return this.ProjectHistoryRedisManager.queueRenameEntity = sinon.stub().yields(); - }); + return describe('updateProjectWithLocks', function () { + describe('rename operations', function () { + beforeEach(function () { + this.firstDocUpdate = { + id: 1, + pathname: 'foo', + newPathname: 'foo' + } + this.secondDocUpdate = { + id: 2, + pathname: 'bar', + newPathname: 'bar2' + } + this.docUpdates = [this.firstDocUpdate, this.secondDocUpdate] + this.firstFileUpdate = { + id: 2, + pathname: 'bar', + newPathname: 'bar2' + } + this.fileUpdates = [this.firstFileUpdate] + this.DocumentManager.renameDocWithLock = sinon.stub().yields() + return (this.ProjectHistoryRedisManager.queueRenameEntity = sinon + .stub() + .yields()) + }) - describe("successfully", function() { - beforeEach(function() { - return this.ProjectManager.updateProjectWithLocks(this.project_id, this.projectHistoryId, this.user_id, this.docUpdates, this.fileUpdates, this.version, this.callback); - }); + describe('successfully', function () { + beforeEach(function () { + return this.ProjectManager.updateProjectWithLocks( + this.project_id, + this.projectHistoryId, + this.user_id, + this.docUpdates, + this.fileUpdates, + this.version, + this.callback + ) + }) - it("should rename the docs in the updates", function() { - const firstDocUpdateWithVersion = _.extend({}, this.firstDocUpdate, {version: `${this.version}.0`}); - const secondDocUpdateWithVersion = _.extend({}, this.secondDocUpdate, {version: `${this.version}.1`}); - this.DocumentManager.renameDocWithLock - .calledWith(this.project_id, this.firstDocUpdate.id, this.user_id, firstDocUpdateWithVersion, this.projectHistoryId) - .should.equal(true); - return this.DocumentManager.renameDocWithLock - .calledWith(this.project_id, this.secondDocUpdate.id, this.user_id, secondDocUpdateWithVersion, this.projectHistoryId) - .should.equal(true); - }); + it('should rename the docs in the updates', function () { + const firstDocUpdateWithVersion = _.extend({}, this.firstDocUpdate, { + version: `${this.version}.0` + }) + const secondDocUpdateWithVersion = _.extend( + {}, + this.secondDocUpdate, + { version: `${this.version}.1` } + ) + this.DocumentManager.renameDocWithLock + .calledWith( + this.project_id, + this.firstDocUpdate.id, + this.user_id, + firstDocUpdateWithVersion, + this.projectHistoryId + ) + .should.equal(true) + return this.DocumentManager.renameDocWithLock + .calledWith( + this.project_id, + this.secondDocUpdate.id, + this.user_id, + secondDocUpdateWithVersion, + this.projectHistoryId + ) + .should.equal(true) + }) - it("should rename the files in the updates", function() { - const firstFileUpdateWithVersion = _.extend({}, this.firstFileUpdate, {version: `${this.version}.2`}); - return this.ProjectHistoryRedisManager.queueRenameEntity - .calledWith(this.project_id, this.projectHistoryId, 'file', this.firstFileUpdate.id, this.user_id, firstFileUpdateWithVersion) - .should.equal(true); - }); + it('should rename the files in the updates', function () { + const firstFileUpdateWithVersion = _.extend( + {}, + this.firstFileUpdate, + { version: `${this.version}.2` } + ) + return this.ProjectHistoryRedisManager.queueRenameEntity + .calledWith( + this.project_id, + this.projectHistoryId, + 'file', + this.firstFileUpdate.id, + this.user_id, + firstFileUpdateWithVersion + ) + .should.equal(true) + }) - it("should not flush the history", function() { - return this.HistoryManager.flushProjectChangesAsync - .calledWith(this.project_id) - .should.equal(false); - }); + it('should not flush the history', function () { + return this.HistoryManager.flushProjectChangesAsync + .calledWith(this.project_id) + .should.equal(false) + }) - 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 renaming a doc fails", function() { - beforeEach(function() { - this.error = new Error('error'); - this.DocumentManager.renameDocWithLock = sinon.stub().yields(this.error); - return this.ProjectManager.updateProjectWithLocks(this.project_id, this.projectHistoryId, this.user_id, this.docUpdates, this.fileUpdates, this.version, this.callback); - }); + describe('when renaming a doc fails', function () { + beforeEach(function () { + this.error = new Error('error') + this.DocumentManager.renameDocWithLock = sinon + .stub() + .yields(this.error) + return this.ProjectManager.updateProjectWithLocks( + this.project_id, + this.projectHistoryId, + this.user_id, + this.docUpdates, + this.fileUpdates, + this.version, + this.callback + ) + }) - 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) + }) + }) - describe("when renaming a file fails", function() { - beforeEach(function() { - this.error = new Error('error'); - this.ProjectHistoryRedisManager.queueRenameEntity = sinon.stub().yields(this.error); - return this.ProjectManager.updateProjectWithLocks(this.project_id, this.projectHistoryId, this.user_id, this.docUpdates, this.fileUpdates, this.version, this.callback); - }); + describe('when renaming a file fails', function () { + beforeEach(function () { + this.error = new Error('error') + this.ProjectHistoryRedisManager.queueRenameEntity = sinon + .stub() + .yields(this.error) + return this.ProjectManager.updateProjectWithLocks( + this.project_id, + this.projectHistoryId, + this.user_id, + this.docUpdates, + this.fileUpdates, + this.version, + this.callback + ) + }) - 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("with enough ops to flush", function() { - beforeEach(function() { - this.HistoryManager.shouldFlushHistoryOps = sinon.stub().returns(true); - return this.ProjectManager.updateProjectWithLocks(this.project_id, this.projectHistoryId, this.user_id, this.docUpdates, this.fileUpdates, this.version, this.callback); - }); + return describe('with enough ops to flush', function () { + beforeEach(function () { + this.HistoryManager.shouldFlushHistoryOps = sinon.stub().returns(true) + return this.ProjectManager.updateProjectWithLocks( + this.project_id, + this.projectHistoryId, + this.user_id, + this.docUpdates, + this.fileUpdates, + this.version, + this.callback + ) + }) - return it("should flush the history", function() { - return this.HistoryManager.flushProjectChangesAsync - .calledWith(this.project_id) - .should.equal(true); - }); - }); - }); + return it('should flush the history', function () { + return this.HistoryManager.flushProjectChangesAsync + .calledWith(this.project_id) + .should.equal(true) + }) + }) + }) - return describe("add operations", function() { - beforeEach(function() { - this.firstDocUpdate = { - id: 1, - docLines: "a\nb" - }; - this.secondDocUpdate = { - id: 2, - docLines: "a\nb" - }; - this.docUpdates = [ this.firstDocUpdate, this.secondDocUpdate ]; - this.firstFileUpdate = { - id: 3, - url: 'filestore.example.com/2' - }; - this.secondFileUpdate = { - id: 4, - url: 'filestore.example.com/3' - }; - this.fileUpdates = [ this.firstFileUpdate, this.secondFileUpdate ]; - return this.ProjectHistoryRedisManager.queueAddEntity = sinon.stub().yields(); - }); + return describe('add operations', function () { + beforeEach(function () { + this.firstDocUpdate = { + id: 1, + docLines: 'a\nb' + } + this.secondDocUpdate = { + id: 2, + docLines: 'a\nb' + } + this.docUpdates = [this.firstDocUpdate, this.secondDocUpdate] + this.firstFileUpdate = { + id: 3, + url: 'filestore.example.com/2' + } + this.secondFileUpdate = { + id: 4, + url: 'filestore.example.com/3' + } + this.fileUpdates = [this.firstFileUpdate, this.secondFileUpdate] + return (this.ProjectHistoryRedisManager.queueAddEntity = sinon + .stub() + .yields()) + }) - describe("successfully", function() { - beforeEach(function() { - return this.ProjectManager.updateProjectWithLocks(this.project_id, this.projectHistoryId, this.user_id, this.docUpdates, this.fileUpdates, this.version, this.callback); - }); + describe('successfully', function () { + beforeEach(function () { + return this.ProjectManager.updateProjectWithLocks( + this.project_id, + this.projectHistoryId, + this.user_id, + this.docUpdates, + this.fileUpdates, + this.version, + this.callback + ) + }) - it("should add the docs in the updates", function() { - const firstDocUpdateWithVersion = _.extend({}, this.firstDocUpdate, {version: `${this.version}.0`}); - const secondDocUpdateWithVersion = _.extend({}, this.secondDocUpdate, {version: `${this.version}.1`}); - this.ProjectHistoryRedisManager.queueAddEntity.getCall(0) - .calledWith(this.project_id, this.projectHistoryId, 'doc', this.firstDocUpdate.id, this.user_id, firstDocUpdateWithVersion) - .should.equal(true); - return this.ProjectHistoryRedisManager.queueAddEntity.getCall(1) - .calledWith(this.project_id, this.projectHistoryId, 'doc', this.secondDocUpdate.id, this.user_id, secondDocUpdateWithVersion) - .should.equal(true); - }); + it('should add the docs in the updates', function () { + const firstDocUpdateWithVersion = _.extend({}, this.firstDocUpdate, { + version: `${this.version}.0` + }) + const secondDocUpdateWithVersion = _.extend( + {}, + this.secondDocUpdate, + { version: `${this.version}.1` } + ) + this.ProjectHistoryRedisManager.queueAddEntity + .getCall(0) + .calledWith( + this.project_id, + this.projectHistoryId, + 'doc', + this.firstDocUpdate.id, + this.user_id, + firstDocUpdateWithVersion + ) + .should.equal(true) + return this.ProjectHistoryRedisManager.queueAddEntity + .getCall(1) + .calledWith( + this.project_id, + this.projectHistoryId, + 'doc', + this.secondDocUpdate.id, + this.user_id, + secondDocUpdateWithVersion + ) + .should.equal(true) + }) - it("should add the files in the updates", function() { - const firstFileUpdateWithVersion = _.extend({}, this.firstFileUpdate, {version: `${this.version}.2`}); - const secondFileUpdateWithVersion = _.extend({}, this.secondFileUpdate, {version: `${this.version}.3`}); - this.ProjectHistoryRedisManager.queueAddEntity.getCall(2) - .calledWith(this.project_id, this.projectHistoryId, 'file', this.firstFileUpdate.id, this.user_id, firstFileUpdateWithVersion) - .should.equal(true); - return this.ProjectHistoryRedisManager.queueAddEntity.getCall(3) - .calledWith(this.project_id, this.projectHistoryId, 'file', this.secondFileUpdate.id, this.user_id, secondFileUpdateWithVersion) - .should.equal(true); - }); + it('should add the files in the updates', function () { + const firstFileUpdateWithVersion = _.extend( + {}, + this.firstFileUpdate, + { version: `${this.version}.2` } + ) + const secondFileUpdateWithVersion = _.extend( + {}, + this.secondFileUpdate, + { version: `${this.version}.3` } + ) + this.ProjectHistoryRedisManager.queueAddEntity + .getCall(2) + .calledWith( + this.project_id, + this.projectHistoryId, + 'file', + this.firstFileUpdate.id, + this.user_id, + firstFileUpdateWithVersion + ) + .should.equal(true) + return this.ProjectHistoryRedisManager.queueAddEntity + .getCall(3) + .calledWith( + this.project_id, + this.projectHistoryId, + 'file', + this.secondFileUpdate.id, + this.user_id, + secondFileUpdateWithVersion + ) + .should.equal(true) + }) - it("should not flush the history", function() { - return this.HistoryManager.flushProjectChangesAsync - .calledWith(this.project_id) - .should.equal(false); - }); + it('should not flush the history', function () { + return this.HistoryManager.flushProjectChangesAsync + .calledWith(this.project_id) + .should.equal(false) + }) - 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 adding a doc fails", function() { - beforeEach(function() { - this.error = new Error('error'); - this.ProjectHistoryRedisManager.queueAddEntity = sinon.stub().yields(this.error); - return this.ProjectManager.updateProjectWithLocks(this.project_id, this.projectHistoryId, this.user_id, this.docUpdates, this.fileUpdates, this.version, this.callback); - }); + describe('when adding a doc fails', function () { + beforeEach(function () { + this.error = new Error('error') + this.ProjectHistoryRedisManager.queueAddEntity = sinon + .stub() + .yields(this.error) + return this.ProjectManager.updateProjectWithLocks( + this.project_id, + this.projectHistoryId, + this.user_id, + this.docUpdates, + this.fileUpdates, + this.version, + this.callback + ) + }) - 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) + }) + }) - describe("when adding a file fails", function() { - beforeEach(function() { - this.error = new Error('error'); - this.ProjectHistoryRedisManager.queueAddEntity = sinon.stub().yields(this.error); - return this.ProjectManager.updateProjectWithLocks(this.project_id, this.projectHistoryId, this.user_id, this.docUpdates, this.fileUpdates, this.version, this.callback); - }); + describe('when adding a file fails', function () { + beforeEach(function () { + this.error = new Error('error') + this.ProjectHistoryRedisManager.queueAddEntity = sinon + .stub() + .yields(this.error) + return this.ProjectManager.updateProjectWithLocks( + this.project_id, + this.projectHistoryId, + this.user_id, + this.docUpdates, + this.fileUpdates, + this.version, + this.callback + ) + }) - 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("with enough ops to flush", function() { - beforeEach(function() { - this.HistoryManager.shouldFlushHistoryOps = sinon.stub().returns(true); - return this.ProjectManager.updateProjectWithLocks(this.project_id, this.projectHistoryId, this.user_id, this.docUpdates, this.fileUpdates, this.version, this.callback); - }); + return describe('with enough ops to flush', function () { + beforeEach(function () { + this.HistoryManager.shouldFlushHistoryOps = sinon.stub().returns(true) + return this.ProjectManager.updateProjectWithLocks( + this.project_id, + this.projectHistoryId, + this.user_id, + this.docUpdates, + this.fileUpdates, + this.version, + this.callback + ) + }) - return it("should flush the history", function() { - return this.HistoryManager.flushProjectChangesAsync - .calledWith(this.project_id) - .should.equal(true); - }); - }); - }); - }); -}); + return it('should flush the history', function () { + return this.HistoryManager.flushProjectChangesAsync + .calledWith(this.project_id) + .should.equal(true) + }) + }) + }) + }) +}) diff --git a/services/document-updater/test/unit/js/RangesManager/RangesManagerTests.js b/services/document-updater/test/unit/js/RangesManager/RangesManagerTests.js index 5634840221..a336125a1c 100644 --- a/services/document-updater/test/unit/js/RangesManager/RangesManagerTests.js +++ b/services/document-updater/test/unit/js/RangesManager/RangesManagerTests.js @@ -12,392 +12,524 @@ * 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/RangesManager.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/RangesManager.js' +const SandboxedModule = require('sandboxed-module') -describe("RangesManager", function() { - beforeEach(function() { - this.RangesManager = SandboxedModule.require(modulePath, { - requires: { - "logger-sharelatex": (this.logger = { error: sinon.stub(), log: sinon.stub(), warn: sinon.stub() }) - } - }); +describe('RangesManager', function () { + beforeEach(function () { + this.RangesManager = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': (this.logger = { + error: sinon.stub(), + log: sinon.stub(), + warn: sinon.stub() + }) + } + }) - this.doc_id = "doc-id-123"; - this.project_id = "project-id-123"; - this.user_id = "user-id-123"; - return this.callback = sinon.stub(); - }); + this.doc_id = 'doc-id-123' + this.project_id = 'project-id-123' + this.user_id = 'user-id-123' + return (this.callback = sinon.stub()) + }) - describe("applyUpdate", function() { - beforeEach(function() { - this.updates = [{ - meta: { - user_id: this.user_id - }, - op: [{ - i: "two ", - p: 4 - }] - }]; - this.entries = { - comments: [{ - op: { - c: "three ", - p: 4 - }, - metadata: { - user_id: this.user_id - } - }], - changes: [{ - op: { - i: "five", - p: 15 - }, - metadata: { - user_id: this.user_id - } - }] - }; - return this.newDocLines = ["one two three four five"];}); // old is "one three four five" - - describe("successfully", function() { - beforeEach(function() { - return this.RangesManager.applyUpdate(this.project_id, this.doc_id, this.entries, this.updates, this.newDocLines, this.callback); - }); - - return it("should return the modified the comments and changes", function() { - this.callback.called.should.equal(true); - const [error, entries, ranges_were_collapsed] = Array.from(this.callback.args[0]); - expect(error).to.be.null; - expect(ranges_were_collapsed).to.equal(false); - entries.comments[0].op.should.deep.equal({ - c: "three ", - p: 8 - }); - return entries.changes[0].op.should.deep.equal({ - i: "five", - p: 19 - }); - }); - }); - - describe("with empty comments", function() { - beforeEach(function() { - this.entries.comments = []; - return this.RangesManager.applyUpdate(this.project_id, this.doc_id, this.entries, this.updates, this.newDocLines, this.callback); - }); - - return it("should return an object with no comments", function() { - // Save space in redis and don't store just {} - this.callback.called.should.equal(true); - const [error, entries] = Array.from(this.callback.args[0]); - expect(error).to.be.null; - return expect(entries.comments).to.be.undefined; - }); - }); - - describe("with empty changes", function() { - beforeEach(function() { - this.entries.changes = []; - return this.RangesManager.applyUpdate(this.project_id, this.doc_id, this.entries, this.updates, this.newDocLines, this.callback); - }); - - return it("should return an object with no changes", function() { - // Save space in redis and don't store just {} - this.callback.called.should.equal(true); - const [error, entries] = Array.from(this.callback.args[0]); - expect(error).to.be.null; - return expect(entries.changes).to.be.undefined; - }); - }); - - describe("with too many comments", function() { - beforeEach(function() { - this.RangesManager.MAX_COMMENTS = 2; - this.updates = [{ - meta: { - user_id: this.user_id - }, - op: [{ - c: "one", - p: 0, - t: "thread-id-1" - }] - }]; - this.entries = { - comments: [{ - op: { - c: "three ", - p: 4, - t: "thread-id-2" - }, - metadata: { - user_id: this.user_id - } - }, { - op: { - c: "four ", - p: 10, - t: "thread-id-3" - }, - metadata: { - user_id: this.user_id - } - }], - changes: [] - }; - return this.RangesManager.applyUpdate(this.project_id, this.doc_id, this.entries, this.updates, this.newDocLines, this.callback); - }); - - return it("should return an error", function() { - this.callback.called.should.equal(true); - const [error, entries] = Array.from(this.callback.args[0]); - expect(error).to.not.be.null; - return expect(error.message).to.equal("too many comments or tracked changes"); - }); - }); - - describe("with too many changes", function() { - beforeEach(function() { - this.RangesManager.MAX_CHANGES = 2; - this.updates = [{ - meta: { - user_id: this.user_id, - tc: "track-changes-id-yes" - }, - op: [{ - i: "one ", - p: 0 - }] - }]; - this.entries = { - changes: [{ - op: { - i: "three", - p: 4 - }, - metadata: { - user_id: this.user_id - } - }, { - op: { - i: "four", - p: 10 - }, - metadata: { - user_id: this.user_id - } - }], - comments: [] - }; - this.newDocLines = ["one two three four"]; - return this.RangesManager.applyUpdate(this.project_id, this.doc_id, this.entries, this.updates, this.newDocLines, this.callback); - }); - - return it("should return an error", function() { - // Save space in redis and don't store just {} - this.callback.called.should.equal(true); - const [error, entries] = Array.from(this.callback.args[0]); - expect(error).to.not.be.null; - return expect(error.message).to.equal("too many comments or tracked changes"); - }); - }); - - describe("inconsistent changes", function() { - beforeEach(function() { - this.updates = [{ - meta: { - user_id: this.user_id - }, - op: [{ - c: "doesn't match", - p: 0 - }] - }]; - return this.RangesManager.applyUpdate(this.project_id, this.doc_id, this.entries, this.updates, this.newDocLines, this.callback); - }); - - return it("should return an error", function() { - // Save space in redis and don't store just {} - this.callback.called.should.equal(true); - const [error, entries] = Array.from(this.callback.args[0]); - expect(error).to.not.be.null; - return expect(error.message).to.equal("Change ({\"op\":{\"i\":\"five\",\"p\":15},\"metadata\":{\"user_id\":\"user-id-123\"}}) doesn't match text (\"our \")"); - }); - }); + describe('applyUpdate', function () { + beforeEach(function () { + this.updates = [ + { + meta: { + user_id: this.user_id + }, + op: [ + { + i: 'two ', + p: 4 + } + ] + } + ] + this.entries = { + comments: [ + { + op: { + c: 'three ', + p: 4 + }, + metadata: { + user_id: this.user_id + } + } + ], + changes: [ + { + op: { + i: 'five', + p: 15 + }, + metadata: { + user_id: this.user_id + } + } + ] + } + return (this.newDocLines = ['one two three four five']) + }) // old is "one three four five" + describe('successfully', function () { + beforeEach(function () { + return this.RangesManager.applyUpdate( + this.project_id, + this.doc_id, + this.entries, + this.updates, + this.newDocLines, + this.callback + ) + }) - return describe("with an update that collapses a range", function() { - beforeEach(function() { - this.updates = [{ - meta: { - user_id: this.user_id - }, - op: [{ - d: "one", - p: 0, - t: "thread-id-1" - }] - }]; - this.entries = { - comments: [{ - op: { - c: "n", - p: 1, - t: "thread-id-2" - }, - metadata: { - user_id: this.user_id - } - }], - changes: [] - }; - return this.RangesManager.applyUpdate(this.project_id, this.doc_id, this.entries, this.updates, this.newDocLines, this.callback); - }); + return it('should return the modified the comments and changes', function () { + this.callback.called.should.equal(true) + const [error, entries, ranges_were_collapsed] = Array.from( + this.callback.args[0] + ) + expect(error).to.be.null + expect(ranges_were_collapsed).to.equal(false) + entries.comments[0].op.should.deep.equal({ + c: 'three ', + p: 8 + }) + return entries.changes[0].op.should.deep.equal({ + i: 'five', + p: 19 + }) + }) + }) - return it("should return ranges_were_collapsed == true", function() { - this.callback.called.should.equal(true); - const [error, entries, ranges_were_collapsed] = Array.from(this.callback.args[0]); - return expect(ranges_were_collapsed).to.equal(true); - }); - }); - }); + describe('with empty comments', function () { + beforeEach(function () { + this.entries.comments = [] + return this.RangesManager.applyUpdate( + this.project_id, + this.doc_id, + this.entries, + this.updates, + this.newDocLines, + this.callback + ) + }) - return describe("acceptChanges", function() { - beforeEach(function() { - this.RangesManager = SandboxedModule.require(modulePath, { - requires: { - "logger-sharelatex": (this.logger = { error: sinon.stub(), log: sinon.stub(), warn: sinon.stub() }), - "./RangesTracker":(this.RangesTracker = SandboxedModule.require("../../../../app/js/RangesTracker.js")) - } - } - ); + return it('should return an object with no comments', function () { + // Save space in redis and don't store just {} + this.callback.called.should.equal(true) + const [error, entries] = Array.from(this.callback.args[0]) + expect(error).to.be.null + return expect(entries.comments).to.be.undefined + }) + }) - this.ranges = { - comments: [], - changes: [{ - id: "a1", - op: { - i: "lorem", - p: 0 - } - }, { - id: "a2", - op: { - i: "ipsum", - p: 10 - } - }, { - id: "a3", - op: { - i: "dolor", - p: 20 - } - }, { - id: "a4", - op: { - i: "sit", - p: 30 - } - }, { - id: "a5", - op: { - i: "amet", - p: 40 - } - }] - }; - return this.removeChangeIdsSpy = sinon.spy(this.RangesTracker.prototype, "removeChangeIds"); - }); + describe('with empty changes', function () { + beforeEach(function () { + this.entries.changes = [] + return this.RangesManager.applyUpdate( + this.project_id, + this.doc_id, + this.entries, + this.updates, + this.newDocLines, + this.callback + ) + }) - describe("successfully with a single change", function() { - beforeEach(function(done) { - this.change_ids = [ this.ranges.changes[1].id ]; - return this.RangesManager.acceptChanges(this.change_ids, this.ranges, (err, ranges) => { - this.rangesResponse = ranges; - return done(); - }); - }); + return it('should return an object with no changes', function () { + // Save space in redis and don't store just {} + this.callback.called.should.equal(true) + const [error, entries] = Array.from(this.callback.args[0]) + expect(error).to.be.null + return expect(entries.changes).to.be.undefined + }) + }) - it("should log the call with the correct number of changes", function() { - return this.logger.log - .calledWith("accepting 1 changes in ranges") - .should.equal(true); - }); + describe('with too many comments', function () { + beforeEach(function () { + this.RangesManager.MAX_COMMENTS = 2 + this.updates = [ + { + meta: { + user_id: this.user_id + }, + op: [ + { + c: 'one', + p: 0, + t: 'thread-id-1' + } + ] + } + ] + this.entries = { + comments: [ + { + op: { + c: 'three ', + p: 4, + t: 'thread-id-2' + }, + metadata: { + user_id: this.user_id + } + }, + { + op: { + c: 'four ', + p: 10, + t: 'thread-id-3' + }, + metadata: { + user_id: this.user_id + } + } + ], + changes: [] + } + return this.RangesManager.applyUpdate( + this.project_id, + this.doc_id, + this.entries, + this.updates, + this.newDocLines, + this.callback + ) + }) - it("should delegate the change removal to the ranges tracker", function() { - return this.removeChangeIdsSpy - .calledWith(this.change_ids) - .should.equal(true); - }); + return it('should return an error', function () { + this.callback.called.should.equal(true) + const [error, entries] = Array.from(this.callback.args[0]) + expect(error).to.not.be.null + return expect(error.message).to.equal( + 'too many comments or tracked changes' + ) + }) + }) - it("should remove the change", function() { - return expect(this.rangesResponse.changes - .find(change => change.id === this.ranges.changes[1].id)) - .to.be.undefined; - }); + describe('with too many changes', function () { + beforeEach(function () { + this.RangesManager.MAX_CHANGES = 2 + this.updates = [ + { + meta: { + user_id: this.user_id, + tc: 'track-changes-id-yes' + }, + op: [ + { + i: 'one ', + p: 0 + } + ] + } + ] + this.entries = { + changes: [ + { + op: { + i: 'three', + p: 4 + }, + metadata: { + user_id: this.user_id + } + }, + { + op: { + i: 'four', + p: 10 + }, + metadata: { + user_id: this.user_id + } + } + ], + comments: [] + } + this.newDocLines = ['one two three four'] + return this.RangesManager.applyUpdate( + this.project_id, + this.doc_id, + this.entries, + this.updates, + this.newDocLines, + this.callback + ) + }) - it("should return the original number of changes minus 1", function() { - return this.rangesResponse.changes.length - .should.equal(this.ranges.changes.length - 1); - }); - - return it("should not touch other changes", function() { - return [ 0, 2, 3, 4].map((i) => - expect(this.rangesResponse.changes - .find(change => change.id === this.ranges.changes[i].id)) - .to.deep.equal(this.ranges.changes[i])); - }); - }); + return it('should return an error', function () { + // Save space in redis and don't store just {} + this.callback.called.should.equal(true) + const [error, entries] = Array.from(this.callback.args[0]) + expect(error).to.not.be.null + return expect(error.message).to.equal( + 'too many comments or tracked changes' + ) + }) + }) - return describe("successfully with multiple changes", function() { - beforeEach(function(done) { - this.change_ids = [ this.ranges.changes[1].id, this.ranges.changes[3].id, this.ranges.changes[4].id ]; - return this.RangesManager.acceptChanges(this.change_ids, this.ranges, (err, ranges) => { - this.rangesResponse = ranges; - return done(); - }); - }); + describe('inconsistent changes', function () { + beforeEach(function () { + this.updates = [ + { + meta: { + user_id: this.user_id + }, + op: [ + { + c: "doesn't match", + p: 0 + } + ] + } + ] + return this.RangesManager.applyUpdate( + this.project_id, + this.doc_id, + this.entries, + this.updates, + this.newDocLines, + this.callback + ) + }) - it("should log the call with the correct number of changes", function() { - return this.logger.log - .calledWith(`accepting ${ this.change_ids.length } changes in ranges`) - .should.equal(true); - }); + return it('should return an error', function () { + // Save space in redis and don't store just {} + this.callback.called.should.equal(true) + const [error, entries] = Array.from(this.callback.args[0]) + expect(error).to.not.be.null + return expect(error.message).to.equal( + 'Change ({"op":{"i":"five","p":15},"metadata":{"user_id":"user-id-123"}}) doesn\'t match text ("our ")' + ) + }) + }) - it("should delegate the change removal to the ranges tracker", function() { - return this.removeChangeIdsSpy - .calledWith(this.change_ids) - .should.equal(true); - }); + return describe('with an update that collapses a range', function () { + beforeEach(function () { + this.updates = [ + { + meta: { + user_id: this.user_id + }, + op: [ + { + d: 'one', + p: 0, + t: 'thread-id-1' + } + ] + } + ] + this.entries = { + comments: [ + { + op: { + c: 'n', + p: 1, + t: 'thread-id-2' + }, + metadata: { + user_id: this.user_id + } + } + ], + changes: [] + } + return this.RangesManager.applyUpdate( + this.project_id, + this.doc_id, + this.entries, + this.updates, + this.newDocLines, + this.callback + ) + }) - it("should remove the changes", function() { - return [ 1, 3, 4].map((i) => - expect(this.rangesResponse.changes - .find(change => change.id === this.ranges.changes[1].id)) - .to.be.undefined); - }); - - it("should return the original number of changes minus the number of accepted changes", function() { - return this.rangesResponse.changes.length - .should.equal(this.ranges.changes.length - 3); - }); + return it('should return ranges_were_collapsed == true', function () { + this.callback.called.should.equal(true) + const [error, entries, ranges_were_collapsed] = Array.from( + this.callback.args[0] + ) + return expect(ranges_were_collapsed).to.equal(true) + }) + }) + }) - return it("should not touch other changes", function() { - return [ 0, 2 ].map((i) => - expect(this.rangesResponse.changes - .find(change => change.id === this.ranges.changes[i].id)) - .to.deep.equal(this.ranges.changes[i])); - }); - }); -}); -}); - + return describe('acceptChanges', function () { + beforeEach(function () { + this.RangesManager = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': (this.logger = { + error: sinon.stub(), + log: sinon.stub(), + warn: sinon.stub() + }), + './RangesTracker': (this.RangesTracker = SandboxedModule.require( + '../../../../app/js/RangesTracker.js' + )) + } + }) + + this.ranges = { + comments: [], + changes: [ + { + id: 'a1', + op: { + i: 'lorem', + p: 0 + } + }, + { + id: 'a2', + op: { + i: 'ipsum', + p: 10 + } + }, + { + id: 'a3', + op: { + i: 'dolor', + p: 20 + } + }, + { + id: 'a4', + op: { + i: 'sit', + p: 30 + } + }, + { + id: 'a5', + op: { + i: 'amet', + p: 40 + } + } + ] + } + return (this.removeChangeIdsSpy = sinon.spy( + this.RangesTracker.prototype, + 'removeChangeIds' + )) + }) + + describe('successfully with a single change', function () { + beforeEach(function (done) { + this.change_ids = [this.ranges.changes[1].id] + return this.RangesManager.acceptChanges( + this.change_ids, + this.ranges, + (err, ranges) => { + this.rangesResponse = ranges + return done() + } + ) + }) + + it('should log the call with the correct number of changes', function () { + return this.logger.log + .calledWith('accepting 1 changes in ranges') + .should.equal(true) + }) + + it('should delegate the change removal to the ranges tracker', function () { + return this.removeChangeIdsSpy + .calledWith(this.change_ids) + .should.equal(true) + }) + + it('should remove the change', function () { + return expect( + this.rangesResponse.changes.find( + (change) => change.id === this.ranges.changes[1].id + ) + ).to.be.undefined + }) + + it('should return the original number of changes minus 1', function () { + return this.rangesResponse.changes.length.should.equal( + this.ranges.changes.length - 1 + ) + }) + + return it('should not touch other changes', function () { + return [0, 2, 3, 4].map((i) => + expect( + this.rangesResponse.changes.find( + (change) => change.id === this.ranges.changes[i].id + ) + ).to.deep.equal(this.ranges.changes[i]) + ) + }) + }) + + return describe('successfully with multiple changes', function () { + beforeEach(function (done) { + this.change_ids = [ + this.ranges.changes[1].id, + this.ranges.changes[3].id, + this.ranges.changes[4].id + ] + return this.RangesManager.acceptChanges( + this.change_ids, + this.ranges, + (err, ranges) => { + this.rangesResponse = ranges + return done() + } + ) + }) + + it('should log the call with the correct number of changes', function () { + return this.logger.log + .calledWith(`accepting ${this.change_ids.length} changes in ranges`) + .should.equal(true) + }) + + it('should delegate the change removal to the ranges tracker', function () { + return this.removeChangeIdsSpy + .calledWith(this.change_ids) + .should.equal(true) + }) + + it('should remove the changes', function () { + return [1, 3, 4].map( + (i) => + expect( + this.rangesResponse.changes.find( + (change) => change.id === this.ranges.changes[1].id + ) + ).to.be.undefined + ) + }) + + it('should return the original number of changes minus the number of accepted changes', function () { + return this.rangesResponse.changes.length.should.equal( + this.ranges.changes.length - 3 + ) + }) + + return it('should not touch other changes', function () { + return [0, 2].map((i) => + expect( + this.rangesResponse.changes.find( + (change) => change.id === this.ranges.changes[i].id + ) + ).to.deep.equal(this.ranges.changes[i]) + ) + }) + }) + }) +}) diff --git a/services/document-updater/test/unit/js/RateLimitManager/RateLimitManager.js b/services/document-updater/test/unit/js/RateLimitManager/RateLimitManager.js index ed41a4834a..e84d557501 100644 --- a/services/document-updater/test/unit/js/RateLimitManager/RateLimitManager.js +++ b/services/document-updater/test/unit/js/RateLimitManager/RateLimitManager.js @@ -10,129 +10,127 @@ * DS206: Consider reworking classes to avoid initClass * 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/RateLimitManager.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/RateLimitManager.js' +const SandboxedModule = require('sandboxed-module') -describe("RateLimitManager", function() { - beforeEach(function() { - let Timer; - this.RateLimitManager = SandboxedModule.require(modulePath, { requires: { - "logger-sharelatex": (this.logger = { log: sinon.stub() }), - "settings-sharelatex": (this.settings = {}), - "./Metrics": (this.Metrics = { - Timer: (Timer = (function() { - Timer = class Timer { - static initClass() { - this.prototype.done = sinon.stub(); - } - }; - Timer.initClass(); - return Timer; - })()), - gauge: sinon.stub() - }) - } - } - ); - this.callback = sinon.stub(); - return this.RateLimiter = new this.RateLimitManager(1); - }); +describe('RateLimitManager', function () { + beforeEach(function () { + let Timer + this.RateLimitManager = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': (this.logger = { log: sinon.stub() }), + 'settings-sharelatex': (this.settings = {}), + './Metrics': (this.Metrics = { + Timer: (Timer = (function () { + Timer = class Timer { + static initClass() { + this.prototype.done = sinon.stub() + } + } + Timer.initClass() + return Timer + })()), + gauge: sinon.stub() + }) + } + }) + this.callback = sinon.stub() + return (this.RateLimiter = new this.RateLimitManager(1)) + }) - describe("for a single task", function() { - beforeEach(function() { - this.task = sinon.stub(); - return this.RateLimiter.run(this.task, this.callback); - }); + describe('for a single task', function () { + beforeEach(function () { + this.task = sinon.stub() + return this.RateLimiter.run(this.task, this.callback) + }) - it("should execute the task in the background", function() { - return this.task.called.should.equal(true); - }); + it('should execute the task in the background', function () { + return this.task.called.should.equal(true) + }) - it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); + it('should call the callback', function () { + return this.callback.called.should.equal(true) + }) - return it("should finish with a worker count of one", function() { - // because it's in the background - return expect(this.RateLimiter.ActiveWorkerCount).to.equal(1); - }); - }); + return it('should finish with a worker count of one', function () { + // because it's in the background + return expect(this.RateLimiter.ActiveWorkerCount).to.equal(1) + }) + }) - describe("for multiple tasks", function() { - beforeEach(function(done) { - this.task = sinon.stub(); - this.finalTask = sinon.stub(); - const task = cb => { - this.task(); - return setTimeout(cb, 100); - }; - const finalTask = cb => { - this.finalTask(); - return setTimeout(cb, 100); - }; - this.RateLimiter.run(task, this.callback); - this.RateLimiter.run(task, this.callback); - this.RateLimiter.run(task, this.callback); - return this.RateLimiter.run(finalTask, err => { - this.callback(err); - return done(); - }); - }); + describe('for multiple tasks', function () { + beforeEach(function (done) { + this.task = sinon.stub() + this.finalTask = sinon.stub() + const task = (cb) => { + this.task() + return setTimeout(cb, 100) + } + const finalTask = (cb) => { + this.finalTask() + return setTimeout(cb, 100) + } + this.RateLimiter.run(task, this.callback) + this.RateLimiter.run(task, this.callback) + this.RateLimiter.run(task, this.callback) + return this.RateLimiter.run(finalTask, (err) => { + this.callback(err) + return done() + }) + }) - it("should execute the first three tasks", function() { - return this.task.calledThrice.should.equal(true); - }); + it('should execute the first three tasks', function () { + return this.task.calledThrice.should.equal(true) + }) - it("should execute the final task", function() { - return this.finalTask.called.should.equal(true); - }); + it('should execute the final task', function () { + return this.finalTask.called.should.equal(true) + }) - it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); + it('should call the callback', function () { + return this.callback.called.should.equal(true) + }) - return it("should finish with worker count of zero", function() { - return expect(this.RateLimiter.ActiveWorkerCount).to.equal(0); - }); - }); + return it('should finish with worker count of zero', function () { + return expect(this.RateLimiter.ActiveWorkerCount).to.equal(0) + }) + }) - return describe("for a mixture of long-running tasks", function() { - beforeEach(function(done) { - this.task = sinon.stub(); - this.finalTask = sinon.stub(); - const finalTask = cb => { - this.finalTask(); - return setTimeout(cb, 100); - }; - this.RateLimiter.run(this.task, this.callback); - this.RateLimiter.run(this.task, this.callback); - this.RateLimiter.run(this.task, this.callback); - return this.RateLimiter.run(finalTask, err => { - this.callback(err); - return done(); - }); - }); + return describe('for a mixture of long-running tasks', function () { + beforeEach(function (done) { + this.task = sinon.stub() + this.finalTask = sinon.stub() + const finalTask = (cb) => { + this.finalTask() + return setTimeout(cb, 100) + } + this.RateLimiter.run(this.task, this.callback) + this.RateLimiter.run(this.task, this.callback) + this.RateLimiter.run(this.task, this.callback) + return this.RateLimiter.run(finalTask, (err) => { + this.callback(err) + return done() + }) + }) - it("should execute the first three tasks", function() { - return this.task.calledThrice.should.equal(true); - }); + it('should execute the first three tasks', function () { + return this.task.calledThrice.should.equal(true) + }) - it("should execute the final task", function() { - return this.finalTask.called.should.equal(true); - }); + it('should execute the final task', function () { + return this.finalTask.called.should.equal(true) + }) - it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); + it('should call the callback', function () { + return this.callback.called.should.equal(true) + }) - return it("should finish with worker count of three", function() { - return expect(this.RateLimiter.ActiveWorkerCount).to.equal(3); - }); - }); -}); + return it('should finish with worker count of three', function () { + return expect(this.RateLimiter.ActiveWorkerCount).to.equal(3) + }) + }) +}) diff --git a/services/document-updater/test/unit/js/RealTimeRedisManager/RealTimeRedisManagerTests.js b/services/document-updater/test/unit/js/RealTimeRedisManager/RealTimeRedisManagerTests.js index ae6cd4bba2..9e2f2e270f 100644 --- a/services/document-updater/test/unit/js/RealTimeRedisManager/RealTimeRedisManagerTests.js +++ b/services/document-updater/test/unit/js/RealTimeRedisManager/RealTimeRedisManagerTests.js @@ -10,127 +10,162 @@ * 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 modulePath = "../../../../app/js/RealTimeRedisManager.js"; -const SandboxedModule = require('sandboxed-module'); -const Errors = require("../../../../app/js/Errors"); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const modulePath = '../../../../app/js/RealTimeRedisManager.js' +const SandboxedModule = require('sandboxed-module') +const Errors = require('../../../../app/js/Errors') -describe("RealTimeRedisManager", function() { - beforeEach(function() { - this.rclient = { - auth() {}, - exec: sinon.stub() - }; - this.rclient.multi = () => this.rclient; - this.pubsubClient = - {publish: sinon.stub()}; - this.RealTimeRedisManager = SandboxedModule.require(modulePath, { requires: { - "redis-sharelatex": { createClient: config => (config.name === 'pubsub') ? this.pubsubClient : this.rclient - }, - "settings-sharelatex": { - redis: { - documentupdater: (this.settings = { - key_schema: { - pendingUpdates({doc_id}) { return `PendingUpdates:${doc_id}`; } - } - }), - pubsub: { - name: "pubsub" - } - } - }, - "logger-sharelatex": { log() {} }, - "crypto": (this.crypto = { randomBytes: sinon.stub().withArgs(4).returns(Buffer.from([0x1, 0x2, 0x3, 0x4])) }), - "os": (this.os = {hostname: sinon.stub().returns("somehost")}), - "./Metrics": (this.metrics = { summary: sinon.stub()}) - } - }); +describe('RealTimeRedisManager', function () { + beforeEach(function () { + this.rclient = { + auth() {}, + exec: sinon.stub() + } + this.rclient.multi = () => this.rclient + this.pubsubClient = { publish: sinon.stub() } + this.RealTimeRedisManager = SandboxedModule.require(modulePath, { + requires: { + 'redis-sharelatex': { + createClient: (config) => + config.name === 'pubsub' ? this.pubsubClient : this.rclient + }, + 'settings-sharelatex': { + redis: { + documentupdater: (this.settings = { + key_schema: { + pendingUpdates({ doc_id }) { + return `PendingUpdates:${doc_id}` + } + } + }), + pubsub: { + name: 'pubsub' + } + } + }, + 'logger-sharelatex': { log() {} }, + crypto: (this.crypto = { + randomBytes: sinon + .stub() + .withArgs(4) + .returns(Buffer.from([0x1, 0x2, 0x3, 0x4])) + }), + os: (this.os = { hostname: sinon.stub().returns('somehost') }), + './Metrics': (this.metrics = { summary: sinon.stub() }) + } + }) - this.doc_id = "doc-id-123"; - this.project_id = "project-id-123"; - return this.callback = sinon.stub(); - }); + this.doc_id = 'doc-id-123' + this.project_id = 'project-id-123' + return (this.callback = sinon.stub()) + }) - describe("getPendingUpdatesForDoc", function() { - beforeEach(function() { - this.rclient.lrange = sinon.stub(); - return this.rclient.ltrim = sinon.stub(); - }); + describe('getPendingUpdatesForDoc', function () { + beforeEach(function () { + this.rclient.lrange = sinon.stub() + return (this.rclient.ltrim = sinon.stub()) + }) - describe("successfully", function() { - beforeEach(function() { - this.updates = [ - { op: [{ i: "foo", p: 4 }] }, - { op: [{ i: "foo", p: 4 }] } - ]; - this.jsonUpdates = this.updates.map(update => JSON.stringify(update)); - this.rclient.exec = sinon.stub().callsArgWith(0, null, [this.jsonUpdates]); - return this.RealTimeRedisManager.getPendingUpdatesForDoc(this.doc_id, this.callback); - }); + describe('successfully', function () { + beforeEach(function () { + this.updates = [ + { op: [{ i: 'foo', p: 4 }] }, + { op: [{ i: 'foo', p: 4 }] } + ] + this.jsonUpdates = this.updates.map((update) => JSON.stringify(update)) + this.rclient.exec = sinon + .stub() + .callsArgWith(0, null, [this.jsonUpdates]) + return this.RealTimeRedisManager.getPendingUpdatesForDoc( + this.doc_id, + this.callback + ) + }) - it("should get the pending updates", function() { - return this.rclient.lrange - .calledWith(`PendingUpdates:${this.doc_id}`, 0, 7) - .should.equal(true); - }); + it('should get the pending updates', function () { + return this.rclient.lrange + .calledWith(`PendingUpdates:${this.doc_id}`, 0, 7) + .should.equal(true) + }) - it("should delete the pending updates", function() { - return this.rclient.ltrim - .calledWith(`PendingUpdates:${this.doc_id}`, 8, -1) - .should.equal(true); - }); + it('should delete the pending updates', function () { + return this.rclient.ltrim + .calledWith(`PendingUpdates:${this.doc_id}`, 8, -1) + .should.equal(true) + }) - return it("should call the callback with the updates", function() { - return this.callback.calledWith(null, this.updates).should.equal(true); - }); - }); + return it('should call the callback with the updates', function () { + return this.callback.calledWith(null, this.updates).should.equal(true) + }) + }) - return describe("when the JSON doesn't parse", function() { - beforeEach(function() { - this.jsonUpdates = [ - JSON.stringify({ op: [{ i: "foo", p: 4 }] }), - "broken json" - ]; - this.rclient.exec = sinon.stub().callsArgWith(0, null, [this.jsonUpdates]); - return this.RealTimeRedisManager.getPendingUpdatesForDoc(this.doc_id, this.callback); - }); + return describe("when the JSON doesn't parse", function () { + beforeEach(function () { + this.jsonUpdates = [ + JSON.stringify({ op: [{ i: 'foo', p: 4 }] }), + 'broken json' + ] + this.rclient.exec = sinon + .stub() + .callsArgWith(0, null, [this.jsonUpdates]) + return this.RealTimeRedisManager.getPendingUpdatesForDoc( + this.doc_id, + this.callback + ) + }) - return it("should return an error to the callback", function() { - return this.callback.calledWith(new Error("JSON parse error")).should.equal(true); - }); - }); - }); + return it('should return an error to the callback', function () { + return this.callback + .calledWith(new Error('JSON parse error')) + .should.equal(true) + }) + }) + }) + describe('getUpdatesLength', function () { + beforeEach(function () { + this.rclient.llen = sinon.stub().yields(null, (this.length = 3)) + return this.RealTimeRedisManager.getUpdatesLength( + this.doc_id, + this.callback + ) + }) - describe("getUpdatesLength", function() { - beforeEach(function() { - this.rclient.llen = sinon.stub().yields(null, (this.length = 3)); - return this.RealTimeRedisManager.getUpdatesLength(this.doc_id, this.callback); - }); + it('should look up the length', function () { + return this.rclient.llen + .calledWith(`PendingUpdates:${this.doc_id}`) + .should.equal(true) + }) - it("should look up the length", function() { - return this.rclient.llen.calledWith(`PendingUpdates:${this.doc_id}`).should.equal(true); - }); + return it('should return the length', function () { + return this.callback.calledWith(null, this.length).should.equal(true) + }) + }) - return it("should return the length", function() { - return this.callback.calledWith(null, this.length).should.equal(true); - }); - }); + return describe('sendData', function () { + beforeEach(function () { + this.message_id = 'doc:somehost:01020304-0' + return this.RealTimeRedisManager.sendData({ op: 'thisop' }) + }) - return describe("sendData", function() { - beforeEach(function() { - this.message_id = "doc:somehost:01020304-0"; - return this.RealTimeRedisManager.sendData({op: "thisop"}); - }); + it('should send the op with a message id', function () { + return this.pubsubClient.publish + .calledWith( + 'applied-ops', + JSON.stringify({ op: 'thisop', _id: this.message_id }) + ) + .should.equal(true) + }) - it("should send the op with a message id", function() { - return this.pubsubClient.publish.calledWith("applied-ops", JSON.stringify({op:"thisop",_id:this.message_id})).should.equal(true); - }); - - return it("should track the payload size", function() { - return this.metrics.summary.calledWith("redis.publish.applied-ops", JSON.stringify({op:"thisop",_id:this.message_id}).length).should.equal(true); - }); - }); -}); + return it('should track the payload size', function () { + return this.metrics.summary + .calledWith( + 'redis.publish.applied-ops', + JSON.stringify({ op: 'thisop', _id: this.message_id }).length + ) + .should.equal(true) + }) + }) +}) diff --git a/services/document-updater/test/unit/js/RedisManager/RedisManagerTests.js b/services/document-updater/test/unit/js/RedisManager/RedisManagerTests.js index 7e8cf40d0e..e36df094f1 100644 --- a/services/document-updater/test/unit/js/RedisManager/RedisManagerTests.js +++ b/services/document-updater/test/unit/js/RedisManager/RedisManagerTests.js @@ -11,997 +11,1408 @@ * 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 modulePath = "../../../../app/js/RedisManager.js"; -const SandboxedModule = require('sandboxed-module'); -const Errors = require("../../../../app/js/Errors"); -const crypto = require("crypto"); -const tk = require("timekeeper"); - -describe("RedisManager", function() { - beforeEach(function() { - let Timer; - this.multi = {exec: sinon.stub()}; - this.rclient = {multi: () => this.multi}; - tk.freeze(new Date()); - this.RedisManager = SandboxedModule.require(modulePath, { - requires: { - "logger-sharelatex": (this.logger = { error: sinon.stub(), log: sinon.stub(), warn: sinon.stub() }), - "./ProjectHistoryRedisManager": (this.ProjectHistoryRedisManager = {}), - "settings-sharelatex": (this.settings = { - documentupdater: {logHashErrors: {write:true, read:true}}, - apis: { - project_history: {enabled: true} - }, - redis: { - documentupdater: { - key_schema: { - blockingKey({doc_id}) { return `Blocking:${doc_id}`; }, - docLines({doc_id}) { return `doclines:${doc_id}`; }, - docOps({doc_id}) { return `DocOps:${doc_id}`; }, - docVersion({doc_id}) { return `DocVersion:${doc_id}`; }, - docHash({doc_id}) { return `DocHash:${doc_id}`; }, - projectKey({doc_id}) { return `ProjectId:${doc_id}`; }, - pendingUpdates({doc_id}) { return `PendingUpdates:${doc_id}`; }, - docsInProject({project_id}) { return `DocsIn:${project_id}`; }, - ranges({doc_id}) { return `Ranges:${doc_id}`; }, - pathname({doc_id}) { return `Pathname:${doc_id}`; }, - projectHistoryId({doc_id}) { return `ProjectHistoryId:${doc_id}`; }, - projectHistoryType({doc_id}) { return `ProjectHistoryType:${doc_id}`; }, - projectState({project_id}) { return `ProjectState:${project_id}`; }, - unflushedTime({doc_id}) { return `UnflushedTime:${doc_id}`; }, - lastUpdatedBy({doc_id}) { return `lastUpdatedBy:${doc_id}`; }, - lastUpdatedAt({doc_id}) { return `lastUpdatedAt:${doc_id}`; } - } - }, - history: { - key_schema: { - uncompressedHistoryOps({doc_id}) { return `UncompressedHistoryOps:${doc_id}`; }, - docsWithHistoryOps({project_id}) { return `DocsWithHistoryOps:${project_id}`; } - } - } - } - }), - "redis-sharelatex": { - createClient: () => this.rclient - }, - "./Metrics": (this.metrics = { - inc: sinon.stub(), - summary: sinon.stub(), - Timer: (Timer = class Timer { - constructor() { - this.start = new Date(); - } - - done() { - const timeSpan = new Date - this.start; - return timeSpan; - } - }) - }), - "./Errors": Errors - }, - globals: { - JSON: (this.JSON = JSON) - } - } - ); - - this.doc_id = "doc-id-123"; - this.project_id = "project-id-123"; - this.projectHistoryId = 123; - return this.callback = sinon.stub(); - }); - - afterEach(function() { return tk.reset(); }); - - describe("getDoc", function() { - beforeEach(function() { - this.lines = ["one", "two", "three", "これは"]; // include some utf8 - this.jsonlines = JSON.stringify(this.lines); - this.version = 42; - this.hash = crypto.createHash('sha1').update(this.jsonlines,'utf8').digest('hex'); - this.ranges = { comments: "mock", entries: "mock" }; - this.json_ranges = JSON.stringify(this.ranges); - this.unflushed_time = 12345; - this.pathname = '/a/b/c.tex'; - this.multi.get = sinon.stub(); - this.multi.exec = sinon.stub().callsArgWith(0, null, [this.jsonlines, this.version, this.hash, this.project_id, this.json_ranges, this.pathname, this.projectHistoryId.toString(), this.unflushed_time]); - return this.rclient.sadd = sinon.stub().yields(null, 0); - }); - - describe("successfully", function() { - beforeEach(function() { - return this.RedisManager.getDoc(this.project_id, this.doc_id, this.callback); - }); - - it("should get the lines from redis", function() { - return this.multi.get - .calledWith(`doclines:${this.doc_id}`) - .should.equal(true); - }); - - it("should get the version from", function() { - return this.multi.get - .calledWith(`DocVersion:${this.doc_id}`) - .should.equal(true); - }); - - it('should get the hash', function() { - return this.multi.get - .calledWith(`DocHash:${this.doc_id}`) - .should.equal(true); - }); - - it("should get the ranges", function() { - return this.multi.get - .calledWith(`Ranges:${this.doc_id}`) - .should.equal(true); - }); - - it("should get the unflushed time", function() { - return this.multi.get - .calledWith(`UnflushedTime:${this.doc_id}`) - .should.equal(true); - }); - - it("should get the pathname", function() { - return this.multi.get - .calledWith(`Pathname:${this.doc_id}`) - .should.equal(true); - }); - - it("should get the projectHistoryId as an integer", function() { - return this.multi.get - .calledWith(`ProjectHistoryId:${this.doc_id}`) - .should.equal(true); - }); - - it("should get lastUpdatedAt", function() { - return this.multi.get - .calledWith(`lastUpdatedAt:${this.doc_id}`) - .should.equal(true); - }); - - it("should get lastUpdatedBy", function() { - return this.multi.get - .calledWith(`lastUpdatedBy:${this.doc_id}`) - .should.equal(true); - }); - - it("should check if the document is in the DocsIn set", function() { - return this.rclient.sadd - .calledWith(`DocsIn:${this.project_id}`) - .should.equal(true); - }); - - it('should return the document', function() { - return this.callback - .calledWithExactly(null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId, this.unflushed_time, this.lastUpdatedAt, this.lastUpdatedBy) - .should.equal(true); - }); - - return it('should not log any errors', function() { - return this.logger.error.calledWith() - .should.equal(false); - }); - }); - - describe("when the document is not present", function() { - beforeEach(function() { - this.multi.exec = sinon.stub().callsArgWith(0, null, [null, null, null, null, null, null, null, null, null, null]); - this.rclient.sadd = sinon.stub().yields(); - return this.RedisManager.getDoc(this.project_id, this.doc_id, this.callback); - }); - - it("should not check if the document is in the DocsIn set", function() { - return this.rclient.sadd - .calledWith(`DocsIn:${this.project_id}`) - .should.equal(false); - }); - - it('should return an empty result', function() { - return this.callback - .calledWithExactly(null, null, 0, {}, null, null, null, null, null) - .should.equal(true); - }); - - return it('should not log any errors', function() { - return this.logger.error.calledWith() - .should.equal(false); - }); - }); - - describe("when the document is missing from the DocsIn set", function() { - beforeEach(function() { - this.rclient.sadd = sinon.stub().yields(null, 1); - return this.RedisManager.getDoc(this.project_id, this.doc_id, this.callback); - }); - - it('should log an error', function() { - return this.logger.error.calledWith() - .should.equal(true); - }); - - return it('should return the document', function() { - return this.callback - .calledWithExactly(null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId, this.unflushed_time, this.lastUpdatedAt, this.lastUpdatedBy) - .should.equal(true); - }); - }); - - describe("with a corrupted document", function() { - beforeEach(function() { - this.badHash = "INVALID-HASH-VALUE"; - this.multi.exec = sinon.stub().callsArgWith(0, null, [this.jsonlines, this.version, this.badHash, this.project_id, this.json_ranges]); - return this.RedisManager.getDoc(this.project_id, this.doc_id, this.callback); - }); - - it('should log a hash error', function() { - return this.logger.error.calledWith() - .should.equal(true); - }); - - return it('should return the document', function() { - return this.callback - .calledWith(null, this.lines, this.version, this.ranges) - .should.equal(true); - }); - }); - - - describe("with a slow request to redis", function() { - beforeEach(function() { - this.multi.exec = sinon.stub().callsArgWith(0, null, [this.jsonlines, this.version, this.badHash, this.project_id, this.json_ranges, this.pathname, this.unflushed_time]); - this.clock = sinon.useFakeTimers(); - this.multi.exec = cb => { - this.clock.tick(6000); - return cb(null, [this.jsonlines, this.version, this.another_project_id, this.json_ranges, this.pathname, this.unflushed_time]); - }; - - return this.RedisManager.getDoc(this.project_id, this.doc_id, this.callback); - }); - - afterEach(function() { - return this.clock.restore(); - }); - - return it('should return an error', function() { - return this.callback - .calledWith(new Error("redis getDoc exceeded timeout")) - .should.equal(true); - }); - }); - - return describe("getDoc with an invalid project id", function() { - beforeEach(function() { - this.another_project_id = "project-id-456"; - this.multi.exec = sinon.stub().callsArgWith(0, null, [this.jsonlines, this.version, this.hash, this.another_project_id, this.json_ranges, this.pathname, this.unflushed_time]); - return this.RedisManager.getDoc(this.project_id, this.doc_id, this.callback); - }); - - return it('should return an error', function() { - return this.callback - .calledWith(new Errors.NotFoundError("not found")) - .should.equal(true); - }); - }); - }); - - describe("getPreviousDocOpsTests", function() { - describe("with a start and an end value", function() { - beforeEach(function() { - this.first_version_in_redis = 30; - this.version = 70; - this.length = this.version - this.first_version_in_redis; - this.start = 50; - this.end = 60; - this.ops = [ - { "mock": "op-1" }, - { "mock": "op-2" } - ]; - this.jsonOps = this.ops.map(op => JSON.stringify(op)); - this.rclient.llen = sinon.stub().callsArgWith(1, null, this.length); - this.rclient.get = sinon.stub().callsArgWith(1, null, this.version.toString()); - this.rclient.lrange = sinon.stub().callsArgWith(3, null, this.jsonOps); - return this.RedisManager.getPreviousDocOps(this.doc_id, this.start, this.end, this.callback); - }); - - it("should get the length of the existing doc ops", function() { - return this.rclient.llen - .calledWith(`DocOps:${this.doc_id}`) - .should.equal(true); - }); - - it("should get the current version of the doc", function() { - return this.rclient.get - .calledWith(`DocVersion:${this.doc_id}`) - .should.equal(true); - }); - - it("should get the appropriate docs ops", function() { - return this.rclient.lrange - .calledWith(`DocOps:${this.doc_id}`, this.start - this.first_version_in_redis, this.end - this.first_version_in_redis) - .should.equal(true); - }); - - return it("should return the docs with the doc ops deserialized", function() { - return this.callback.calledWith(null, this.ops).should.equal(true); - }); - }); - - describe("with an end value of -1", function() { - beforeEach(function() { - this.first_version_in_redis = 30; - this.version = 70; - this.length = this.version - this.first_version_in_redis; - this.start = 50; - this.end = -1; - this.ops = [ - { "mock": "op-1" }, - { "mock": "op-2" } - ]; - this.jsonOps = this.ops.map(op => JSON.stringify(op)); - this.rclient.llen = sinon.stub().callsArgWith(1, null, this.length); - this.rclient.get = sinon.stub().callsArgWith(1, null, this.version.toString()); - this.rclient.lrange = sinon.stub().callsArgWith(3, null, this.jsonOps); - return this.RedisManager.getPreviousDocOps(this.doc_id, this.start, this.end, this.callback); - }); - - it("should get the appropriate docs ops to the end of list", function() { - return this.rclient.lrange - .calledWith(`DocOps:${this.doc_id}`, this.start - this.first_version_in_redis, -1) - .should.equal(true); - }); - - return it("should return the docs with the doc ops deserialized", function() { - return this.callback.calledWith(null, this.ops).should.equal(true); - }); - }); - - describe("when the requested range is not in Redis", function() { - beforeEach(function() { - this.first_version_in_redis = 30; - this.version = 70; - this.length = this.version - this.first_version_in_redis; - this.start = 20; - this.end = -1; - this.ops = [ - { "mock": "op-1" }, - { "mock": "op-2" } - ]; - this.jsonOps = this.ops.map(op => JSON.stringify(op)); - this.rclient.llen = sinon.stub().callsArgWith(1, null, this.length); - this.rclient.get = sinon.stub().callsArgWith(1, null, this.version.toString()); - this.rclient.lrange = sinon.stub().callsArgWith(3, null, this.jsonOps); - return this.RedisManager.getPreviousDocOps(this.doc_id, this.start, this.end, this.callback); - }); - - it("should return an error", function() { - return this.callback.calledWith(new Errors.OpRangeNotAvailableError("doc ops range is not loaded in redis")).should.equal(true); - }); - - return it("should log out the problem", function() { - return this.logger.warn.called.should.equal(true); - }); - }); - - return describe("with a slow request to redis", function() { - beforeEach(function() { - this.first_version_in_redis = 30; - this.version = 70; - this.length = this.version - this.first_version_in_redis; - this.start = 50; - this.end = 60; - this.ops = [ - { "mock": "op-1" }, - { "mock": "op-2" } - ]; - this.jsonOps = this.ops.map(op => JSON.stringify(op)); - this.rclient.llen = sinon.stub().callsArgWith(1, null, this.length); - this.rclient.get = sinon.stub().callsArgWith(1, null, this.version.toString()); - this.clock = sinon.useFakeTimers(); - this.rclient.lrange = (key, start, end, cb) => { - this.clock.tick(6000); - return cb(null, this.jsonOps); - }; - return this.RedisManager.getPreviousDocOps(this.doc_id, this.start, this.end, this.callback); - }); - - afterEach(function() { - return this.clock.restore(); - }); - - return it('should return an error', function() { - return this.callback - .calledWith(new Error("redis getPreviousDocOps exceeded timeout")) - .should.equal(true); - }); - }); - }); - - - describe("updateDocument", function() { - beforeEach(function() { - this.lines = ["one", "two", "three", "これは"]; - this.ops = [{ op: [{ i: "foo", p: 4 }] },{ op: [{ i: "bar", p: 8 }] }]; - this.version = 42; - this.hash = crypto.createHash('sha1').update(JSON.stringify(this.lines),'utf8').digest('hex'); - this.ranges = { comments: "mock", entries: "mock" }; - this.updateMeta = { user_id: 'last-author-fake-id' }; - this.doc_update_list_length = sinon.stub(); - this.project_update_list_length = sinon.stub(); - - this.RedisManager.getDocVersion = sinon.stub(); - this.multi.set = sinon.stub(); - this.multi.rpush = sinon.stub(); - this.multi.expire = sinon.stub(); - this.multi.ltrim = sinon.stub(); - this.multi.del = sinon.stub(); - this.multi.exec = sinon.stub().callsArgWith(0, null, - [this.hash, null, null, null, null, null, null, this.doc_update_list_length, null, null] - ); - return this.ProjectHistoryRedisManager.queueOps = sinon.stub().callsArgWith( - this.ops.length + 1, null, this.project_update_list_length - ); - }); - - describe("with a consistent version", function() { - beforeEach(function() {}); - - - describe("with project history enabled", function() { - beforeEach(function() { - this.settings.apis.project_history.enabled = true; - this.RedisManager.getDocVersion.withArgs(this.doc_id).yields(null, this.version - this.ops.length); - return this.RedisManager.updateDocument(this.project_id, this.doc_id, this.lines, this.version, this.ops, this.ranges, this.updateMeta, this.callback); - }); - - it("should get the current doc version to check for consistency", function() { - return this.RedisManager.getDocVersion - .calledWith(this.doc_id) - .should.equal(true); - }); - - it("should set the doclines", function() { - return this.multi.set - .calledWith(`doclines:${this.doc_id}`, JSON.stringify(this.lines)) - .should.equal(true); - }); - - it("should set the version", function() { - return this.multi.set - .calledWith(`DocVersion:${this.doc_id}`, this.version) - .should.equal(true); - }); - - it("should set the hash", function() { - return this.multi.set - .calledWith(`DocHash:${this.doc_id}`, this.hash) - .should.equal(true); - }); - - it("should set the ranges", function() { - return this.multi.set - .calledWith(`Ranges:${this.doc_id}`, JSON.stringify(this.ranges)) - .should.equal(true); - }); - - it("should set the unflushed time", function() { - return this.multi.set - .calledWith(`UnflushedTime:${this.doc_id}`, Date.now(), "NX") - .should.equal(true); - }); - - it("should set the last updated time", function() { - return this.multi.set - .calledWith(`lastUpdatedAt:${this.doc_id}`, Date.now()) - .should.equal(true); - }); - - it("should set the last updater", function() { - return this.multi.set - .calledWith(`lastUpdatedBy:${this.doc_id}`, 'last-author-fake-id') - .should.equal(true); - }); - - it("should push the doc op into the doc ops list", function() { - return this.multi.rpush - .calledWith(`DocOps:${this.doc_id}`, JSON.stringify(this.ops[0]), JSON.stringify(this.ops[1])) - .should.equal(true); - }); - - it("should renew the expiry ttl on the doc ops array", function() { - return this.multi.expire - .calledWith(`DocOps:${this.doc_id}`, this.RedisManager.DOC_OPS_TTL) - .should.equal(true); - }); - - it("should truncate the list to 100 members", function() { - return this.multi.ltrim - .calledWith(`DocOps:${this.doc_id}`, -this.RedisManager.DOC_OPS_MAX_LENGTH, -1) - .should.equal(true); - }); - - it("should push the updates into the history ops list", function() { - return this.multi.rpush - .calledWith(`UncompressedHistoryOps:${this.doc_id}`, JSON.stringify(this.ops[0]), JSON.stringify(this.ops[1])) - .should.equal(true); - }); - - it("should push the updates into the project history ops list", function() { - return this.ProjectHistoryRedisManager.queueOps - .calledWith(this.project_id, JSON.stringify(this.ops[0])) - .should.equal(true); - }); - - it("should call the callback", function() { - return this.callback - .calledWith(null, this.doc_update_list_length, this.project_update_list_length) - .should.equal(true); - }); - - return it('should not log any errors', function() { - return this.logger.error.calledWith() - .should.equal(false); - }); - }); - - describe("with project history disabled", function() { - beforeEach(function() { - this.settings.apis.project_history.enabled = false; - this.RedisManager.getDocVersion.withArgs(this.doc_id).yields(null, this.version - this.ops.length); - return this.RedisManager.updateDocument(this.project_id, this.doc_id, this.lines, this.version, this.ops, this.ranges, this.updateMeta, this.callback); - }); - - it("should not push the updates into the project history ops list", function() { - return this.ProjectHistoryRedisManager.queueOps.called.should.equal(false); - }); - - return it("should call the callback", function() { - return this.callback - .calledWith(null, this.doc_update_list_length) - .should.equal(true); - }); - }); - - return describe("with a doc using project history only", function() { - beforeEach(function() { - this.RedisManager.getDocVersion.withArgs(this.doc_id).yields(null, this.version - this.ops.length, 'project-history'); - return this.RedisManager.updateDocument(this.project_id, this.doc_id, this.lines, this.version, this.ops, this.ranges, this.updateMeta, this.callback); - }); - - it("should not push the updates to the track-changes ops list", function() { - return this.multi.rpush - .calledWith(`UncompressedHistoryOps:${this.doc_id}`) - .should.equal(false); - }); - - it("should push the updates into the project history ops list", function() { - return this.ProjectHistoryRedisManager.queueOps - .calledWith(this.project_id, JSON.stringify(this.ops[0])) - .should.equal(true); - }); - - return it("should call the callback with the project update count only", function() { - return this.callback - .calledWith(null, undefined, this.project_update_list_length) - .should.equal(true); - }); - }); - }); - - describe("with an inconsistent version", function() { - beforeEach(function() { - this.RedisManager.getDocVersion.withArgs(this.doc_id).yields(null, this.version - this.ops.length - 1); - return this.RedisManager.updateDocument(this.project_id, this.doc_id, this.lines, this.version, this.ops, this.ranges, this.updateMeta, this.callback); - }); - - it("should not call multi.exec", function() { - return this.multi.exec.called.should.equal(false); - }); - - return it("should call the callback with an error", function() { - return this.callback - .calledWith(new Error(`Version mismatch. '${this.doc_id}' is corrupted.`)) - .should.equal(true); - }); - }); - - describe("with no updates", function() { - beforeEach(function() { - this.RedisManager.getDocVersion.withArgs(this.doc_id).yields(null, this.version); - return this.RedisManager.updateDocument(this.project_id, this.doc_id, this.lines, this.version, [], this.ranges, this.updateMeta, this.callback); - }); - - it("should not try to enqueue doc updates", function() { - return this.multi.rpush - .called - .should.equal(false); - }); - - it("should not try to enqueue project updates", function() { - return this.ProjectHistoryRedisManager.queueOps - .called - .should.equal(false); - }); - - return it("should still set the doclines", function() { - return this.multi.set - .calledWith(`doclines:${this.doc_id}`, JSON.stringify(this.lines)) - .should.equal(true); - }); - }); - - describe("with empty ranges", function() { - beforeEach(function() { - this.RedisManager.getDocVersion.withArgs(this.doc_id).yields(null, this.version - this.ops.length); - return this.RedisManager.updateDocument(this.project_id, this.doc_id, this.lines, this.version, this.ops, {}, this.updateMeta, this.callback); - }); - - it("should not set the ranges", function() { - return this.multi.set - .calledWith(`Ranges:${this.doc_id}`, JSON.stringify(this.ranges)) - .should.equal(false); - }); - - return it("should delete the ranges key", function() { - return this.multi.del - .calledWith(`Ranges:${this.doc_id}`) - .should.equal(true); - }); - }); - - describe("with null bytes in the serialized doc lines", function() { - beforeEach(function() { - this.RedisManager.getDocVersion.withArgs(this.doc_id).yields(null, this.version - this.ops.length); - this._stringify = JSON.stringify; - this.JSON.stringify = () => '["bad bytes! \u0000 <- here"]'; - return this.RedisManager.updateDocument(this.project_id, this.doc_id, this.lines, this.version, this.ops, this.ranges, this.updateMeta, this.callback); - }); - - afterEach(function() { - return this.JSON.stringify = this._stringify; - }); - - it("should log an error", function() { - return this.logger.error.called.should.equal(true); - }); - - return it("should call the callback with an error", function() { - return this.callback.calledWith(new Error("null bytes found in doc lines")).should.equal(true); - }); - }); - - describe("with ranges that are too big", function() { - beforeEach(function() { - this.RedisManager.getDocVersion.withArgs(this.doc_id).yields(null, this.version - this.ops.length); - this.RedisManager._serializeRanges = sinon.stub().yields(new Error("ranges are too large")); - return this.RedisManager.updateDocument(this.project_id, this.doc_id, this.lines, this.version, this.ops, this.ranges, this.updateMeta, this.callback); - }); - - it('should log an error', function() { - return this.logger.error.called.should.equal(true); - }); - - return it("should call the callback with the error", function() { - return this.callback.calledWith(new Error("ranges are too large")).should.equal(true); - }); - }); - - return describe("without user id from meta", function() { - beforeEach(function() { - this.RedisManager.getDocVersion.withArgs(this.doc_id).yields(null, this.version - this.ops.length); - return this.RedisManager.updateDocument(this.project_id, this.doc_id, this.lines, this.version, this.ops, this.ranges, {}, this.callback); - }); - - it("should set the last updater to null", function() { - return this.multi.del - .calledWith(`lastUpdatedBy:${this.doc_id}`) - .should.equal(true); - }); - - return it("should still set the last updated time", function() { - return this.multi.set - .calledWith(`lastUpdatedAt:${this.doc_id}`, Date.now()) - .should.equal(true); - }); - }); - }); - - describe("putDocInMemory", function() { - beforeEach(function() { - this.multi.set = sinon.stub(); - this.rclient.sadd = sinon.stub().yields(); - this.multi.del = sinon.stub(); - this.lines = ["one", "two", "three", "これは"]; - this.version = 42; - this.hash = crypto.createHash('sha1').update(JSON.stringify(this.lines),'utf8').digest('hex'); - this.multi.exec = sinon.stub().callsArgWith(0, null, [this.hash]); - this.ranges = { comments: "mock", entries: "mock" }; - return this.pathname = '/a/b/c.tex'; - }); - - describe("with non-empty ranges", function() { - beforeEach(function(done) { - return this.RedisManager.putDocInMemory(this.project_id, this.doc_id, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId, done); - }); - - it("should set the lines", function() { - return this.multi.set - .calledWith(`doclines:${this.doc_id}`, JSON.stringify(this.lines)) - .should.equal(true); - }); - - it("should set the version", function() { - return this.multi.set - .calledWith(`DocVersion:${this.doc_id}`, this.version) - .should.equal(true); - }); - - it("should set the hash", function() { - return this.multi.set - .calledWith(`DocHash:${this.doc_id}`, this.hash) - .should.equal(true); - }); - - it("should set the ranges", function() { - return this.multi.set - .calledWith(`Ranges:${this.doc_id}`, JSON.stringify(this.ranges)) - .should.equal(true); - }); - - it("should set the project_id for the doc", function() { - return this.multi.set - .calledWith(`ProjectId:${this.doc_id}`, this.project_id) - .should.equal(true); - }); - - it("should set the pathname for the doc", function() { - return this.multi.set - .calledWith(`Pathname:${this.doc_id}`, this.pathname) - .should.equal(true); - }); - - it("should set the projectHistoryId for the doc", function() { - return this.multi.set - .calledWith(`ProjectHistoryId:${this.doc_id}`, this.projectHistoryId) - .should.equal(true); - }); - - it("should add the doc_id to the project set", function() { - return this.rclient.sadd - .calledWith(`DocsIn:${this.project_id}`, this.doc_id) - .should.equal(true); - }); - - return it('should not log any errors', function() { - return this.logger.error.calledWith() - .should.equal(false); - }); - }); - - describe("with empty ranges", function() { - beforeEach(function(done) { - return this.RedisManager.putDocInMemory(this.project_id, this.doc_id, this.lines, this.version, {}, this.pathname, this.projectHistoryId, done); - }); - - it("should delete the ranges key", function() { - return this.multi.del - .calledWith(`Ranges:${this.doc_id}`) - .should.equal(true); - }); - - return it("should not set the ranges", function() { - return this.multi.set - .calledWith(`Ranges:${this.doc_id}`, JSON.stringify(this.ranges)) - .should.equal(false); - }); - }); - - describe("with null bytes in the serialized doc lines", function() { - beforeEach(function() { - this._stringify = JSON.stringify; - this.JSON.stringify = () => '["bad bytes! \u0000 <- here"]'; - return this.RedisManager.putDocInMemory(this.project_id, this.doc_id, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId, this.callback); - }); - - afterEach(function() { - return this.JSON.stringify = this._stringify; - }); - - it("should log an error", function() { - return this.logger.error.called.should.equal(true); - }); - - return it("should call the callback with an error", function() { - return this.callback.calledWith(new Error("null bytes found in doc lines")).should.equal(true); - }); - }); - - return describe("with ranges that are too big", function() { - beforeEach(function() { - this.RedisManager._serializeRanges = sinon.stub().yields(new Error("ranges are too large")); - return this.RedisManager.putDocInMemory(this.project_id, this.doc_id, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId, this.callback); - }); - - it('should log an error', function() { - return this.logger.error.called.should.equal(true); - }); - - return it("should call the callback with the error", function() { - return this.callback.calledWith(new Error("ranges are too large")).should.equal(true); - }); - }); - }); - - describe("removeDocFromMemory", function() { - beforeEach(function(done) { - this.multi.strlen = sinon.stub(); - this.multi.del = sinon.stub(); - this.multi.srem = sinon.stub(); - this.multi.exec.yields(); - return this.RedisManager.removeDocFromMemory(this.project_id, this.doc_id, done); - }); - - it("should check the length of the current doclines", function() { - return this.multi.strlen - .calledWith(`doclines:${this.doc_id}`) - .should.equal(true); - }); - - it("should delete the lines", function() { - return this.multi.del - .calledWith(`doclines:${this.doc_id}`) - .should.equal(true); - }); - - it("should delete the version", function() { - return this.multi.del - .calledWith(`DocVersion:${this.doc_id}`) - .should.equal(true); - }); - - it("should delete the hash", function() { - return this.multi.del - .calledWith(`DocHash:${this.doc_id}`) - .should.equal(true); - }); - - it("should delete the unflushed time", function() { - return this.multi.del - .calledWith(`UnflushedTime:${this.doc_id}`) - .should.equal(true); - }); - - it("should delete the project_id for the doc", function() { - return this.multi.del - .calledWith(`ProjectId:${this.doc_id}`) - .should.equal(true); - }); - - it("should remove the doc_id from the project set", function() { - return this.multi.srem - .calledWith(`DocsIn:${this.project_id}`, this.doc_id) - .should.equal(true); - }); - - it("should delete the pathname for the doc", function() { - return this.multi.del - .calledWith(`Pathname:${this.doc_id}`) - .should.equal(true); - }); - - it("should delete the pathname for the doc", function() { - return this.multi.del - .calledWith(`ProjectHistoryId:${this.doc_id}`) - .should.equal(true); - }); - - it("should delete lastUpdatedAt", function() { - return this.multi.del - .calledWith(`lastUpdatedAt:${this.doc_id}`) - .should.equal(true); - }); - - return it("should delete lastUpdatedBy", function() { - return this.multi.del - .calledWith(`lastUpdatedBy:${this.doc_id}`) - .should.equal(true); - }); - }); - - - describe("clearProjectState", function() { - beforeEach(function(done) { - this.rclient.del = sinon.stub().callsArg(1); - return this.RedisManager.clearProjectState(this.project_id, done); - }); - - return it("should delete the project state", function() { - return this.rclient.del - .calledWith(`ProjectState:${this.project_id}`) - .should.equal(true); - }); - }); - - return describe("renameDoc", function() { - beforeEach(function() { - this.rclient.rpush = sinon.stub().yields(); - this.rclient.set = sinon.stub().yields(); - return this.update = { - id: this.doc_id, - pathname: (this.pathname = 'pathname'), - newPathname: (this.newPathname = 'new-pathname') - }; - }); - - describe("the document is cached in redis", function() { - beforeEach(function() { - this.RedisManager.getDoc = sinon.stub().callsArgWith(2, null, 'lines', 'version'); - this.ProjectHistoryRedisManager.queueRenameEntity = sinon.stub().yields(); - return this.RedisManager.renameDoc(this.project_id, this.doc_id, this.userId, this.update, this.projectHistoryId, this.callback); - }); - - it("update the cached pathname", function() { - return this.rclient.set - .calledWith(`Pathname:${this.doc_id}`, this.newPathname) - .should.equal(true); - }); - - return it("should queue an update", function() { - return this.ProjectHistoryRedisManager.queueRenameEntity - .calledWithExactly(this.project_id, this.projectHistoryId, 'doc', this.doc_id, this.userId, this.update, this.callback) - .should.equal(true); - }); - }); - - describe("the document is not cached in redis", function() { - beforeEach(function() { - this.RedisManager.getDoc = sinon.stub().callsArgWith(2, null, null, null); - this.ProjectHistoryRedisManager.queueRenameEntity = sinon.stub().yields(); - return this.RedisManager.renameDoc(this.project_id, this.doc_id, this.userId, this.update, this.projectHistoryId, this.callback); - }); - - it("does not update the cached pathname", function() { - return this.rclient.set.called.should.equal(false); - }); - - return it("should queue an update", function() { - return this.ProjectHistoryRedisManager.queueRenameEntity - .calledWithExactly(this.project_id, this.projectHistoryId, 'doc', this.doc_id, this.userId, this.update, this.callback) - .should.equal(true); - }); - }); - - return describe("getDocVersion", function() { - beforeEach(function() { - return this.version = 12345; - }); - - describe("when the document does not have a project history type set", function() { - beforeEach(function() { - this.rclient.mget = sinon.stub().withArgs(`DocVersion:${this.doc_id}`, `ProjectHistoryType:${this.doc_id}`).callsArgWith(2, null, [`${this.version}`]); - return this.RedisManager.getDocVersion(this.doc_id, this.callback); - }); - - return it("should return the document version and an undefined history type", function() { - return this.callback.calledWithExactly(null, this.version, undefined).should.equal(true); - }); - }); - - return describe("when the document has a project history type set", function() { - beforeEach(function() { - this.rclient.mget = sinon.stub().withArgs(`DocVersion:${this.doc_id}`, `ProjectHistoryType:${this.doc_id}`).callsArgWith(2, null, [`${this.version}`, 'project-history']); - return this.RedisManager.getDocVersion(this.doc_id, this.callback); - }); - - return it("should return the document version and history type", function() { - return this.callback.calledWithExactly(null, this.version, 'project-history').should.equal(true); - }); - }); - }); - }); -}); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const modulePath = '../../../../app/js/RedisManager.js' +const SandboxedModule = require('sandboxed-module') +const Errors = require('../../../../app/js/Errors') +const crypto = require('crypto') +const tk = require('timekeeper') + +describe('RedisManager', function () { + beforeEach(function () { + let Timer + this.multi = { exec: sinon.stub() } + this.rclient = { multi: () => this.multi } + tk.freeze(new Date()) + this.RedisManager = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': (this.logger = { + error: sinon.stub(), + log: sinon.stub(), + warn: sinon.stub() + }), + './ProjectHistoryRedisManager': (this.ProjectHistoryRedisManager = {}), + 'settings-sharelatex': (this.settings = { + documentupdater: { logHashErrors: { write: true, read: true } }, + apis: { + project_history: { enabled: true } + }, + redis: { + documentupdater: { + key_schema: { + blockingKey({ doc_id }) { + return `Blocking:${doc_id}` + }, + docLines({ doc_id }) { + return `doclines:${doc_id}` + }, + docOps({ doc_id }) { + return `DocOps:${doc_id}` + }, + docVersion({ doc_id }) { + return `DocVersion:${doc_id}` + }, + docHash({ doc_id }) { + return `DocHash:${doc_id}` + }, + projectKey({ doc_id }) { + return `ProjectId:${doc_id}` + }, + pendingUpdates({ doc_id }) { + return `PendingUpdates:${doc_id}` + }, + docsInProject({ project_id }) { + return `DocsIn:${project_id}` + }, + ranges({ doc_id }) { + return `Ranges:${doc_id}` + }, + pathname({ doc_id }) { + return `Pathname:${doc_id}` + }, + projectHistoryId({ doc_id }) { + return `ProjectHistoryId:${doc_id}` + }, + projectHistoryType({ doc_id }) { + return `ProjectHistoryType:${doc_id}` + }, + projectState({ project_id }) { + return `ProjectState:${project_id}` + }, + unflushedTime({ doc_id }) { + return `UnflushedTime:${doc_id}` + }, + lastUpdatedBy({ doc_id }) { + return `lastUpdatedBy:${doc_id}` + }, + lastUpdatedAt({ doc_id }) { + return `lastUpdatedAt:${doc_id}` + } + } + }, + history: { + key_schema: { + uncompressedHistoryOps({ doc_id }) { + return `UncompressedHistoryOps:${doc_id}` + }, + docsWithHistoryOps({ project_id }) { + return `DocsWithHistoryOps:${project_id}` + } + } + } + } + }), + 'redis-sharelatex': { + createClient: () => this.rclient + }, + './Metrics': (this.metrics = { + inc: sinon.stub(), + summary: sinon.stub(), + Timer: (Timer = class Timer { + constructor() { + this.start = new Date() + } + + done() { + const timeSpan = new Date() - this.start + return timeSpan + } + }) + }), + './Errors': Errors + }, + globals: { + JSON: (this.JSON = JSON) + } + }) + + this.doc_id = 'doc-id-123' + this.project_id = 'project-id-123' + this.projectHistoryId = 123 + return (this.callback = sinon.stub()) + }) + + afterEach(function () { + return tk.reset() + }) + + describe('getDoc', function () { + beforeEach(function () { + this.lines = ['one', 'two', 'three', 'これは'] // include some utf8 + this.jsonlines = JSON.stringify(this.lines) + this.version = 42 + this.hash = crypto + .createHash('sha1') + .update(this.jsonlines, 'utf8') + .digest('hex') + this.ranges = { comments: 'mock', entries: 'mock' } + this.json_ranges = JSON.stringify(this.ranges) + this.unflushed_time = 12345 + this.pathname = '/a/b/c.tex' + this.multi.get = sinon.stub() + this.multi.exec = sinon + .stub() + .callsArgWith(0, null, [ + this.jsonlines, + this.version, + this.hash, + this.project_id, + this.json_ranges, + this.pathname, + this.projectHistoryId.toString(), + this.unflushed_time + ]) + return (this.rclient.sadd = sinon.stub().yields(null, 0)) + }) + + describe('successfully', function () { + beforeEach(function () { + return this.RedisManager.getDoc( + this.project_id, + this.doc_id, + this.callback + ) + }) + + it('should get the lines from redis', function () { + return this.multi.get + .calledWith(`doclines:${this.doc_id}`) + .should.equal(true) + }) + + it('should get the version from', function () { + return this.multi.get + .calledWith(`DocVersion:${this.doc_id}`) + .should.equal(true) + }) + + it('should get the hash', function () { + return this.multi.get + .calledWith(`DocHash:${this.doc_id}`) + .should.equal(true) + }) + + it('should get the ranges', function () { + return this.multi.get + .calledWith(`Ranges:${this.doc_id}`) + .should.equal(true) + }) + + it('should get the unflushed time', function () { + return this.multi.get + .calledWith(`UnflushedTime:${this.doc_id}`) + .should.equal(true) + }) + + it('should get the pathname', function () { + return this.multi.get + .calledWith(`Pathname:${this.doc_id}`) + .should.equal(true) + }) + + it('should get the projectHistoryId as an integer', function () { + return this.multi.get + .calledWith(`ProjectHistoryId:${this.doc_id}`) + .should.equal(true) + }) + + it('should get lastUpdatedAt', function () { + return this.multi.get + .calledWith(`lastUpdatedAt:${this.doc_id}`) + .should.equal(true) + }) + + it('should get lastUpdatedBy', function () { + return this.multi.get + .calledWith(`lastUpdatedBy:${this.doc_id}`) + .should.equal(true) + }) + + it('should check if the document is in the DocsIn set', function () { + return this.rclient.sadd + .calledWith(`DocsIn:${this.project_id}`) + .should.equal(true) + }) + + it('should return the document', function () { + return this.callback + .calledWithExactly( + null, + this.lines, + this.version, + this.ranges, + this.pathname, + this.projectHistoryId, + this.unflushed_time, + this.lastUpdatedAt, + this.lastUpdatedBy + ) + .should.equal(true) + }) + + return it('should not log any errors', function () { + return this.logger.error.calledWith().should.equal(false) + }) + }) + + describe('when the document is not present', function () { + beforeEach(function () { + this.multi.exec = sinon + .stub() + .callsArgWith(0, null, [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ]) + this.rclient.sadd = sinon.stub().yields() + return this.RedisManager.getDoc( + this.project_id, + this.doc_id, + this.callback + ) + }) + + it('should not check if the document is in the DocsIn set', function () { + return this.rclient.sadd + .calledWith(`DocsIn:${this.project_id}`) + .should.equal(false) + }) + + it('should return an empty result', function () { + return this.callback + .calledWithExactly(null, null, 0, {}, null, null, null, null, null) + .should.equal(true) + }) + + return it('should not log any errors', function () { + return this.logger.error.calledWith().should.equal(false) + }) + }) + + describe('when the document is missing from the DocsIn set', function () { + beforeEach(function () { + this.rclient.sadd = sinon.stub().yields(null, 1) + return this.RedisManager.getDoc( + this.project_id, + this.doc_id, + this.callback + ) + }) + + it('should log an error', function () { + return this.logger.error.calledWith().should.equal(true) + }) + + return it('should return the document', function () { + return this.callback + .calledWithExactly( + null, + this.lines, + this.version, + this.ranges, + this.pathname, + this.projectHistoryId, + this.unflushed_time, + this.lastUpdatedAt, + this.lastUpdatedBy + ) + .should.equal(true) + }) + }) + + describe('with a corrupted document', function () { + beforeEach(function () { + this.badHash = 'INVALID-HASH-VALUE' + this.multi.exec = sinon + .stub() + .callsArgWith(0, null, [ + this.jsonlines, + this.version, + this.badHash, + this.project_id, + this.json_ranges + ]) + return this.RedisManager.getDoc( + this.project_id, + this.doc_id, + this.callback + ) + }) + + it('should log a hash error', function () { + return this.logger.error.calledWith().should.equal(true) + }) + + return it('should return the document', function () { + return this.callback + .calledWith(null, this.lines, this.version, this.ranges) + .should.equal(true) + }) + }) + + describe('with a slow request to redis', function () { + beforeEach(function () { + this.multi.exec = sinon + .stub() + .callsArgWith(0, null, [ + this.jsonlines, + this.version, + this.badHash, + this.project_id, + this.json_ranges, + this.pathname, + this.unflushed_time + ]) + this.clock = sinon.useFakeTimers() + this.multi.exec = (cb) => { + this.clock.tick(6000) + return cb(null, [ + this.jsonlines, + this.version, + this.another_project_id, + this.json_ranges, + this.pathname, + this.unflushed_time + ]) + } + + return this.RedisManager.getDoc( + this.project_id, + this.doc_id, + this.callback + ) + }) + + afterEach(function () { + return this.clock.restore() + }) + + return it('should return an error', function () { + return this.callback + .calledWith(new Error('redis getDoc exceeded timeout')) + .should.equal(true) + }) + }) + + return describe('getDoc with an invalid project id', function () { + beforeEach(function () { + this.another_project_id = 'project-id-456' + this.multi.exec = sinon + .stub() + .callsArgWith(0, null, [ + this.jsonlines, + this.version, + this.hash, + this.another_project_id, + this.json_ranges, + this.pathname, + this.unflushed_time + ]) + return this.RedisManager.getDoc( + this.project_id, + this.doc_id, + this.callback + ) + }) + + return it('should return an error', function () { + return this.callback + .calledWith(new Errors.NotFoundError('not found')) + .should.equal(true) + }) + }) + }) + + describe('getPreviousDocOpsTests', function () { + describe('with a start and an end value', function () { + beforeEach(function () { + this.first_version_in_redis = 30 + this.version = 70 + this.length = this.version - this.first_version_in_redis + this.start = 50 + this.end = 60 + this.ops = [{ mock: 'op-1' }, { mock: 'op-2' }] + this.jsonOps = this.ops.map((op) => JSON.stringify(op)) + this.rclient.llen = sinon.stub().callsArgWith(1, null, this.length) + this.rclient.get = sinon + .stub() + .callsArgWith(1, null, this.version.toString()) + this.rclient.lrange = sinon.stub().callsArgWith(3, null, this.jsonOps) + return this.RedisManager.getPreviousDocOps( + this.doc_id, + this.start, + this.end, + this.callback + ) + }) + + it('should get the length of the existing doc ops', function () { + return this.rclient.llen + .calledWith(`DocOps:${this.doc_id}`) + .should.equal(true) + }) + + it('should get the current version of the doc', function () { + return this.rclient.get + .calledWith(`DocVersion:${this.doc_id}`) + .should.equal(true) + }) + + it('should get the appropriate docs ops', function () { + return this.rclient.lrange + .calledWith( + `DocOps:${this.doc_id}`, + this.start - this.first_version_in_redis, + this.end - this.first_version_in_redis + ) + .should.equal(true) + }) + + return it('should return the docs with the doc ops deserialized', function () { + return this.callback.calledWith(null, this.ops).should.equal(true) + }) + }) + + describe('with an end value of -1', function () { + beforeEach(function () { + this.first_version_in_redis = 30 + this.version = 70 + this.length = this.version - this.first_version_in_redis + this.start = 50 + this.end = -1 + this.ops = [{ mock: 'op-1' }, { mock: 'op-2' }] + this.jsonOps = this.ops.map((op) => JSON.stringify(op)) + this.rclient.llen = sinon.stub().callsArgWith(1, null, this.length) + this.rclient.get = sinon + .stub() + .callsArgWith(1, null, this.version.toString()) + this.rclient.lrange = sinon.stub().callsArgWith(3, null, this.jsonOps) + return this.RedisManager.getPreviousDocOps( + this.doc_id, + this.start, + this.end, + this.callback + ) + }) + + it('should get the appropriate docs ops to the end of list', function () { + return this.rclient.lrange + .calledWith( + `DocOps:${this.doc_id}`, + this.start - this.first_version_in_redis, + -1 + ) + .should.equal(true) + }) + + return it('should return the docs with the doc ops deserialized', function () { + return this.callback.calledWith(null, this.ops).should.equal(true) + }) + }) + + describe('when the requested range is not in Redis', function () { + beforeEach(function () { + this.first_version_in_redis = 30 + this.version = 70 + this.length = this.version - this.first_version_in_redis + this.start = 20 + this.end = -1 + this.ops = [{ mock: 'op-1' }, { mock: 'op-2' }] + this.jsonOps = this.ops.map((op) => JSON.stringify(op)) + this.rclient.llen = sinon.stub().callsArgWith(1, null, this.length) + this.rclient.get = sinon + .stub() + .callsArgWith(1, null, this.version.toString()) + this.rclient.lrange = sinon.stub().callsArgWith(3, null, this.jsonOps) + return this.RedisManager.getPreviousDocOps( + this.doc_id, + this.start, + this.end, + this.callback + ) + }) + + it('should return an error', function () { + return this.callback + .calledWith( + new Errors.OpRangeNotAvailableError( + 'doc ops range is not loaded in redis' + ) + ) + .should.equal(true) + }) + + return it('should log out the problem', function () { + return this.logger.warn.called.should.equal(true) + }) + }) + + return describe('with a slow request to redis', function () { + beforeEach(function () { + this.first_version_in_redis = 30 + this.version = 70 + this.length = this.version - this.first_version_in_redis + this.start = 50 + this.end = 60 + this.ops = [{ mock: 'op-1' }, { mock: 'op-2' }] + this.jsonOps = this.ops.map((op) => JSON.stringify(op)) + this.rclient.llen = sinon.stub().callsArgWith(1, null, this.length) + this.rclient.get = sinon + .stub() + .callsArgWith(1, null, this.version.toString()) + this.clock = sinon.useFakeTimers() + this.rclient.lrange = (key, start, end, cb) => { + this.clock.tick(6000) + return cb(null, this.jsonOps) + } + return this.RedisManager.getPreviousDocOps( + this.doc_id, + this.start, + this.end, + this.callback + ) + }) + + afterEach(function () { + return this.clock.restore() + }) + + return it('should return an error', function () { + return this.callback + .calledWith(new Error('redis getPreviousDocOps exceeded timeout')) + .should.equal(true) + }) + }) + }) + + describe('updateDocument', function () { + beforeEach(function () { + this.lines = ['one', 'two', 'three', 'これは'] + this.ops = [{ op: [{ i: 'foo', p: 4 }] }, { op: [{ i: 'bar', p: 8 }] }] + this.version = 42 + this.hash = crypto + .createHash('sha1') + .update(JSON.stringify(this.lines), 'utf8') + .digest('hex') + this.ranges = { comments: 'mock', entries: 'mock' } + this.updateMeta = { user_id: 'last-author-fake-id' } + this.doc_update_list_length = sinon.stub() + this.project_update_list_length = sinon.stub() + + this.RedisManager.getDocVersion = sinon.stub() + this.multi.set = sinon.stub() + this.multi.rpush = sinon.stub() + this.multi.expire = sinon.stub() + this.multi.ltrim = sinon.stub() + this.multi.del = sinon.stub() + this.multi.exec = sinon + .stub() + .callsArgWith(0, null, [ + this.hash, + null, + null, + null, + null, + null, + null, + this.doc_update_list_length, + null, + null + ]) + return (this.ProjectHistoryRedisManager.queueOps = sinon + .stub() + .callsArgWith( + this.ops.length + 1, + null, + this.project_update_list_length + )) + }) + + describe('with a consistent version', function () { + beforeEach(function () {}) + + describe('with project history enabled', function () { + beforeEach(function () { + this.settings.apis.project_history.enabled = true + this.RedisManager.getDocVersion + .withArgs(this.doc_id) + .yields(null, this.version - this.ops.length) + return this.RedisManager.updateDocument( + this.project_id, + this.doc_id, + this.lines, + this.version, + this.ops, + this.ranges, + this.updateMeta, + this.callback + ) + }) + + it('should get the current doc version to check for consistency', function () { + return this.RedisManager.getDocVersion + .calledWith(this.doc_id) + .should.equal(true) + }) + + it('should set the doclines', function () { + return this.multi.set + .calledWith(`doclines:${this.doc_id}`, JSON.stringify(this.lines)) + .should.equal(true) + }) + + it('should set the version', function () { + return this.multi.set + .calledWith(`DocVersion:${this.doc_id}`, this.version) + .should.equal(true) + }) + + it('should set the hash', function () { + return this.multi.set + .calledWith(`DocHash:${this.doc_id}`, this.hash) + .should.equal(true) + }) + + it('should set the ranges', function () { + return this.multi.set + .calledWith(`Ranges:${this.doc_id}`, JSON.stringify(this.ranges)) + .should.equal(true) + }) + + it('should set the unflushed time', function () { + return this.multi.set + .calledWith(`UnflushedTime:${this.doc_id}`, Date.now(), 'NX') + .should.equal(true) + }) + + it('should set the last updated time', function () { + return this.multi.set + .calledWith(`lastUpdatedAt:${this.doc_id}`, Date.now()) + .should.equal(true) + }) + + it('should set the last updater', function () { + return this.multi.set + .calledWith(`lastUpdatedBy:${this.doc_id}`, 'last-author-fake-id') + .should.equal(true) + }) + + it('should push the doc op into the doc ops list', function () { + return this.multi.rpush + .calledWith( + `DocOps:${this.doc_id}`, + JSON.stringify(this.ops[0]), + JSON.stringify(this.ops[1]) + ) + .should.equal(true) + }) + + it('should renew the expiry ttl on the doc ops array', function () { + return this.multi.expire + .calledWith(`DocOps:${this.doc_id}`, this.RedisManager.DOC_OPS_TTL) + .should.equal(true) + }) + + it('should truncate the list to 100 members', function () { + return this.multi.ltrim + .calledWith( + `DocOps:${this.doc_id}`, + -this.RedisManager.DOC_OPS_MAX_LENGTH, + -1 + ) + .should.equal(true) + }) + + it('should push the updates into the history ops list', function () { + return this.multi.rpush + .calledWith( + `UncompressedHistoryOps:${this.doc_id}`, + JSON.stringify(this.ops[0]), + JSON.stringify(this.ops[1]) + ) + .should.equal(true) + }) + + it('should push the updates into the project history ops list', function () { + return this.ProjectHistoryRedisManager.queueOps + .calledWith(this.project_id, JSON.stringify(this.ops[0])) + .should.equal(true) + }) + + it('should call the callback', function () { + return this.callback + .calledWith( + null, + this.doc_update_list_length, + this.project_update_list_length + ) + .should.equal(true) + }) + + return it('should not log any errors', function () { + return this.logger.error.calledWith().should.equal(false) + }) + }) + + describe('with project history disabled', function () { + beforeEach(function () { + this.settings.apis.project_history.enabled = false + this.RedisManager.getDocVersion + .withArgs(this.doc_id) + .yields(null, this.version - this.ops.length) + return this.RedisManager.updateDocument( + this.project_id, + this.doc_id, + this.lines, + this.version, + this.ops, + this.ranges, + this.updateMeta, + this.callback + ) + }) + + it('should not push the updates into the project history ops list', function () { + return this.ProjectHistoryRedisManager.queueOps.called.should.equal( + false + ) + }) + + return it('should call the callback', function () { + return this.callback + .calledWith(null, this.doc_update_list_length) + .should.equal(true) + }) + }) + + return describe('with a doc using project history only', function () { + beforeEach(function () { + this.RedisManager.getDocVersion + .withArgs(this.doc_id) + .yields(null, this.version - this.ops.length, 'project-history') + return this.RedisManager.updateDocument( + this.project_id, + this.doc_id, + this.lines, + this.version, + this.ops, + this.ranges, + this.updateMeta, + this.callback + ) + }) + + it('should not push the updates to the track-changes ops list', function () { + return this.multi.rpush + .calledWith(`UncompressedHistoryOps:${this.doc_id}`) + .should.equal(false) + }) + + it('should push the updates into the project history ops list', function () { + return this.ProjectHistoryRedisManager.queueOps + .calledWith(this.project_id, JSON.stringify(this.ops[0])) + .should.equal(true) + }) + + return it('should call the callback with the project update count only', function () { + return this.callback + .calledWith(null, undefined, this.project_update_list_length) + .should.equal(true) + }) + }) + }) + + describe('with an inconsistent version', function () { + beforeEach(function () { + this.RedisManager.getDocVersion + .withArgs(this.doc_id) + .yields(null, this.version - this.ops.length - 1) + return this.RedisManager.updateDocument( + this.project_id, + this.doc_id, + this.lines, + this.version, + this.ops, + this.ranges, + this.updateMeta, + this.callback + ) + }) + + it('should not call multi.exec', function () { + return this.multi.exec.called.should.equal(false) + }) + + return it('should call the callback with an error', function () { + return this.callback + .calledWith( + new Error(`Version mismatch. '${this.doc_id}' is corrupted.`) + ) + .should.equal(true) + }) + }) + + describe('with no updates', function () { + beforeEach(function () { + this.RedisManager.getDocVersion + .withArgs(this.doc_id) + .yields(null, this.version) + return this.RedisManager.updateDocument( + this.project_id, + this.doc_id, + this.lines, + this.version, + [], + this.ranges, + this.updateMeta, + this.callback + ) + }) + + it('should not try to enqueue doc updates', function () { + return this.multi.rpush.called.should.equal(false) + }) + + it('should not try to enqueue project updates', function () { + return this.ProjectHistoryRedisManager.queueOps.called.should.equal( + false + ) + }) + + return it('should still set the doclines', function () { + return this.multi.set + .calledWith(`doclines:${this.doc_id}`, JSON.stringify(this.lines)) + .should.equal(true) + }) + }) + + describe('with empty ranges', function () { + beforeEach(function () { + this.RedisManager.getDocVersion + .withArgs(this.doc_id) + .yields(null, this.version - this.ops.length) + return this.RedisManager.updateDocument( + this.project_id, + this.doc_id, + this.lines, + this.version, + this.ops, + {}, + this.updateMeta, + this.callback + ) + }) + + it('should not set the ranges', function () { + return this.multi.set + .calledWith(`Ranges:${this.doc_id}`, JSON.stringify(this.ranges)) + .should.equal(false) + }) + + return it('should delete the ranges key', function () { + return this.multi.del + .calledWith(`Ranges:${this.doc_id}`) + .should.equal(true) + }) + }) + + describe('with null bytes in the serialized doc lines', function () { + beforeEach(function () { + this.RedisManager.getDocVersion + .withArgs(this.doc_id) + .yields(null, this.version - this.ops.length) + this._stringify = JSON.stringify + this.JSON.stringify = () => '["bad bytes! \u0000 <- here"]' + return this.RedisManager.updateDocument( + this.project_id, + this.doc_id, + this.lines, + this.version, + this.ops, + this.ranges, + this.updateMeta, + this.callback + ) + }) + + afterEach(function () { + return (this.JSON.stringify = this._stringify) + }) + + it('should log an error', function () { + return this.logger.error.called.should.equal(true) + }) + + return it('should call the callback with an error', function () { + return this.callback + .calledWith(new Error('null bytes found in doc lines')) + .should.equal(true) + }) + }) + + describe('with ranges that are too big', function () { + beforeEach(function () { + this.RedisManager.getDocVersion + .withArgs(this.doc_id) + .yields(null, this.version - this.ops.length) + this.RedisManager._serializeRanges = sinon + .stub() + .yields(new Error('ranges are too large')) + return this.RedisManager.updateDocument( + this.project_id, + this.doc_id, + this.lines, + this.version, + this.ops, + this.ranges, + this.updateMeta, + this.callback + ) + }) + + it('should log an error', function () { + return this.logger.error.called.should.equal(true) + }) + + return it('should call the callback with the error', function () { + return this.callback + .calledWith(new Error('ranges are too large')) + .should.equal(true) + }) + }) + + return describe('without user id from meta', function () { + beforeEach(function () { + this.RedisManager.getDocVersion + .withArgs(this.doc_id) + .yields(null, this.version - this.ops.length) + return this.RedisManager.updateDocument( + this.project_id, + this.doc_id, + this.lines, + this.version, + this.ops, + this.ranges, + {}, + this.callback + ) + }) + + it('should set the last updater to null', function () { + return this.multi.del + .calledWith(`lastUpdatedBy:${this.doc_id}`) + .should.equal(true) + }) + + return it('should still set the last updated time', function () { + return this.multi.set + .calledWith(`lastUpdatedAt:${this.doc_id}`, Date.now()) + .should.equal(true) + }) + }) + }) + + describe('putDocInMemory', function () { + beforeEach(function () { + this.multi.set = sinon.stub() + this.rclient.sadd = sinon.stub().yields() + this.multi.del = sinon.stub() + this.lines = ['one', 'two', 'three', 'これは'] + this.version = 42 + this.hash = crypto + .createHash('sha1') + .update(JSON.stringify(this.lines), 'utf8') + .digest('hex') + this.multi.exec = sinon.stub().callsArgWith(0, null, [this.hash]) + this.ranges = { comments: 'mock', entries: 'mock' } + return (this.pathname = '/a/b/c.tex') + }) + + describe('with non-empty ranges', function () { + beforeEach(function (done) { + return this.RedisManager.putDocInMemory( + this.project_id, + this.doc_id, + this.lines, + this.version, + this.ranges, + this.pathname, + this.projectHistoryId, + done + ) + }) + + it('should set the lines', function () { + return this.multi.set + .calledWith(`doclines:${this.doc_id}`, JSON.stringify(this.lines)) + .should.equal(true) + }) + + it('should set the version', function () { + return this.multi.set + .calledWith(`DocVersion:${this.doc_id}`, this.version) + .should.equal(true) + }) + + it('should set the hash', function () { + return this.multi.set + .calledWith(`DocHash:${this.doc_id}`, this.hash) + .should.equal(true) + }) + + it('should set the ranges', function () { + return this.multi.set + .calledWith(`Ranges:${this.doc_id}`, JSON.stringify(this.ranges)) + .should.equal(true) + }) + + it('should set the project_id for the doc', function () { + return this.multi.set + .calledWith(`ProjectId:${this.doc_id}`, this.project_id) + .should.equal(true) + }) + + it('should set the pathname for the doc', function () { + return this.multi.set + .calledWith(`Pathname:${this.doc_id}`, this.pathname) + .should.equal(true) + }) + + it('should set the projectHistoryId for the doc', function () { + return this.multi.set + .calledWith(`ProjectHistoryId:${this.doc_id}`, this.projectHistoryId) + .should.equal(true) + }) + + it('should add the doc_id to the project set', function () { + return this.rclient.sadd + .calledWith(`DocsIn:${this.project_id}`, this.doc_id) + .should.equal(true) + }) + + return it('should not log any errors', function () { + return this.logger.error.calledWith().should.equal(false) + }) + }) + + describe('with empty ranges', function () { + beforeEach(function (done) { + return this.RedisManager.putDocInMemory( + this.project_id, + this.doc_id, + this.lines, + this.version, + {}, + this.pathname, + this.projectHistoryId, + done + ) + }) + + it('should delete the ranges key', function () { + return this.multi.del + .calledWith(`Ranges:${this.doc_id}`) + .should.equal(true) + }) + + return it('should not set the ranges', function () { + return this.multi.set + .calledWith(`Ranges:${this.doc_id}`, JSON.stringify(this.ranges)) + .should.equal(false) + }) + }) + + describe('with null bytes in the serialized doc lines', function () { + beforeEach(function () { + this._stringify = JSON.stringify + this.JSON.stringify = () => '["bad bytes! \u0000 <- here"]' + return this.RedisManager.putDocInMemory( + this.project_id, + this.doc_id, + this.lines, + this.version, + this.ranges, + this.pathname, + this.projectHistoryId, + this.callback + ) + }) + + afterEach(function () { + return (this.JSON.stringify = this._stringify) + }) + + it('should log an error', function () { + return this.logger.error.called.should.equal(true) + }) + + return it('should call the callback with an error', function () { + return this.callback + .calledWith(new Error('null bytes found in doc lines')) + .should.equal(true) + }) + }) + + return describe('with ranges that are too big', function () { + beforeEach(function () { + this.RedisManager._serializeRanges = sinon + .stub() + .yields(new Error('ranges are too large')) + return this.RedisManager.putDocInMemory( + this.project_id, + this.doc_id, + this.lines, + this.version, + this.ranges, + this.pathname, + this.projectHistoryId, + this.callback + ) + }) + + it('should log an error', function () { + return this.logger.error.called.should.equal(true) + }) + + return it('should call the callback with the error', function () { + return this.callback + .calledWith(new Error('ranges are too large')) + .should.equal(true) + }) + }) + }) + + describe('removeDocFromMemory', function () { + beforeEach(function (done) { + this.multi.strlen = sinon.stub() + this.multi.del = sinon.stub() + this.multi.srem = sinon.stub() + this.multi.exec.yields() + return this.RedisManager.removeDocFromMemory( + this.project_id, + this.doc_id, + done + ) + }) + + it('should check the length of the current doclines', function () { + return this.multi.strlen + .calledWith(`doclines:${this.doc_id}`) + .should.equal(true) + }) + + it('should delete the lines', function () { + return this.multi.del + .calledWith(`doclines:${this.doc_id}`) + .should.equal(true) + }) + + it('should delete the version', function () { + return this.multi.del + .calledWith(`DocVersion:${this.doc_id}`) + .should.equal(true) + }) + + it('should delete the hash', function () { + return this.multi.del + .calledWith(`DocHash:${this.doc_id}`) + .should.equal(true) + }) + + it('should delete the unflushed time', function () { + return this.multi.del + .calledWith(`UnflushedTime:${this.doc_id}`) + .should.equal(true) + }) + + it('should delete the project_id for the doc', function () { + return this.multi.del + .calledWith(`ProjectId:${this.doc_id}`) + .should.equal(true) + }) + + it('should remove the doc_id from the project set', function () { + return this.multi.srem + .calledWith(`DocsIn:${this.project_id}`, this.doc_id) + .should.equal(true) + }) + + it('should delete the pathname for the doc', function () { + return this.multi.del + .calledWith(`Pathname:${this.doc_id}`) + .should.equal(true) + }) + + it('should delete the pathname for the doc', function () { + return this.multi.del + .calledWith(`ProjectHistoryId:${this.doc_id}`) + .should.equal(true) + }) + + it('should delete lastUpdatedAt', function () { + return this.multi.del + .calledWith(`lastUpdatedAt:${this.doc_id}`) + .should.equal(true) + }) + + return it('should delete lastUpdatedBy', function () { + return this.multi.del + .calledWith(`lastUpdatedBy:${this.doc_id}`) + .should.equal(true) + }) + }) + + describe('clearProjectState', function () { + beforeEach(function (done) { + this.rclient.del = sinon.stub().callsArg(1) + return this.RedisManager.clearProjectState(this.project_id, done) + }) + + return it('should delete the project state', function () { + return this.rclient.del + .calledWith(`ProjectState:${this.project_id}`) + .should.equal(true) + }) + }) + + return describe('renameDoc', function () { + beforeEach(function () { + this.rclient.rpush = sinon.stub().yields() + this.rclient.set = sinon.stub().yields() + return (this.update = { + id: this.doc_id, + pathname: (this.pathname = 'pathname'), + newPathname: (this.newPathname = 'new-pathname') + }) + }) + + describe('the document is cached in redis', function () { + beforeEach(function () { + this.RedisManager.getDoc = sinon + .stub() + .callsArgWith(2, null, 'lines', 'version') + this.ProjectHistoryRedisManager.queueRenameEntity = sinon + .stub() + .yields() + return this.RedisManager.renameDoc( + this.project_id, + this.doc_id, + this.userId, + this.update, + this.projectHistoryId, + this.callback + ) + }) + + it('update the cached pathname', function () { + return this.rclient.set + .calledWith(`Pathname:${this.doc_id}`, this.newPathname) + .should.equal(true) + }) + + return it('should queue an update', function () { + return this.ProjectHistoryRedisManager.queueRenameEntity + .calledWithExactly( + this.project_id, + this.projectHistoryId, + 'doc', + this.doc_id, + this.userId, + this.update, + this.callback + ) + .should.equal(true) + }) + }) + + describe('the document is not cached in redis', function () { + beforeEach(function () { + this.RedisManager.getDoc = sinon + .stub() + .callsArgWith(2, null, null, null) + this.ProjectHistoryRedisManager.queueRenameEntity = sinon + .stub() + .yields() + return this.RedisManager.renameDoc( + this.project_id, + this.doc_id, + this.userId, + this.update, + this.projectHistoryId, + this.callback + ) + }) + + it('does not update the cached pathname', function () { + return this.rclient.set.called.should.equal(false) + }) + + return it('should queue an update', function () { + return this.ProjectHistoryRedisManager.queueRenameEntity + .calledWithExactly( + this.project_id, + this.projectHistoryId, + 'doc', + this.doc_id, + this.userId, + this.update, + this.callback + ) + .should.equal(true) + }) + }) + + return describe('getDocVersion', function () { + beforeEach(function () { + return (this.version = 12345) + }) + + describe('when the document does not have a project history type set', function () { + beforeEach(function () { + this.rclient.mget = sinon + .stub() + .withArgs( + `DocVersion:${this.doc_id}`, + `ProjectHistoryType:${this.doc_id}` + ) + .callsArgWith(2, null, [`${this.version}`]) + return this.RedisManager.getDocVersion(this.doc_id, this.callback) + }) + + return it('should return the document version and an undefined history type', function () { + return this.callback + .calledWithExactly(null, this.version, undefined) + .should.equal(true) + }) + }) + + return describe('when the document has a project history type set', function () { + beforeEach(function () { + this.rclient.mget = sinon + .stub() + .withArgs( + `DocVersion:${this.doc_id}`, + `ProjectHistoryType:${this.doc_id}` + ) + .callsArgWith(2, null, [`${this.version}`, 'project-history']) + return this.RedisManager.getDocVersion(this.doc_id, this.callback) + }) + + return it('should return the document version and history type', function () { + return this.callback + .calledWithExactly(null, this.version, 'project-history') + .should.equal(true) + }) + }) + }) + }) +}) diff --git a/services/document-updater/test/unit/js/ShareJS/TextTransformTests.js b/services/document-updater/test/unit/js/ShareJS/TextTransformTests.js index 4989b77034..8ea99aee5c 100644 --- a/services/document-updater/test/unit/js/ShareJS/TextTransformTests.js +++ b/services/document-updater/test/unit/js/ShareJS/TextTransformTests.js @@ -13,353 +13,427 @@ * DS205: Consider reworking code to avoid use of IIFEs * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const text = require("../../../../app/js/sharejs/types/text"); -require("chai").should(); -const RangesTracker = require("../../../../app/js/RangesTracker"); +const text = require('../../../../app/js/sharejs/types/text') +require('chai').should() +const RangesTracker = require('../../../../app/js/RangesTracker') -describe("ShareJS text type", function() { - beforeEach(function() { - return this.t = "mock-thread-id"; - }); - - describe("transform", function() { - describe("insert / insert", function() { - it("with an insert before", function() { - const dest = []; - text._tc(dest, { i: "foo", p: 9 }, { i: "bar", p: 3 }); - return dest.should.deep.equal([{ i: "foo", p: 12 }]); - }); +describe('ShareJS text type', function () { + beforeEach(function () { + return (this.t = 'mock-thread-id') + }) - it("with an insert after", function() { - const dest = []; - text._tc(dest, { i: "foo", p: 3 }, { i: "bar", p: 9 }); - return dest.should.deep.equal([{ i: "foo", p: 3 }]); - }); + describe('transform', function () { + describe('insert / insert', function () { + it('with an insert before', function () { + const dest = [] + text._tc(dest, { i: 'foo', p: 9 }, { i: 'bar', p: 3 }) + return dest.should.deep.equal([{ i: 'foo', p: 12 }]) + }) - it("with an insert at the same place with side == 'right'", function() { - const dest = []; - text._tc(dest, { i: "foo", p: 3 }, { i: "bar", p: 3 }, 'right'); - return dest.should.deep.equal([{ i: "foo", p: 6 }]); - }); + it('with an insert after', function () { + const dest = [] + text._tc(dest, { i: 'foo', p: 3 }, { i: 'bar', p: 9 }) + return dest.should.deep.equal([{ i: 'foo', p: 3 }]) + }) - return it("with an insert at the same place with side == 'left'", function() { - const dest = []; - text._tc(dest, { i: "foo", p: 3 }, { i: "bar", p: 3 }, 'left'); - return dest.should.deep.equal([{ i: "foo", p: 3 }]); - }); - }); + it("with an insert at the same place with side == 'right'", function () { + const dest = [] + text._tc(dest, { i: 'foo', p: 3 }, { i: 'bar', p: 3 }, 'right') + return dest.should.deep.equal([{ i: 'foo', p: 6 }]) + }) - describe("insert / delete", function() { - it("with a delete before", function() { - const dest = []; - text._tc(dest, { i: "foo", p: 9 }, { d: "bar", p: 3 }); - return dest.should.deep.equal([{ i: "foo", p: 6 }]); - }); + return it("with an insert at the same place with side == 'left'", function () { + const dest = [] + text._tc(dest, { i: 'foo', p: 3 }, { i: 'bar', p: 3 }, 'left') + return dest.should.deep.equal([{ i: 'foo', p: 3 }]) + }) + }) - it("with a delete after", function() { - const dest = []; - text._tc(dest, { i: "foo", p: 3 }, { d: "bar", p: 9 }); - return dest.should.deep.equal([{ i: "foo", p: 3 }]); - }); + describe('insert / delete', function () { + it('with a delete before', function () { + const dest = [] + text._tc(dest, { i: 'foo', p: 9 }, { d: 'bar', p: 3 }) + return dest.should.deep.equal([{ i: 'foo', p: 6 }]) + }) - it("with a delete at the same place with side == 'right'", function() { - const dest = []; - text._tc(dest, { i: "foo", p: 3 }, { d: "bar", p: 3 }, 'right'); - return dest.should.deep.equal([{ i: "foo", p: 3 }]); - }); + it('with a delete after', function () { + const dest = [] + text._tc(dest, { i: 'foo', p: 3 }, { d: 'bar', p: 9 }) + return dest.should.deep.equal([{ i: 'foo', p: 3 }]) + }) - return it("with a delete at the same place with side == 'left'", function() { - const dest = []; - - text._tc(dest, { i: "foo", p: 3 }, { d: "bar", p: 3 }, 'left'); - return dest.should.deep.equal([{ i: "foo", p: 3 }]); - }); - }); + it("with a delete at the same place with side == 'right'", function () { + const dest = [] + text._tc(dest, { i: 'foo', p: 3 }, { d: 'bar', p: 3 }, 'right') + return dest.should.deep.equal([{ i: 'foo', p: 3 }]) + }) - describe("delete / insert", function() { - it("with an insert before", function() { - const dest = []; - text._tc(dest, { d: "foo", p: 9 }, { i: "bar", p: 3 }); - return dest.should.deep.equal([{ d: "foo", p: 12 }]); - }); + return it("with a delete at the same place with side == 'left'", function () { + const dest = [] - it("with an insert after", function() { - const dest = []; - text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 9 }); - return dest.should.deep.equal([{ d: "foo", p: 3 }]); - }); + text._tc(dest, { i: 'foo', p: 3 }, { d: 'bar', p: 3 }, 'left') + return dest.should.deep.equal([{ i: 'foo', p: 3 }]) + }) + }) - it("with an insert at the same place with side == 'right'", function() { - const dest = []; - text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 3 }, 'right'); - return dest.should.deep.equal([{ d: "foo", p: 6 }]); - }); + describe('delete / insert', function () { + it('with an insert before', function () { + const dest = [] + text._tc(dest, { d: 'foo', p: 9 }, { i: 'bar', p: 3 }) + return dest.should.deep.equal([{ d: 'foo', p: 12 }]) + }) - it("with an insert at the same place with side == 'left'", function() { - const dest = []; - text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 3 }, 'left'); - return dest.should.deep.equal([{ d: "foo", p: 6 }]); - }); - - return it("with a delete that overlaps the insert location", function() { - const dest = []; - text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 4 }); - return dest.should.deep.equal([{ d: "f", p: 3 }, { d: "oo", p: 6 }]); - }); - }); - + it('with an insert after', function () { + const dest = [] + text._tc(dest, { d: 'foo', p: 3 }, { i: 'bar', p: 9 }) + return dest.should.deep.equal([{ d: 'foo', p: 3 }]) + }) - describe("delete / delete", function() { - it("with a delete before", function() { - const dest = []; - text._tc(dest, { d: "foo", p: 9 }, { d: "bar", p: 3 }); - return dest.should.deep.equal([{ d: "foo", p: 6 }]); - }); + it("with an insert at the same place with side == 'right'", function () { + const dest = [] + text._tc(dest, { d: 'foo', p: 3 }, { i: 'bar', p: 3 }, 'right') + return dest.should.deep.equal([{ d: 'foo', p: 6 }]) + }) - it("with a delete after", function() { - const dest = []; - text._tc(dest, { d: "foo", p: 3 }, { d: "bar", p: 9 }); - return dest.should.deep.equal([{ d: "foo", p: 3 }]); - }); + it("with an insert at the same place with side == 'left'", function () { + const dest = [] + text._tc(dest, { d: 'foo', p: 3 }, { i: 'bar', p: 3 }, 'left') + return dest.should.deep.equal([{ d: 'foo', p: 6 }]) + }) - it("with deleting the same content", function() { - const dest = []; - text._tc(dest, { d: "foo", p: 3 }, { d: "foo", p: 3 }, 'right'); - return dest.should.deep.equal([]); - }); + return it('with a delete that overlaps the insert location', function () { + const dest = [] + text._tc(dest, { d: 'foo', p: 3 }, { i: 'bar', p: 4 }) + return dest.should.deep.equal([ + { d: 'f', p: 3 }, + { d: 'oo', p: 6 } + ]) + }) + }) - it("with the delete overlapping before", function() { - const dest = []; - text._tc(dest, { d: "foobar", p: 3 }, { d: "abcfoo", p: 0 }, 'right'); - return dest.should.deep.equal([{ d: "bar", p: 0 }]); - }); + describe('delete / delete', function () { + it('with a delete before', function () { + const dest = [] + text._tc(dest, { d: 'foo', p: 9 }, { d: 'bar', p: 3 }) + return dest.should.deep.equal([{ d: 'foo', p: 6 }]) + }) - it("with the delete overlapping after", function() { - const dest = []; - text._tc(dest, { d: "abcfoo", p: 3 }, { d: "foobar", p: 6 }); - return dest.should.deep.equal([{ d: "abc", p: 3 }]); - }); + it('with a delete after', function () { + const dest = [] + text._tc(dest, { d: 'foo', p: 3 }, { d: 'bar', p: 9 }) + return dest.should.deep.equal([{ d: 'foo', p: 3 }]) + }) - it("with the delete overlapping the whole delete", function() { - const dest = []; - text._tc(dest, { d: "abcfoo123", p: 3 }, { d: "foo", p: 6 }); - return dest.should.deep.equal([{ d: "abc123", p: 3 }]); - }); + it('with deleting the same content', function () { + const dest = [] + text._tc(dest, { d: 'foo', p: 3 }, { d: 'foo', p: 3 }, 'right') + return dest.should.deep.equal([]) + }) - return it("with the delete inside the whole delete", function() { - const dest = []; - text._tc(dest, { d: "foo", p: 6 }, { d: "abcfoo123", p: 3 }); - return dest.should.deep.equal([]); - }); - }); - - describe("comment / insert", function() { - it("with an insert before", function() { - const dest = []; - text._tc(dest, { c: "foo", p: 9, t: this.t }, { i: "bar", p: 3 }); - return dest.should.deep.equal([{ c: "foo", p: 12, t: this.t }]); - }); + it('with the delete overlapping before', function () { + const dest = [] + text._tc(dest, { d: 'foobar', p: 3 }, { d: 'abcfoo', p: 0 }, 'right') + return dest.should.deep.equal([{ d: 'bar', p: 0 }]) + }) - it("with an insert after", function() { - const dest = []; - text._tc(dest, { c: "foo", p: 3, t: this.t }, { i: "bar", p: 9 }); - return dest.should.deep.equal([{ c: "foo", p: 3, t: this.t }]); - }); + it('with the delete overlapping after', function () { + const dest = [] + text._tc(dest, { d: 'abcfoo', p: 3 }, { d: 'foobar', p: 6 }) + return dest.should.deep.equal([{ d: 'abc', p: 3 }]) + }) - it("with an insert at the left edge", function() { - const dest = []; - text._tc(dest, { c: "foo", p: 3, t: this.t }, { i: "bar", p: 3 }); - // RangesTracker doesn't inject inserts into comments on edges, so neither should we - return dest.should.deep.equal([{ c: "foo", p: 6, t: this.t }]); - }); + it('with the delete overlapping the whole delete', function () { + const dest = [] + text._tc(dest, { d: 'abcfoo123', p: 3 }, { d: 'foo', p: 6 }) + return dest.should.deep.equal([{ d: 'abc123', p: 3 }]) + }) - it("with an insert at the right edge", function() { - const dest = []; - text._tc(dest, { c: "foo", p: 3, t: this.t }, { i: "bar", p: 6 }); - // RangesTracker doesn't inject inserts into comments on edges, so neither should we - return dest.should.deep.equal([{ c: "foo", p: 3, t: this.t }]); - }); + return it('with the delete inside the whole delete', function () { + const dest = [] + text._tc(dest, { d: 'foo', p: 6 }, { d: 'abcfoo123', p: 3 }) + return dest.should.deep.equal([]) + }) + }) - return it("with an insert in the middle", function() { - const dest = []; - text._tc(dest, { c: "foo", p: 3, t: this.t }, { i: "bar", p: 5 }); - return dest.should.deep.equal([{ c: "fobaro", p: 3, t: this.t }]); - }); - }); - - describe("comment / delete", function() { - it("with a delete before", function() { - const dest = []; - text._tc(dest, { c: "foo", p: 9, t: this.t }, { d: "bar", p: 3 }); - return dest.should.deep.equal([{ c: "foo", p: 6, t: this.t }]); - }); + describe('comment / insert', function () { + it('with an insert before', function () { + const dest = [] + text._tc(dest, { c: 'foo', p: 9, t: this.t }, { i: 'bar', p: 3 }) + return dest.should.deep.equal([{ c: 'foo', p: 12, t: this.t }]) + }) - it("with a delete after", function() { - const dest = []; - text._tc(dest, { c: "foo", p: 3, t: this.t }, { i: "bar", p: 9 }); - return dest.should.deep.equal([{ c: "foo", p: 3, t: this.t }]); - }); + it('with an insert after', function () { + const dest = [] + text._tc(dest, { c: 'foo', p: 3, t: this.t }, { i: 'bar', p: 9 }) + return dest.should.deep.equal([{ c: 'foo', p: 3, t: this.t }]) + }) - it("with a delete overlapping the comment content before", function() { - const dest = []; - text._tc(dest, { c: "foobar", p: 6, t: this.t }, { d: "123foo", p: 3 }); - return dest.should.deep.equal([{ c: "bar", p: 3, t: this.t }]); - }); + it('with an insert at the left edge', function () { + const dest = [] + text._tc(dest, { c: 'foo', p: 3, t: this.t }, { i: 'bar', p: 3 }) + // RangesTracker doesn't inject inserts into comments on edges, so neither should we + return dest.should.deep.equal([{ c: 'foo', p: 6, t: this.t }]) + }) - it("with a delete overlapping the comment content after", function() { - const dest = []; - text._tc(dest, { c: "foobar", p: 6, t: this.t }, { d: "bar123", p: 9 }); - return dest.should.deep.equal([{ c: "foo", p: 6, t: this.t }]); - }); + it('with an insert at the right edge', function () { + const dest = [] + text._tc(dest, { c: 'foo', p: 3, t: this.t }, { i: 'bar', p: 6 }) + // RangesTracker doesn't inject inserts into comments on edges, so neither should we + return dest.should.deep.equal([{ c: 'foo', p: 3, t: this.t }]) + }) - it("with a delete overlapping the comment content in the middle", function() { - const dest = []; - text._tc(dest, { c: "foo123bar", p: 6, t: this.t }, { d: "123", p: 9 }); - return dest.should.deep.equal([{ c: "foobar", p: 6, t: this.t }]); - }); + return it('with an insert in the middle', function () { + const dest = [] + text._tc(dest, { c: 'foo', p: 3, t: this.t }, { i: 'bar', p: 5 }) + return dest.should.deep.equal([{ c: 'fobaro', p: 3, t: this.t }]) + }) + }) - return it("with a delete overlapping the whole comment", function() { - const dest = []; - text._tc(dest, { c: "foo", p: 6, t: this.t }, { d: "123foo456", p: 3 }); - return dest.should.deep.equal([{ c: "", p: 3, t: this.t }]); - }); - }); - - describe("comment / insert", function() { return it("should not do anything", function() { - const dest = []; - text._tc(dest, { i: "foo", p: 6 }, { c: "bar", p: 3 }); - return dest.should.deep.equal([{ i: "foo", p: 6 }]); - }); }); - - describe("comment / delete", function() { return it("should not do anything", function() { - const dest = []; - text._tc(dest, { d: "foo", p: 6 }, { c: "bar", p: 3 }); - return dest.should.deep.equal([{ d: "foo", p: 6 }]); - }); }); - - return describe("comment / comment", function() { return it("should not do anything", function() { - const dest = []; - text._tc(dest, { c: "foo", p: 6 }, { c: "bar", p: 3 }); - return dest.should.deep.equal([{ c: "foo", p: 6 }]); - }); }); -}); + describe('comment / delete', function () { + it('with a delete before', function () { + const dest = [] + text._tc(dest, { c: 'foo', p: 9, t: this.t }, { d: 'bar', p: 3 }) + return dest.should.deep.equal([{ c: 'foo', p: 6, t: this.t }]) + }) - describe("apply", function() { - it("should apply an insert", function() { return text.apply("foo", [{ i: "bar", p: 2 }]).should.equal("fobaro"); }); + it('with a delete after', function () { + const dest = [] + text._tc(dest, { c: 'foo', p: 3, t: this.t }, { i: 'bar', p: 9 }) + return dest.should.deep.equal([{ c: 'foo', p: 3, t: this.t }]) + }) - it("should apply a delete", function() { return text.apply("foo123bar", [{ d: "123", p: 3 }]).should.equal("foobar"); }); + it('with a delete overlapping the comment content before', function () { + const dest = [] + text._tc(dest, { c: 'foobar', p: 6, t: this.t }, { d: '123foo', p: 3 }) + return dest.should.deep.equal([{ c: 'bar', p: 3, t: this.t }]) + }) - it("should do nothing with a comment", function() { return text.apply("foo123bar", [{ c: "123", p: 3 }]).should.equal("foo123bar"); }); - - it("should throw an error when deleted content does not match", function() { return ((() => text.apply("foo123bar", [{ d: "456", p: 3 }]))).should.throw(Error); }); - - return it("should throw an error when comment content does not match", function() { return ((() => text.apply("foo123bar", [{ c: "456", p: 3 }]))).should.throw(Error); }); - }); - - return describe("applying ops and comments in different orders", function() { return it("should not matter which op or comment is applied first", function() { - let length, p; - let asc, end; - let asc1, end1; - let asc3, end3; - const transform = function(op1, op2, side) { - const d = []; - text._tc(d, op1, op2, side); - return d; - }; - - const applySnapshot = (snapshot, op) => text.apply(snapshot, op); - - const applyRanges = function(rangesTracker, ops) { - for (const op of Array.from(ops)) { - rangesTracker.applyOp(op, {}); - } - return rangesTracker; - }; - - const commentsEqual = function(comments1, comments2) { - if (comments1.length !== comments2.length) { return false; } - comments1.sort((a,b) => { - if ((a.offset - b.offset) === 0) { - return a.length - b.length; + it('with a delete overlapping the comment content after', function () { + const dest = [] + text._tc(dest, { c: 'foobar', p: 6, t: this.t }, { d: 'bar123', p: 9 }) + return dest.should.deep.equal([{ c: 'foo', p: 6, t: this.t }]) + }) + + it('with a delete overlapping the comment content in the middle', function () { + const dest = [] + text._tc(dest, { c: 'foo123bar', p: 6, t: this.t }, { d: '123', p: 9 }) + return dest.should.deep.equal([{ c: 'foobar', p: 6, t: this.t }]) + }) + + return it('with a delete overlapping the whole comment', function () { + const dest = [] + text._tc(dest, { c: 'foo', p: 6, t: this.t }, { d: '123foo456', p: 3 }) + return dest.should.deep.equal([{ c: '', p: 3, t: this.t }]) + }) + }) + + describe('comment / insert', function () { + return it('should not do anything', function () { + const dest = [] + text._tc(dest, { i: 'foo', p: 6 }, { c: 'bar', p: 3 }) + return dest.should.deep.equal([{ i: 'foo', p: 6 }]) + }) + }) + + describe('comment / delete', function () { + return it('should not do anything', function () { + const dest = [] + text._tc(dest, { d: 'foo', p: 6 }, { c: 'bar', p: 3 }) + return dest.should.deep.equal([{ d: 'foo', p: 6 }]) + }) + }) + + return describe('comment / comment', function () { + return it('should not do anything', function () { + const dest = [] + text._tc(dest, { c: 'foo', p: 6 }, { c: 'bar', p: 3 }) + return dest.should.deep.equal([{ c: 'foo', p: 6 }]) + }) + }) + }) + + describe('apply', function () { + it('should apply an insert', function () { + return text.apply('foo', [{ i: 'bar', p: 2 }]).should.equal('fobaro') + }) + + it('should apply a delete', function () { + return text + .apply('foo123bar', [{ d: '123', p: 3 }]) + .should.equal('foobar') + }) + + it('should do nothing with a comment', function () { + return text + .apply('foo123bar', [{ c: '123', p: 3 }]) + .should.equal('foo123bar') + }) + + it('should throw an error when deleted content does not match', function () { + return (() => text.apply('foo123bar', [{ d: '456', p: 3 }])).should.throw( + Error + ) + }) + + return it('should throw an error when comment content does not match', function () { + return (() => text.apply('foo123bar', [{ c: '456', p: 3 }])).should.throw( + Error + ) + }) + }) + + return describe('applying ops and comments in different orders', function () { + return it('should not matter which op or comment is applied first', function () { + let length, p + let asc, end + let asc1, end1 + let asc3, end3 + const transform = function (op1, op2, side) { + const d = [] + text._tc(d, op1, op2, side) + return d + } + + const applySnapshot = (snapshot, op) => text.apply(snapshot, op) + + const applyRanges = function (rangesTracker, ops) { + for (const op of Array.from(ops)) { + rangesTracker.applyOp(op, {}) + } + return rangesTracker + } + + const commentsEqual = function (comments1, comments2) { + if (comments1.length !== comments2.length) { + return false + } + comments1.sort((a, b) => { + if (a.offset - b.offset === 0) { + return a.length - b.length + } else { + return a.offset - b.offset + } + }) + comments2.sort((a, b) => { + if (a.offset - b.offset === 0) { + return a.length - b.length + } else { + return a.offset - b.offset + } + }) + for (let i = 0; i < comments1.length; i++) { + const comment1 = comments1[i] + const comment2 = comments2[i] + if ( + comment1.offset !== comment2.offset || + comment1.length !== comment2.length + ) { + return false + } + } + return true + } + + const SNAPSHOT = '123' + + const OPS = [] + // Insert ops + for ( + p = 0, end = SNAPSHOT.length, asc = end >= 0; + asc ? p <= end : p >= end; + asc ? p++ : p-- + ) { + OPS.push({ i: 'a', p }) + OPS.push({ i: 'bc', p }) + } + for ( + p = 0, end1 = SNAPSHOT.length - 1, asc1 = end1 >= 0; + asc1 ? p <= end1 : p >= end1; + asc1 ? p++ : p-- + ) { + var asc2, end2 + for ( + length = 1, end2 = SNAPSHOT.length - p, asc2 = end2 >= 1; + asc2 ? length <= end2 : length >= end2; + asc2 ? length++ : length-- + ) { + OPS.push({ d: SNAPSHOT.slice(p, p + length), p }) + } + } + for ( + p = 0, end3 = SNAPSHOT.length - 1, asc3 = end3 >= 0; + asc3 ? p <= end3 : p >= end3; + asc3 ? p++ : p-- + ) { + var asc4, end4 + for ( + length = 1, end4 = SNAPSHOT.length - p, asc4 = end4 >= 1; + asc4 ? length <= end4 : length >= end4; + asc4 ? length++ : length-- + ) { + OPS.push({ c: SNAPSHOT.slice(p, p + length), p, t: this.t }) + } + } + + return (() => { + const result = [] + for (var op1 of Array.from(OPS)) { + result.push( + (() => { + const result1 = [] + for (const op2 of Array.from(OPS)) { + const op1_t = transform(op1, op2, 'left') + const op2_t = transform(op2, op1, 'right') + + const rt12 = new RangesTracker() + const snapshot12 = applySnapshot( + applySnapshot(SNAPSHOT, [op1]), + op2_t + ) + applyRanges(rt12, [op1]) + applyRanges(rt12, op2_t) + + const rt21 = new RangesTracker() + const snapshot21 = applySnapshot( + applySnapshot(SNAPSHOT, [op2]), + op1_t + ) + applyRanges(rt21, [op2]) + applyRanges(rt21, op1_t) + + if (snapshot12 !== snapshot21) { + console.error( + { op1, op2, op1_t, op2_t, snapshot12, snapshot21 }, + 'Ops are not consistent' + ) + throw new Error('OT is inconsistent') + } + + if (!commentsEqual(rt12.comments, rt21.comments)) { + console.log(rt12.comments) + console.log(rt21.comments) + console.error( + { + op1, + op2, + op1_t, + op2_t, + rt12_comments: rt12.comments, + rt21_comments: rt21.comments + }, + 'Comments are not consistent' + ) + throw new Error('OT is inconsistent') } else { - return a.offset - b.offset; + result1.push(undefined) } - }); - comments2.sort((a,b) => { - if ((a.offset - b.offset) === 0) { - return a.length - b.length; - } else { - return a.offset - b.offset; - } - }); - for (let i = 0; i < comments1.length; i++) { - const comment1 = comments1[i]; - const comment2 = comments2[i]; - if ((comment1.offset !== comment2.offset) || (comment1.length !== comment2.length)) { - return false; - } - } - return true; - }; - - const SNAPSHOT = "123"; - - const OPS = []; - // Insert ops - for (p = 0, end = SNAPSHOT.length, asc = end >= 0; asc ? p <= end : p >= end; asc ? p++ : p--) { - OPS.push({i: "a", p}); - OPS.push({i: "bc", p}); + } + return result1 + })() + ) } - for (p = 0, end1 = SNAPSHOT.length-1, asc1 = end1 >= 0; asc1 ? p <= end1 : p >= end1; asc1 ? p++ : p--) { - var asc2, end2; - for (length = 1, end2 = SNAPSHOT.length - p, asc2 = end2 >= 1; asc2 ? length <= end2 : length >= end2; asc2 ? length++ : length--) { - OPS.push({d: SNAPSHOT.slice(p, p+length), p}); - } - } - for (p = 0, end3 = SNAPSHOT.length-1, asc3 = end3 >= 0; asc3 ? p <= end3 : p >= end3; asc3 ? p++ : p--) { - var asc4, end4; - for (length = 1, end4 = SNAPSHOT.length - p, asc4 = end4 >= 1; asc4 ? length <= end4 : length >= end4; asc4 ? length++ : length--) { - OPS.push({c: SNAPSHOT.slice(p, p+length), p, t: this.t}); - } - } - - return (() => { - const result = []; - for (var op1 of Array.from(OPS)) { - result.push((() => { - const result1 = []; - for (const op2 of Array.from(OPS)) { - const op1_t = transform(op1, op2, "left"); - const op2_t = transform(op2, op1, "right"); - - const rt12 = new RangesTracker(); - const snapshot12 = applySnapshot(applySnapshot(SNAPSHOT, [op1]), op2_t); - applyRanges(rt12, [op1]); - applyRanges(rt12, op2_t); - - const rt21 = new RangesTracker(); - const snapshot21 = applySnapshot(applySnapshot(SNAPSHOT, [op2]), op1_t); - applyRanges(rt21, [op2]); - applyRanges(rt21, op1_t); - - if (snapshot12 !== snapshot21) { - console.error({op1, op2, op1_t, op2_t, snapshot12, snapshot21}, "Ops are not consistent"); - throw new Error("OT is inconsistent"); - } - - if (!commentsEqual(rt12.comments, rt21.comments)) { - console.log(rt12.comments); - console.log(rt21.comments); - console.error({op1, op2, op1_t, op2_t, rt12_comments: rt12.comments, rt21_comments: rt21.comments}, "Comments are not consistent"); - throw new Error("OT is inconsistent"); - } else { - result1.push(undefined); - } - } - return result1; - })()); - } - return result; - })(); - }); }); -}); + return result + })() + }) + }) +}) diff --git a/services/document-updater/test/unit/js/ShareJsDB/ShareJsDBTests.js b/services/document-updater/test/unit/js/ShareJsDB/ShareJsDBTests.js index 165e8fcdf0..7f7d377c1d 100644 --- a/services/document-updater/test/unit/js/ShareJsDB/ShareJsDBTests.js +++ b/services/document-updater/test/unit/js/ShareJsDB/ShareJsDBTests.js @@ -10,126 +10,142 @@ * 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/ShareJsDB.js"; -const SandboxedModule = require('sandboxed-module'); -const Errors = require("../../../../app/js/Errors"); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/js/ShareJsDB.js' +const SandboxedModule = require('sandboxed-module') +const Errors = require('../../../../app/js/Errors') -describe("ShareJsDB", function() { - beforeEach(function() { - this.doc_id = "document-id"; - this.project_id = "project-id"; - this.doc_key = `${this.project_id}:${this.doc_id}`; - this.callback = sinon.stub(); - this.ShareJsDB = SandboxedModule.require(modulePath, { requires: { - "./RedisManager": (this.RedisManager = {}) - } - }); +describe('ShareJsDB', function () { + beforeEach(function () { + this.doc_id = 'document-id' + this.project_id = 'project-id' + this.doc_key = `${this.project_id}:${this.doc_id}` + this.callback = sinon.stub() + this.ShareJsDB = SandboxedModule.require(modulePath, { + requires: { + './RedisManager': (this.RedisManager = {}) + } + }) - this.version = 42; - this.lines = ["one", "two", "three"]; - return this.db = new this.ShareJsDB(this.project_id, this.doc_id, this.lines, this.version); - }); + this.version = 42 + this.lines = ['one', 'two', 'three'] + return (this.db = new this.ShareJsDB( + this.project_id, + this.doc_id, + this.lines, + this.version + )) + }) - describe("getSnapshot", function() { - describe("successfully", function() { - beforeEach(function() { - return this.db.getSnapshot(this.doc_key, this.callback); - }); + describe('getSnapshot', function () { + describe('successfully', function () { + beforeEach(function () { + return this.db.getSnapshot(this.doc_key, this.callback) + }) - it("should return the doc lines", function() { - return this.callback.args[0][1].snapshot.should.equal(this.lines.join("\n")); - }); + it('should return the doc lines', function () { + return this.callback.args[0][1].snapshot.should.equal( + this.lines.join('\n') + ) + }) - it("should return the doc version", function() { - return this.callback.args[0][1].v.should.equal(this.version); - }); + it('should return the doc version', function () { + return this.callback.args[0][1].v.should.equal(this.version) + }) - return it("should return the type as text", function() { - return this.callback.args[0][1].type.should.equal("text"); - }); - }); + return it('should return the type as text', function () { + return this.callback.args[0][1].type.should.equal('text') + }) + }) - return describe("when the key does not match", function() { - beforeEach(function() { - return this.db.getSnapshot("bad:key", this.callback); - }); + return describe('when the key does not match', function () { + beforeEach(function () { + return this.db.getSnapshot('bad:key', this.callback) + }) - return it("should return the callback with a NotFoundError", function() { - return this.callback.calledWith(new Errors.NotFoundError("not found")).should.equal(true); - }); - }); - }); + return it('should return the callback with a NotFoundError', function () { + return this.callback + .calledWith(new Errors.NotFoundError('not found')) + .should.equal(true) + }) + }) + }) - describe("getOps", function() { - describe("with start == end", function() { - beforeEach(function() { - this.start = (this.end = 42); - return this.db.getOps(this.doc_key, this.start, this.end, this.callback); - }); + describe('getOps', function () { + describe('with start == end', function () { + beforeEach(function () { + this.start = this.end = 42 + return this.db.getOps(this.doc_key, this.start, this.end, this.callback) + }) - return it("should return an empty array", function() { - return this.callback.calledWith(null, []).should.equal(true); - }); - }); - - describe("with a non empty range", function() { - beforeEach(function() { - this.start = 35; - this.end = 42; - this.RedisManager.getPreviousDocOps = sinon.stub().callsArgWith(3, null, this.ops); - return this.db.getOps(this.doc_key, this.start, this.end, this.callback); - }); + return it('should return an empty array', function () { + return this.callback.calledWith(null, []).should.equal(true) + }) + }) - it("should get the range from redis", function() { - return this.RedisManager.getPreviousDocOps - .calledWith(this.doc_id, this.start, this.end-1) - .should.equal(true); - }); + describe('with a non empty range', function () { + beforeEach(function () { + this.start = 35 + this.end = 42 + this.RedisManager.getPreviousDocOps = sinon + .stub() + .callsArgWith(3, null, this.ops) + return this.db.getOps(this.doc_key, this.start, this.end, this.callback) + }) - return it("should return the ops", function() { - return this.callback.calledWith(null, this.ops).should.equal(true); - }); - }); + it('should get the range from redis', function () { + return this.RedisManager.getPreviousDocOps + .calledWith(this.doc_id, this.start, this.end - 1) + .should.equal(true) + }) - return describe("with no specified end", function() { - beforeEach(function() { - this.start = 35; - this.end = null; - this.RedisManager.getPreviousDocOps = sinon.stub().callsArgWith(3, null, this.ops); - return this.db.getOps(this.doc_key, this.start, this.end, this.callback); - }); - - return it("should get until the end of the list", function() { - return this.RedisManager.getPreviousDocOps - .calledWith(this.doc_id, this.start, -1) - .should.equal(true); - }); - }); - }); + return it('should return the ops', function () { + return this.callback.calledWith(null, this.ops).should.equal(true) + }) + }) - return describe("writeOps", function() { return describe("writing an op", function() { - beforeEach(function() { - this.opData = { - op: {p: 20, t: "foo"}, - meta: {source: "bar"}, - v: this.version - }; - return this.db.writeOp(this.doc_key, this.opData, this.callback); - }); + return describe('with no specified end', function () { + beforeEach(function () { + this.start = 35 + this.end = null + this.RedisManager.getPreviousDocOps = sinon + .stub() + .callsArgWith(3, null, this.ops) + return this.db.getOps(this.doc_key, this.start, this.end, this.callback) + }) - it("should write into appliedOps", function() { - return expect(this.db.appliedOps[this.doc_key]).to.deep.equal([this.opData]); - }); + return it('should get until the end of the list', function () { + return this.RedisManager.getPreviousDocOps + .calledWith(this.doc_id, this.start, -1) + .should.equal(true) + }) + }) + }) - return it("should call the callback without an error", function() { - this.callback.called.should.equal(true); - return (this.callback.args[0][0] != null).should.equal(false); - }); - }); }); -}); + return describe('writeOps', function () { + return describe('writing an op', function () { + beforeEach(function () { + this.opData = { + op: { p: 20, t: 'foo' }, + meta: { source: 'bar' }, + v: this.version + } + return this.db.writeOp(this.doc_key, this.opData, this.callback) + }) + + it('should write into appliedOps', function () { + return expect(this.db.appliedOps[this.doc_key]).to.deep.equal([ + this.opData + ]) + }) + + return it('should call the callback without an error', function () { + this.callback.called.should.equal(true) + return (this.callback.args[0][0] != null).should.equal(false) + }) + }) + }) +}) diff --git a/services/document-updater/test/unit/js/ShareJsUpdateManager/ShareJsUpdateManagerTests.js b/services/document-updater/test/unit/js/ShareJsUpdateManager/ShareJsUpdateManagerTests.js index c6a3fbac33..56afe4c584 100644 --- a/services/document-updater/test/unit/js/ShareJsUpdateManager/ShareJsUpdateManagerTests.js +++ b/services/document-updater/test/unit/js/ShareJsUpdateManager/ShareJsUpdateManagerTests.js @@ -9,179 +9,230 @@ * 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 modulePath = "../../../../app/js/ShareJsUpdateManager.js"; -const SandboxedModule = require('sandboxed-module'); -const crypto = require('crypto'); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const modulePath = '../../../../app/js/ShareJsUpdateManager.js' +const SandboxedModule = require('sandboxed-module') +const crypto = require('crypto') -describe("ShareJsUpdateManager", function() { - beforeEach(function() { - let Model; - this.project_id = "project-id-123"; - this.doc_id = "document-id-123"; - this.callback = sinon.stub(); - return this.ShareJsUpdateManager = SandboxedModule.require(modulePath, { - requires: { - "./sharejs/server/model": - (Model = class Model { - constructor(db) { - this.db = db; - } - }), - "./ShareJsDB" : (this.ShareJsDB = { mockDB: true }), - "redis-sharelatex" : { createClient: () => { return this.rclient = {auth() {}}; } - }, - "logger-sharelatex": (this.logger = { log: sinon.stub() }), - "./RealTimeRedisManager": (this.RealTimeRedisManager = {}), - "./Metrics": (this.metrics = { inc: sinon.stub() }) - }, - globals: { - clearTimeout: (this.clearTimeout = sinon.stub()) - } - } - ); - }); +describe('ShareJsUpdateManager', function () { + beforeEach(function () { + let Model + this.project_id = 'project-id-123' + this.doc_id = 'document-id-123' + this.callback = sinon.stub() + return (this.ShareJsUpdateManager = SandboxedModule.require(modulePath, { + requires: { + './sharejs/server/model': (Model = class Model { + constructor(db) { + this.db = db + } + }), + './ShareJsDB': (this.ShareJsDB = { mockDB: true }), + 'redis-sharelatex': { + createClient: () => { + return (this.rclient = { auth() {} }) + } + }, + 'logger-sharelatex': (this.logger = { log: sinon.stub() }), + './RealTimeRedisManager': (this.RealTimeRedisManager = {}), + './Metrics': (this.metrics = { inc: sinon.stub() }) + }, + globals: { + clearTimeout: (this.clearTimeout = sinon.stub()) + } + })) + }) - describe("applyUpdate", function() { - beforeEach(function() { - this.lines = ["one", "two"]; - this.version = 34; - this.updatedDocLines = ["onefoo", "two"]; - const content = this.updatedDocLines.join("\n"); - this.hash = crypto.createHash('sha1').update("blob " + content.length + "\x00").update(content, 'utf8').digest('hex'); - this.update = {p: 4, t: "foo", v:this.version, hash:this.hash}; - this.model = { - applyOp: sinon.stub().callsArg(2), - getSnapshot: sinon.stub(), - db: { - appliedOps: {} - } - }; - this.ShareJsUpdateManager.getNewShareJsModel = sinon.stub().returns(this.model); - this.ShareJsUpdateManager._listenForOps = sinon.stub(); - return this.ShareJsUpdateManager.removeDocFromCache = sinon.stub().callsArg(1); - }); + describe('applyUpdate', function () { + beforeEach(function () { + this.lines = ['one', 'two'] + this.version = 34 + this.updatedDocLines = ['onefoo', 'two'] + const content = this.updatedDocLines.join('\n') + this.hash = crypto + .createHash('sha1') + .update('blob ' + content.length + '\x00') + .update(content, 'utf8') + .digest('hex') + this.update = { p: 4, t: 'foo', v: this.version, hash: this.hash } + this.model = { + applyOp: sinon.stub().callsArg(2), + getSnapshot: sinon.stub(), + db: { + appliedOps: {} + } + } + this.ShareJsUpdateManager.getNewShareJsModel = sinon + .stub() + .returns(this.model) + this.ShareJsUpdateManager._listenForOps = sinon.stub() + return (this.ShareJsUpdateManager.removeDocFromCache = sinon + .stub() + .callsArg(1)) + }) - describe("successfully", function() { - beforeEach(function(done) { - this.model.getSnapshot.callsArgWith(1, null, {snapshot: this.updatedDocLines.join("\n"), v: this.version}); - this.model.db.appliedOps[`${this.project_id}:${this.doc_id}`] = (this.appliedOps = ["mock-ops"]); - return this.ShareJsUpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.lines, this.version, (err, docLines, version, appliedOps) => { - this.callback(err, docLines, version, appliedOps); - return done(); - }); - }); + describe('successfully', function () { + beforeEach(function (done) { + this.model.getSnapshot.callsArgWith(1, null, { + snapshot: this.updatedDocLines.join('\n'), + v: this.version + }) + this.model.db.appliedOps[ + `${this.project_id}:${this.doc_id}` + ] = this.appliedOps = ['mock-ops'] + return this.ShareJsUpdateManager.applyUpdate( + this.project_id, + this.doc_id, + this.update, + this.lines, + this.version, + (err, docLines, version, appliedOps) => { + this.callback(err, docLines, version, appliedOps) + return done() + } + ) + }) - it("should create a new ShareJs model", function() { - return this.ShareJsUpdateManager.getNewShareJsModel - .calledWith(this.project_id, this.doc_id, this.lines, this.version) - .should.equal(true); - }); + it('should create a new ShareJs model', function () { + return this.ShareJsUpdateManager.getNewShareJsModel + .calledWith(this.project_id, this.doc_id, this.lines, this.version) + .should.equal(true) + }) - it("should listen for ops on the model", function() { - return this.ShareJsUpdateManager._listenForOps - .calledWith(this.model) - .should.equal(true); - }); + it('should listen for ops on the model', function () { + return this.ShareJsUpdateManager._listenForOps + .calledWith(this.model) + .should.equal(true) + }) - it("should send the update to ShareJs", function() { - return this.model.applyOp - .calledWith(`${this.project_id}:${this.doc_id}`, this.update) - .should.equal(true); - }); + it('should send the update to ShareJs', function () { + return this.model.applyOp + .calledWith(`${this.project_id}:${this.doc_id}`, this.update) + .should.equal(true) + }) - it("should get the updated doc lines", function() { - return this.model.getSnapshot - .calledWith(`${this.project_id}:${this.doc_id}`) - .should.equal(true); - }); + it('should get the updated doc lines', function () { + return this.model.getSnapshot + .calledWith(`${this.project_id}:${this.doc_id}`) + .should.equal(true) + }) - return it("should return the updated doc lines, version and ops", function() { - return this.callback.calledWith(null, this.updatedDocLines, this.version, this.appliedOps).should.equal(true); - }); - }); + return it('should return the updated doc lines, version and ops', function () { + return this.callback + .calledWith(null, this.updatedDocLines, this.version, this.appliedOps) + .should.equal(true) + }) + }) - describe("when applyOp fails", function() { - beforeEach(function(done) { - this.error = new Error("Something went wrong"); - this.model.applyOp = sinon.stub().callsArgWith(2, this.error); - return this.ShareJsUpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.lines, this.version, (err, docLines, version) => { - this.callback(err, docLines, version); - return done(); - }); - }); + describe('when applyOp fails', function () { + beforeEach(function (done) { + this.error = new Error('Something went wrong') + this.model.applyOp = sinon.stub().callsArgWith(2, this.error) + return this.ShareJsUpdateManager.applyUpdate( + this.project_id, + this.doc_id, + this.update, + this.lines, + this.version, + (err, docLines, version) => { + this.callback(err, docLines, version) + return done() + } + ) + }) - 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) + }) + }) - describe("when getSnapshot fails", function() { - beforeEach(function(done) { - this.error = new Error("Something went wrong"); - this.model.getSnapshot.callsArgWith(1, this.error); - return this.ShareJsUpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.lines, this.version, (err, docLines, version) => { - this.callback(err, docLines, version); - return done(); - }); - }); + describe('when getSnapshot fails', function () { + beforeEach(function (done) { + this.error = new Error('Something went wrong') + this.model.getSnapshot.callsArgWith(1, this.error) + return this.ShareJsUpdateManager.applyUpdate( + this.project_id, + this.doc_id, + this.update, + this.lines, + this.version, + (err, docLines, version) => { + this.callback(err, docLines, version) + return done() + } + ) + }) - 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("with an invalid hash", function() { - beforeEach(function(done) { - this.error = new Error("invalid hash"); - this.model.getSnapshot.callsArgWith(1, null, {snapshot: "unexpected content", v: this.version}); - this.model.db.appliedOps[`${this.project_id}:${this.doc_id}`] = (this.appliedOps = ["mock-ops"]); - return this.ShareJsUpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.lines, this.version, (err, docLines, version, appliedOps) => { - this.callback(err, docLines, version, appliedOps); - return done(); - }); - }); + return describe('with an invalid hash', function () { + beforeEach(function (done) { + this.error = new Error('invalid hash') + this.model.getSnapshot.callsArgWith(1, null, { + snapshot: 'unexpected content', + v: this.version + }) + this.model.db.appliedOps[ + `${this.project_id}:${this.doc_id}` + ] = this.appliedOps = ['mock-ops'] + return this.ShareJsUpdateManager.applyUpdate( + this.project_id, + this.doc_id, + this.update, + this.lines, + this.version, + (err, docLines, version, appliedOps) => { + this.callback(err, docLines, version, appliedOps) + return done() + } + ) + }) - 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("_listenForOps", function() { - beforeEach(function() { - this.model = { on: (event, callback) => { - return this.callback = callback; - } - }; - sinon.spy(this.model, "on"); - return this.ShareJsUpdateManager._listenForOps(this.model); - }); + return describe('_listenForOps', function () { + beforeEach(function () { + this.model = { + on: (event, callback) => { + return (this.callback = callback) + } + } + sinon.spy(this.model, 'on') + return this.ShareJsUpdateManager._listenForOps(this.model) + }) - it("should listen to the model for updates", function() { - return this.model.on.calledWith("applyOp") - .should.equal(true); - }); + it('should listen to the model for updates', function () { + return this.model.on.calledWith('applyOp').should.equal(true) + }) - return describe("the callback", function() { - beforeEach(function() { - this.opData = { - op: {t: "foo", p: 1}, - meta: { source: "bar" - } - }; - this.RealTimeRedisManager.sendData = sinon.stub(); - return this.callback(`${this.project_id}:${this.doc_id}`, this.opData); - }); - - return it("should publish the op to redis", function() { - return this.RealTimeRedisManager.sendData - .calledWith({project_id: this.project_id, doc_id: this.doc_id, op: this.opData}) - .should.equal(true); - }); - }); - }); -}); + return describe('the callback', function () { + beforeEach(function () { + this.opData = { + op: { t: 'foo', p: 1 }, + meta: { source: 'bar' } + } + this.RealTimeRedisManager.sendData = sinon.stub() + return this.callback(`${this.project_id}:${this.doc_id}`, this.opData) + }) + return it('should publish the op to redis', function () { + return this.RealTimeRedisManager.sendData + .calledWith({ + project_id: this.project_id, + doc_id: this.doc_id, + op: this.opData + }) + .should.equal(true) + }) + }) + }) +}) diff --git a/services/document-updater/test/unit/js/UpdateManager/UpdateManagerTests.js b/services/document-updater/test/unit/js/UpdateManager/UpdateManagerTests.js index c5f89ab81c..4e39089490 100644 --- a/services/document-updater/test/unit/js/UpdateManager/UpdateManagerTests.js +++ b/services/document-updater/test/unit/js/UpdateManager/UpdateManagerTests.js @@ -11,477 +11,682 @@ * DS206: Consider reworking classes to avoid initClass * 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 modulePath = "../../../../app/js/UpdateManager.js"; -const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const modulePath = '../../../../app/js/UpdateManager.js' +const SandboxedModule = require('sandboxed-module') -describe("UpdateManager", function() { - beforeEach(function() { - let Profiler, Timer; - this.project_id = "project-id-123"; - this.projectHistoryId = "history-id-123"; - this.doc_id = "document-id-123"; - this.callback = sinon.stub(); - return this.UpdateManager = SandboxedModule.require(modulePath, { requires: { - "./LockManager" : (this.LockManager = {}), - "./RedisManager" : (this.RedisManager = {}), - "./RealTimeRedisManager" : (this.RealTimeRedisManager = {}), - "./ShareJsUpdateManager" : (this.ShareJsUpdateManager = {}), - "./HistoryManager" : (this.HistoryManager = {}), - "logger-sharelatex": (this.logger = { log: sinon.stub() }), - "./Metrics": (this.Metrics = { - Timer: (Timer = (function() { - Timer = class Timer { - static initClass() { - this.prototype.done = sinon.stub(); - } - }; - Timer.initClass(); - return Timer; - })()) - }), - "settings-sharelatex": (this.Settings = {}), - "./DocumentManager": (this.DocumentManager = {}), - "./RangesManager": (this.RangesManager = {}), - "./SnapshotManager": (this.SnapshotManager = {}), - "./Profiler": (Profiler = (function() { - Profiler = class Profiler { - static initClass() { - this.prototype.log = sinon.stub().returns({ end: sinon.stub() }); - this.prototype.end = sinon.stub(); - } - }; - Profiler.initClass(); - return Profiler; - })()) - } - } - ); - }); - - describe("processOutstandingUpdates", function() { - beforeEach(function() { - this.UpdateManager.fetchAndApplyUpdates = sinon.stub().callsArg(2); - return this.UpdateManager.processOutstandingUpdates(this.project_id, this.doc_id, this.callback); - }); - - it("should apply the updates", function() { - return this.UpdateManager.fetchAndApplyUpdates.calledWith(this.project_id, this.doc_id).should.equal(true); - }); - - it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - - return it("should time the execution", function() { - return this.Metrics.Timer.prototype.done.called.should.equal(true); - }); - }); - - describe("processOutstandingUpdatesWithLock", function() { - describe("when the lock is free", function() { - beforeEach(function() { - this.LockManager.tryLock = sinon.stub().callsArgWith(1, null, true, (this.lockValue = "mock-lock-value")); - this.LockManager.releaseLock = sinon.stub().callsArg(2); - this.UpdateManager.continueProcessingUpdatesWithLock = sinon.stub().callsArg(2); - return this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2); - }); - - describe("successfully", function() { - beforeEach(function() { - return this.UpdateManager.processOutstandingUpdatesWithLock(this.project_id, this.doc_id, this.callback); - }); - - it("should acquire the lock", function() { - return this.LockManager.tryLock.calledWith(this.doc_id).should.equal(true); - }); - - it("should free the lock", function() { - return this.LockManager.releaseLock.calledWith(this.doc_id, this.lockValue).should.equal(true); - }); - - it("should process the outstanding updates", function() { - return this.UpdateManager.processOutstandingUpdates.calledWith(this.project_id, this.doc_id).should.equal(true); - }); - - it("should do everything with the lock acquired", function() { - this.UpdateManager.processOutstandingUpdates.calledAfter(this.LockManager.tryLock).should.equal(true); - return this.UpdateManager.processOutstandingUpdates.calledBefore(this.LockManager.releaseLock).should.equal(true); - }); - - it("should continue processing new updates that may have come in", function() { - return this.UpdateManager.continueProcessingUpdatesWithLock.calledWith(this.project_id, this.doc_id).should.equal(true); - }); - - return it("should return the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - - return describe("when processOutstandingUpdates returns an error", function() { - beforeEach(function() { - this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArgWith(2, (this.error = new Error("Something went wrong"))); - return this.UpdateManager.processOutstandingUpdatesWithLock(this.project_id, this.doc_id, this.callback); - }); - - it("should free the lock", function() { - return this.LockManager.releaseLock.calledWith(this.doc_id, this.lockValue).should.equal(true); - }); - - return it("should return the error in the callback", function() { - return this.callback.calledWith(this.error).should.equal(true); - }); - }); - }); - - return describe("when the lock is taken", function() { - beforeEach(function() { - this.LockManager.tryLock = sinon.stub().callsArgWith(1, null, false); - this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2); - return this.UpdateManager.processOutstandingUpdatesWithLock(this.project_id, this.doc_id, this.callback); - }); - - it("should return the callback", function() { - return this.callback.called.should.equal(true); - }); - - return it("should not process the updates", function() { - return this.UpdateManager.processOutstandingUpdates.called.should.equal(false); - }); - }); - }); - - describe("continueProcessingUpdatesWithLock", function() { - describe("when there are outstanding updates", function() { - beforeEach(function() { - this.RealTimeRedisManager.getUpdatesLength = sinon.stub().callsArgWith(1, null, 3); - this.UpdateManager.processOutstandingUpdatesWithLock = sinon.stub().callsArg(2); - return this.UpdateManager.continueProcessingUpdatesWithLock(this.project_id, this.doc_id, this.callback); - }); - - it("should process the outstanding updates", function() { - return this.UpdateManager.processOutstandingUpdatesWithLock.calledWith(this.project_id, this.doc_id).should.equal(true); - }); - - return it("should return the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - - return describe("when there are no outstanding updates", function() { - beforeEach(function() { - this.RealTimeRedisManager.getUpdatesLength = sinon.stub().callsArgWith(1, null, 0); - this.UpdateManager.processOutstandingUpdatesWithLock = sinon.stub().callsArg(2); - return this.UpdateManager.continueProcessingUpdatesWithLock(this.project_id, this.doc_id, this.callback); - }); - - it("should not try to process the outstanding updates", function() { - return this.UpdateManager.processOutstandingUpdatesWithLock.called.should.equal(false); - }); - - return it("should return the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - }); - - describe("fetchAndApplyUpdates", function() { - describe("with updates", function() { - beforeEach(function() { - this.updates = [{p: 1, t: "foo"}]; - this.updatedDocLines = ["updated", "lines"]; - this.version = 34; - this.RealTimeRedisManager.getPendingUpdatesForDoc = sinon.stub().callsArgWith(1, null, this.updates); - this.UpdateManager.applyUpdate = sinon.stub().callsArgWith(3, null, this.updatedDocLines, this.version); - return this.UpdateManager.fetchAndApplyUpdates(this.project_id, this.doc_id, this.callback); - }); - - it("should get the pending updates", function() { - return this.RealTimeRedisManager.getPendingUpdatesForDoc.calledWith(this.doc_id).should.equal(true); - }); - - it("should apply the updates", function() { - return Array.from(this.updates).map((update) => - this.UpdateManager.applyUpdate - .calledWith(this.project_id, this.doc_id, update) - .should.equal(true)); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - - return describe("when there are no updates", function() { - beforeEach(function() { - this.updates = []; - this.RealTimeRedisManager.getPendingUpdatesForDoc = sinon.stub().callsArgWith(1, null, this.updates); - this.UpdateManager.applyUpdate = sinon.stub(); - this.RedisManager.setDocument = sinon.stub(); - return this.UpdateManager.fetchAndApplyUpdates(this.project_id, this.doc_id, this.callback); - }); - - it("should not call applyUpdate", function() { - return this.UpdateManager.applyUpdate.called.should.equal(false); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - }); - - describe("applyUpdate", function() { - beforeEach(function() { - this.updateMeta = { user_id: 'last-author-fake-id' }; - this.update = {op: [{p: 42, i: "foo"}], meta: this.updateMeta}; - this.updatedDocLines = ["updated", "lines"]; - this.version = 34; - this.lines = ["original", "lines"]; - this.ranges = { entries: "mock", comments: "mock" }; - this.updated_ranges = { entries: "updated", comments: "updated" }; - this.appliedOps = [ {v: 42, op: "mock-op-42"}, { v: 45, op: "mock-op-45" }]; - this.doc_ops_length = sinon.stub(); - this.project_ops_length = sinon.stub(); - this.pathname = '/a/b/c.tex'; - this.DocumentManager.getDoc = sinon.stub().yields(null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId); - this.RangesManager.applyUpdate = sinon.stub().yields(null, this.updated_ranges, false); - this.ShareJsUpdateManager.applyUpdate = sinon.stub().yields(null, this.updatedDocLines, this.version, this.appliedOps); - this.RedisManager.updateDocument = sinon.stub().yields(null, this.doc_ops_length, this.project_ops_length); - this.RealTimeRedisManager.sendData = sinon.stub(); - this.UpdateManager._addProjectHistoryMetadataToOps = sinon.stub(); - return this.HistoryManager.recordAndFlushHistoryOps = sinon.stub().callsArg(5); - }); - - describe("normally", function() { - beforeEach(function() { - return this.UpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.callback); - }); - - it("should apply the updates via ShareJS", function() { - return this.ShareJsUpdateManager.applyUpdate - .calledWith(this.project_id, this.doc_id, this.update, this.lines, this.version) - .should.equal(true); - }); - - it("should update the ranges", function() { - return this.RangesManager.applyUpdate - .calledWith(this.project_id, this.doc_id, this.ranges, this.appliedOps, this.updatedDocLines) - .should.equal(true); - }); - - it("should save the document", function() { - return this.RedisManager.updateDocument - .calledWith(this.project_id, this.doc_id, this.updatedDocLines, this.version, this.appliedOps, this.updated_ranges, this.updateMeta) - .should.equal(true); - }); - - it("should add metadata to the ops" , function() { - return this.UpdateManager._addProjectHistoryMetadataToOps - .calledWith(this.appliedOps, this.pathname, this.projectHistoryId, this.lines) - .should.equal(true); - }); - - it("should push the applied ops into the history queue", function() { - return this.HistoryManager.recordAndFlushHistoryOps - .calledWith(this.project_id, this.doc_id, this.appliedOps, this.doc_ops_length, this.project_ops_length) - .should.equal(true); - }); - - return it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - }); - - describe("with UTF-16 surrogate pairs in the update", function() { - beforeEach(function() { - this.update = {op: [{p: 42, i: "\uD835\uDC00"}]}; - return this.UpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.callback); - }); - - return it("should apply the update but with surrogate pairs removed", function() { - this.ShareJsUpdateManager.applyUpdate - .calledWith(this.project_id, this.doc_id, this.update) - .should.equal(true); - - // \uFFFD is 'replacement character' - return this.update.op[0].i.should.equal("\uFFFD\uFFFD"); - }); - }); - - describe("with an error", function() { - beforeEach(function() { - this.error = new Error("something went wrong"); - this.ShareJsUpdateManager.applyUpdate = sinon.stub().yields(this.error); - return this.UpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.callback); - }); - - it("should call RealTimeRedisManager.sendData with the error", function() { - return this.RealTimeRedisManager.sendData - .calledWith({ - project_id: this.project_id, - doc_id: this.doc_id, - error: this.error.message - }) - .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 ranges get collapsed", function() { - beforeEach(function() { - this.RangesManager.applyUpdate = sinon.stub().yields(null, this.updated_ranges, true); - this.SnapshotManager.recordSnapshot = sinon.stub().yields(); - return this.UpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.callback); - }); - - return it("should call SnapshotManager.recordSnapshot", function() { - return this.SnapshotManager.recordSnapshot - .calledWith( - this.project_id, - this.doc_id, - this.version, - this.pathname, - this.lines, - this.ranges - ) - .should.equal(true); - }); - }); - }); - - - describe("_addProjectHistoryMetadataToOps", function() { return it("should add projectHistoryId, pathname and doc_length metadata to the ops", function() { - const lines = [ - 'some', - 'test', - 'data' - ]; - const appliedOps = [ - { v: 42, op: [{i: "foo", p: 4}, { i: "bar", p: 6 }] }, - { v: 45, op: [{d: "qux", p: 4}, { i: "bazbaz", p: 14 }] }, - { v: 49, op: [{i: "penguin", p: 18}] } - ]; - this.UpdateManager._addProjectHistoryMetadataToOps(appliedOps, this.pathname, this.projectHistoryId, lines); - return appliedOps.should.deep.equal([{ - projectHistoryId: this.projectHistoryId, - v: 42, - op: [{i: "foo", p: 4}, { i: "bar", p: 6 }], - meta: { - pathname: this.pathname, - doc_length: 14 +describe('UpdateManager', function () { + beforeEach(function () { + let Profiler, Timer + this.project_id = 'project-id-123' + this.projectHistoryId = 'history-id-123' + this.doc_id = 'document-id-123' + this.callback = sinon.stub() + return (this.UpdateManager = SandboxedModule.require(modulePath, { + requires: { + './LockManager': (this.LockManager = {}), + './RedisManager': (this.RedisManager = {}), + './RealTimeRedisManager': (this.RealTimeRedisManager = {}), + './ShareJsUpdateManager': (this.ShareJsUpdateManager = {}), + './HistoryManager': (this.HistoryManager = {}), + 'logger-sharelatex': (this.logger = { log: sinon.stub() }), + './Metrics': (this.Metrics = { + Timer: (Timer = (function () { + Timer = class Timer { + static initClass() { + this.prototype.done = sinon.stub() + } } - }, { - projectHistoryId: this.projectHistoryId, - v: 45, - op: [{d: "qux", p: 4}, { i: "bazbaz", p: 14 }], - meta: { - pathname: this.pathname, - doc_length: 20 - } // 14 + 'foo' + 'bar' - }, { - projectHistoryId: this.projectHistoryId, - v: 49, - op: [{i: "penguin", p: 18}], - meta: { - pathname: this.pathname, - doc_length: 23 - } // 14 - 'qux' + 'bazbaz' - }]); -}); }); + Timer.initClass() + return Timer + })()) + }), + 'settings-sharelatex': (this.Settings = {}), + './DocumentManager': (this.DocumentManager = {}), + './RangesManager': (this.RangesManager = {}), + './SnapshotManager': (this.SnapshotManager = {}), + './Profiler': (Profiler = (function () { + Profiler = class Profiler { + static initClass() { + this.prototype.log = sinon.stub().returns({ end: sinon.stub() }) + this.prototype.end = sinon.stub() + } + } + Profiler.initClass() + return Profiler + })()) + } + })) + }) - return describe("lockUpdatesAndDo", function() { - beforeEach(function() { - this.method = sinon.stub().callsArgWith(3, null, this.response_arg1); - this.callback = sinon.stub(); - this.arg1 = "argument 1"; - this.response_arg1 = "response argument 1"; - this.lockValue = "mock-lock-value"; - this.LockManager.getLock = sinon.stub().callsArgWith(1, null, this.lockValue); - return this.LockManager.releaseLock = sinon.stub().callsArg(2); - }); + describe('processOutstandingUpdates', function () { + beforeEach(function () { + this.UpdateManager.fetchAndApplyUpdates = sinon.stub().callsArg(2) + return this.UpdateManager.processOutstandingUpdates( + this.project_id, + this.doc_id, + this.callback + ) + }) - describe("successfully", function() { - beforeEach(function() { - this.UpdateManager.continueProcessingUpdatesWithLock = sinon.stub(); - this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2); - return this.UpdateManager.lockUpdatesAndDo(this.method, this.project_id, this.doc_id, this.arg1, this.callback); - }); + it('should apply the updates', function () { + return this.UpdateManager.fetchAndApplyUpdates + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) - it("should lock the doc", function() { - return this.LockManager.getLock - .calledWith(this.doc_id) - .should.equal(true); - }); + it('should call the callback', function () { + return this.callback.called.should.equal(true) + }) - it("should process any outstanding updates", function() { - return this.UpdateManager.processOutstandingUpdates - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); + return it('should time the execution', function () { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + }) - it("should call the method", function() { - return this.method - .calledWith(this.project_id, this.doc_id, this.arg1) - .should.equal(true); - }); + describe('processOutstandingUpdatesWithLock', function () { + describe('when the lock is free', function () { + beforeEach(function () { + this.LockManager.tryLock = sinon + .stub() + .callsArgWith(1, null, true, (this.lockValue = 'mock-lock-value')) + this.LockManager.releaseLock = sinon.stub().callsArg(2) + this.UpdateManager.continueProcessingUpdatesWithLock = sinon + .stub() + .callsArg(2) + return (this.UpdateManager.processOutstandingUpdates = sinon + .stub() + .callsArg(2)) + }) - it("should return the method response to the callback", function() { - return this.callback - .calledWith(null, this.response_arg1) - .should.equal(true); - }); + describe('successfully', function () { + beforeEach(function () { + return this.UpdateManager.processOutstandingUpdatesWithLock( + this.project_id, + this.doc_id, + this.callback + ) + }) - it("should release the lock", function() { - return this.LockManager.releaseLock - .calledWith(this.doc_id, this.lockValue) - .should.equal(true); - }); + it('should acquire the lock', function () { + return this.LockManager.tryLock + .calledWith(this.doc_id) + .should.equal(true) + }) - return it("should continue processing updates", function() { - return this.UpdateManager.continueProcessingUpdatesWithLock - .calledWith(this.project_id, this.doc_id) - .should.equal(true); - }); - }); + it('should free the lock', function () { + return this.LockManager.releaseLock + .calledWith(this.doc_id, this.lockValue) + .should.equal(true) + }) - describe("when processOutstandingUpdates returns an error", function() { - beforeEach(function() { - this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArgWith(2, (this.error = new Error("Something went wrong"))); - return this.UpdateManager.lockUpdatesAndDo(this.method, this.project_id, this.doc_id, this.arg1, this.callback); - }); + it('should process the outstanding updates', function () { + return this.UpdateManager.processOutstandingUpdates + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) - it("should free the lock", function() { - return this.LockManager.releaseLock.calledWith(this.doc_id, this.lockValue).should.equal(true); - }); + it('should do everything with the lock acquired', function () { + this.UpdateManager.processOutstandingUpdates + .calledAfter(this.LockManager.tryLock) + .should.equal(true) + return this.UpdateManager.processOutstandingUpdates + .calledBefore(this.LockManager.releaseLock) + .should.equal(true) + }) - return it("should return the error in the callback", function() { - return this.callback.calledWith(this.error).should.equal(true); - }); - }); + it('should continue processing new updates that may have come in', function () { + return this.UpdateManager.continueProcessingUpdatesWithLock + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) - return describe("when the method returns an error", function() { - beforeEach(function() { - this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2); - this.method = sinon.stub().callsArgWith(3, (this.error = new Error("something went wrong")), this.response_arg1); - return this.UpdateManager.lockUpdatesAndDo(this.method, this.project_id, this.doc_id, this.arg1, this.callback); - }); + return it('should return the callback', function () { + return this.callback.called.should.equal(true) + }) + }) - it("should free the lock", function() { - return this.LockManager.releaseLock.calledWith(this.doc_id, this.lockValue).should.equal(true); - }); + return describe('when processOutstandingUpdates returns an error', function () { + beforeEach(function () { + this.UpdateManager.processOutstandingUpdates = sinon + .stub() + .callsArgWith(2, (this.error = new Error('Something went wrong'))) + return this.UpdateManager.processOutstandingUpdatesWithLock( + this.project_id, + this.doc_id, + this.callback + ) + }) - return it("should return the error in the callback", function() { - return this.callback.calledWith(this.error).should.equal(true); - }); - }); - }); -}); + it('should free the lock', function () { + return this.LockManager.releaseLock + .calledWith(this.doc_id, this.lockValue) + .should.equal(true) + }) + return it('should return the error in the callback', function () { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) + }) + return describe('when the lock is taken', function () { + beforeEach(function () { + this.LockManager.tryLock = sinon.stub().callsArgWith(1, null, false) + this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2) + return this.UpdateManager.processOutstandingUpdatesWithLock( + this.project_id, + this.doc_id, + this.callback + ) + }) + it('should return the callback', function () { + return this.callback.called.should.equal(true) + }) + + return it('should not process the updates', function () { + return this.UpdateManager.processOutstandingUpdates.called.should.equal( + false + ) + }) + }) + }) + + describe('continueProcessingUpdatesWithLock', function () { + describe('when there are outstanding updates', function () { + beforeEach(function () { + this.RealTimeRedisManager.getUpdatesLength = sinon + .stub() + .callsArgWith(1, null, 3) + this.UpdateManager.processOutstandingUpdatesWithLock = sinon + .stub() + .callsArg(2) + return this.UpdateManager.continueProcessingUpdatesWithLock( + this.project_id, + this.doc_id, + this.callback + ) + }) + + it('should process the outstanding updates', function () { + return this.UpdateManager.processOutstandingUpdatesWithLock + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) + + return it('should return the callback', function () { + return this.callback.called.should.equal(true) + }) + }) + + return describe('when there are no outstanding updates', function () { + beforeEach(function () { + this.RealTimeRedisManager.getUpdatesLength = sinon + .stub() + .callsArgWith(1, null, 0) + this.UpdateManager.processOutstandingUpdatesWithLock = sinon + .stub() + .callsArg(2) + return this.UpdateManager.continueProcessingUpdatesWithLock( + this.project_id, + this.doc_id, + this.callback + ) + }) + + it('should not try to process the outstanding updates', function () { + return this.UpdateManager.processOutstandingUpdatesWithLock.called.should.equal( + false + ) + }) + + return it('should return the callback', function () { + return this.callback.called.should.equal(true) + }) + }) + }) + + describe('fetchAndApplyUpdates', function () { + describe('with updates', function () { + beforeEach(function () { + this.updates = [{ p: 1, t: 'foo' }] + this.updatedDocLines = ['updated', 'lines'] + this.version = 34 + this.RealTimeRedisManager.getPendingUpdatesForDoc = sinon + .stub() + .callsArgWith(1, null, this.updates) + this.UpdateManager.applyUpdate = sinon + .stub() + .callsArgWith(3, null, this.updatedDocLines, this.version) + return this.UpdateManager.fetchAndApplyUpdates( + this.project_id, + this.doc_id, + this.callback + ) + }) + + it('should get the pending updates', function () { + return this.RealTimeRedisManager.getPendingUpdatesForDoc + .calledWith(this.doc_id) + .should.equal(true) + }) + + it('should apply the updates', function () { + return Array.from(this.updates).map((update) => + this.UpdateManager.applyUpdate + .calledWith(this.project_id, this.doc_id, update) + .should.equal(true) + ) + }) + + return it('should call the callback', function () { + return this.callback.called.should.equal(true) + }) + }) + + return describe('when there are no updates', function () { + beforeEach(function () { + this.updates = [] + this.RealTimeRedisManager.getPendingUpdatesForDoc = sinon + .stub() + .callsArgWith(1, null, this.updates) + this.UpdateManager.applyUpdate = sinon.stub() + this.RedisManager.setDocument = sinon.stub() + return this.UpdateManager.fetchAndApplyUpdates( + this.project_id, + this.doc_id, + this.callback + ) + }) + + it('should not call applyUpdate', function () { + return this.UpdateManager.applyUpdate.called.should.equal(false) + }) + + return it('should call the callback', function () { + return this.callback.called.should.equal(true) + }) + }) + }) + + describe('applyUpdate', function () { + beforeEach(function () { + this.updateMeta = { user_id: 'last-author-fake-id' } + this.update = { op: [{ p: 42, i: 'foo' }], meta: this.updateMeta } + this.updatedDocLines = ['updated', 'lines'] + this.version = 34 + this.lines = ['original', 'lines'] + this.ranges = { entries: 'mock', comments: 'mock' } + this.updated_ranges = { entries: 'updated', comments: 'updated' } + this.appliedOps = [ + { v: 42, op: 'mock-op-42' }, + { v: 45, op: 'mock-op-45' } + ] + this.doc_ops_length = sinon.stub() + this.project_ops_length = sinon.stub() + this.pathname = '/a/b/c.tex' + this.DocumentManager.getDoc = sinon + .stub() + .yields( + null, + this.lines, + this.version, + this.ranges, + this.pathname, + this.projectHistoryId + ) + this.RangesManager.applyUpdate = sinon + .stub() + .yields(null, this.updated_ranges, false) + this.ShareJsUpdateManager.applyUpdate = sinon + .stub() + .yields(null, this.updatedDocLines, this.version, this.appliedOps) + this.RedisManager.updateDocument = sinon + .stub() + .yields(null, this.doc_ops_length, this.project_ops_length) + this.RealTimeRedisManager.sendData = sinon.stub() + this.UpdateManager._addProjectHistoryMetadataToOps = sinon.stub() + return (this.HistoryManager.recordAndFlushHistoryOps = sinon + .stub() + .callsArg(5)) + }) + + describe('normally', function () { + beforeEach(function () { + return this.UpdateManager.applyUpdate( + this.project_id, + this.doc_id, + this.update, + this.callback + ) + }) + + it('should apply the updates via ShareJS', function () { + return this.ShareJsUpdateManager.applyUpdate + .calledWith( + this.project_id, + this.doc_id, + this.update, + this.lines, + this.version + ) + .should.equal(true) + }) + + it('should update the ranges', function () { + return this.RangesManager.applyUpdate + .calledWith( + this.project_id, + this.doc_id, + this.ranges, + this.appliedOps, + this.updatedDocLines + ) + .should.equal(true) + }) + + it('should save the document', function () { + return this.RedisManager.updateDocument + .calledWith( + this.project_id, + this.doc_id, + this.updatedDocLines, + this.version, + this.appliedOps, + this.updated_ranges, + this.updateMeta + ) + .should.equal(true) + }) + + it('should add metadata to the ops', function () { + return this.UpdateManager._addProjectHistoryMetadataToOps + .calledWith( + this.appliedOps, + this.pathname, + this.projectHistoryId, + this.lines + ) + .should.equal(true) + }) + + it('should push the applied ops into the history queue', function () { + return this.HistoryManager.recordAndFlushHistoryOps + .calledWith( + this.project_id, + this.doc_id, + this.appliedOps, + this.doc_ops_length, + this.project_ops_length + ) + .should.equal(true) + }) + + return it('should call the callback', function () { + return this.callback.called.should.equal(true) + }) + }) + + describe('with UTF-16 surrogate pairs in the update', function () { + beforeEach(function () { + this.update = { op: [{ p: 42, i: '\uD835\uDC00' }] } + return this.UpdateManager.applyUpdate( + this.project_id, + this.doc_id, + this.update, + this.callback + ) + }) + + return it('should apply the update but with surrogate pairs removed', function () { + this.ShareJsUpdateManager.applyUpdate + .calledWith(this.project_id, this.doc_id, this.update) + .should.equal(true) + + // \uFFFD is 'replacement character' + return this.update.op[0].i.should.equal('\uFFFD\uFFFD') + }) + }) + + describe('with an error', function () { + beforeEach(function () { + this.error = new Error('something went wrong') + this.ShareJsUpdateManager.applyUpdate = sinon.stub().yields(this.error) + return this.UpdateManager.applyUpdate( + this.project_id, + this.doc_id, + this.update, + this.callback + ) + }) + + it('should call RealTimeRedisManager.sendData with the error', function () { + return this.RealTimeRedisManager.sendData + .calledWith({ + project_id: this.project_id, + doc_id: this.doc_id, + error: this.error.message + }) + .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 ranges get collapsed', function () { + beforeEach(function () { + this.RangesManager.applyUpdate = sinon + .stub() + .yields(null, this.updated_ranges, true) + this.SnapshotManager.recordSnapshot = sinon.stub().yields() + return this.UpdateManager.applyUpdate( + this.project_id, + this.doc_id, + this.update, + this.callback + ) + }) + + return it('should call SnapshotManager.recordSnapshot', function () { + return this.SnapshotManager.recordSnapshot + .calledWith( + this.project_id, + this.doc_id, + this.version, + this.pathname, + this.lines, + this.ranges + ) + .should.equal(true) + }) + }) + }) + + describe('_addProjectHistoryMetadataToOps', function () { + return it('should add projectHistoryId, pathname and doc_length metadata to the ops', function () { + const lines = ['some', 'test', 'data'] + const appliedOps = [ + { + v: 42, + op: [ + { i: 'foo', p: 4 }, + { i: 'bar', p: 6 } + ] + }, + { + v: 45, + op: [ + { d: 'qux', p: 4 }, + { i: 'bazbaz', p: 14 } + ] + }, + { v: 49, op: [{ i: 'penguin', p: 18 }] } + ] + this.UpdateManager._addProjectHistoryMetadataToOps( + appliedOps, + this.pathname, + this.projectHistoryId, + lines + ) + return appliedOps.should.deep.equal([ + { + projectHistoryId: this.projectHistoryId, + v: 42, + op: [ + { i: 'foo', p: 4 }, + { i: 'bar', p: 6 } + ], + meta: { + pathname: this.pathname, + doc_length: 14 + } + }, + { + projectHistoryId: this.projectHistoryId, + v: 45, + op: [ + { d: 'qux', p: 4 }, + { i: 'bazbaz', p: 14 } + ], + meta: { + pathname: this.pathname, + doc_length: 20 + } // 14 + 'foo' + 'bar' + }, + { + projectHistoryId: this.projectHistoryId, + v: 49, + op: [{ i: 'penguin', p: 18 }], + meta: { + pathname: this.pathname, + doc_length: 23 + } // 14 - 'qux' + 'bazbaz' + } + ]) + }) + }) + + return describe('lockUpdatesAndDo', function () { + beforeEach(function () { + this.method = sinon.stub().callsArgWith(3, null, this.response_arg1) + this.callback = sinon.stub() + this.arg1 = 'argument 1' + this.response_arg1 = 'response argument 1' + this.lockValue = 'mock-lock-value' + this.LockManager.getLock = sinon + .stub() + .callsArgWith(1, null, this.lockValue) + return (this.LockManager.releaseLock = sinon.stub().callsArg(2)) + }) + + describe('successfully', function () { + beforeEach(function () { + this.UpdateManager.continueProcessingUpdatesWithLock = sinon.stub() + this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2) + return this.UpdateManager.lockUpdatesAndDo( + this.method, + this.project_id, + this.doc_id, + this.arg1, + this.callback + ) + }) + + it('should lock the doc', function () { + return this.LockManager.getLock + .calledWith(this.doc_id) + .should.equal(true) + }) + + it('should process any outstanding updates', function () { + return this.UpdateManager.processOutstandingUpdates + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) + + it('should call the method', function () { + return this.method + .calledWith(this.project_id, this.doc_id, this.arg1) + .should.equal(true) + }) + + it('should return the method response to the callback', function () { + return this.callback + .calledWith(null, this.response_arg1) + .should.equal(true) + }) + + it('should release the lock', function () { + return this.LockManager.releaseLock + .calledWith(this.doc_id, this.lockValue) + .should.equal(true) + }) + + return it('should continue processing updates', function () { + return this.UpdateManager.continueProcessingUpdatesWithLock + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) + }) + + describe('when processOutstandingUpdates returns an error', function () { + beforeEach(function () { + this.UpdateManager.processOutstandingUpdates = sinon + .stub() + .callsArgWith(2, (this.error = new Error('Something went wrong'))) + return this.UpdateManager.lockUpdatesAndDo( + this.method, + this.project_id, + this.doc_id, + this.arg1, + this.callback + ) + }) + + it('should free the lock', function () { + return this.LockManager.releaseLock + .calledWith(this.doc_id, this.lockValue) + .should.equal(true) + }) + + return it('should return the error in the callback', function () { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) + + return describe('when the method returns an error', function () { + beforeEach(function () { + this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2) + this.method = sinon + .stub() + .callsArgWith( + 3, + (this.error = new Error('something went wrong')), + this.response_arg1 + ) + return this.UpdateManager.lockUpdatesAndDo( + this.method, + this.project_id, + this.doc_id, + this.arg1, + this.callback + ) + }) + + it('should free the lock', function () { + return this.LockManager.releaseLock + .calledWith(this.doc_id, this.lockValue) + .should.equal(true) + }) + + return it('should return the error in the callback', function () { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) + }) +})