mirror of
https://github.com/overleaf/overleaf.git
synced 2024-12-30 21:20:48 +00:00
692 lines
21 KiB
JavaScript
692 lines
21 KiB
JavaScript
/* eslint-disable
|
|
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
|
|
* 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')
|
|
|
|
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
|
|
}
|
|
},
|
|
{
|
|
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)
|
|
})
|
|
})
|
|
})
|
|
})
|