mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
* feat: update doc manager to return a list of contributors to the accepted change * feat: add new notification type for accepting a tracked change * update email with tracked changes accepted * feat: update tests * fix: feedback on consistent api and returns * feat: adding new tests * feat: self accepted changes shouldnt trigger notification, and using existing changesAccepted hook * Add better subject and activity list for track change accepted (#33094) * feat: add better activity list entry and subject header for accepted changes, to match other notifications * feat: updating tests * feat: updating accepting_user_id to just user_id * fix: adding users in emailBuilder test to userCache GitOrigin-RevId: 6114f77916b5f503b7bbbb5ca8fed99e58edc31b
1357 lines
38 KiB
JavaScript
1357 lines
38 KiB
JavaScript
const sinon = require('sinon')
|
|
const modulePath = '../../../../app/js/HttpController.js'
|
|
const SandboxedModule = require('sandboxed-module')
|
|
const Errors = require('../../../../app/js/Errors.js')
|
|
|
|
describe('HttpController', function () {
|
|
beforeEach(function () {
|
|
this.project_id = 'project-id-123'
|
|
this.projectHistoryId = '123'
|
|
this.doc_id = 'doc-id-123'
|
|
this.source = 'editor'
|
|
this.next = sinon.stub()
|
|
this.res = {
|
|
send: sinon.stub(),
|
|
sendStatus: sinon.stub(),
|
|
json: sinon.stub(),
|
|
status: sinon.stub().returnsThis(),
|
|
}
|
|
|
|
this.DocumentManager = {
|
|
promises: {
|
|
getDocAndRecentOpsWithLock: sinon.stub(),
|
|
getCommentWithLock: sinon.stub(),
|
|
setDocWithLock: sinon.stub(),
|
|
flushDocIfLoadedWithLock: sinon.stub().resolves(),
|
|
flushAndDeleteDocWithLock: sinon.stub().resolves(),
|
|
acceptChangesWithLock: sinon.stub().resolves(),
|
|
updateCommentStateWithLock: sinon.stub().resolves(),
|
|
deleteCommentWithLock: sinon.stub().resolves(),
|
|
appendToDocWithLock: sinon.stub(),
|
|
},
|
|
}
|
|
|
|
this.HistoryManager = {
|
|
flushProjectChangesAsync: sinon.stub(),
|
|
promises: {
|
|
resyncProjectHistory: sinon.stub().resolves(),
|
|
},
|
|
}
|
|
|
|
this.ProjectHistoryRedisManager = {
|
|
promises: {
|
|
queueOps: sinon.stub().resolves(),
|
|
},
|
|
}
|
|
|
|
this.ProjectManager = {
|
|
promises: {
|
|
flushProjectWithLocks: sinon.stub().resolves(),
|
|
flushAndDeleteProjectWithLocks: sinon.stub().resolves(),
|
|
queueFlushAndDeleteProject: sinon.stub().resolves(),
|
|
getProjectDocsAndFlushIfOld: sinon.stub(),
|
|
updateProjectWithLocks: sinon.stub().resolves(),
|
|
},
|
|
}
|
|
|
|
this.DeleteQueueManager = {}
|
|
|
|
this.RedisManager = {
|
|
DOC_OPS_TTL: 42,
|
|
}
|
|
|
|
this.Metrics = {
|
|
Timer: class Timer {},
|
|
}
|
|
this.Metrics.Timer.prototype.done = sinon.stub()
|
|
|
|
this.Utils = {
|
|
addTrackedDeletesToContent: sinon.stub().returnsArg(0),
|
|
}
|
|
|
|
this.HistoryConversions = {
|
|
toHistoryRanges: sinon.stub().returnsArg(0),
|
|
}
|
|
|
|
this.HttpController = SandboxedModule.require(modulePath, {
|
|
requires: {
|
|
'./DocumentManager': this.DocumentManager,
|
|
'./HistoryManager': this.HistoryManager,
|
|
'./ProjectHistoryRedisManager': this.ProjectHistoryRedisManager,
|
|
'./ProjectManager': this.ProjectManager,
|
|
'./DeleteQueueManager': this.DeleteQueueManager,
|
|
'./RedisManager': this.RedisManager,
|
|
'./Metrics': this.Metrics,
|
|
'./Errors': Errors,
|
|
'./Utils': this.Utils,
|
|
'./HistoryConversions': this.HistoryConversions,
|
|
'@overleaf/settings': { max_doc_length: 2 * 1024 * 1024 },
|
|
},
|
|
})
|
|
})
|
|
|
|
describe('getDoc', function () {
|
|
beforeEach(function () {
|
|
this.lines = ['one', 'two', 'three']
|
|
this.ops = ['mock-op-1', 'mock-op-2']
|
|
this.version = 42
|
|
this.fromVersion = 42
|
|
this.ranges = { changes: 'mock', comments: 'mock' }
|
|
this.pathname = '/a/b/c'
|
|
this.req = {
|
|
params: {
|
|
project_id: this.project_id,
|
|
doc_id: this.doc_id,
|
|
},
|
|
query: {},
|
|
body: {},
|
|
}
|
|
})
|
|
|
|
describe('when the document exists and no recent ops are requested', function () {
|
|
beforeEach(async function () {
|
|
this.DocumentManager.promises.getDocAndRecentOpsWithLock.resolves({
|
|
lines: this.lines,
|
|
version: this.version,
|
|
ops: [],
|
|
ranges: this.ranges,
|
|
pathname: this.pathname,
|
|
projectHistoryId: this.projectHistoryId,
|
|
type: 'sharejs-text-ot',
|
|
})
|
|
await this.HttpController.getDoc(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should get the doc', function () {
|
|
this.DocumentManager.promises.getDocAndRecentOpsWithLock.should.have.been.calledWith(
|
|
this.project_id,
|
|
this.doc_id,
|
|
-1
|
|
)
|
|
})
|
|
|
|
it('should return the doc as JSON', function () {
|
|
this.res.json.should.have.been.calledWith({
|
|
id: this.doc_id,
|
|
lines: this.lines,
|
|
version: this.version,
|
|
ops: [],
|
|
ranges: this.ranges,
|
|
pathname: this.pathname,
|
|
ttlInS: 42,
|
|
type: 'sharejs-text-ot',
|
|
})
|
|
})
|
|
|
|
it('should log the request', function () {
|
|
this.logger.debug
|
|
.calledWith(
|
|
{
|
|
docId: this.doc_id,
|
|
projectId: this.project_id,
|
|
historyRanges: false,
|
|
},
|
|
'getting doc via http'
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should time the request', function () {
|
|
this.Metrics.Timer.prototype.done.called.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when recent ops are requested', function () {
|
|
beforeEach(async function () {
|
|
this.DocumentManager.promises.getDocAndRecentOpsWithLock.resolves({
|
|
lines: this.lines,
|
|
version: this.version,
|
|
ops: this.ops,
|
|
ranges: this.ranges,
|
|
pathname: this.pathname,
|
|
projectHistoryId: this.projectHistoryId,
|
|
type: 'sharejs-text-ot',
|
|
})
|
|
this.req.query = { fromVersion: `${this.fromVersion}` }
|
|
await this.HttpController.getDoc(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should get the doc', function () {
|
|
this.DocumentManager.promises.getDocAndRecentOpsWithLock.should.have.been.calledWith(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.fromVersion
|
|
)
|
|
})
|
|
|
|
it('should return the doc as JSON', function () {
|
|
this.res.json.should.have.been.calledWith({
|
|
id: this.doc_id,
|
|
lines: this.lines,
|
|
version: this.version,
|
|
ops: this.ops,
|
|
ranges: this.ranges,
|
|
pathname: this.pathname,
|
|
ttlInS: 42,
|
|
type: 'sharejs-text-ot',
|
|
})
|
|
})
|
|
|
|
it('should log the request', function () {
|
|
this.logger.debug
|
|
.calledWith(
|
|
{
|
|
docId: this.doc_id,
|
|
projectId: this.project_id,
|
|
historyRanges: false,
|
|
},
|
|
'getting doc via http'
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should time the request', function () {
|
|
this.Metrics.Timer.prototype.done.called.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when historyRanges query param is true', function () {
|
|
beforeEach(async function () {
|
|
this.DocumentManager.promises.getDocAndRecentOpsWithLock.resolves({
|
|
lines: this.lines,
|
|
version: this.version,
|
|
ops: [],
|
|
ranges: this.ranges,
|
|
pathname: this.pathname,
|
|
projectHistoryId: this.projectHistoryId,
|
|
type: 'sharejs-text-ot',
|
|
})
|
|
this.req.query = { historyRanges: 'true' }
|
|
await this.HttpController.getDoc(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should get the doc', function () {
|
|
this.DocumentManager.promises.getDocAndRecentOpsWithLock.should.have.been.calledWith(
|
|
this.project_id,
|
|
this.doc_id,
|
|
-1
|
|
)
|
|
})
|
|
|
|
it('should return the doc as JSON with history ranges processing', function () {
|
|
this.res.json.should.have.been.calledWith({
|
|
id: this.doc_id,
|
|
lines: this.lines,
|
|
version: this.version,
|
|
ops: [],
|
|
ranges: this.ranges,
|
|
pathname: this.pathname,
|
|
ttlInS: 42,
|
|
type: 'sharejs-text-ot',
|
|
})
|
|
})
|
|
|
|
it('should call addTrackedDeletesToContent for history ranges processing', function () {
|
|
this.Utils.addTrackedDeletesToContent.called.should.equal(true)
|
|
})
|
|
|
|
it('should call toHistoryRanges for range conversion', function () {
|
|
this.HistoryConversions.toHistoryRanges.called.should.equal(true)
|
|
})
|
|
|
|
it('should log the request with historyRanges: true', function () {
|
|
this.logger.debug
|
|
.calledWith(
|
|
{
|
|
docId: this.doc_id,
|
|
projectId: this.project_id,
|
|
historyRanges: true,
|
|
},
|
|
'getting doc via http'
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should time the request', function () {
|
|
this.Metrics.Timer.prototype.done.called.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the document does not exist', function () {
|
|
beforeEach(async function () {
|
|
this.DocumentManager.promises.getDocAndRecentOpsWithLock.resolves({
|
|
lines: null,
|
|
version: null,
|
|
})
|
|
await this.HttpController.getDoc(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should call next with NotFoundError', function () {
|
|
this.next
|
|
.calledWith(sinon.match.instanceOf(Errors.NotFoundError))
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when an errors occurs', function () {
|
|
beforeEach(async function () {
|
|
this.DocumentManager.promises.getDocAndRecentOpsWithLock.rejects(
|
|
new Error('oops')
|
|
)
|
|
await this.HttpController.getDoc(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should call next with the error', function () {
|
|
this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('getComment', function () {
|
|
beforeEach(function () {
|
|
this.ranges = {
|
|
changes: 'mock',
|
|
comments: [
|
|
{
|
|
id: 'comment-id-1',
|
|
},
|
|
{
|
|
id: 'comment-id-2',
|
|
},
|
|
],
|
|
}
|
|
this.req = {
|
|
params: {
|
|
project_id: this.project_id,
|
|
doc_id: this.doc_id,
|
|
comment_id: this.comment_id,
|
|
},
|
|
query: {},
|
|
body: {},
|
|
}
|
|
})
|
|
|
|
beforeEach(async function () {
|
|
this.DocumentManager.promises.getCommentWithLock.resolves(
|
|
this.ranges.comments[0]
|
|
)
|
|
await this.HttpController.getComment(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should get the comment', function () {
|
|
this.DocumentManager.promises.getCommentWithLock.should.have.been.calledWith(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.comment_id
|
|
)
|
|
})
|
|
|
|
it('should return the comment as JSON', function () {
|
|
this.res.json
|
|
.calledWith({
|
|
id: 'comment-id-1',
|
|
})
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should log the request', function () {
|
|
this.logger.debug
|
|
.calledWith(
|
|
{
|
|
projectId: this.project_id,
|
|
docId: this.doc_id,
|
|
commentId: this.comment_id,
|
|
},
|
|
'getting comment via http'
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('setDoc', function () {
|
|
beforeEach(function () {
|
|
this.lines = ['one', 'two', 'three']
|
|
this.source = 'dropbox'
|
|
this.user_id = 'user-id-123'
|
|
this.req = {
|
|
headers: {},
|
|
params: {
|
|
project_id: this.project_id,
|
|
doc_id: this.doc_id,
|
|
},
|
|
query: {},
|
|
body: {
|
|
lines: this.lines,
|
|
source: this.source,
|
|
user_id: this.user_id,
|
|
undoing: (this.undoing = true),
|
|
},
|
|
}
|
|
})
|
|
|
|
describe('successfully', function () {
|
|
beforeEach(async function () {
|
|
this.DocumentManager.promises.setDocWithLock.resolves({ rev: '123' })
|
|
await this.HttpController.setDoc(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should set the doc', function () {
|
|
this.DocumentManager.promises.setDocWithLock.should.have.been.calledWith(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.lines,
|
|
this.source,
|
|
this.user_id,
|
|
this.undoing,
|
|
true
|
|
)
|
|
})
|
|
|
|
it('should return a json response with the document rev from web', function () {
|
|
this.res.json.calledWithMatch({ rev: '123' }).should.equal(true)
|
|
})
|
|
|
|
it('should log the request', function () {
|
|
this.logger.debug
|
|
.calledWith(
|
|
{
|
|
docId: this.doc_id,
|
|
projectId: this.project_id,
|
|
lines: this.lines,
|
|
source: this.source,
|
|
userId: this.user_id,
|
|
undoing: this.undoing,
|
|
},
|
|
'setting doc via http'
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should time the request', function () {
|
|
this.Metrics.Timer.prototype.done.called.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when an errors occurs', function () {
|
|
beforeEach(async function () {
|
|
this.DocumentManager.promises.setDocWithLock.rejects(new Error('oops'))
|
|
await this.HttpController.setDoc(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should call next with the error', function () {
|
|
this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the payload is too large', function () {
|
|
beforeEach(async function () {
|
|
const lines = []
|
|
for (let _ = 0; _ <= 200000; _++) {
|
|
lines.push('test test test')
|
|
}
|
|
this.req.body.lines = lines
|
|
this.DocumentManager.promises.setDocWithLock.resolves()
|
|
await this.HttpController.setDoc(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should send back a 406 response', function () {
|
|
this.res.sendStatus.calledWith(406).should.equal(true)
|
|
})
|
|
|
|
it('should not call setDocWithLock', function () {
|
|
this.DocumentManager.promises.setDocWithLock.should.not.have.been.called
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('flushProject', function () {
|
|
beforeEach(function () {
|
|
this.req = {
|
|
params: {
|
|
project_id: this.project_id,
|
|
},
|
|
query: {},
|
|
body: {},
|
|
}
|
|
})
|
|
|
|
describe('successfully', function () {
|
|
beforeEach(async function () {
|
|
await this.HttpController.flushProject(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should flush the project', function () {
|
|
this.ProjectManager.promises.flushProjectWithLocks.should.have.been.calledWith(
|
|
this.project_id
|
|
)
|
|
})
|
|
|
|
it('should return a successful No Content response', function () {
|
|
this.res.sendStatus.calledWith(204).should.equal(true)
|
|
})
|
|
|
|
it('should log the request', function () {
|
|
this.logger.debug
|
|
.calledWith(
|
|
{ projectId: this.project_id },
|
|
'flushing project via http'
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should time the request', function () {
|
|
this.Metrics.Timer.prototype.done.called.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when an errors occurs', function () {
|
|
beforeEach(async function () {
|
|
this.ProjectManager.promises.flushProjectWithLocks.rejects(
|
|
new Error('oops')
|
|
)
|
|
await this.HttpController.flushProject(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should call next with the error', function () {
|
|
this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('flushDocIfLoaded', function () {
|
|
beforeEach(function () {
|
|
this.lines = ['one', 'two', 'three']
|
|
this.version = 42
|
|
this.req = {
|
|
params: {
|
|
project_id: this.project_id,
|
|
doc_id: this.doc_id,
|
|
},
|
|
query: {},
|
|
body: {},
|
|
}
|
|
})
|
|
|
|
describe('successfully', function () {
|
|
beforeEach(async function () {
|
|
await this.HttpController.flushDocIfLoaded(
|
|
this.req,
|
|
this.res,
|
|
this.next
|
|
)
|
|
})
|
|
|
|
it('should flush the doc', function () {
|
|
this.DocumentManager.promises.flushDocIfLoadedWithLock.should.have.been.calledWith(
|
|
this.project_id,
|
|
this.doc_id
|
|
)
|
|
})
|
|
|
|
it('should return a successful No Content response', function () {
|
|
this.res.sendStatus.calledWith(204).should.equal(true)
|
|
})
|
|
|
|
it('should log the request', function () {
|
|
this.logger.debug
|
|
.calledWith(
|
|
{ docId: this.doc_id, projectId: this.project_id },
|
|
'flushing doc via http'
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should time the request', function () {
|
|
this.Metrics.Timer.prototype.done.called.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when an errors occurs', function () {
|
|
beforeEach(async function () {
|
|
this.DocumentManager.promises.flushDocIfLoadedWithLock.rejects(
|
|
new Error('oops')
|
|
)
|
|
await this.HttpController.flushDocIfLoaded(
|
|
this.req,
|
|
this.res,
|
|
this.next
|
|
)
|
|
})
|
|
|
|
it('should call next with the error', function () {
|
|
this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('deleteDoc', function () {
|
|
beforeEach(function () {
|
|
this.req = {
|
|
params: {
|
|
project_id: this.project_id,
|
|
doc_id: this.doc_id,
|
|
},
|
|
query: {},
|
|
body: {},
|
|
}
|
|
})
|
|
|
|
describe('successfully', function () {
|
|
beforeEach(async function () {
|
|
await this.HttpController.deleteDoc(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should flush and delete the doc', function () {
|
|
this.DocumentManager.promises.flushAndDeleteDocWithLock.should.have.been.calledWith(
|
|
this.project_id,
|
|
this.doc_id,
|
|
{ ignoreFlushErrors: false }
|
|
)
|
|
})
|
|
|
|
it('should flush project history', function () {
|
|
this.HistoryManager.flushProjectChangesAsync
|
|
.calledWithExactly(this.project_id)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should return a successful No Content response', function () {
|
|
this.res.sendStatus.calledWith(204).should.equal(true)
|
|
})
|
|
|
|
it('should log the request', function () {
|
|
this.logger.debug
|
|
.calledWith(
|
|
{ docId: this.doc_id, projectId: this.project_id },
|
|
'deleting doc via http'
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should time the request', function () {
|
|
this.Metrics.Timer.prototype.done.called.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('ignoring errors', function () {
|
|
beforeEach(async function () {
|
|
this.req.query.ignore_flush_errors = 'true'
|
|
await this.HttpController.deleteDoc(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should delete the doc', function () {
|
|
this.DocumentManager.promises.flushAndDeleteDocWithLock.should.have.been.calledWith(
|
|
this.project_id,
|
|
this.doc_id,
|
|
{ ignoreFlushErrors: true }
|
|
)
|
|
})
|
|
|
|
it('should return a successful No Content response', function () {
|
|
this.res.sendStatus.calledWith(204).should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when an errors occurs', function () {
|
|
beforeEach(async function () {
|
|
this.DocumentManager.promises.flushAndDeleteDocWithLock.rejects(
|
|
new Error('oops')
|
|
)
|
|
await this.HttpController.deleteDoc(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should flush project history', function () {
|
|
this.HistoryManager.flushProjectChangesAsync
|
|
.calledWithExactly(this.project_id)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should call next with the error', function () {
|
|
this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('deleteProject', function () {
|
|
beforeEach(function () {
|
|
this.req = {
|
|
params: {
|
|
project_id: this.project_id,
|
|
},
|
|
query: {},
|
|
body: {},
|
|
}
|
|
})
|
|
|
|
describe('successfully', function () {
|
|
beforeEach(async function () {
|
|
await this.HttpController.deleteProject(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should delete the project', function () {
|
|
this.ProjectManager.promises.flushAndDeleteProjectWithLocks.should.have.been.calledWith(
|
|
this.project_id
|
|
)
|
|
})
|
|
|
|
it('should return a successful No Content response', function () {
|
|
this.res.sendStatus.calledWith(204).should.equal(true)
|
|
})
|
|
|
|
it('should log the request', function () {
|
|
this.logger.debug
|
|
.calledWith(
|
|
{ projectId: this.project_id },
|
|
'deleting project via http'
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should time the request', function () {
|
|
this.Metrics.Timer.prototype.done.called.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('with the background=true option from realtime', function () {
|
|
beforeEach(async function () {
|
|
this.req.query = { background: true, shutdown: true }
|
|
await this.HttpController.deleteProject(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should queue the flush and delete', function () {
|
|
this.ProjectManager.promises.queueFlushAndDeleteProject.should.have.been.calledWith(
|
|
this.project_id
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when an errors occurs', function () {
|
|
beforeEach(async function () {
|
|
this.ProjectManager.promises.flushAndDeleteProjectWithLocks.rejects(
|
|
new Error('oops')
|
|
)
|
|
await this.HttpController.deleteProject(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should call next with the error', function () {
|
|
this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('acceptChanges', function () {
|
|
beforeEach(function () {
|
|
this.req = {
|
|
params: {
|
|
project_id: this.project_id,
|
|
doc_id: this.doc_id,
|
|
change_id: (this.change_id = 'mock-change-od-1'),
|
|
},
|
|
query: {},
|
|
body: {},
|
|
}
|
|
})
|
|
|
|
describe('successfully with a single change', function () {
|
|
beforeEach(async function () {
|
|
this.changeContributors = ['user-id-1', 'user-id-2']
|
|
this.DocumentManager.promises.acceptChangesWithLock.resolves(
|
|
this.changeContributors
|
|
)
|
|
await this.HttpController.acceptChanges(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should accept the change', function () {
|
|
this.DocumentManager.promises.acceptChangesWithLock.should.have.been.calledWith(
|
|
this.project_id,
|
|
this.doc_id,
|
|
[this.change_id]
|
|
)
|
|
})
|
|
|
|
it('should return a successful 200 with a list of the change contributors', function () {
|
|
this.res.status.should.have.been.calledWith(200)
|
|
this.res.json.should.have.been.calledWith({
|
|
changeContributors: this.changeContributors,
|
|
})
|
|
})
|
|
|
|
it('should log the request', function () {
|
|
this.logger.debug
|
|
.calledWith(
|
|
{ projectId: this.project_id, docId: this.doc_id },
|
|
'accepting 1 changes via http'
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should time the request', function () {
|
|
this.Metrics.Timer.prototype.done.called.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('succesfully with with multiple changes', function () {
|
|
beforeEach(async function () {
|
|
this.change_ids = [
|
|
'mock-change-od-1',
|
|
'mock-change-od-2',
|
|
'mock-change-od-3',
|
|
'mock-change-od-4',
|
|
]
|
|
this.req.body = { change_ids: this.change_ids }
|
|
await this.HttpController.acceptChanges(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should accept the changes in the body payload', function () {
|
|
this.DocumentManager.promises.acceptChangesWithLock.should.have.been.calledWith(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.change_ids
|
|
)
|
|
})
|
|
|
|
it('should log the request with the correct number of changes', function () {
|
|
this.logger.debug
|
|
.calledWith(
|
|
{ projectId: this.project_id, docId: this.doc_id },
|
|
`accepting ${this.change_ids.length} changes via http`
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when an errors occurs', function () {
|
|
beforeEach(async function () {
|
|
this.DocumentManager.promises.acceptChangesWithLock.rejects(
|
|
new Error('oops')
|
|
)
|
|
await this.HttpController.acceptChanges(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should call next with the error', function () {
|
|
this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('resolveComment', function () {
|
|
beforeEach(function () {
|
|
this.user_id = 'user-id-123'
|
|
this.req = {
|
|
params: {
|
|
project_id: this.project_id,
|
|
doc_id: this.doc_id,
|
|
comment_id: (this.comment_id = 'mock-comment-id'),
|
|
},
|
|
query: {},
|
|
body: {
|
|
user_id: this.user_id,
|
|
},
|
|
}
|
|
this.resolved = true
|
|
})
|
|
|
|
describe('successfully', function () {
|
|
beforeEach(async function () {
|
|
await this.HttpController.resolveComment(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should accept the change', function () {
|
|
this.DocumentManager.promises.updateCommentStateWithLock.should.have.been.calledWith(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.comment_id,
|
|
this.user_id,
|
|
this.resolved
|
|
)
|
|
})
|
|
|
|
it('should return a successful No Content response', function () {
|
|
this.res.sendStatus.calledWith(204).should.equal(true)
|
|
})
|
|
|
|
it('should log the request', function () {
|
|
this.logger.debug
|
|
.calledWith(
|
|
{
|
|
projectId: this.project_id,
|
|
docId: this.doc_id,
|
|
commentId: this.comment_id,
|
|
},
|
|
'resolving comment via http'
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when an errors occurs', function () {
|
|
beforeEach(async function () {
|
|
this.DocumentManager.promises.updateCommentStateWithLock.rejects(
|
|
new Error('oops')
|
|
)
|
|
await this.HttpController.resolveComment(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should call next with the error', function () {
|
|
this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('reopenComment', function () {
|
|
beforeEach(function () {
|
|
this.user_id = 'user-id-123'
|
|
this.req = {
|
|
params: {
|
|
project_id: this.project_id,
|
|
doc_id: this.doc_id,
|
|
comment_id: (this.comment_id = 'mock-comment-id'),
|
|
},
|
|
query: {},
|
|
body: {
|
|
user_id: this.user_id,
|
|
},
|
|
}
|
|
this.resolved = false
|
|
})
|
|
|
|
describe('successfully', function () {
|
|
beforeEach(async function () {
|
|
await this.HttpController.reopenComment(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should accept the change', function () {
|
|
this.DocumentManager.promises.updateCommentStateWithLock.should.have.been.calledWith(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.comment_id,
|
|
this.user_id,
|
|
this.resolved
|
|
)
|
|
})
|
|
|
|
it('should return a successful No Content response', function () {
|
|
this.res.sendStatus.calledWith(204).should.equal(true)
|
|
})
|
|
|
|
it('should log the request', function () {
|
|
this.logger.debug
|
|
.calledWith(
|
|
{
|
|
projectId: this.project_id,
|
|
docId: this.doc_id,
|
|
commentId: this.comment_id,
|
|
},
|
|
'reopening comment via http'
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when an errors occurs', function () {
|
|
beforeEach(async function () {
|
|
this.DocumentManager.promises.updateCommentStateWithLock.rejects(
|
|
new Error('oops')
|
|
)
|
|
await this.HttpController.reopenComment(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should call next with the error', function () {
|
|
this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('deleteComment', function () {
|
|
beforeEach(function () {
|
|
this.user_id = 'user-id-123'
|
|
this.req = {
|
|
params: {
|
|
project_id: this.project_id,
|
|
doc_id: this.doc_id,
|
|
comment_id: (this.comment_id = 'mock-comment-id'),
|
|
},
|
|
query: {},
|
|
body: {
|
|
user_id: this.user_id,
|
|
},
|
|
}
|
|
})
|
|
|
|
describe('successfully', function () {
|
|
beforeEach(async function () {
|
|
await this.HttpController.deleteComment(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should accept the change', function () {
|
|
this.DocumentManager.promises.deleteCommentWithLock.should.have.been.calledWith(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.comment_id,
|
|
this.user_id
|
|
)
|
|
})
|
|
|
|
it('should return a successful No Content response', function () {
|
|
this.res.sendStatus.calledWith(204).should.equal(true)
|
|
})
|
|
|
|
it('should log the request', function () {
|
|
this.logger.debug
|
|
.calledWith(
|
|
{
|
|
projectId: this.project_id,
|
|
docId: this.doc_id,
|
|
commentId: this.comment_id,
|
|
},
|
|
'deleting comment via http'
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should time the request', function () {
|
|
this.Metrics.Timer.prototype.done.called.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when an errors occurs', function () {
|
|
beforeEach(async function () {
|
|
this.DocumentManager.promises.deleteCommentWithLock.rejects(
|
|
new Error('oops')
|
|
)
|
|
await this.HttpController.deleteComment(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should call next with the error', function () {
|
|
this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('getProjectDocsAndFlushIfOld', function () {
|
|
beforeEach(function () {
|
|
this.state = '01234567890abcdef'
|
|
this.docs = [
|
|
{ _id: '1234', lines: 'hello', v: 23 },
|
|
{ _id: '4567', lines: 'world', v: 45 },
|
|
]
|
|
this.req = {
|
|
params: {
|
|
project_id: this.project_id,
|
|
},
|
|
query: {
|
|
state: this.state,
|
|
},
|
|
body: {},
|
|
}
|
|
})
|
|
|
|
describe('successfully', function () {
|
|
beforeEach(async function () {
|
|
this.ProjectManager.promises.getProjectDocsAndFlushIfOld.resolves(
|
|
this.docs
|
|
)
|
|
await this.HttpController.getProjectDocsAndFlushIfOld(
|
|
this.req,
|
|
this.res,
|
|
this.next
|
|
)
|
|
})
|
|
|
|
it('should get docs from the project manager', function () {
|
|
this.ProjectManager.promises.getProjectDocsAndFlushIfOld.should.have.been.calledWith(
|
|
this.project_id,
|
|
this.state,
|
|
{}
|
|
)
|
|
})
|
|
|
|
it('should return a successful response', function () {
|
|
this.res.send.calledWith(this.docs).should.equal(true)
|
|
})
|
|
|
|
it('should log the request', function () {
|
|
this.logger.debug
|
|
.calledWith(
|
|
{ projectId: this.project_id, exclude: [] },
|
|
'getting docs via http'
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should log the response', function () {
|
|
this.logger.debug
|
|
.calledWith(
|
|
{ projectId: this.project_id, result: ['1234:23', '4567:45'] },
|
|
'got docs via http'
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should time the request', function () {
|
|
this.Metrics.Timer.prototype.done.called.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when there is a conflict', function () {
|
|
beforeEach(async function () {
|
|
this.ProjectManager.promises.getProjectDocsAndFlushIfOld.rejects(
|
|
new Errors.ProjectStateChangedError('project state changed')
|
|
)
|
|
await this.HttpController.getProjectDocsAndFlushIfOld(
|
|
this.req,
|
|
this.res,
|
|
this.next
|
|
)
|
|
})
|
|
|
|
it('should return an HTTP 409 Conflict response', function () {
|
|
this.res.sendStatus.calledWith(409).should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when an error occurs', function () {
|
|
beforeEach(async function () {
|
|
this.ProjectManager.promises.getProjectDocsAndFlushIfOld.rejects(
|
|
new Error('oops')
|
|
)
|
|
await this.HttpController.getProjectDocsAndFlushIfOld(
|
|
this.req,
|
|
this.res,
|
|
this.next
|
|
)
|
|
})
|
|
|
|
it('should call next with the error', function () {
|
|
this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('updateProject', function () {
|
|
beforeEach(function () {
|
|
this.projectHistoryId = 'history-id-123'
|
|
this.userId = 'user-id-123'
|
|
this.updates = [
|
|
{
|
|
type: 'rename-doc',
|
|
id: 1,
|
|
pathname: 'thesis.tex',
|
|
newPathname: 'book.tex',
|
|
},
|
|
{ type: 'add-doc', id: 2, pathname: 'article.tex', docLines: 'hello' },
|
|
{
|
|
type: 'rename-file',
|
|
id: 3,
|
|
pathname: 'apple.png',
|
|
newPathname: 'banana.png',
|
|
},
|
|
{ type: 'add-file', id: 4, url: 'filestore.example.com/4' },
|
|
]
|
|
this.version = 1234567
|
|
this.req = {
|
|
query: {},
|
|
body: {
|
|
projectHistoryId: this.projectHistoryId,
|
|
userId: this.userId,
|
|
updates: this.updates,
|
|
version: this.version,
|
|
source: this.source,
|
|
},
|
|
params: {
|
|
project_id: this.project_id,
|
|
},
|
|
}
|
|
})
|
|
|
|
describe('successfully', function () {
|
|
beforeEach(async function () {
|
|
await this.HttpController.updateProject(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should accept the change', function () {
|
|
this.ProjectManager.promises.updateProjectWithLocks.should.have.been.calledWith(
|
|
this.project_id,
|
|
this.projectHistoryId,
|
|
this.userId,
|
|
this.updates,
|
|
this.version,
|
|
this.source
|
|
)
|
|
})
|
|
|
|
it('should return a successful No Content response', function () {
|
|
this.res.sendStatus.calledWith(204).should.equal(true)
|
|
})
|
|
|
|
it('should time the request', function () {
|
|
this.Metrics.Timer.prototype.done.called.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when an errors occurs', function () {
|
|
beforeEach(async function () {
|
|
this.ProjectManager.promises.updateProjectWithLocks.rejects(
|
|
new Error('oops')
|
|
)
|
|
await this.HttpController.updateProject(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should call next with the error', function () {
|
|
this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('resyncProjectHistory', function () {
|
|
beforeEach(function () {
|
|
this.projectHistoryId = 'history-id-123'
|
|
this.docs = sinon.stub()
|
|
this.files = sinon.stub()
|
|
this.fileUpdates = sinon.stub()
|
|
this.req = {
|
|
query: {},
|
|
body: {
|
|
projectHistoryId: this.projectHistoryId,
|
|
docs: this.docs,
|
|
files: this.files,
|
|
},
|
|
params: {
|
|
project_id: this.project_id,
|
|
},
|
|
}
|
|
})
|
|
|
|
describe('successfully', function () {
|
|
beforeEach(async function () {
|
|
await this.HttpController.resyncProjectHistory(
|
|
this.req,
|
|
this.res,
|
|
this.next
|
|
)
|
|
})
|
|
|
|
it('should accept the change', function () {
|
|
this.HistoryManager.promises.resyncProjectHistory.should.have.been.calledWith(
|
|
this.project_id,
|
|
this.projectHistoryId,
|
|
this.docs,
|
|
this.files,
|
|
{}
|
|
)
|
|
})
|
|
|
|
it('should return a successful No Content response', function () {
|
|
this.res.sendStatus.should.have.been.calledWith(204)
|
|
})
|
|
})
|
|
|
|
describe('when an errors occurs', function () {
|
|
beforeEach(async function () {
|
|
this.HistoryManager.promises.resyncProjectHistory.rejects(
|
|
new Error('oops')
|
|
)
|
|
await this.HttpController.resyncProjectHistory(
|
|
this.req,
|
|
this.res,
|
|
this.next
|
|
)
|
|
})
|
|
|
|
it('should call next with the error', function () {
|
|
this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('appendToDoc', function () {
|
|
beforeEach(function () {
|
|
this.lines = ['one', 'two', 'three']
|
|
this.source = 'dropbox'
|
|
this.user_id = 'user-id-123'
|
|
this.req = {
|
|
headers: {},
|
|
params: {
|
|
project_id: this.project_id,
|
|
doc_id: this.doc_id,
|
|
},
|
|
query: {},
|
|
body: {
|
|
lines: this.lines,
|
|
source: this.source,
|
|
user_id: this.user_id,
|
|
undoing: (this.undoing = true),
|
|
},
|
|
}
|
|
})
|
|
|
|
describe('successfully', function () {
|
|
beforeEach(async function () {
|
|
this.DocumentManager.promises.appendToDocWithLock.resolves({
|
|
rev: '123',
|
|
})
|
|
await this.HttpController.appendToDoc(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should append to the doc', function () {
|
|
this.DocumentManager.promises.appendToDocWithLock.should.have.been.calledWith(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.lines,
|
|
this.source,
|
|
this.user_id
|
|
)
|
|
})
|
|
|
|
it('should return a json response with the document rev from web', function () {
|
|
this.res.json.calledWithMatch({ rev: '123' }).should.equal(true)
|
|
})
|
|
|
|
it('should log the request', function () {
|
|
this.logger.debug
|
|
.calledWith(
|
|
{
|
|
docId: this.doc_id,
|
|
projectId: this.project_id,
|
|
lines: this.lines,
|
|
source: this.source,
|
|
userId: this.user_id,
|
|
},
|
|
'appending to doc via http'
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should time the request', function () {
|
|
this.Metrics.Timer.prototype.done.called.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when an errors occurs', function () {
|
|
beforeEach(async function () {
|
|
this.DocumentManager.promises.appendToDocWithLock.rejects(
|
|
new Error('oops')
|
|
)
|
|
await this.HttpController.appendToDoc(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should call next with the error', function () {
|
|
this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the payload is too large', function () {
|
|
beforeEach(async function () {
|
|
this.DocumentManager.promises.appendToDocWithLock.rejects(
|
|
new Errors.FileTooLargeError()
|
|
)
|
|
await this.HttpController.appendToDoc(this.req, this.res, this.next)
|
|
})
|
|
|
|
it('should send back a 422 response', function () {
|
|
this.res.sendStatus.calledWith(422).should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
})
|