/* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from * DS102: Remove unnecessary code created because of implicit returns * DS206: Consider reworking classes to avoid initClass * 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(() => 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", () => 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", () => 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); }); }); }); });