mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
4d70bd664f
These changes were previously merged, not deployed, and reverted. This reverts the revert. This reverts commit a6b8c6c658b33b6eee78b8b99e43308f32211ae2, reversing changes made to 93c98921372eed4244d22fce800716cb27eca299.
1404 lines
42 KiB
JavaScript
1404 lines
42 KiB
JavaScript
/* eslint-disable
|
|
camelcase,
|
|
mocha/no-identical-title,
|
|
no-return-assign,
|
|
no-unused-vars,
|
|
*/
|
|
// TODO: This file was created by bulk-decaffeinate.
|
|
// Fix any style issues and re-enable lint.
|
|
/*
|
|
* decaffeinate suggestions:
|
|
* DS102: Remove unnecessary code created because of implicit returns
|
|
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
*/
|
|
const sinon = require('sinon')
|
|
const modulePath = '../../../../app/js/RedisManager.js'
|
|
const SandboxedModule = require('sandboxed-module')
|
|
const Errors = require('../../../../app/js/Errors')
|
|
const crypto = require('crypto')
|
|
const tk = require('timekeeper')
|
|
|
|
describe('RedisManager', function () {
|
|
beforeEach(function () {
|
|
let Timer
|
|
this.multi = { exec: sinon.stub() }
|
|
this.rclient = { multi: () => this.multi }
|
|
tk.freeze(new Date())
|
|
this.RedisManager = SandboxedModule.require(modulePath, {
|
|
requires: {
|
|
'./ProjectHistoryRedisManager': (this.ProjectHistoryRedisManager = {}),
|
|
'settings-sharelatex': (this.settings = {
|
|
documentupdater: { logHashErrors: { write: true, read: true } },
|
|
apis: {
|
|
project_history: { enabled: true }
|
|
},
|
|
redis: {
|
|
documentupdater: {
|
|
key_schema: {
|
|
blockingKey({ doc_id }) {
|
|
return `Blocking:${doc_id}`
|
|
},
|
|
docLines({ doc_id }) {
|
|
return `doclines:${doc_id}`
|
|
},
|
|
docOps({ doc_id }) {
|
|
return `DocOps:${doc_id}`
|
|
},
|
|
docVersion({ doc_id }) {
|
|
return `DocVersion:${doc_id}`
|
|
},
|
|
docHash({ doc_id }) {
|
|
return `DocHash:${doc_id}`
|
|
},
|
|
projectKey({ doc_id }) {
|
|
return `ProjectId:${doc_id}`
|
|
},
|
|
pendingUpdates({ doc_id }) {
|
|
return `PendingUpdates:${doc_id}`
|
|
},
|
|
docsInProject({ project_id }) {
|
|
return `DocsIn:${project_id}`
|
|
},
|
|
ranges({ doc_id }) {
|
|
return `Ranges:${doc_id}`
|
|
},
|
|
pathname({ doc_id }) {
|
|
return `Pathname:${doc_id}`
|
|
},
|
|
projectHistoryId({ doc_id }) {
|
|
return `ProjectHistoryId:${doc_id}`
|
|
},
|
|
projectHistoryType({ doc_id }) {
|
|
return `ProjectHistoryType:${doc_id}`
|
|
},
|
|
projectState({ project_id }) {
|
|
return `ProjectState:${project_id}`
|
|
},
|
|
unflushedTime({ doc_id }) {
|
|
return `UnflushedTime:${doc_id}`
|
|
},
|
|
lastUpdatedBy({ doc_id }) {
|
|
return `lastUpdatedBy:${doc_id}`
|
|
},
|
|
lastUpdatedAt({ doc_id }) {
|
|
return `lastUpdatedAt:${doc_id}`
|
|
}
|
|
}
|
|
},
|
|
history: {
|
|
key_schema: {
|
|
uncompressedHistoryOps({ doc_id }) {
|
|
return `UncompressedHistoryOps:${doc_id}`
|
|
},
|
|
docsWithHistoryOps({ project_id }) {
|
|
return `DocsWithHistoryOps:${project_id}`
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}),
|
|
'@overleaf/redis-wrapper': {
|
|
createClient: () => this.rclient
|
|
},
|
|
'./Metrics': (this.metrics = {
|
|
inc: sinon.stub(),
|
|
summary: sinon.stub(),
|
|
Timer: (Timer = class Timer {
|
|
constructor() {
|
|
this.start = new Date()
|
|
}
|
|
|
|
done() {
|
|
const timeSpan = new Date() - this.start
|
|
return timeSpan
|
|
}
|
|
})
|
|
}),
|
|
'./Errors': Errors
|
|
}
|
|
})
|
|
|
|
this.doc_id = 'doc-id-123'
|
|
this.project_id = 'project-id-123'
|
|
this.projectHistoryId = 123
|
|
return (this.callback = sinon.stub())
|
|
})
|
|
|
|
afterEach(function () {
|
|
return tk.reset()
|
|
})
|
|
|
|
describe('getDoc', function () {
|
|
beforeEach(function () {
|
|
this.lines = ['one', 'two', 'three', 'これは'] // include some utf8
|
|
this.jsonlines = JSON.stringify(this.lines)
|
|
this.version = 42
|
|
this.hash = crypto
|
|
.createHash('sha1')
|
|
.update(this.jsonlines, 'utf8')
|
|
.digest('hex')
|
|
this.ranges = { comments: 'mock', entries: 'mock' }
|
|
this.json_ranges = JSON.stringify(this.ranges)
|
|
this.unflushed_time = 12345
|
|
this.pathname = '/a/b/c.tex'
|
|
this.multi.get = sinon.stub()
|
|
this.multi.exec = sinon
|
|
.stub()
|
|
.callsArgWith(0, null, [
|
|
this.jsonlines,
|
|
this.version,
|
|
this.hash,
|
|
this.project_id,
|
|
this.json_ranges,
|
|
this.pathname,
|
|
this.projectHistoryId.toString(),
|
|
this.unflushed_time
|
|
])
|
|
return (this.rclient.sadd = sinon.stub().yields(null, 0))
|
|
})
|
|
|
|
describe('successfully', function () {
|
|
beforeEach(function () {
|
|
return this.RedisManager.getDoc(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should get the lines from redis', function () {
|
|
return this.multi.get
|
|
.calledWith(`doclines:${this.doc_id}`)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should get the version from', function () {
|
|
return this.multi.get
|
|
.calledWith(`DocVersion:${this.doc_id}`)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should get the hash', function () {
|
|
return this.multi.get
|
|
.calledWith(`DocHash:${this.doc_id}`)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should get the ranges', function () {
|
|
return this.multi.get
|
|
.calledWith(`Ranges:${this.doc_id}`)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should get the unflushed time', function () {
|
|
return this.multi.get
|
|
.calledWith(`UnflushedTime:${this.doc_id}`)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should get the pathname', function () {
|
|
return this.multi.get
|
|
.calledWith(`Pathname:${this.doc_id}`)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should get the projectHistoryId as an integer', function () {
|
|
return this.multi.get
|
|
.calledWith(`ProjectHistoryId:${this.doc_id}`)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should get lastUpdatedAt', function () {
|
|
return this.multi.get
|
|
.calledWith(`lastUpdatedAt:${this.doc_id}`)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should get lastUpdatedBy', function () {
|
|
return this.multi.get
|
|
.calledWith(`lastUpdatedBy:${this.doc_id}`)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should check if the document is in the DocsIn set', function () {
|
|
return this.rclient.sadd
|
|
.calledWith(`DocsIn:${this.project_id}`)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should return the document', function () {
|
|
return this.callback
|
|
.calledWithExactly(
|
|
null,
|
|
this.lines,
|
|
this.version,
|
|
this.ranges,
|
|
this.pathname,
|
|
this.projectHistoryId,
|
|
this.unflushed_time,
|
|
this.lastUpdatedAt,
|
|
this.lastUpdatedBy
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
|
|
return it('should not log any errors', function () {
|
|
return this.logger.error.calledWith().should.equal(false)
|
|
})
|
|
})
|
|
|
|
describe('when the document is not present', function () {
|
|
beforeEach(function () {
|
|
this.multi.exec = sinon
|
|
.stub()
|
|
.callsArgWith(0, null, [
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null
|
|
])
|
|
this.rclient.sadd = sinon.stub().yields()
|
|
return this.RedisManager.getDoc(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should not check if the document is in the DocsIn set', function () {
|
|
return this.rclient.sadd
|
|
.calledWith(`DocsIn:${this.project_id}`)
|
|
.should.equal(false)
|
|
})
|
|
|
|
it('should return an empty result', function () {
|
|
return this.callback
|
|
.calledWithExactly(null, null, 0, {}, null, null, null, null, null)
|
|
.should.equal(true)
|
|
})
|
|
|
|
return it('should not log any errors', function () {
|
|
return this.logger.error.calledWith().should.equal(false)
|
|
})
|
|
})
|
|
|
|
describe('when the document is missing from the DocsIn set', function () {
|
|
beforeEach(function () {
|
|
this.rclient.sadd = sinon.stub().yields(null, 1)
|
|
return this.RedisManager.getDoc(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should log an error', function () {
|
|
return this.logger.error.calledWith().should.equal(true)
|
|
})
|
|
|
|
return it('should return the document', function () {
|
|
return this.callback
|
|
.calledWithExactly(
|
|
null,
|
|
this.lines,
|
|
this.version,
|
|
this.ranges,
|
|
this.pathname,
|
|
this.projectHistoryId,
|
|
this.unflushed_time,
|
|
this.lastUpdatedAt,
|
|
this.lastUpdatedBy
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('with a corrupted document', function () {
|
|
beforeEach(function () {
|
|
this.badHash = 'INVALID-HASH-VALUE'
|
|
this.multi.exec = sinon
|
|
.stub()
|
|
.callsArgWith(0, null, [
|
|
this.jsonlines,
|
|
this.version,
|
|
this.badHash,
|
|
this.project_id,
|
|
this.json_ranges
|
|
])
|
|
return this.RedisManager.getDoc(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should log a hash error', function () {
|
|
return this.logger.error.calledWith().should.equal(true)
|
|
})
|
|
|
|
return it('should return the document', function () {
|
|
return this.callback
|
|
.calledWith(null, this.lines, this.version, this.ranges)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('with a slow request to redis', function () {
|
|
beforeEach(function () {
|
|
this.multi.exec = sinon
|
|
.stub()
|
|
.callsArgWith(0, null, [
|
|
this.jsonlines,
|
|
this.version,
|
|
this.badHash,
|
|
this.project_id,
|
|
this.json_ranges,
|
|
this.pathname,
|
|
this.unflushed_time
|
|
])
|
|
this.clock = sinon.useFakeTimers()
|
|
this.multi.exec = (cb) => {
|
|
this.clock.tick(6000)
|
|
return cb(null, [
|
|
this.jsonlines,
|
|
this.version,
|
|
this.another_project_id,
|
|
this.json_ranges,
|
|
this.pathname,
|
|
this.unflushed_time
|
|
])
|
|
}
|
|
|
|
return this.RedisManager.getDoc(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
afterEach(function () {
|
|
return this.clock.restore()
|
|
})
|
|
|
|
return it('should return an error', function () {
|
|
return this.callback
|
|
.calledWith(sinon.match.instanceOf(Error))
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
return describe('getDoc with an invalid project id', function () {
|
|
beforeEach(function () {
|
|
this.another_project_id = 'project-id-456'
|
|
this.multi.exec = sinon
|
|
.stub()
|
|
.callsArgWith(0, null, [
|
|
this.jsonlines,
|
|
this.version,
|
|
this.hash,
|
|
this.another_project_id,
|
|
this.json_ranges,
|
|
this.pathname,
|
|
this.unflushed_time
|
|
])
|
|
return this.RedisManager.getDoc(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
return it('should return an error', function () {
|
|
return this.callback
|
|
.calledWith(sinon.match.instanceOf(Errors.NotFoundError))
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('getPreviousDocOpsTests', function () {
|
|
describe('with a start and an end value', function () {
|
|
beforeEach(function () {
|
|
this.first_version_in_redis = 30
|
|
this.version = 70
|
|
this.length = this.version - this.first_version_in_redis
|
|
this.start = 50
|
|
this.end = 60
|
|
this.ops = [{ mock: 'op-1' }, { mock: 'op-2' }]
|
|
this.jsonOps = this.ops.map((op) => JSON.stringify(op))
|
|
this.rclient.llen = sinon.stub().callsArgWith(1, null, this.length)
|
|
this.rclient.get = sinon
|
|
.stub()
|
|
.callsArgWith(1, null, this.version.toString())
|
|
this.rclient.lrange = sinon.stub().callsArgWith(3, null, this.jsonOps)
|
|
return this.RedisManager.getPreviousDocOps(
|
|
this.doc_id,
|
|
this.start,
|
|
this.end,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should get the length of the existing doc ops', function () {
|
|
return this.rclient.llen
|
|
.calledWith(`DocOps:${this.doc_id}`)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should get the current version of the doc', function () {
|
|
return this.rclient.get
|
|
.calledWith(`DocVersion:${this.doc_id}`)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should get the appropriate docs ops', function () {
|
|
return this.rclient.lrange
|
|
.calledWith(
|
|
`DocOps:${this.doc_id}`,
|
|
this.start - this.first_version_in_redis,
|
|
this.end - this.first_version_in_redis
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
|
|
return it('should return the docs with the doc ops deserialized', function () {
|
|
return this.callback.calledWith(null, this.ops).should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('with an end value of -1', function () {
|
|
beforeEach(function () {
|
|
this.first_version_in_redis = 30
|
|
this.version = 70
|
|
this.length = this.version - this.first_version_in_redis
|
|
this.start = 50
|
|
this.end = -1
|
|
this.ops = [{ mock: 'op-1' }, { mock: 'op-2' }]
|
|
this.jsonOps = this.ops.map((op) => JSON.stringify(op))
|
|
this.rclient.llen = sinon.stub().callsArgWith(1, null, this.length)
|
|
this.rclient.get = sinon
|
|
.stub()
|
|
.callsArgWith(1, null, this.version.toString())
|
|
this.rclient.lrange = sinon.stub().callsArgWith(3, null, this.jsonOps)
|
|
return this.RedisManager.getPreviousDocOps(
|
|
this.doc_id,
|
|
this.start,
|
|
this.end,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should get the appropriate docs ops to the end of list', function () {
|
|
return this.rclient.lrange
|
|
.calledWith(
|
|
`DocOps:${this.doc_id}`,
|
|
this.start - this.first_version_in_redis,
|
|
-1
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
|
|
return it('should return the docs with the doc ops deserialized', function () {
|
|
return this.callback.calledWith(null, this.ops).should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the requested range is not in Redis', function () {
|
|
beforeEach(function () {
|
|
this.first_version_in_redis = 30
|
|
this.version = 70
|
|
this.length = this.version - this.first_version_in_redis
|
|
this.start = 20
|
|
this.end = -1
|
|
this.ops = [{ mock: 'op-1' }, { mock: 'op-2' }]
|
|
this.jsonOps = this.ops.map((op) => JSON.stringify(op))
|
|
this.rclient.llen = sinon.stub().callsArgWith(1, null, this.length)
|
|
this.rclient.get = sinon
|
|
.stub()
|
|
.callsArgWith(1, null, this.version.toString())
|
|
this.rclient.lrange = sinon.stub().callsArgWith(3, null, this.jsonOps)
|
|
return this.RedisManager.getPreviousDocOps(
|
|
this.doc_id,
|
|
this.start,
|
|
this.end,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should return an error', function () {
|
|
return this.callback
|
|
.calledWith(sinon.match.instanceOf(Errors.OpRangeNotAvailableError))
|
|
.should.equal(true)
|
|
})
|
|
|
|
return it('should log out the problem', function () {
|
|
return this.logger.warn.called.should.equal(true)
|
|
})
|
|
})
|
|
|
|
return describe('with a slow request to redis', function () {
|
|
beforeEach(function () {
|
|
this.first_version_in_redis = 30
|
|
this.version = 70
|
|
this.length = this.version - this.first_version_in_redis
|
|
this.start = 50
|
|
this.end = 60
|
|
this.ops = [{ mock: 'op-1' }, { mock: 'op-2' }]
|
|
this.jsonOps = this.ops.map((op) => JSON.stringify(op))
|
|
this.rclient.llen = sinon.stub().callsArgWith(1, null, this.length)
|
|
this.rclient.get = sinon
|
|
.stub()
|
|
.callsArgWith(1, null, this.version.toString())
|
|
this.clock = sinon.useFakeTimers()
|
|
this.rclient.lrange = (key, start, end, cb) => {
|
|
this.clock.tick(6000)
|
|
return cb(null, this.jsonOps)
|
|
}
|
|
return this.RedisManager.getPreviousDocOps(
|
|
this.doc_id,
|
|
this.start,
|
|
this.end,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
afterEach(function () {
|
|
return this.clock.restore()
|
|
})
|
|
|
|
return it('should return an error', function () {
|
|
return this.callback
|
|
.calledWith(sinon.match.instanceOf(Error))
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('updateDocument', function () {
|
|
beforeEach(function () {
|
|
this.lines = ['one', 'two', 'three', 'これは']
|
|
this.ops = [{ op: [{ i: 'foo', p: 4 }] }, { op: [{ i: 'bar', p: 8 }] }]
|
|
this.version = 42
|
|
this.hash = crypto
|
|
.createHash('sha1')
|
|
.update(JSON.stringify(this.lines), 'utf8')
|
|
.digest('hex')
|
|
this.ranges = { comments: 'mock', entries: 'mock' }
|
|
this.updateMeta = { user_id: 'last-author-fake-id' }
|
|
this.doc_update_list_length = sinon.stub()
|
|
this.project_update_list_length = sinon.stub()
|
|
|
|
this.RedisManager.getDocVersion = sinon.stub()
|
|
this.multi.set = sinon.stub()
|
|
this.multi.rpush = sinon.stub()
|
|
this.multi.expire = sinon.stub()
|
|
this.multi.ltrim = sinon.stub()
|
|
this.multi.del = sinon.stub()
|
|
this.multi.exec = sinon
|
|
.stub()
|
|
.callsArgWith(0, null, [
|
|
this.hash,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
this.doc_update_list_length,
|
|
null,
|
|
null
|
|
])
|
|
return (this.ProjectHistoryRedisManager.queueOps = sinon
|
|
.stub()
|
|
.callsArgWith(
|
|
this.ops.length + 1,
|
|
null,
|
|
this.project_update_list_length
|
|
))
|
|
})
|
|
|
|
describe('with a consistent version', function () {
|
|
beforeEach(function () {})
|
|
|
|
describe('with project history enabled', function () {
|
|
beforeEach(function () {
|
|
this.settings.apis.project_history.enabled = true
|
|
this.RedisManager.getDocVersion
|
|
.withArgs(this.doc_id)
|
|
.yields(null, this.version - this.ops.length)
|
|
return this.RedisManager.updateDocument(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.lines,
|
|
this.version,
|
|
this.ops,
|
|
this.ranges,
|
|
this.updateMeta,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should get the current doc version to check for consistency', function () {
|
|
return this.RedisManager.getDocVersion
|
|
.calledWith(this.doc_id)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should set the doclines', function () {
|
|
return this.multi.set
|
|
.calledWith(`doclines:${this.doc_id}`, JSON.stringify(this.lines))
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should set the version', function () {
|
|
return this.multi.set
|
|
.calledWith(`DocVersion:${this.doc_id}`, this.version)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should set the hash', function () {
|
|
return this.multi.set
|
|
.calledWith(`DocHash:${this.doc_id}`, this.hash)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should set the ranges', function () {
|
|
return this.multi.set
|
|
.calledWith(`Ranges:${this.doc_id}`, JSON.stringify(this.ranges))
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should set the unflushed time', function () {
|
|
return this.multi.set
|
|
.calledWith(`UnflushedTime:${this.doc_id}`, Date.now(), 'NX')
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should set the last updated time', function () {
|
|
return this.multi.set
|
|
.calledWith(`lastUpdatedAt:${this.doc_id}`, Date.now())
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should set the last updater', function () {
|
|
return this.multi.set
|
|
.calledWith(`lastUpdatedBy:${this.doc_id}`, 'last-author-fake-id')
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should push the doc op into the doc ops list', function () {
|
|
return this.multi.rpush
|
|
.calledWith(
|
|
`DocOps:${this.doc_id}`,
|
|
JSON.stringify(this.ops[0]),
|
|
JSON.stringify(this.ops[1])
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should renew the expiry ttl on the doc ops array', function () {
|
|
return this.multi.expire
|
|
.calledWith(`DocOps:${this.doc_id}`, this.RedisManager.DOC_OPS_TTL)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should truncate the list to 100 members', function () {
|
|
return this.multi.ltrim
|
|
.calledWith(
|
|
`DocOps:${this.doc_id}`,
|
|
-this.RedisManager.DOC_OPS_MAX_LENGTH,
|
|
-1
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should push the updates into the history ops list', function () {
|
|
return this.multi.rpush
|
|
.calledWith(
|
|
`UncompressedHistoryOps:${this.doc_id}`,
|
|
JSON.stringify(this.ops[0]),
|
|
JSON.stringify(this.ops[1])
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should push the updates into the project history ops list', function () {
|
|
return this.ProjectHistoryRedisManager.queueOps
|
|
.calledWith(this.project_id, JSON.stringify(this.ops[0]))
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should call the callback', function () {
|
|
return this.callback
|
|
.calledWith(
|
|
null,
|
|
this.doc_update_list_length,
|
|
this.project_update_list_length
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
|
|
return it('should not log any errors', function () {
|
|
return this.logger.error.calledWith().should.equal(false)
|
|
})
|
|
})
|
|
|
|
describe('with project history disabled', function () {
|
|
beforeEach(function () {
|
|
this.settings.apis.project_history.enabled = false
|
|
this.RedisManager.getDocVersion
|
|
.withArgs(this.doc_id)
|
|
.yields(null, this.version - this.ops.length)
|
|
return this.RedisManager.updateDocument(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.lines,
|
|
this.version,
|
|
this.ops,
|
|
this.ranges,
|
|
this.updateMeta,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should not push the updates into the project history ops list', function () {
|
|
return this.ProjectHistoryRedisManager.queueOps.called.should.equal(
|
|
false
|
|
)
|
|
})
|
|
|
|
return it('should call the callback', function () {
|
|
return this.callback
|
|
.calledWith(null, this.doc_update_list_length)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
return describe('with a doc using project history only', function () {
|
|
beforeEach(function () {
|
|
this.RedisManager.getDocVersion
|
|
.withArgs(this.doc_id)
|
|
.yields(null, this.version - this.ops.length, 'project-history')
|
|
return this.RedisManager.updateDocument(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.lines,
|
|
this.version,
|
|
this.ops,
|
|
this.ranges,
|
|
this.updateMeta,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should not push the updates to the track-changes ops list', function () {
|
|
return this.multi.rpush
|
|
.calledWith(`UncompressedHistoryOps:${this.doc_id}`)
|
|
.should.equal(false)
|
|
})
|
|
|
|
it('should push the updates into the project history ops list', function () {
|
|
return this.ProjectHistoryRedisManager.queueOps
|
|
.calledWith(this.project_id, JSON.stringify(this.ops[0]))
|
|
.should.equal(true)
|
|
})
|
|
|
|
return it('should call the callback with the project update count only', function () {
|
|
return this.callback
|
|
.calledWith(null, undefined, this.project_update_list_length)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('with an inconsistent version', function () {
|
|
beforeEach(function () {
|
|
this.RedisManager.getDocVersion
|
|
.withArgs(this.doc_id)
|
|
.yields(null, this.version - this.ops.length - 1)
|
|
return this.RedisManager.updateDocument(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.lines,
|
|
this.version,
|
|
this.ops,
|
|
this.ranges,
|
|
this.updateMeta,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should not call multi.exec', function () {
|
|
return this.multi.exec.called.should.equal(false)
|
|
})
|
|
|
|
return it('should call the callback with an error', function () {
|
|
return this.callback
|
|
.calledWith(sinon.match.instanceOf(Error))
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('with no updates', function () {
|
|
beforeEach(function () {
|
|
this.RedisManager.getDocVersion
|
|
.withArgs(this.doc_id)
|
|
.yields(null, this.version)
|
|
return this.RedisManager.updateDocument(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.lines,
|
|
this.version,
|
|
[],
|
|
this.ranges,
|
|
this.updateMeta,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should not try to enqueue doc updates', function () {
|
|
return this.multi.rpush.called.should.equal(false)
|
|
})
|
|
|
|
it('should not try to enqueue project updates', function () {
|
|
return this.ProjectHistoryRedisManager.queueOps.called.should.equal(
|
|
false
|
|
)
|
|
})
|
|
|
|
return it('should still set the doclines', function () {
|
|
return this.multi.set
|
|
.calledWith(`doclines:${this.doc_id}`, JSON.stringify(this.lines))
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('with empty ranges', function () {
|
|
beforeEach(function () {
|
|
this.RedisManager.getDocVersion
|
|
.withArgs(this.doc_id)
|
|
.yields(null, this.version - this.ops.length)
|
|
return this.RedisManager.updateDocument(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.lines,
|
|
this.version,
|
|
this.ops,
|
|
{},
|
|
this.updateMeta,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should not set the ranges', function () {
|
|
return this.multi.set
|
|
.calledWith(`Ranges:${this.doc_id}`, JSON.stringify(this.ranges))
|
|
.should.equal(false)
|
|
})
|
|
|
|
return it('should delete the ranges key', function () {
|
|
return this.multi.del
|
|
.calledWith(`Ranges:${this.doc_id}`)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('with null bytes in the serialized doc lines', function () {
|
|
beforeEach(function () {
|
|
this.RedisManager.getDocVersion
|
|
.withArgs(this.doc_id)
|
|
.yields(null, this.version - this.ops.length)
|
|
this.stringifyStub = sinon
|
|
.stub(JSON, 'stringify')
|
|
.callsFake(() => '["bad bytes! \u0000 <- here"]')
|
|
return this.RedisManager.updateDocument(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.lines,
|
|
this.version,
|
|
this.ops,
|
|
this.ranges,
|
|
this.updateMeta,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
afterEach(function () {
|
|
this.stringifyStub.restore()
|
|
})
|
|
|
|
it('should log an error', function () {
|
|
return this.logger.error.called.should.equal(true)
|
|
})
|
|
|
|
return it('should call the callback with an error', function () {
|
|
return this.callback
|
|
.calledWith(sinon.match.instanceOf(Error))
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('with ranges that are too big', function () {
|
|
beforeEach(function () {
|
|
this.RedisManager.getDocVersion
|
|
.withArgs(this.doc_id)
|
|
.yields(null, this.version - this.ops.length)
|
|
this.RedisManager._serializeRanges = sinon
|
|
.stub()
|
|
.yields(new Error('ranges are too large'))
|
|
return this.RedisManager.updateDocument(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.lines,
|
|
this.version,
|
|
this.ops,
|
|
this.ranges,
|
|
this.updateMeta,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should log an error', function () {
|
|
return this.logger.error.called.should.equal(true)
|
|
})
|
|
|
|
return it('should call the callback with the error', function () {
|
|
return this.callback
|
|
.calledWith(sinon.match.instanceOf(Error))
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
return describe('without user id from meta', function () {
|
|
beforeEach(function () {
|
|
this.RedisManager.getDocVersion
|
|
.withArgs(this.doc_id)
|
|
.yields(null, this.version - this.ops.length)
|
|
return this.RedisManager.updateDocument(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.lines,
|
|
this.version,
|
|
this.ops,
|
|
this.ranges,
|
|
{},
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should set the last updater to null', function () {
|
|
return this.multi.del
|
|
.calledWith(`lastUpdatedBy:${this.doc_id}`)
|
|
.should.equal(true)
|
|
})
|
|
|
|
return it('should still set the last updated time', function () {
|
|
return this.multi.set
|
|
.calledWith(`lastUpdatedAt:${this.doc_id}`, Date.now())
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('putDocInMemory', function () {
|
|
beforeEach(function () {
|
|
this.multi.set = sinon.stub()
|
|
this.rclient.sadd = sinon.stub().yields()
|
|
this.multi.del = sinon.stub()
|
|
this.lines = ['one', 'two', 'three', 'これは']
|
|
this.version = 42
|
|
this.hash = crypto
|
|
.createHash('sha1')
|
|
.update(JSON.stringify(this.lines), 'utf8')
|
|
.digest('hex')
|
|
this.multi.exec = sinon.stub().callsArgWith(0, null, [this.hash])
|
|
this.ranges = { comments: 'mock', entries: 'mock' }
|
|
return (this.pathname = '/a/b/c.tex')
|
|
})
|
|
|
|
describe('with non-empty ranges', function () {
|
|
beforeEach(function (done) {
|
|
return this.RedisManager.putDocInMemory(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.lines,
|
|
this.version,
|
|
this.ranges,
|
|
this.pathname,
|
|
this.projectHistoryId,
|
|
done
|
|
)
|
|
})
|
|
|
|
it('should set the lines', function () {
|
|
return this.multi.set
|
|
.calledWith(`doclines:${this.doc_id}`, JSON.stringify(this.lines))
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should set the version', function () {
|
|
return this.multi.set
|
|
.calledWith(`DocVersion:${this.doc_id}`, this.version)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should set the hash', function () {
|
|
return this.multi.set
|
|
.calledWith(`DocHash:${this.doc_id}`, this.hash)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should set the ranges', function () {
|
|
return this.multi.set
|
|
.calledWith(`Ranges:${this.doc_id}`, JSON.stringify(this.ranges))
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should set the project_id for the doc', function () {
|
|
return this.multi.set
|
|
.calledWith(`ProjectId:${this.doc_id}`, this.project_id)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should set the pathname for the doc', function () {
|
|
return this.multi.set
|
|
.calledWith(`Pathname:${this.doc_id}`, this.pathname)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should set the projectHistoryId for the doc', function () {
|
|
return this.multi.set
|
|
.calledWith(`ProjectHistoryId:${this.doc_id}`, this.projectHistoryId)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should add the doc_id to the project set', function () {
|
|
return this.rclient.sadd
|
|
.calledWith(`DocsIn:${this.project_id}`, this.doc_id)
|
|
.should.equal(true)
|
|
})
|
|
|
|
return it('should not log any errors', function () {
|
|
return this.logger.error.calledWith().should.equal(false)
|
|
})
|
|
})
|
|
|
|
describe('with empty ranges', function () {
|
|
beforeEach(function (done) {
|
|
return this.RedisManager.putDocInMemory(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.lines,
|
|
this.version,
|
|
{},
|
|
this.pathname,
|
|
this.projectHistoryId,
|
|
done
|
|
)
|
|
})
|
|
|
|
it('should delete the ranges key', function () {
|
|
return this.multi.del
|
|
.calledWith(`Ranges:${this.doc_id}`)
|
|
.should.equal(true)
|
|
})
|
|
|
|
return it('should not set the ranges', function () {
|
|
return this.multi.set
|
|
.calledWith(`Ranges:${this.doc_id}`, JSON.stringify(this.ranges))
|
|
.should.equal(false)
|
|
})
|
|
})
|
|
|
|
describe('with null bytes in the serialized doc lines', function () {
|
|
beforeEach(function () {
|
|
this.stringifyStub = sinon
|
|
.stub(JSON, 'stringify')
|
|
.callsFake(() => '["bad bytes! \u0000 <- here"]')
|
|
return this.RedisManager.putDocInMemory(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.lines,
|
|
this.version,
|
|
this.ranges,
|
|
this.pathname,
|
|
this.projectHistoryId,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
afterEach(function () {
|
|
this.stringifyStub.restore()
|
|
})
|
|
|
|
it('should log an error', function () {
|
|
return this.logger.error.called.should.equal(true)
|
|
})
|
|
|
|
return it('should call the callback with an error', function () {
|
|
return this.callback
|
|
.calledWith(sinon.match.instanceOf(Error))
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
return describe('with ranges that are too big', function () {
|
|
beforeEach(function () {
|
|
this.RedisManager._serializeRanges = sinon
|
|
.stub()
|
|
.yields(new Error('ranges are too large'))
|
|
return this.RedisManager.putDocInMemory(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.lines,
|
|
this.version,
|
|
this.ranges,
|
|
this.pathname,
|
|
this.projectHistoryId,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should log an error', function () {
|
|
return this.logger.error.called.should.equal(true)
|
|
})
|
|
|
|
return it('should call the callback with the error', function () {
|
|
return this.callback
|
|
.calledWith(sinon.match.instanceOf(Error))
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('removeDocFromMemory', function () {
|
|
beforeEach(function (done) {
|
|
this.multi.strlen = sinon.stub()
|
|
this.multi.del = sinon.stub()
|
|
this.multi.srem = sinon.stub()
|
|
this.multi.exec.yields()
|
|
return this.RedisManager.removeDocFromMemory(
|
|
this.project_id,
|
|
this.doc_id,
|
|
done
|
|
)
|
|
})
|
|
|
|
it('should check the length of the current doclines', function () {
|
|
return this.multi.strlen
|
|
.calledWith(`doclines:${this.doc_id}`)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should delete the lines', function () {
|
|
return this.multi.del
|
|
.calledWith(`doclines:${this.doc_id}`)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should delete the version', function () {
|
|
return this.multi.del
|
|
.calledWith(`DocVersion:${this.doc_id}`)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should delete the hash', function () {
|
|
return this.multi.del
|
|
.calledWith(`DocHash:${this.doc_id}`)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should delete the unflushed time', function () {
|
|
return this.multi.del
|
|
.calledWith(`UnflushedTime:${this.doc_id}`)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should delete the project_id for the doc', function () {
|
|
return this.multi.del
|
|
.calledWith(`ProjectId:${this.doc_id}`)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should remove the doc_id from the project set', function () {
|
|
return this.multi.srem
|
|
.calledWith(`DocsIn:${this.project_id}`, this.doc_id)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should delete the pathname for the doc', function () {
|
|
return this.multi.del
|
|
.calledWith(`Pathname:${this.doc_id}`)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should delete the pathname for the doc', function () {
|
|
return this.multi.del
|
|
.calledWith(`ProjectHistoryId:${this.doc_id}`)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should delete lastUpdatedAt', function () {
|
|
return this.multi.del
|
|
.calledWith(`lastUpdatedAt:${this.doc_id}`)
|
|
.should.equal(true)
|
|
})
|
|
|
|
return it('should delete lastUpdatedBy', function () {
|
|
return this.multi.del
|
|
.calledWith(`lastUpdatedBy:${this.doc_id}`)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('clearProjectState', function () {
|
|
beforeEach(function (done) {
|
|
this.rclient.del = sinon.stub().callsArg(1)
|
|
return this.RedisManager.clearProjectState(this.project_id, done)
|
|
})
|
|
|
|
return it('should delete the project state', function () {
|
|
return this.rclient.del
|
|
.calledWith(`ProjectState:${this.project_id}`)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
return describe('renameDoc', function () {
|
|
beforeEach(function () {
|
|
this.rclient.rpush = sinon.stub().yields()
|
|
this.rclient.set = sinon.stub().yields()
|
|
return (this.update = {
|
|
id: this.doc_id,
|
|
pathname: (this.pathname = 'pathname'),
|
|
newPathname: (this.newPathname = 'new-pathname')
|
|
})
|
|
})
|
|
|
|
describe('the document is cached in redis', function () {
|
|
beforeEach(function () {
|
|
this.RedisManager.getDoc = sinon
|
|
.stub()
|
|
.callsArgWith(2, null, 'lines', 'version')
|
|
this.ProjectHistoryRedisManager.queueRenameEntity = sinon
|
|
.stub()
|
|
.yields()
|
|
return this.RedisManager.renameDoc(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.userId,
|
|
this.update,
|
|
this.projectHistoryId,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('update the cached pathname', function () {
|
|
return this.rclient.set
|
|
.calledWith(`Pathname:${this.doc_id}`, this.newPathname)
|
|
.should.equal(true)
|
|
})
|
|
|
|
return it('should queue an update', function () {
|
|
return this.ProjectHistoryRedisManager.queueRenameEntity
|
|
.calledWithExactly(
|
|
this.project_id,
|
|
this.projectHistoryId,
|
|
'doc',
|
|
this.doc_id,
|
|
this.userId,
|
|
this.update,
|
|
this.callback
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('the document is not cached in redis', function () {
|
|
beforeEach(function () {
|
|
this.RedisManager.getDoc = sinon
|
|
.stub()
|
|
.callsArgWith(2, null, null, null)
|
|
this.ProjectHistoryRedisManager.queueRenameEntity = sinon
|
|
.stub()
|
|
.yields()
|
|
return this.RedisManager.renameDoc(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.userId,
|
|
this.update,
|
|
this.projectHistoryId,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('does not update the cached pathname', function () {
|
|
return this.rclient.set.called.should.equal(false)
|
|
})
|
|
|
|
return it('should queue an update', function () {
|
|
return this.ProjectHistoryRedisManager.queueRenameEntity
|
|
.calledWithExactly(
|
|
this.project_id,
|
|
this.projectHistoryId,
|
|
'doc',
|
|
this.doc_id,
|
|
this.userId,
|
|
this.update,
|
|
this.callback
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
return describe('getDocVersion', function () {
|
|
beforeEach(function () {
|
|
return (this.version = 12345)
|
|
})
|
|
|
|
describe('when the document does not have a project history type set', function () {
|
|
beforeEach(function () {
|
|
this.rclient.mget = sinon
|
|
.stub()
|
|
.withArgs(
|
|
`DocVersion:${this.doc_id}`,
|
|
`ProjectHistoryType:${this.doc_id}`
|
|
)
|
|
.callsArgWith(2, null, [`${this.version}`])
|
|
return this.RedisManager.getDocVersion(this.doc_id, this.callback)
|
|
})
|
|
|
|
return it('should return the document version and an undefined history type', function () {
|
|
return this.callback
|
|
.calledWithExactly(null, this.version, undefined)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
return describe('when the document has a project history type set', function () {
|
|
beforeEach(function () {
|
|
this.rclient.mget = sinon
|
|
.stub()
|
|
.withArgs(
|
|
`DocVersion:${this.doc_id}`,
|
|
`ProjectHistoryType:${this.doc_id}`
|
|
)
|
|
.callsArgWith(2, null, [`${this.version}`, 'project-history'])
|
|
return this.RedisManager.getDocVersion(this.doc_id, this.callback)
|
|
})
|
|
|
|
return it('should return the document version and history type', function () {
|
|
return this.callback
|
|
.calledWithExactly(null, this.version, 'project-history')
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|