mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-05 07:09:02 +02:00
b00d1336d4
GitOrigin-RevId: 11e09528c153de6b7766d18c3c90d94962190371
1607 lines
46 KiB
JavaScript
1607 lines
46 KiB
JavaScript
const sinon = require('sinon')
|
|
const SandboxedModule = require('sandboxed-module')
|
|
const path = require('path')
|
|
const { ObjectId } = require('mongodb-legacy')
|
|
const modulePath = path.join(
|
|
__dirname,
|
|
'../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler'
|
|
)
|
|
|
|
describe('DocumentUpdaterHandler', function () {
|
|
beforeEach(function () {
|
|
this.project_id = 'project-id-923'
|
|
this.projectHistoryId = 'ol-project-id-1'
|
|
this.doc_id = 'doc-id-394'
|
|
this.lines = ['one', 'two', 'three']
|
|
this.version = 42
|
|
this.user_id = 'mock-user-id-123'
|
|
this.project = { _id: this.project_id }
|
|
|
|
this.request = sinon.stub()
|
|
this.projectEntityHandler = {}
|
|
this.settings = {
|
|
apis: {
|
|
documentupdater: {
|
|
url: 'http://document_updater.example.com',
|
|
},
|
|
project_history: {
|
|
url: 'http://project_history.example.com',
|
|
},
|
|
},
|
|
}
|
|
this.source = 'dropbox'
|
|
|
|
this.callback = sinon.stub()
|
|
this.handler = SandboxedModule.require(modulePath, {
|
|
requires: {
|
|
request: {
|
|
defaults: () => {
|
|
return this.request
|
|
},
|
|
},
|
|
'@overleaf/settings': this.settings,
|
|
'../Project/ProjectEntityHandler': this.projectEntityHandler,
|
|
'../../models/Project': {
|
|
Project: (this.Project = {}),
|
|
},
|
|
'../Project/ProjectGetter': (this.ProjectGetter = {
|
|
getProjectWithoutLock: sinon.stub(),
|
|
}),
|
|
'../../Features/Project/ProjectLocator': {},
|
|
'@overleaf/metrics': {
|
|
Timer: class {
|
|
done() {}
|
|
},
|
|
},
|
|
'../FileStore/FileStoreHandler': {
|
|
_buildUrl: sinon.stub().callsFake((projectId, fileId) => {
|
|
return `http://filestore/project/${projectId}/file/${fileId}`
|
|
}),
|
|
},
|
|
},
|
|
})
|
|
this.ProjectGetter.getProjectWithoutLock
|
|
.withArgs(this.project_id)
|
|
.yields(null, this.project)
|
|
})
|
|
|
|
describe('flushProjectToMongo', function () {
|
|
describe('successfully', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(1, null, { statusCode: 204 }, '')
|
|
this.handler.flushProjectToMongo(this.project_id, this.callback)
|
|
})
|
|
|
|
it('should flush the document from the document updater', function () {
|
|
this.request
|
|
.calledWithMatch({
|
|
url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/flush`,
|
|
method: 'POST',
|
|
})
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should call the callback with no error', function () {
|
|
this.callback.calledWith(null).should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the document updater API returns an error', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(
|
|
1,
|
|
new Error('something went wrong'),
|
|
null,
|
|
null
|
|
)
|
|
this.handler.flushProjectToMongo(this.project_id, this.callback)
|
|
})
|
|
|
|
it('should return an error to the callback', function () {
|
|
this.callback
|
|
.calledWith(sinon.match.instanceOf(Error))
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the document updater returns a failure error code', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(1, null, { statusCode: 500 }, '')
|
|
this.handler.flushProjectToMongo(this.project_id, this.callback)
|
|
})
|
|
|
|
it('should return the callback with an error', function () {
|
|
this.callback
|
|
.calledWith(
|
|
sinon.match
|
|
.instanceOf(Error)
|
|
.and(
|
|
sinon.match.has(
|
|
'message',
|
|
'document updater returned a failure status code: 500'
|
|
)
|
|
)
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('flushProjectToMongoAndDelete', function () {
|
|
describe('successfully', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(1, null, { statusCode: 204 }, '')
|
|
this.handler.flushProjectToMongoAndDelete(
|
|
this.project_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should delete the project from the document updater', function () {
|
|
this.request
|
|
.calledWithMatch({
|
|
url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}`,
|
|
method: 'DELETE',
|
|
})
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should call the callback with no error', function () {
|
|
this.callback.calledWith(null).should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the document updater API returns an error', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(
|
|
1,
|
|
new Error('something went wrong'),
|
|
null,
|
|
null
|
|
)
|
|
this.handler.flushProjectToMongoAndDelete(
|
|
this.project_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should return an error to the callback', function () {
|
|
this.callback
|
|
.calledWith(sinon.match.instanceOf(Error))
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the document updater returns a failure error code', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(1, null, { statusCode: 500 }, '')
|
|
this.handler.flushProjectToMongoAndDelete(
|
|
this.project_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should return the callback with an error', function () {
|
|
this.callback
|
|
.calledWith(
|
|
sinon.match
|
|
.instanceOf(Error)
|
|
.and(
|
|
sinon.match.has(
|
|
'message',
|
|
'document updater returned a failure status code: 500'
|
|
)
|
|
)
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('flushDocToMongo', function () {
|
|
describe('successfully', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(1, null, { statusCode: 204 }, '')
|
|
this.handler.flushDocToMongo(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should flush the document from the document updater', function () {
|
|
this.request
|
|
.calledWithMatch({
|
|
url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/flush`,
|
|
method: 'POST',
|
|
})
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should call the callback with no error', function () {
|
|
this.callback.calledWith(null).should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the document updater API returns an error', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(
|
|
1,
|
|
new Error('something went wrong'),
|
|
null,
|
|
null
|
|
)
|
|
this.handler.flushDocToMongo(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should return an error to the callback', function () {
|
|
this.callback
|
|
.calledWith(sinon.match.instanceOf(Error))
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the document updater returns a failure error code', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(1, null, { statusCode: 500 }, '')
|
|
this.handler.flushDocToMongo(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should return the callback with an error', function () {
|
|
this.callback
|
|
.calledWith(
|
|
sinon.match
|
|
.instanceOf(Error)
|
|
.and(
|
|
sinon.match.has(
|
|
'message',
|
|
'document updater returned a failure status code: 500'
|
|
)
|
|
)
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('deleteDoc', function () {
|
|
describe('successfully', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(1, null, { statusCode: 204 }, '')
|
|
this.handler.deleteDoc(this.project_id, this.doc_id, this.callback)
|
|
})
|
|
|
|
it('should delete the document from the document updater', function () {
|
|
this.request
|
|
.calledWithMatch({
|
|
url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}`,
|
|
method: 'DELETE',
|
|
})
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should call the callback with no error', function () {
|
|
this.callback.calledWith(null).should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the document updater API returns an error', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(
|
|
1,
|
|
new Error('something went wrong'),
|
|
null,
|
|
null
|
|
)
|
|
this.handler.deleteDoc(this.project_id, this.doc_id, this.callback)
|
|
})
|
|
|
|
it('should return an error to the callback', function () {
|
|
this.callback
|
|
.calledWith(sinon.match.instanceOf(Error))
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the document updater returns a failure error code', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(1, null, { statusCode: 500 }, '')
|
|
this.handler.deleteDoc(this.project_id, this.doc_id, this.callback)
|
|
})
|
|
|
|
it('should return the callback with an error', function () {
|
|
this.callback
|
|
.calledWith(
|
|
sinon.match
|
|
.instanceOf(Error)
|
|
.and(
|
|
sinon.match.has(
|
|
'message',
|
|
'document updater returned a failure status code: 500'
|
|
)
|
|
)
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe("with 'ignoreFlushErrors' option", function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(1, null, { statusCode: 204 }, '')
|
|
})
|
|
|
|
it('when option is true, should send a `ignore_flush_errors=true` URL query to document-updater', function () {
|
|
this.handler.deleteDoc(
|
|
this.project_id,
|
|
this.doc_id,
|
|
true,
|
|
this.callback
|
|
)
|
|
this.request
|
|
.calledWithMatch({
|
|
url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}?ignore_flush_errors=true`,
|
|
method: 'DELETE',
|
|
})
|
|
.should.equal(true)
|
|
})
|
|
|
|
it("when option is false, shouldn't send any URL query to document-updater", function () {
|
|
this.handler.deleteDoc(
|
|
this.project_id,
|
|
this.doc_id,
|
|
false,
|
|
this.callback
|
|
)
|
|
this.request
|
|
.calledWithMatch({
|
|
url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}`,
|
|
method: 'DELETE',
|
|
})
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('setDocument', function () {
|
|
describe('successfully', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(1, null, { statusCode: 204 }, '')
|
|
this.handler.setDocument(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.user_id,
|
|
this.lines,
|
|
this.source,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should set the document in the document updater', function () {
|
|
this.request
|
|
.calledWith({
|
|
url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}`,
|
|
json: {
|
|
lines: this.lines,
|
|
source: this.source,
|
|
user_id: this.user_id,
|
|
},
|
|
method: 'POST',
|
|
timeout: 30 * 1000,
|
|
})
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should call the callback with no error', function () {
|
|
this.callback.calledWith(null).should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the document updater API returns an error', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(
|
|
1,
|
|
new Error('something went wrong'),
|
|
null,
|
|
null
|
|
)
|
|
this.handler.setDocument(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.user_id,
|
|
this.lines,
|
|
this.source,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should return an error to the callback', function () {
|
|
this.callback
|
|
.calledWith(sinon.match.instanceOf(Error))
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the document updater returns a failure error code', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(1, null, { statusCode: 500 }, '')
|
|
this.handler.setDocument(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.user_id,
|
|
this.lines,
|
|
this.source,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should return the callback with an error', function () {
|
|
this.callback
|
|
.calledWith(
|
|
sinon.match
|
|
.instanceOf(Error)
|
|
.and(
|
|
sinon.match.has(
|
|
'message',
|
|
'document updater returned a failure status code: 500'
|
|
)
|
|
)
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('getDocument', function () {
|
|
describe('successfully', function () {
|
|
beforeEach(function () {
|
|
this.body = {
|
|
lines: this.lines,
|
|
version: this.version,
|
|
ops: (this.ops = ['mock-op-1', 'mock-op-2']),
|
|
ranges: (this.ranges = { mock: 'ranges' }),
|
|
}
|
|
this.fromVersion = 2
|
|
this.request.callsArgWith(1, null, { statusCode: 200 }, this.body)
|
|
this.handler.getDocument(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.fromVersion,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should get the document from the document updater', function () {
|
|
this.request
|
|
.calledWith({
|
|
url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}?fromVersion=${this.fromVersion}`,
|
|
method: 'GET',
|
|
json: true,
|
|
timeout: 30 * 1000,
|
|
})
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should call the callback with the lines and version', function () {
|
|
this.callback
|
|
.calledWith(null, this.lines, this.version, this.ranges, this.ops)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the document updater API returns an error', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(
|
|
1,
|
|
new Error('something went wrong'),
|
|
null,
|
|
null
|
|
)
|
|
this.handler.getDocument(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.fromVersion,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should return an error to the callback', function () {
|
|
this.callback
|
|
.calledWith(sinon.match.instanceOf(Error))
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the document updater returns a failure error code', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(1, null, { statusCode: 500 }, '')
|
|
this.handler.getDocument(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.fromVersion,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should return the callback with an error', function () {
|
|
this.callback
|
|
.calledWith(
|
|
sinon.match
|
|
.instanceOf(Error)
|
|
.and(
|
|
sinon.match.has(
|
|
'message',
|
|
'document updater returned a failure status code: 500'
|
|
)
|
|
)
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('getProjectDocsIfMatch', function () {
|
|
beforeEach(function () {
|
|
this.project_state_hash = '1234567890abcdef'
|
|
})
|
|
|
|
describe('successfully', function () {
|
|
beforeEach(function () {
|
|
this.doc0 = {
|
|
_id: this.doc_id,
|
|
lines: this.lines,
|
|
v: this.version,
|
|
}
|
|
this.docs = [this.doc0, this.doc0, this.doc0]
|
|
this.body = JSON.stringify(this.docs)
|
|
this.request.post = sinon
|
|
.stub()
|
|
.callsArgWith(1, null, { statusCode: 200 }, this.body)
|
|
this.handler.getProjectDocsIfMatch(
|
|
this.project_id,
|
|
this.project_state_hash,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should get the documents from the document updater', function () {
|
|
const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}/get_and_flush_if_old?state=${this.project_state_hash}`
|
|
this.request.post.calledWith(url).should.equal(true)
|
|
})
|
|
|
|
it('should call the callback with the documents', function () {
|
|
this.callback.calledWithExactly(null, this.docs).should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the document updater API returns an error', function () {
|
|
beforeEach(function () {
|
|
this.request.post = sinon
|
|
.stub()
|
|
.callsArgWith(1, new Error('something went wrong'), null, null)
|
|
this.handler.getProjectDocsIfMatch(
|
|
this.project_id,
|
|
this.project_state_hash,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should return an error to the callback', function () {
|
|
this.callback
|
|
.calledWith(sinon.match.instanceOf(Error))
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the document updater returns a conflict error code', function () {
|
|
beforeEach(function () {
|
|
this.request.post = sinon
|
|
.stub()
|
|
.callsArgWith(1, null, { statusCode: 409 }, 'Conflict')
|
|
this.handler.getProjectDocsIfMatch(
|
|
this.project_id,
|
|
this.project_state_hash,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should return the callback with no documents', function () {
|
|
this.callback.alwaysCalledWithExactly().should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('clearProjectState', function () {
|
|
describe('successfully', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(1, null, { statusCode: 200 })
|
|
this.handler.clearProjectState(this.project_id, this.callback)
|
|
})
|
|
|
|
it('should clear the project state from the document updater', function () {
|
|
this.request
|
|
.calledWithMatch({
|
|
url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/clearState`,
|
|
method: 'POST',
|
|
})
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should call the callback', function () {
|
|
this.callback.calledWith(null).should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the document updater API returns an error', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(
|
|
1,
|
|
new Error('something went wrong'),
|
|
null,
|
|
null
|
|
)
|
|
this.handler.clearProjectState(this.project_id, this.callback)
|
|
})
|
|
|
|
it('should return an error to the callback', function () {
|
|
this.callback
|
|
.calledWith(sinon.match.instanceOf(Error))
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the document updater returns an error code', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(1, null, { statusCode: 500 }, null)
|
|
this.handler.clearProjectState(this.project_id, this.callback)
|
|
})
|
|
|
|
it('should return the callback with no documents', function () {
|
|
this.callback
|
|
.calledWith(
|
|
sinon.match
|
|
.instanceOf(Error)
|
|
.and(
|
|
sinon.match.has(
|
|
'message',
|
|
'document updater returned a failure status code: 500'
|
|
)
|
|
)
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('acceptChanges', function () {
|
|
beforeEach(function () {
|
|
this.change_id = 'mock-change-id-1'
|
|
})
|
|
|
|
describe('successfully', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(1, null, { statusCode: 200 }, this.body)
|
|
this.handler.acceptChanges(
|
|
this.project_id,
|
|
this.doc_id,
|
|
[this.change_id],
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should accept the change in the document updater', function () {
|
|
this.request
|
|
.calledWith({
|
|
url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/change/accept`,
|
|
json: {
|
|
change_ids: [this.change_id],
|
|
},
|
|
method: 'POST',
|
|
timeout: 30 * 1000,
|
|
})
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should call the callback', function () {
|
|
this.callback.calledWith(null).should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the document updater API returns an error', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(
|
|
1,
|
|
new Error('something went wrong'),
|
|
null,
|
|
null
|
|
)
|
|
this.handler.acceptChanges(
|
|
this.project_id,
|
|
this.doc_id,
|
|
[this.change_id],
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should return an error to the callback', function () {
|
|
this.callback
|
|
.calledWith(sinon.match.instanceOf(Error))
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the document updater returns a failure error code', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(1, null, { statusCode: 500 }, '')
|
|
this.handler.acceptChanges(
|
|
this.project_id,
|
|
this.doc_id,
|
|
[this.change_id],
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should return the callback with an error', function () {
|
|
this.callback
|
|
.calledWith(
|
|
sinon.match
|
|
.instanceOf(Error)
|
|
.and(
|
|
sinon.match.has(
|
|
'message',
|
|
'document updater returned a failure status code: 500'
|
|
)
|
|
)
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('deleteThread', function () {
|
|
beforeEach(function () {
|
|
this.thread_id = 'mock-thread-id-1'
|
|
})
|
|
|
|
describe('successfully', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(1, null, { statusCode: 200 }, this.body)
|
|
this.handler.deleteThread(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.thread_id,
|
|
this.user_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should delete the thread in the document updater', function () {
|
|
this.request
|
|
.calledWithMatch({
|
|
url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}`,
|
|
method: 'DELETE',
|
|
})
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should call the callback', function () {
|
|
this.callback.calledWith(null).should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the document updater API returns an error', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(
|
|
1,
|
|
new Error('something went wrong'),
|
|
null,
|
|
null
|
|
)
|
|
this.handler.deleteThread(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.thread_id,
|
|
this.user_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should return an error to the callback', function () {
|
|
this.callback
|
|
.calledWith(sinon.match.instanceOf(Error))
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the document updater returns a failure error code', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(1, null, { statusCode: 500 }, '')
|
|
this.handler.deleteThread(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.thread_id,
|
|
this.user_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should return the callback with an error', function () {
|
|
this.callback
|
|
.calledWith(
|
|
sinon.match
|
|
.instanceOf(Error)
|
|
.and(
|
|
sinon.match.has(
|
|
'message',
|
|
'document updater returned a failure status code: 500'
|
|
)
|
|
)
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('resolveThread', function () {
|
|
beforeEach(function () {
|
|
this.thread_id = 'mock-thread-id-1'
|
|
})
|
|
|
|
describe('successfully', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(1, null, { statusCode: 200 }, this.body)
|
|
this.handler.resolveThread(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.thread_id,
|
|
this.user_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should resolve the thread in the document updater', function () {
|
|
this.request
|
|
.calledWithMatch({
|
|
url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}/resolve`,
|
|
method: 'POST',
|
|
})
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should call the callback', function () {
|
|
this.callback.calledWith(null).should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the document updater API returns an error', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(
|
|
1,
|
|
new Error('something went wrong'),
|
|
null,
|
|
null
|
|
)
|
|
this.handler.resolveThread(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.thread_id,
|
|
this.user_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should return an error to the callback', function () {
|
|
this.callback
|
|
.calledWith(sinon.match.instanceOf(Error))
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the document updater returns a failure error code', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(1, null, { statusCode: 500 }, '')
|
|
this.handler.resolveThread(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.thread_id,
|
|
this.user_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should return the callback with an error', function () {
|
|
this.callback
|
|
.calledWith(
|
|
sinon.match
|
|
.instanceOf(Error)
|
|
.and(
|
|
sinon.match.has(
|
|
'message',
|
|
'document updater returned a failure status code: 500'
|
|
)
|
|
)
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('reopenThread', function () {
|
|
beforeEach(function () {
|
|
this.thread_id = 'mock-thread-id-1'
|
|
})
|
|
|
|
describe('successfully', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(1, null, { statusCode: 200 }, this.body)
|
|
this.handler.reopenThread(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.thread_id,
|
|
this.user_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should reopen the thread in the document updater', function () {
|
|
this.request
|
|
.calledWithMatch({
|
|
url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}/reopen`,
|
|
method: 'POST',
|
|
})
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should call the callback', function () {
|
|
this.callback.calledWith(null).should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the document updater API returns an error', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(
|
|
1,
|
|
new Error('something went wrong'),
|
|
null,
|
|
null
|
|
)
|
|
this.handler.reopenThread(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.thread_id,
|
|
this.user_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should return an error to the callback', function () {
|
|
this.callback
|
|
.calledWith(sinon.match.instanceOf(Error))
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the document updater returns a failure error code', function () {
|
|
beforeEach(function () {
|
|
this.request.callsArgWith(1, null, { statusCode: 500 }, '')
|
|
this.handler.reopenThread(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.thread_id,
|
|
this.user_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should return the callback with an error', function () {
|
|
this.callback
|
|
.calledWith(
|
|
sinon.match
|
|
.instanceOf(Error)
|
|
.and(
|
|
sinon.match.has(
|
|
'message',
|
|
'document updater returned a failure status code: 500'
|
|
)
|
|
)
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('updateProjectStructure ', function () {
|
|
beforeEach(function () {
|
|
this.user_id = 1234
|
|
this.version = 999
|
|
})
|
|
|
|
describe('with project history disabled', function () {
|
|
beforeEach(function () {
|
|
this.settings.apis.project_history.sendProjectStructureOps = false
|
|
this.handler.updateProjectStructure(
|
|
this.project_id,
|
|
this.projectHistoryId,
|
|
this.user_id,
|
|
{},
|
|
this.source,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('does not make a web request', function () {
|
|
this.request.called.should.equal(false)
|
|
})
|
|
|
|
it('calls the callback', function () {
|
|
this.callback.called.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('with project history enabled', function () {
|
|
beforeEach(function () {
|
|
this.settings.apis.project_history.sendProjectStructureOps = true
|
|
this.url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}`
|
|
this.request.callsArgWith(1, null, { statusCode: 204 }, '')
|
|
})
|
|
|
|
describe('when an entity has changed name', function () {
|
|
it('should send the structure update to the document updater', function (done) {
|
|
this.docIdA = new ObjectId()
|
|
this.docIdB = new ObjectId()
|
|
this.changes = {
|
|
oldDocs: [
|
|
{ path: '/old_a', doc: { _id: this.docIdA } },
|
|
{ path: '/old_b', doc: { _id: this.docIdB } },
|
|
],
|
|
// create new instances of the same ObjectIds so that == doesn't pass
|
|
newDocs: [
|
|
{
|
|
path: '/old_a',
|
|
doc: { _id: new ObjectId(this.docIdA.toString()) },
|
|
},
|
|
{
|
|
path: '/new_b',
|
|
doc: { _id: new ObjectId(this.docIdB.toString()) },
|
|
},
|
|
],
|
|
newProject: { version: this.version },
|
|
}
|
|
|
|
const updates = [
|
|
{
|
|
type: 'rename-doc',
|
|
id: this.docIdB.toString(),
|
|
pathname: '/old_b',
|
|
newPathname: '/new_b',
|
|
},
|
|
]
|
|
|
|
this.handler.updateProjectStructure(
|
|
this.project_id,
|
|
this.projectHistoryId,
|
|
this.user_id,
|
|
this.changes,
|
|
this.source,
|
|
() => {
|
|
this.request
|
|
.calledWith({
|
|
url: this.url,
|
|
method: 'POST',
|
|
json: {
|
|
updates,
|
|
userId: this.user_id,
|
|
version: this.version,
|
|
projectHistoryId: this.projectHistoryId,
|
|
source: this.source,
|
|
},
|
|
timeout: 30 * 1000,
|
|
})
|
|
.should.equal(true)
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when a doc has been added', function () {
|
|
it('should send the structure update to the document updater', function (done) {
|
|
this.docId = new ObjectId()
|
|
this.changes = {
|
|
newDocs: [
|
|
{ path: '/foo', docLines: 'a\nb', doc: { _id: this.docId } },
|
|
],
|
|
newProject: { version: this.version },
|
|
}
|
|
|
|
const updates = [
|
|
{
|
|
type: 'add-doc',
|
|
id: this.docId.toString(),
|
|
pathname: '/foo',
|
|
docLines: 'a\nb',
|
|
historyRangesSupport: false,
|
|
url: undefined,
|
|
hash: undefined,
|
|
ranges: undefined,
|
|
metadata: undefined,
|
|
},
|
|
]
|
|
|
|
this.handler.updateProjectStructure(
|
|
this.project_id,
|
|
this.projectHistoryId,
|
|
this.user_id,
|
|
this.changes,
|
|
this.source,
|
|
() => {
|
|
this.request.should.have.been.calledWith({
|
|
url: this.url,
|
|
method: 'POST',
|
|
json: {
|
|
updates,
|
|
userId: this.user_id,
|
|
version: this.version,
|
|
projectHistoryId: this.projectHistoryId,
|
|
source: this.source,
|
|
},
|
|
timeout: 30 * 1000,
|
|
})
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when a file has been added', function () {
|
|
it('should send the structure update to the document updater', function (done) {
|
|
this.fileId = new ObjectId()
|
|
this.changes = {
|
|
newFiles: [
|
|
{
|
|
path: '/bar',
|
|
url: 'filestore.example.com/file',
|
|
file: { _id: this.fileId, hash: '12345' },
|
|
},
|
|
],
|
|
newProject: { version: this.version },
|
|
}
|
|
|
|
const updates = [
|
|
{
|
|
type: 'add-file',
|
|
id: this.fileId.toString(),
|
|
pathname: '/bar',
|
|
url: 'filestore.example.com/file',
|
|
docLines: undefined,
|
|
historyRangesSupport: false,
|
|
hash: '12345',
|
|
ranges: undefined,
|
|
metadata: undefined,
|
|
},
|
|
]
|
|
|
|
this.handler.updateProjectStructure(
|
|
this.project_id,
|
|
this.projectHistoryId,
|
|
this.user_id,
|
|
this.changes,
|
|
this.source,
|
|
() => {
|
|
this.request.should.have.been.calledWith({
|
|
url: this.url,
|
|
method: 'POST',
|
|
json: {
|
|
updates,
|
|
userId: this.user_id,
|
|
version: this.version,
|
|
projectHistoryId: this.projectHistoryId,
|
|
source: this.source,
|
|
},
|
|
timeout: 30 * 1000,
|
|
})
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when an entity has been deleted', function () {
|
|
it('should end the structure update to the document updater', function (done) {
|
|
this.docId = new ObjectId()
|
|
this.changes = {
|
|
oldDocs: [
|
|
{ path: '/foo', docLines: 'a\nb', doc: { _id: this.docId } },
|
|
],
|
|
newProject: { version: this.version },
|
|
}
|
|
|
|
const updates = [
|
|
{
|
|
type: 'rename-doc',
|
|
id: this.docId.toString(),
|
|
pathname: '/foo',
|
|
newPathname: '',
|
|
},
|
|
]
|
|
|
|
this.handler.updateProjectStructure(
|
|
this.project_id,
|
|
this.projectHistoryId,
|
|
this.user_id,
|
|
this.changes,
|
|
this.source,
|
|
() => {
|
|
this.request.should.have.been.calledWith({
|
|
url: this.url,
|
|
method: 'POST',
|
|
json: {
|
|
updates,
|
|
userId: this.user_id,
|
|
version: this.version,
|
|
projectHistoryId: this.projectHistoryId,
|
|
source: this.source,
|
|
},
|
|
timeout: 30 * 1000,
|
|
})
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when a file is converted to a doc', function () {
|
|
it('should send the delete first', function (done) {
|
|
this.docId = new ObjectId()
|
|
this.fileId = new ObjectId()
|
|
this.changes = {
|
|
oldFiles: [
|
|
{
|
|
path: '/foo.doc',
|
|
url: 'filestore.example.com/file',
|
|
file: { _id: this.fileId },
|
|
},
|
|
],
|
|
newDocs: [
|
|
{
|
|
path: '/foo.doc',
|
|
docLines: 'hello there',
|
|
doc: { _id: this.docId },
|
|
},
|
|
],
|
|
newProject: { version: this.version },
|
|
}
|
|
|
|
const updates = [
|
|
{
|
|
type: 'rename-file',
|
|
id: this.fileId.toString(),
|
|
pathname: '/foo.doc',
|
|
newPathname: '',
|
|
},
|
|
{
|
|
type: 'add-doc',
|
|
id: this.docId.toString(),
|
|
pathname: '/foo.doc',
|
|
docLines: 'hello there',
|
|
historyRangesSupport: false,
|
|
url: undefined,
|
|
hash: undefined,
|
|
ranges: undefined,
|
|
metadata: undefined,
|
|
},
|
|
]
|
|
|
|
this.handler.updateProjectStructure(
|
|
this.project_id,
|
|
this.projectHistoryId,
|
|
this.user_id,
|
|
this.changes,
|
|
this.source,
|
|
() => {
|
|
this.request.should.have.been.calledWith({
|
|
url: this.url,
|
|
method: 'POST',
|
|
json: {
|
|
updates,
|
|
userId: this.user_id,
|
|
version: this.version,
|
|
projectHistoryId: this.projectHistoryId,
|
|
source: this.source,
|
|
},
|
|
timeout: 30 * 1000,
|
|
})
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when the project version is missing', function () {
|
|
it('should call the callback with an error', function () {
|
|
this.docId = new ObjectId()
|
|
this.changes = {
|
|
oldDocs: [
|
|
{ path: '/foo', docLines: 'a\nb', doc: { _id: this.docId } },
|
|
],
|
|
}
|
|
|
|
this.handler.updateProjectStructure(
|
|
this.project_id,
|
|
this.projectHistoryId,
|
|
this.user_id,
|
|
this.changes,
|
|
this.source,
|
|
this.callback
|
|
)
|
|
|
|
this.callback
|
|
.calledWith(sinon.match.instanceOf(Error))
|
|
.should.equal(true)
|
|
const firstCallArgs = this.callback.args[0]
|
|
firstCallArgs[0].message.should.equal(
|
|
'did not receive project version in changes'
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when ranges are present', function () {
|
|
beforeEach(function () {
|
|
this.docId = new ObjectId()
|
|
this.ranges = {
|
|
changes: [
|
|
{
|
|
op: { p: 0, i: 'foo' },
|
|
metadata: { ts: '2024-01-01T00:00:00.000Z', user_id: 'user-1' },
|
|
},
|
|
{
|
|
op: { p: 7, d: ' baz' },
|
|
metadata: { ts: '2024-02-01T00:00:00.000Z', user_id: 'user-1' },
|
|
},
|
|
],
|
|
comments: [
|
|
{
|
|
op: { p: 4, c: 'bar', t: 'comment-1' },
|
|
metadata: { resolved: false },
|
|
},
|
|
],
|
|
}
|
|
this.changes = {
|
|
newDocs: [
|
|
{
|
|
path: '/foo',
|
|
docLines: 'foo\nbar',
|
|
doc: { _id: this.docId },
|
|
ranges: this.ranges,
|
|
},
|
|
],
|
|
newProject: { version: this.version },
|
|
}
|
|
})
|
|
|
|
it('should forward ranges', function (done) {
|
|
const updates = [
|
|
{
|
|
type: 'add-doc',
|
|
id: this.docId.toString(),
|
|
pathname: '/foo',
|
|
docLines: 'foo\nbar',
|
|
historyRangesSupport: false,
|
|
url: undefined,
|
|
hash: undefined,
|
|
ranges: this.ranges,
|
|
metadata: undefined,
|
|
},
|
|
]
|
|
|
|
this.handler.updateProjectStructure(
|
|
this.project_id,
|
|
this.projectHistoryId,
|
|
this.user_id,
|
|
this.changes,
|
|
this.source,
|
|
() => {
|
|
this.request.should.have.been.calledWith({
|
|
url: this.url,
|
|
method: 'POST',
|
|
json: {
|
|
updates,
|
|
userId: this.user_id,
|
|
version: this.version,
|
|
projectHistoryId: this.projectHistoryId,
|
|
source: this.source,
|
|
},
|
|
timeout: 30 * 1000,
|
|
})
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should include flag when history ranges support is enabled', function (done) {
|
|
this.ProjectGetter.getProjectWithoutLock
|
|
.withArgs(this.project_id)
|
|
.yields(null, {
|
|
_id: this.project_id,
|
|
overleaf: { history: { rangesSupportEnabled: true } },
|
|
})
|
|
|
|
const updates = [
|
|
{
|
|
type: 'add-doc',
|
|
id: this.docId.toString(),
|
|
pathname: '/foo',
|
|
docLines: 'foo\nbar',
|
|
historyRangesSupport: true,
|
|
url: undefined,
|
|
hash: undefined,
|
|
ranges: this.ranges,
|
|
metadata: undefined,
|
|
},
|
|
]
|
|
|
|
this.handler.updateProjectStructure(
|
|
this.project_id,
|
|
this.projectHistoryId,
|
|
this.user_id,
|
|
this.changes,
|
|
this.source,
|
|
() => {
|
|
this.request.should.have.been.calledWith({
|
|
url: this.url,
|
|
method: 'POST',
|
|
json: {
|
|
updates,
|
|
userId: this.user_id,
|
|
version: this.version,
|
|
projectHistoryId: this.projectHistoryId,
|
|
source: this.source,
|
|
},
|
|
timeout: 30 * 1000,
|
|
})
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('resyncProjectHistory', function () {
|
|
it('should add docs', function (done) {
|
|
const docId1 = new ObjectId()
|
|
const docId2 = new ObjectId()
|
|
const docs = [
|
|
{ doc: { _id: docId1 }, path: 'main.tex' },
|
|
{ doc: { _id: docId2 }, path: 'references.bib' },
|
|
]
|
|
const files = []
|
|
this.request.yields(null, { statusCode: 200 })
|
|
const projectId = new ObjectId()
|
|
const projectHistoryId = 99
|
|
this.handler.resyncProjectHistory(
|
|
projectId,
|
|
projectHistoryId,
|
|
docs,
|
|
files,
|
|
{},
|
|
() => {
|
|
this.request.should.have.been.calledWith({
|
|
url: `${this.settings.apis.documentupdater.url}/project/${projectId}/history/resync`,
|
|
method: 'POST',
|
|
json: {
|
|
docs: [
|
|
{ doc: docId1, path: 'main.tex' },
|
|
{ doc: docId2, path: 'references.bib' },
|
|
],
|
|
files: [],
|
|
projectHistoryId,
|
|
},
|
|
timeout: 6 * 60 * 1000,
|
|
})
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
it('should add files', function (done) {
|
|
const fileId1 = new ObjectId()
|
|
const fileId2 = new ObjectId()
|
|
const fileId3 = new ObjectId()
|
|
const fileCreated2 = new Date()
|
|
const fileCreated3 = new Date()
|
|
const otherProjectId = new ObjectId().toString()
|
|
const files = [
|
|
{ file: { _id: fileId1, hash: '42' }, path: '1.png' },
|
|
{
|
|
file: {
|
|
_id: fileId2,
|
|
hash: '1337',
|
|
created: fileCreated2,
|
|
linkedFileData: {
|
|
provider: 'references-provider',
|
|
},
|
|
},
|
|
path: '1.bib',
|
|
},
|
|
{
|
|
file: {
|
|
_id: fileId3,
|
|
hash: '21',
|
|
created: fileCreated3,
|
|
linkedFileData: {
|
|
provider: 'project_output_file',
|
|
build_id: '1234-abc',
|
|
clsiServerId: 'server-1',
|
|
source_project_id: otherProjectId,
|
|
source_output_file_path: 'foo/bar.txt',
|
|
},
|
|
},
|
|
path: 'bar.txt',
|
|
},
|
|
]
|
|
const docs = []
|
|
this.request.yields(null, { statusCode: 200 })
|
|
const projectId = new ObjectId()
|
|
const projectHistoryId = 99
|
|
this.handler.resyncProjectHistory(
|
|
projectId,
|
|
projectHistoryId,
|
|
docs,
|
|
files,
|
|
{},
|
|
() => {
|
|
this.request.should.have.been.calledWith({
|
|
url: `${this.settings.apis.documentupdater.url}/project/${projectId}/history/resync`,
|
|
method: 'POST',
|
|
json: {
|
|
docs: [],
|
|
files: [
|
|
{
|
|
file: fileId1,
|
|
_hash: '42',
|
|
path: '1.png',
|
|
url: `http://filestore/project/${projectId}/file/${fileId1}`,
|
|
metadata: undefined,
|
|
},
|
|
{
|
|
file: fileId2,
|
|
_hash: '1337',
|
|
path: '1.bib',
|
|
url: `http://filestore/project/${projectId}/file/${fileId2}`,
|
|
metadata: {
|
|
importedAt: fileCreated2,
|
|
provider: 'references-provider',
|
|
},
|
|
},
|
|
{
|
|
file: fileId3,
|
|
_hash: '21',
|
|
path: 'bar.txt',
|
|
url: `http://filestore/project/${projectId}/file/${fileId3}`,
|
|
metadata: {
|
|
importedAt: fileCreated3,
|
|
provider: 'project_output_file',
|
|
source_project_id: otherProjectId,
|
|
source_output_file_path: 'foo/bar.txt',
|
|
// build_id and clsiServerId are omitted
|
|
},
|
|
},
|
|
],
|
|
projectHistoryId,
|
|
},
|
|
timeout: 6 * 60 * 1000,
|
|
})
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
})
|
|
})
|