/* eslint-disable camelcase, handle-callback-err, no-return-assign, no-unused-vars, */ // TODO: This file was created by bulk-decaffeinate. // Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from * DS102: Remove unnecessary code created because of implicit returns * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ const sinon = require('sinon'); const chai = require('chai'); const should = chai.should(); const { expect } = chai; const modulePath = "../../../../app/js/UpdatesManager.js"; const SandboxedModule = require('sandboxed-module'); describe("UpdatesManager", function() { beforeEach(function() { this.UpdatesManager = SandboxedModule.require(modulePath, { requires: { "./UpdateCompressor": (this.UpdateCompressor = {}), "./MongoManager" : (this.MongoManager = {}), "./PackManager" : (this.PackManager = {}), "./RedisManager" : (this.RedisManager = {}), "./LockManager" : (this.LockManager = {}), "./WebApiManager": (this.WebApiManager = {}), "./UpdateTrimmer": (this.UpdateTrimmer = {}), "./DocArchiveManager": (this.DocArchiveManager = {}), "logger-sharelatex": { log: sinon.stub(), error: sinon.stub() }, "settings-sharelatex": { redis: { lock: { key_schema: { historyLock({doc_id}) { return `HistoryLock:${doc_id}`; } } } } } } } ); this.doc_id = "doc-id-123"; this.project_id = "project-id-123"; this.callback = sinon.stub(); return this.temporary = "temp-mock"; }); describe("compressAndSaveRawUpdates", function() { describe("when there are no raw ops", function() { beforeEach(function() { this.MongoManager.peekLastCompressedUpdate = sinon.stub(); return this.UpdatesManager.compressAndSaveRawUpdates(this.project_id, this.doc_id, [], this.temporary, this.callback); }); it("should not need to access the database", function() { return this.MongoManager.peekLastCompressedUpdate.called.should.equal(false); }); return it("should call the callback", function() { return this.callback.called.should.equal(true); }); }); describe("when there is no compressed history to begin with", function() { beforeEach(function() { this.rawUpdates = [{ v: 12, op: "mock-op-12" }, { v: 13, op: "mock-op-13" }]; this.compressedUpdates = [ { v: 13, op: "compressed-op-12" } ]; this.MongoManager.peekLastCompressedUpdate = sinon.stub().callsArgWith(1, null, null); this.PackManager.insertCompressedUpdates = sinon.stub().callsArg(5); this.UpdateCompressor.compressRawUpdates = sinon.stub().returns(this.compressedUpdates); return this.UpdatesManager.compressAndSaveRawUpdates(this.project_id, this.doc_id, this.rawUpdates, this.temporary, this.callback); }); it("should look at the last compressed op", function() { return this.MongoManager.peekLastCompressedUpdate .calledWith(this.doc_id) .should.equal(true); }); it("should save the compressed ops as a pack", function() { return this.PackManager.insertCompressedUpdates .calledWith(this.project_id, this.doc_id, null, this.compressedUpdates, this.temporary) .should.equal(true); }); return it("should call the callback", function() { return this.callback.called.should.equal(true); }); }); describe("when the raw ops need appending to existing history", function() { beforeEach(function() { this.lastCompressedUpdate = { v: 11, op: "compressed-op-11" }; this.compressedUpdates = [ { v: 12, op: "compressed-op-11+12" }, { v: 13, op: "compressed-op-12" } ]; this.MongoManager.peekLastCompressedUpdate = sinon.stub().callsArgWith(1, null, this.lastCompressedUpdate, this.lastCompressedUpdate.v); this.PackManager.insertCompressedUpdates = sinon.stub().callsArg(5); return this.UpdateCompressor.compressRawUpdates = sinon.stub().returns(this.compressedUpdates); }); describe("when the raw ops start where the existing history ends", function() { beforeEach(function() { this.rawUpdates = [{ v: 12, op: "mock-op-12" }, { v: 13, op: "mock-op-13" }]; return this.UpdatesManager.compressAndSaveRawUpdates(this.project_id, this.doc_id, this.rawUpdates, this.temporary, this.callback); }); it("should look at the last compressed op", function() { return this.MongoManager.peekLastCompressedUpdate .calledWith(this.doc_id) .should.equal(true); }); it("should compress the raw ops", function() { return this.UpdateCompressor.compressRawUpdates .calledWith(null, this.rawUpdates) .should.equal(true); }); it("should save the new compressed ops into a pack", function() { return this.PackManager.insertCompressedUpdates .calledWith(this.project_id, this.doc_id, this.lastCompressedUpdate, this.compressedUpdates, this.temporary) .should.equal(true); }); return it("should call the callback", function() { return this.callback.called.should.equal(true); }); }); describe("when the raw ops start where the existing history ends and the history is in a pack", function() { beforeEach(function() { this.lastCompressedUpdate = {pack: [{ v: 11, op: "compressed-op-11" }], v:11}; this.rawUpdates = [{ v: 12, op: "mock-op-12" }, { v: 13, op: "mock-op-13" }]; this.MongoManager.peekLastCompressedUpdate = sinon.stub().callsArgWith(1, null, this.lastCompressedUpdate, this.lastCompressedUpdate.v); return this.UpdatesManager.compressAndSaveRawUpdates(this.project_id, this.doc_id, this.rawUpdates, this.temporary, this.callback); }); it("should look at the last compressed op", function() { return this.MongoManager.peekLastCompressedUpdate .calledWith(this.doc_id) .should.equal(true); }); it("should compress the raw ops", function() { return this.UpdateCompressor.compressRawUpdates .calledWith(null, this.rawUpdates) .should.equal(true); }); it("should save the new compressed ops into a pack", function() { return this.PackManager.insertCompressedUpdates .calledWith(this.project_id, this.doc_id, this.lastCompressedUpdate, this.compressedUpdates, this.temporary) .should.equal(true); }); return it("should call the callback", function() { return this.callback.called.should.equal(true); }); }); describe("when some raw ops are passed that have already been compressed", function() { beforeEach(function() { this.rawUpdates = [{ v: 10, op: "mock-op-10" }, { v: 11, op: "mock-op-11"}, { v: 12, op: "mock-op-12" }, { v: 13, op: "mock-op-13" }]; return this.UpdatesManager.compressAndSaveRawUpdates(this.project_id, this.doc_id, this.rawUpdates, this.temporary, this.callback); }); return it("should only compress the more recent raw ops", function() { return this.UpdateCompressor.compressRawUpdates .calledWith(null, this.rawUpdates.slice(-2)) .should.equal(true); }); }); describe("when the raw ops do not follow from the last compressed op version", function() { beforeEach(function() { this.rawUpdates = [{ v: 13, op: "mock-op-13" }]; return this.UpdatesManager.compressAndSaveRawUpdates(this.project_id, this.doc_id, this.rawUpdates, this.temporary, this.callback); }); it("should call the callback with an error", function() { return this.callback .calledWith(sinon.match.has('message', "Tried to apply raw op at version 13 to last compressed update with version 11 from unknown time")) .should.equal(true); }); return it("should not insert any update into mongo", function() { return this.PackManager.insertCompressedUpdates.called.should.equal(false); }); }); return describe("when the raw ops are out of order", function() { beforeEach(function() { this.rawUpdates = [{ v: 13, op: "mock-op-13" }, { v: 12, op: "mock-op-12" }]; return this.UpdatesManager.compressAndSaveRawUpdates(this.project_id, this.doc_id, this.rawUpdates, this.temporary, this.callback); }); it("should call the callback with an error", function() { return this.callback .calledWith(sinon.match.has('message')) .should.equal(true); }); return it("should not insert any update into mongo", function() { return this.PackManager.insertCompressedUpdates.called.should.equal(false); }); }); }); return describe("when the raw ops need appending to existing history which is in S3", function() { beforeEach(function() { this.lastCompressedUpdate = null; this.lastVersion = 11; this.compressedUpdates = [ { v: 13, op: "compressed-op-12" } ]; this.MongoManager.peekLastCompressedUpdate = sinon.stub().callsArgWith(1, null, null, this.lastVersion); this.PackManager.insertCompressedUpdates = sinon.stub().callsArg(5); return this.UpdateCompressor.compressRawUpdates = sinon.stub().returns(this.compressedUpdates); }); return describe("when the raw ops start where the existing history ends", function() { beforeEach(function() { this.rawUpdates = [{ v: 12, op: "mock-op-12" }, { v: 13, op: "mock-op-13" }]; return this.UpdatesManager.compressAndSaveRawUpdates(this.project_id, this.doc_id, this.rawUpdates, this.temporary, this.callback); }); it("should try to look at the last compressed op", function() { return this.MongoManager.peekLastCompressedUpdate .calledWith(this.doc_id) .should.equal(true); }); it("should compress the last compressed op and the raw ops", function() { return this.UpdateCompressor.compressRawUpdates .calledWith(this.lastCompressedUpdate, this.rawUpdates) .should.equal(true); }); it("should save the compressed ops", function() { return this.PackManager.insertCompressedUpdates .calledWith(this.project_id, this.doc_id, null, this.compressedUpdates, this.temporary) .should.equal(true); }); return it("should call the callback", function() { return this.callback.called.should.equal(true); }); }); }); }); describe("processUncompressedUpdates", function() { beforeEach(function() { this.UpdatesManager.compressAndSaveRawUpdates = sinon.stub().callsArgWith(4); this.RedisManager.deleteAppliedDocUpdates = sinon.stub().callsArg(3); this.MongoManager.backportProjectId = sinon.stub().callsArg(2); return this.UpdateTrimmer.shouldTrimUpdates = sinon.stub().callsArgWith(1, null, (this.temporary = "temp mock")); }); describe("when there is fewer than one batch to send", function() { beforeEach(function() { this.updates = ["mock-update"]; this.RedisManager.getOldestDocUpdates = sinon.stub().callsArgWith(2, null, this.updates); this.RedisManager.expandDocUpdates = sinon.stub().callsArgWith(1, null, this.updates); return this.UpdatesManager.processUncompressedUpdates(this.project_id, this.doc_id, this.temporary, this.callback); }); it("should get the oldest updates", function() { return this.RedisManager.getOldestDocUpdates .calledWith(this.doc_id, this.UpdatesManager.REDIS_READ_BATCH_SIZE) .should.equal(true); }); it("should compress and save the updates", function() { return this.UpdatesManager.compressAndSaveRawUpdates .calledWith(this.project_id, this.doc_id, this.updates, this.temporary) .should.equal(true); }); it("should delete the batch of uncompressed updates that was just processed", function() { return this.RedisManager.deleteAppliedDocUpdates .calledWith(this.project_id, this.doc_id, this.updates) .should.equal(true); }); return it("should call the callback", function() { return this.callback.called.should.equal(true); }); }); return describe("when there are multiple batches to send", function() { beforeEach(function(done) { this.UpdatesManager.REDIS_READ_BATCH_SIZE = 2; this.updates = ["mock-update-0", "mock-update-1", "mock-update-2", "mock-update-3", "mock-update-4"]; this.redisArray = this.updates.slice(); this.RedisManager.getOldestDocUpdates = (doc_id, batchSize, callback) => { if (callback == null) { callback = function(error, updates) {}; } const updates = this.redisArray.slice(0, batchSize); this.redisArray = this.redisArray.slice(batchSize); return callback(null, updates); }; sinon.spy(this.RedisManager, "getOldestDocUpdates"); this.RedisManager.expandDocUpdates = (jsonUpdates, callback) => { return callback(null, jsonUpdates); }; sinon.spy(this.RedisManager, "expandDocUpdates"); return this.UpdatesManager.processUncompressedUpdates(this.project_id, this.doc_id, this.temporary, (...args) => { this.callback(...Array.from(args || [])); return done(); }); }); it("should get the oldest updates in three batches ", function() { return this.RedisManager.getOldestDocUpdates.callCount.should.equal(3); }); it("should compress and save the updates in batches", function() { this.UpdatesManager.compressAndSaveRawUpdates .calledWith(this.project_id, this.doc_id, this.updates.slice(0,2), this.temporary) .should.equal(true); this.UpdatesManager.compressAndSaveRawUpdates .calledWith(this.project_id, this.doc_id, this.updates.slice(2,4), this.temporary) .should.equal(true); return this.UpdatesManager.compressAndSaveRawUpdates .calledWith(this.project_id, this.doc_id, this.updates.slice(4,5), this.temporary) .should.equal(true); }); it("should delete the batches of uncompressed updates", function() { return this.RedisManager.deleteAppliedDocUpdates.callCount.should.equal(3); }); return it("should call the callback", function() { return this.callback.called.should.equal(true); }); }); }); describe("processCompressedUpdatesWithLock", function() { beforeEach(function() { this.UpdateTrimmer.shouldTrimUpdates = sinon.stub().callsArgWith(1, null, (this.temporary = "temp mock")); this.MongoManager.backportProjectId = sinon.stub().callsArg(2); this.UpdatesManager._processUncompressedUpdates = sinon.stub().callsArg(3); this.LockManager.runWithLock = sinon.stub().callsArg(2); return this.UpdatesManager.processUncompressedUpdatesWithLock(this.project_id, this.doc_id, this.callback); }); it("should check if the updates are temporary", function() { return this.UpdateTrimmer.shouldTrimUpdates .calledWith(this.project_id) .should.equal(true); }); it("should backport the project id", function() { return this.MongoManager.backportProjectId .calledWith(this.project_id, this.doc_id) .should.equal(true); }); it("should run processUncompressedUpdates with the lock", function() { return this.LockManager.runWithLock .calledWith( `HistoryLock:${this.doc_id}` ) .should.equal(true); }); return it("should call the callback", function() { return this.callback.called.should.equal(true); }); }); describe("getDocUpdates", function() { beforeEach(function() { this.updates = ["mock-updates"]; this.options = { to: "mock-to", limit: "mock-limit" }; this.PackManager.getOpsByVersionRange = sinon.stub().callsArgWith(4, null, this.updates); this.UpdatesManager.processUncompressedUpdatesWithLock = sinon.stub().callsArg(2); return this.UpdatesManager.getDocUpdates(this.project_id, this.doc_id, this.options, this.callback); }); it("should process outstanding updates", function() { return this.UpdatesManager.processUncompressedUpdatesWithLock .calledWith(this.project_id, this.doc_id) .should.equal(true); }); it("should get the updates from the database", function() { return this.PackManager.getOpsByVersionRange .calledWith(this.project_id, this.doc_id, this.options.from, this.options.to) .should.equal(true); }); return it("should return the updates", function() { return this.callback .calledWith(null, this.updates) .should.equal(true); }); }); describe("getDocUpdatesWithUserInfo", function() { beforeEach(function() { this.updates = ["mock-updates"]; this.options = { to: "mock-to", limit: "mock-limit" }; this.updatesWithUserInfo = ["updates-with-user-info"]; this.UpdatesManager.getDocUpdates = sinon.stub().callsArgWith(3, null, this.updates); this.UpdatesManager.fillUserInfo = sinon.stub().callsArgWith(1, null, this.updatesWithUserInfo); return this.UpdatesManager.getDocUpdatesWithUserInfo(this.project_id, this.doc_id, this.options, this.callback); }); it("should get the updates", function() { return this.UpdatesManager.getDocUpdates .calledWith(this.project_id, this.doc_id, this.options) .should.equal(true); }); it("should file the updates with the user info", function() { return this.UpdatesManager.fillUserInfo .calledWith(this.updates) .should.equal(true); }); return it("should return the updates with the filled details", function() { return this.callback.calledWith(null, this.updatesWithUserInfo).should.equal(true); }); }); describe("processUncompressedUpdatesForProject", function() { beforeEach(function(done) { this.doc_ids = ["mock-id-1", "mock-id-2"]; this.UpdateTrimmer.shouldTrimUpdates = sinon.stub().callsArgWith(1, null, (this.temporary = "temp mock")); this.MongoManager.backportProjectId = sinon.stub().callsArg(2); this.UpdatesManager._processUncompressedUpdatesForDocWithLock = sinon.stub().callsArg(3); this.RedisManager.getDocIdsWithHistoryOps = sinon.stub().callsArgWith(1, null, this.doc_ids); return this.UpdatesManager.processUncompressedUpdatesForProject(this.project_id, () => { this.callback(); return done(); }); }); it("should get all the docs with history ops", function() { return this.RedisManager.getDocIdsWithHistoryOps .calledWith(this.project_id) .should.equal(true); }); it("should process the doc ops for the each doc_id", function() { return Array.from(this.doc_ids).map((doc_id) => this.UpdatesManager._processUncompressedUpdatesForDocWithLock .calledWith(this.project_id, doc_id, this.temporary) .should.equal(true)); }); return it("should call the callback", function() { return this.callback.called.should.equal(true); }); }); describe("getSummarizedProjectUpdates", function() { beforeEach(function() { this.updates = [{doc_id: 123, v:456, op: "mock-updates", meta: {user_id: 123, start_ts: 1233, end_ts:1234}}]; this.options = { before: "mock-before", limit: "mock-limit" }; this.summarizedUpdates = [ {meta: {user_ids: [123], start_ts: 1233, end_ts:1234},docs:{"123":{fromV:456,toV:456}}} ]; this.updatesWithUserInfo = ["updates-with-user-info"]; this.done_state = false; this.iterator = { next: cb => { this.done_state = true; return cb(null, this.updates); }, done: () => { return this.done_state; } }; this.PackManager.makeProjectIterator = sinon.stub().callsArgWith(2, null, this.iterator); this.UpdatesManager.processUncompressedUpdatesForProject = sinon.stub().callsArg(1); this.UpdatesManager.fillSummarizedUserInfo = sinon.stub().callsArgWith(1, null, this.updatesWithUserInfo); return this.UpdatesManager.getSummarizedProjectUpdates(this.project_id, this.options, this.callback); }); it("should process any outstanding updates", function() { return this.UpdatesManager.processUncompressedUpdatesForProject .calledWith(this.project_id) .should.equal(true); }); it("should get the updates", function() { return this.PackManager.makeProjectIterator .calledWith(this.project_id, this.options.before) .should.equal(true); }); it("should fill the updates with the user info", function() { return this.UpdatesManager.fillSummarizedUserInfo .calledWith(this.summarizedUpdates) .should.equal(true); }); return it("should return the updates with the filled details", function() { return this.callback.calledWith(null, this.updatesWithUserInfo).should.equal(true); }); }); // describe "_extendBatchOfSummarizedUpdates", -> // beforeEach -> // @before = Date.now() // @min_count = 2 // @existingSummarizedUpdates = ["summarized-updates-3"] // @summarizedUpdates = ["summarized-updates-3", "summarized-update-2", "summarized-update-1"] // describe "when there are updates to get", -> // beforeEach -> // @updates = [ // {op: "mock-op-1", meta: end_ts: @before - 10}, // {op: "mock-op-1", meta: end_ts: @nextBeforeTimestamp = @before - 20} // ] // @existingSummarizedUpdates = ["summarized-updates-3"] // @summarizedUpdates = ["summarized-updates-3", "summarized-update-2", "summarized-update-1"] // @UpdatesManager._summarizeUpdates = sinon.stub().returns(@summarizedUpdates) // @UpdatesManager.getProjectUpdatesWithUserInfo = sinon.stub().callsArgWith(2, null, @updates) // @UpdatesManager._extendBatchOfSummarizedUpdates @project_id, @existingSummarizedUpdates, @before, @min_count, @callback // it "should get the updates", -> // @UpdatesManager.getProjectUpdatesWithUserInfo // .calledWith(@project_id, { before: @before, limit: 3 * @min_count }) // .should.equal true // it "should summarize the updates", -> // @UpdatesManager._summarizeUpdates // .calledWith(@updates, @existingSummarizedUpdates) // .should.equal true // it "should call the callback with the summarized updates and the next before timestamp", -> // @callback.calledWith(null, @summarizedUpdates, @nextBeforeTimestamp).should.equal true // describe "when there are no more updates", -> // beforeEach -> // @updates = [] // @UpdatesManager._summarizeUpdates = sinon.stub().returns(@summarizedUpdates) // @UpdatesManager.getProjectUpdatesWithUserInfo = sinon.stub().callsArgWith(2, null, @updates) // @UpdatesManager._extendBatchOfSummarizedUpdates @project_id, @existingSummarizedUpdates, @before, @min_count, @callback // it "should call the callback with the summarized updates and null for nextBeforeTimestamp", -> // @callback.calledWith(null, @summarizedUpdates, null).should.equal true // describe "getSummarizedProjectUpdates", -> // describe "when one batch of updates is enough to meet the limit", -> // beforeEach -> // @before = Date.now() // @min_count = 2 // @updates = ["summarized-updates-3", "summarized-updates-2"] // @nextBeforeTimestamp = @before - 100 // @UpdatesManager._extendBatchOfSummarizedUpdates = sinon.stub().callsArgWith(4, null, @updates, @nextBeforeTimestamp) // @UpdatesManager.getSummarizedProjectUpdates @project_id, { before: @before, min_count: @min_count }, @callback // it "should get the batch of summarized updates", -> // @UpdatesManager._extendBatchOfSummarizedUpdates // .calledWith(@project_id, [], @before, @min_count) // .should.equal true // it "should call the callback with the updates", -> // @callback.calledWith(null, @updates, @nextBeforeTimestamp).should.equal true // describe "when multiple batches are needed to meet the limit", -> // beforeEach -> // @before = Date.now() // @min_count = 4 // @firstBatch = [{ toV: 6, fromV: 6 }, { toV: 5, fromV: 5 }] // @nextBeforeTimestamp = @before - 100 // @secondBatch = [{ toV: 4, fromV: 4 }, { toV: 3, fromV: 3 }] // @nextNextBeforeTimestamp = @before - 200 // @UpdatesManager._extendBatchOfSummarizedUpdates = (project_id, existingUpdates, before, desiredLength, callback) => // if existingUpdates.length == 0 // callback null, @firstBatch, @nextBeforeTimestamp // else // callback null, @firstBatch.concat(@secondBatch), @nextNextBeforeTimestamp // sinon.spy @UpdatesManager, "_extendBatchOfSummarizedUpdates" // @UpdatesManager.getSummarizedProjectUpdates @project_id, { before: @before, min_count: @min_count }, @callback // it "should get the first batch of summarized updates", -> // @UpdatesManager._extendBatchOfSummarizedUpdates // .calledWith(@project_id, [], @before, @min_count) // .should.equal true // it "should get the second batch of summarized updates", -> // @UpdatesManager._extendBatchOfSummarizedUpdates // .calledWith(@project_id, @firstBatch, @nextBeforeTimestamp, @min_count) // .should.equal true // it "should call the callback with all the updates", -> // @callback.calledWith(null, @firstBatch.concat(@secondBatch), @nextNextBeforeTimestamp).should.equal true // describe "when the end of the database is hit", -> // beforeEach -> // @before = Date.now() // @min_count = 4 // @updates = [{ toV: 6, fromV: 6 }, { toV: 5, fromV: 5 }] // @UpdatesManager._extendBatchOfSummarizedUpdates = sinon.stub().callsArgWith(4, null, @updates, null) // @UpdatesManager.getSummarizedProjectUpdates @project_id, { before: @before, min_count: @min_count }, @callback // it "should get the batch of summarized updates", -> // @UpdatesManager._extendBatchOfSummarizedUpdates // .calledWith(@project_id, [], @before, @min_count) // .should.equal true // it "should call the callback with the updates", -> // @callback.calledWith(null, @updates, null).should.equal true describe("fillUserInfo", function() { describe("with valid users", function() { beforeEach(function(done) { const {ObjectId} = require("mongojs"); this.user_id_1 = ObjectId().toString(); this.user_id_2 = ObjectId().toString(); this.updates = [{ meta: { user_id: this.user_id_1 }, op: "mock-op-1" }, { meta: { user_id: this.user_id_1 }, op: "mock-op-2" }, { meta: { user_id: this.user_id_2 }, op: "mock-op-3" }]; this.user_info = {}; this.user_info[this.user_id_1] = {email: "user1@sharelatex.com"}; this.user_info[this.user_id_2] = {email: "user2@sharelatex.com"}; this.WebApiManager.getUserInfo = (user_id, callback) => { if (callback == null) { callback = function(error, userInfo) {}; } return callback(null, this.user_info[user_id]); }; sinon.spy(this.WebApiManager, "getUserInfo"); return this.UpdatesManager.fillUserInfo(this.updates, (error, results) => { this.results = results; return done(); }); }); it("should only call getUserInfo once for each user_id", function() { this.WebApiManager.getUserInfo.calledTwice.should.equal(true); this.WebApiManager.getUserInfo .calledWith(this.user_id_1) .should.equal(true); return this.WebApiManager.getUserInfo .calledWith(this.user_id_2) .should.equal(true); }); return it("should return the updates with the user info filled", function() { return expect(this.results).to.deep.equal([{ meta: { user: { email: "user1@sharelatex.com" } }, op: "mock-op-1" }, { meta: { user: { email: "user1@sharelatex.com" } }, op: "mock-op-2" }, { meta: { user: { email: "user2@sharelatex.com" } }, op: "mock-op-3" }]); }); }); return describe("with invalid user ids", function() { beforeEach(function(done) { this.updates = [{ meta: { user_id: null }, op: "mock-op-1" }, { meta: { user_id: "anonymous-user" }, op: "mock-op-2" }]; this.WebApiManager.getUserInfo = (user_id, callback) => { if (callback == null) { callback = function(error, userInfo) {}; } return callback(null, this.user_info[user_id]); }; sinon.spy(this.WebApiManager, "getUserInfo"); return this.UpdatesManager.fillUserInfo(this.updates, (error, results) => { this.results = results; return done(); }); }); it("should not call getUserInfo", function() { return this.WebApiManager.getUserInfo.called.should.equal(false); }); return it("should return the updates without the user info filled", function() { return expect(this.results).to.deep.equal([{ meta: {}, op: "mock-op-1" }, { meta: {}, op: "mock-op-2" }]); }); }); }); return describe("_summarizeUpdates", function() { beforeEach(function() { this.now = Date.now(); this.user_1 = { id: "mock-user-1" }; return this.user_2 = { id: "mock-user-2" };}); it("should concat updates that are close in time", function() { const result = this.UpdatesManager._summarizeUpdates([{ doc_id: "doc-id-1", meta: { user_id: this.user_1.id, start_ts: this.now + 20, end_ts: this.now + 30 }, v: 5 }, { doc_id: "doc-id-1", meta: { user_id: this.user_2.id, start_ts: this.now, end_ts: this.now + 10 }, v: 4 }]); return expect(result).to.deep.equal([{ docs: { "doc-id-1": { fromV: 4, toV: 5 } }, meta: { user_ids: [this.user_1.id, this.user_2.id], start_ts: this.now, end_ts: this.now + 30 } }]); }); it("should leave updates that are far apart in time", function() { const oneDay = 1000 * 60 * 60 * 24; const result = this.UpdatesManager._summarizeUpdates([{ doc_id: "doc-id-1", meta: { user_id: this.user_2.id, start_ts: this.now + oneDay, end_ts: this.now + oneDay + 10 }, v: 5 }, { doc_id: "doc-id-1", meta: { user_id: this.user_1.id, start_ts: this.now, end_ts: this.now + 10 }, v: 4 }]); return expect(result).to.deep.equal([{ docs: { "doc-id-1": { fromV: 5, toV: 5 } }, meta: { user_ids: [this.user_2.id], start_ts: this.now + oneDay, end_ts: this.now + oneDay + 10 } }, { docs: { "doc-id-1": { fromV: 4, toV: 4 } }, meta: { user_ids: [this.user_1.id], start_ts: this.now, end_ts: this.now + 10 } }]); }); it("should concat onto existing summarized updates", function() { const result = this.UpdatesManager._summarizeUpdates([{ doc_id: "doc-id-2", meta: { user_id: this.user_1.id, start_ts: this.now + 20, end_ts: this.now + 30 }, v: 5 }, { doc_id: "doc-id-2", meta: { user_id: this.user_2.id, start_ts: this.now, end_ts: this.now + 10 }, v: 4 }], [{ docs: { "doc-id-1": { fromV: 6, toV: 8 } }, meta: { user_ids: [this.user_1.id], start_ts: this.now + 40, end_ts: this.now + 50 } }]); return expect(result).to.deep.equal([{ docs: { "doc-id-1": { toV: 8, fromV: 6 }, "doc-id-2": { toV: 5, fromV: 4 } }, meta: { user_ids: [this.user_1.id, this.user_2.id], start_ts: this.now, end_ts: this.now + 50 } }]); }); it("should include null user values", function() { const result = this.UpdatesManager._summarizeUpdates([{ doc_id: "doc-id-1", meta: { user_id: this.user_1.id, start_ts: this.now + 20, end_ts: this.now + 30 }, v: 5 }, { doc_id: "doc-id-1", meta: { user_id: null, start_ts: this.now, end_ts: this.now + 10 }, v: 4 }]); return expect(result).to.deep.equal([{ docs: { "doc-id-1": { fromV: 4, toV: 5 } }, meta: { user_ids: [this.user_1.id, null], start_ts: this.now, end_ts: this.now + 30 } }]); }); it("should include null user values, when the null is earlier in the updates list", function() { const result = this.UpdatesManager._summarizeUpdates([{ doc_id: "doc-id-1", meta: { user_id: null, start_ts: this.now, end_ts: this.now + 10 }, v: 4 }, { doc_id: "doc-id-1", meta: { user_id: this.user_1.id, start_ts: this.now + 20, end_ts: this.now + 30 }, v: 5 }]); return expect(result).to.deep.equal([{ docs: { "doc-id-1": { fromV: 4, toV: 5 } }, meta: { user_ids: [null, this.user_1.id], start_ts: this.now, end_ts: this.now + 30 } }]); }); it("should roll several null user values into one", function() { const result = this.UpdatesManager._summarizeUpdates([{ doc_id: "doc-id-1", meta: { user_id: this.user_1.id, start_ts: this.now + 20, end_ts: this.now + 30 }, v: 5 }, { doc_id: "doc-id-1", meta: { user_id: null, start_ts: this.now, end_ts: this.now + 10 }, v: 4 }, { doc_id: "doc-id-1", meta: { user_id: null, start_ts: this.now + 2, end_ts: this.now + 4 }, v: 4 }]); return expect(result).to.deep.equal([{ docs: { "doc-id-1": { fromV: 4, toV: 5 } }, meta: { user_ids: [this.user_1.id, null], start_ts: this.now, end_ts: this.now + 30 } }]); }); return it("should split updates before a big delete", function() { const result = this.UpdatesManager._summarizeUpdates([{ doc_id: "doc-id-1", op: [{ d: "this is a long long long long long delete", p: 34 }], meta: { user_id: this.user_1.id, start_ts: this.now + 20, end_ts: this.now + 30 }, v: 5 }, { doc_id: "doc-id-1", meta: { user_id: this.user_2.id, start_ts: this.now, end_ts: this.now + 10 }, v: 4 }]); return expect(result).to.deep.equal([{ docs: { "doc-id-1": { fromV: 5, toV: 5 } }, meta: { user_ids: [this.user_1.id], start_ts: this.now + 20, end_ts: this.now + 30 } }, { docs: { "doc-id-1": { fromV: 4, toV: 4 } }, meta: { user_ids: [this.user_2.id], start_ts: this.now, end_ts: this.now + 10 } }]); }); }); });