overleaf/services/document-updater/test/unit/js/RedisManager/RedisManagerTests.js

1418 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 chai = require('chai')
const should = chai.should()
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: {
'logger-sharelatex': (this.logger = {
error: sinon.stub(),
log: sinon.stub(),
warn: sinon.stub()
}),
'./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}`
}
}
}
}
}),
'redis-sharelatex': {
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
},
globals: {
JSON: (this.JSON = JSON)
}
})
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(new Error('redis getDoc exceeded timeout'))
.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(new Errors.NotFoundError('not found'))
.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(
new Errors.OpRangeNotAvailableError(
'doc ops range is not loaded in redis'
)
)
.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(new Error('redis getPreviousDocOps exceeded timeout'))
.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(
new Error(`Version mismatch. '${this.doc_id}' is corrupted.`)
)
.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._stringify = JSON.stringify
this.JSON.stringify = () => '["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 () {
return (this.JSON.stringify = this._stringify)
})
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(new Error('null bytes found in doc lines'))
.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(new Error('ranges are too large'))
.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._stringify = JSON.stringify
this.JSON.stringify = () => '["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 () {
return (this.JSON.stringify = this._stringify)
})
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(new Error('null bytes found in doc lines'))
.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(new Error('ranges are too large'))
.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)
})
})
})
})
})