overleaf/services/document-updater/test/unit/coffee/DocOpsManager/DocOpsManagerTests.coffee

330 lines
11 KiB
CoffeeScript

sinon = require('sinon')
chai = require('chai')
should = chai.should()
modulePath = "../../../../app/js/DocOpsManager.js"
SandboxedModule = require('sandboxed-module')
ObjectId = require("../../../../app/js/mongojs").ObjectId
describe "DocOpsManager", ->
beforeEach ->
@doc_id = ObjectId().toString()
@project_id = "project-id"
@callback = sinon.stub()
@DocOpsManager = SandboxedModule.require modulePath, requires:
"./RedisManager": @RedisManager = {}
"./mongojs":
db: @db = { docOps: {} }
ObjectId: ObjectId
"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
"./Metrics": @Metrics =
Timer: class Timer
done: sinon.stub()
"./TrackChangesManager": @TrackChangesManager = {}
describe "flushDocOpsToMongo", ->
describe "when versions are consistent", ->
beforeEach ->
@mongo_version = 40
@redis_version = 42
@ops = [ "mock-op-1", "mock-op-2" ]
@DocOpsManager.getDocVersionInMongo = sinon.stub().callsArgWith(1, null, @mongo_version)
@RedisManager.getDocVersion = sinon.stub().callsArgWith(1, null, @redis_version)
@RedisManager.getPreviousDocOps = sinon.stub().callsArgWith(3, null, @ops)
@DocOpsManager._appendDocOpsInMongo = sinon.stub().callsArg(3)
@DocOpsManager.flushDocOpsToMongo @project_id, @doc_id, @callback
it "should get the version from Mongo", ->
@DocOpsManager.getDocVersionInMongo
.calledWith(@doc_id)
.should.equal true
it "should get the version from REdis", ->
@RedisManager.getDocVersion
.calledWith(@doc_id)
.should.equal true
it "should get all doc ops since the version in Mongo", ->
@RedisManager.getPreviousDocOps
.calledWith(@doc_id, @mongo_version, -1)
.should.equal true
it "should update Mongo with the new ops", ->
@DocOpsManager._appendDocOpsInMongo
.calledWith(@doc_id, @ops, @redis_version)
.should.equal true
it "should call the callback", ->
@callback.called.should.equal true
it "should time the execution", ->
@Metrics.Timer::done.called.should.equal true
describe "when the number of ops does not match the difference in versions", ->
beforeEach ->
@mongo_version = 40
@redis_version = 45
@ops = [ "mock-op-1", "mock-op-2" ]
@DocOpsManager.getDocVersionInMongo = sinon.stub().callsArgWith(1, null, @mongo_version)
@RedisManager.getDocVersion = sinon.stub().callsArgWith(1, null, @redis_version)
@RedisManager.getPreviousDocOps = sinon.stub().callsArgWith(3, null, @ops)
@DocOpsManager._appendDocOpsInMongo = sinon.stub().callsArg(3)
@DocOpsManager.flushDocOpsToMongo @project_id, @doc_id, @callback
it "should call the callback with an error", ->
@callback.calledWith(new Error("inconsistet versions")).should.equal true
it "should log an error", ->
@logger.error
.calledWith(doc_id: @doc_id, mongoVersion: @mongo_version, redisVersion: @redis_version, opsLength: @ops.length, "version difference does not match ops length")
.should.equal true
it "should not modify mongo", ->
@DocOpsManager._appendDocOpsInMongo.called.should.equal false
it "should time the execution", ->
@Metrics.Timer::done.called.should.equal true
describe "when redis version is behind mongo version", ->
beforeEach ->
@mongo_version = 40
@redis_version = 30
@DocOpsManager.getDocVersionInMongo = sinon.stub().callsArgWith(1, null, @mongo_version)
@RedisManager.getDocVersion = sinon.stub().callsArgWith(1, null, @redis_version)
@RedisManager.getPreviousDocOps = sinon.stub().callsArgWith(3, null, @ops)
@DocOpsManager._appendDocOpsInMongo = sinon.stub().callsArg(3)
@DocOpsManager.flushDocOpsToMongo @project_id, @doc_id, @callback
it "should call the callback with an error", ->
@callback.calledWith(new Error("inconsistet versions")).should.equal true
it "should log an error", ->
@logger.error
.calledWith(doc_id: @doc_id, mongoVersion: @mongo_version, redisVersion: @redis_version, "mongo version is ahead of redis")
.should.equal true
it "should not modify mongo", ->
@DocOpsManager._appendDocOpsInMongo.called.should.equal false
it "should time the execution", ->
@Metrics.Timer::done.called.should.equal true
describe "getPreviousDocOps", ->
beforeEach ->
@ops = [ "mock-op-1", "mock-op-2" ]
@start = 30
@end = 32
@RedisManager.getPreviousDocOps = sinon.stub().callsArgWith(3, null, @ops)
@DocOpsManager._ensureOpsAreLoaded = sinon.stub().callsArg(3)
@DocOpsManager.getPreviousDocOps @project_id, @doc_id, @start, @end, @callback
it "should ensure the ops are loaded back far enough", ->
@DocOpsManager._ensureOpsAreLoaded
.calledWith(@project_id, @doc_id, @start)
.should.equal true
it "should get the previous doc ops", ->
@RedisManager.getPreviousDocOps
.calledWith(@doc_id, @start, @end)
.should.equal true
it "should call the callback with the ops", ->
@callback.calledWith(null, @ops).should.equal true
it "should time the execution", ->
@Metrics.Timer::done.called.should.equal true
describe "_ensureOpsAreLoaded", ->
describe "when the ops are not loaded", ->
beforeEach ->
@redisVersion = 42
@redisOpsLength = 10
@backToVersion = 30
@ops = [ "mock-op-1", "mock-op-2" ]
@RedisManager.getDocVersion = sinon.stub().callsArgWith(1, null, @redisVersion)
@RedisManager.getDocOpsLength = sinon.stub().callsArgWith(1, null, @redisOpsLength)
@DocOpsManager._getDocOpsFromMongo = sinon.stub().callsArgWith(3, null, @ops)
@RedisManager.prependDocOps = sinon.stub().callsArgWith(2, null)
@DocOpsManager._ensureOpsAreLoaded @project_id, @doc_id, @backToVersion, @callback
it "should get the doc version from redis", ->
@RedisManager.getDocVersion
.calledWith(@doc_id)
.should.equal true
it "should get the doc ops length in redis", ->
@RedisManager.getDocOpsLength
.calledWith(@doc_id)
.should.equal true
it "should get the doc ops that need loading from Mongo", ->
@DocOpsManager._getDocOpsFromMongo
.calledWith(@doc_id, @backToVersion, @redisVersion - @redisOpsLength)
.should.equal true
it "should prepend the retrieved ops to redis", ->
@RedisManager.prependDocOps
.calledWith(@doc_id, @ops)
.should.equal true
it "should call the callback", ->
@callback.called.should.equal true
describe "when the ops are loaded", ->
beforeEach ->
@redisVersion = 42
@redisOpsLength = 10
@backToVersion = 35
@RedisManager.getDocVersion = sinon.stub().callsArgWith(1, null, @redisVersion)
@RedisManager.getDocOpsLength = sinon.stub().callsArgWith(1, null, @redisOpsLength)
@DocOpsManager._getDocOpsFromMongo = sinon.stub().callsArgWith(3, null, @ops)
@RedisManager.prependDocOps = sinon.stub().callsArgWith(2, null)
@DocOpsManager._ensureOpsAreLoaded @project_id, @doc_id, @backToVersion, @callback
it "should not need to get the docs from Mongo or put any into redis", ->
@DocOpsManager._getDocOpsFromMongo.called.should.equal false
@RedisManager.prependDocOps.called.should.equal false
it "should call the callback", ->
@callback.called.should.equal true
describe "getDocVersionInMongo", ->
describe "when the doc exists", ->
beforeEach ->
@doc =
version: @version = 42
@db.docOps.find = sinon.stub().callsArgWith(2, null, [@doc])
@DocOpsManager.getDocVersionInMongo @doc_id, @callback
it "should look for the doc in the database", ->
@db.docOps.find
.calledWith({ doc_id: ObjectId(@doc_id) }, {version: 1})
.should.equal true
it "should call the callback with the version", ->
@callback.calledWith(null, @version).should.equal true
describe "when the doc doesn't exist", ->
beforeEach ->
@db.docOps.find = sinon.stub().callsArgWith(2, null, [])
@DocOpsManager.getDocVersionInMongo @doc_id, @callback
it "should call the callback with 0", ->
@callback.calledWith(null, 0).should.equal true
describe "_appendDocOpsInMongo", ->
describe "with a small set of updates", ->
beforeEach (done) ->
@ops = [ "mock-op-1", "mock-op-2" ]
@version = 42
@db.docOps.update = sinon.stub().callsArg(3)
@DocOpsManager._appendDocOpsInMongo @doc_id, @ops, @version, (error) =>
@callback(error)
done()
it "should update the database", ->
@db.docOps.update
.calledWith({
doc_id: ObjectId(@doc_id)
}, {
$push: docOps: { $each: @ops, $slice: -100 }
$set: version: @version
}, {
upsert: true
})
.should.equal true
it "should call the callbak", ->
@callback.called.should.equal true
describe "with a large set of updates", ->
beforeEach (done) ->
@ops = [ "mock-op-1", "mock-op-2", "mock-op-3", "mock-op-4", "mock-op-5" ]
@version = 42
@DocOpsManager.APPEND_OPS_BATCH_SIZE = 2
@db.docOps.update = sinon.stub().callsArg(3)
@DocOpsManager._appendDocOpsInMongo @doc_id, @ops, @version, (error) =>
@callback(error)
done()
it "should update the database in batches", ->
@db.docOps.update
.calledWith({ doc_id: ObjectId(@doc_id) }, {
$push: docOps: { $each: @ops.slice(0,2), $slice: -100 }
$set: version: @version - 3
}, { upsert: true })
.should.equal true
@db.docOps.update
.calledWith({ doc_id: ObjectId(@doc_id) }, {
$push: docOps: { $each: @ops.slice(2,4), $slice: -100 }
$set: version: @version - 1
}, { upsert: true })
.should.equal true
@db.docOps.update
.calledWith({ doc_id: ObjectId(@doc_id) }, {
$push: docOps: { $each: @ops.slice(4,5), $slice: -100 }
$set: version: @version
}, { upsert: true })
.should.equal true
it "should call the callbak", ->
@callback.called.should.equal true
describe "with no updates", ->
beforeEach (done) ->
@ops = []
@version = 42
@db.docOps.update = sinon.stub().callsArg(3)
@DocOpsManager._appendDocOpsInMongo @doc_id, @ops, @version, (error) =>
@callback(error)
done()
it "should not try to update the database", ->
@db.docOps.update.called.should.equal false
describe "_getDocsOpsFromMongo", ->
beforeEach ->
@version = 42
@start = 32
@limit = 5
@doc =
docOps: ["mock-ops"]
@DocOpsManager.getDocVersionInMongo = sinon.stub().callsArgWith(1, null, @version)
@db.docOps.find = sinon.stub().callsArgWith(2, null, [@doc])
@DocOpsManager._getDocOpsFromMongo @doc_id, @start, @start + @limit, @callback
it "should get the current version", ->
@DocOpsManager.getDocVersionInMongo
.calledWith(@doc_id)
.should.equal true
it "should get the doc ops", ->
@db.docOps.find
.calledWith({ doc_id: ObjectId(@doc_id) }, {
docOps: $slice: [-(@version - @start), @limit]
})
.should.equal true
it "should return the ops", ->
@callback.calledWith(null, @doc.docOps).should.equal true
describe "pushDocOp", ->
beforeEach ->
@op = "mock-op"
@RedisManager.pushDocOp = sinon.stub().callsArgWith(2, null, @version = 42)
@TrackChangesManager.pushUncompressedHistoryOp = sinon.stub().callsArg(3)
@DocOpsManager.pushDocOp @project_id, @doc_id, @op, @callback
it "should push the op in to the docOps list", ->
@RedisManager.pushDocOp
.calledWith(@doc_id, @op)
.should.equal true
it "should push the op into the pushUncompressedHistoryOp", ->
@TrackChangesManager.pushUncompressedHistoryOp
.calledWith(@project_id, @doc_id, @op)
.should.equal true
it "should call the callback with the version", ->
@callback.calledWith(null, @version).should.equal true