Files
overleaf-cep/services/document-updater/test/unit/js/HttpController/HttpControllerTests.js
Jimmy Domagala-Tang 994932b8e3 [Web + Doc-Updater] Add track changes accepted notification (#32752)
* 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
2026-04-30 08:06:19 +00:00

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)
})
})
})
})