2020-05-06 06:11:22 -04:00
|
|
|
/* eslint-disable
|
|
|
|
no-return-assign,
|
|
|
|
no-unused-vars,
|
|
|
|
*/
|
|
|
|
// TODO: This file was created by bulk-decaffeinate.
|
|
|
|
// Fix any style issues and re-enable lint.
|
2020-05-06 06:10:51 -04:00
|
|
|
/*
|
|
|
|
* decaffeinate suggestions:
|
|
|
|
* DS102: Remove unnecessary code created because of implicit returns
|
|
|
|
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
|
|
*/
|
2020-05-06 06:11:36 -04:00
|
|
|
const sinon = require('sinon')
|
|
|
|
const modulePath = '../../../../app/js/ShareJsUpdateManager.js'
|
|
|
|
const SandboxedModule = require('sandboxed-module')
|
2024-11-08 05:21:56 -05:00
|
|
|
const crypto = require('node:crypto')
|
2020-05-06 06:11:36 -04:00
|
|
|
|
|
|
|
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 }),
|
2020-11-10 06:32:04 -05:00
|
|
|
'@overleaf/redis-wrapper': {
|
2020-05-06 06:11:36 -04:00
|
|
|
createClient: () => {
|
|
|
|
return (this.rclient = { auth() {} })
|
2021-07-13 07:04:42 -04:00
|
|
|
},
|
2020-05-06 06:11:36 -04:00
|
|
|
},
|
2024-07-18 09:01:09 -04:00
|
|
|
'./RealTimeRedisManager': (this.RealTimeRedisManager = {
|
|
|
|
sendCanaryAppliedOp: sinon.stub(),
|
|
|
|
}),
|
2021-07-13 07:04:42 -04:00
|
|
|
'./Metrics': (this.metrics = { inc: sinon.stub() }),
|
2020-05-06 06:11:36 -04:00
|
|
|
},
|
|
|
|
globals: {
|
2021-07-13 07:04:42 -04:00
|
|
|
clearTimeout: (this.clearTimeout = sinon.stub()),
|
|
|
|
},
|
2020-05-06 06:11:36 -04:00
|
|
|
}))
|
|
|
|
})
|
|
|
|
|
|
|
|
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: {
|
2021-07-13 07:04:42 -04:00
|
|
|
appliedOps: {},
|
|
|
|
},
|
2020-05-06 06:11:36 -04:00
|
|
|
}
|
|
|
|
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'),
|
2021-07-13 07:04:42 -04:00
|
|
|
v: this.version,
|
2020-05-06 06:11:36 -04:00
|
|
|
})
|
2021-07-13 07:04:42 -04:00
|
|
|
this.model.db.appliedOps[`${this.project_id}:${this.doc_id}`] =
|
|
|
|
this.appliedOps = ['mock-ops']
|
2020-05-06 06:11:36 -04:00
|
|
|
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 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 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)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
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)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
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 describe('with an invalid hash', function () {
|
|
|
|
beforeEach(function (done) {
|
|
|
|
this.error = new Error('invalid hash')
|
|
|
|
this.model.getSnapshot.callsArgWith(1, null, {
|
|
|
|
snapshot: 'unexpected content',
|
2021-07-13 07:04:42 -04:00
|
|
|
v: this.version,
|
2020-05-06 06:11:36 -04:00
|
|
|
})
|
2021-07-13 07:04:42 -04:00
|
|
|
this.model.db.appliedOps[`${this.project_id}:${this.doc_id}`] =
|
|
|
|
this.appliedOps = ['mock-ops']
|
2020-05-06 06:11:36 -04:00
|
|
|
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 () {
|
2020-05-15 14:29:49 -04:00
|
|
|
return this.callback
|
|
|
|
.calledWith(sinon.match.instanceOf(Error))
|
|
|
|
.should.equal(true)
|
2020-05-06 06:11:36 -04:00
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
return describe('_listenForOps', function () {
|
|
|
|
beforeEach(function () {
|
|
|
|
this.model = {
|
|
|
|
on: (event, callback) => {
|
|
|
|
return (this.callback = callback)
|
2021-07-13 07:04:42 -04:00
|
|
|
},
|
2020-05-06 06:11:36 -04:00
|
|
|
}
|
|
|
|
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)
|
|
|
|
})
|
|
|
|
|
|
|
|
return describe('the callback', function () {
|
|
|
|
beforeEach(function () {
|
|
|
|
this.opData = {
|
|
|
|
op: { t: 'foo', p: 1 },
|
2021-07-13 07:04:42 -04:00
|
|
|
meta: { source: 'bar' },
|
2020-05-06 06:11:36 -04:00
|
|
|
}
|
|
|
|
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,
|
2021-07-13 07:04:42 -04:00
|
|
|
op: this.opData,
|
2020-05-06 06:11:36 -04:00
|
|
|
})
|
|
|
|
.should.equal(true)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|