Merge pull request #135 from overleaf/em-remove-backwards-compat

Remove backwards-compatible project updates API
This commit is contained in:
Eric Mc Sween 2020-05-21 09:04:34 -04:00 committed by GitHub
commit df7f534440
4 changed files with 199 additions and 420 deletions

View file

@ -325,28 +325,13 @@ function deleteComment(req, res, next) {
function updateProject(req, res, next) {
const timer = new Metrics.Timer('http.updateProject')
const projectId = req.params.project_id
const {
projectHistoryId,
userId,
docUpdates,
fileUpdates,
updates,
version
} = req.body
logger.log(
{ projectId, updates, docUpdates, fileUpdates, version },
'updating project via http'
)
const allUpdates = _mergeUpdates(
docUpdates || [],
fileUpdates || [],
updates || []
)
const { projectHistoryId, userId, updates = [], version } = req.body
logger.log({ projectId, updates, version }, 'updating project via http')
ProjectManager.updateProjectWithLocks(
projectId,
projectHistoryId,
userId,
allUpdates,
updates,
version,
(error) => {
timer.done()
@ -416,23 +401,3 @@ function flushQueuedProjects(req, res, next) {
}
})
}
/**
* Merge updates from the previous project update interface (docUpdates +
* fileUpdates) and the new update interface (updates).
*/
function _mergeUpdates(docUpdates, fileUpdates, updates) {
const mergedUpdates = []
for (const update of docUpdates) {
const type = update.docLines != null ? 'add-doc' : 'rename-doc'
mergedUpdates.push({ type, ...update })
}
for (const update of fileUpdates) {
const type = update.url != null ? 'add-file' : 'rename-file'
mergedUpdates.push({ type, ...update })
}
for (const update of updates) {
mergedUpdates.push(update)
}
return mergedUpdates
}

View file

@ -1,21 +1,8 @@
/* eslint-disable
camelcase,
handle-callback-err,
no-return-assign,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const sinon = require('sinon')
const chai = require('chai')
chai.should()
const Settings = require('settings-sharelatex')
const rclient_project_history = require('redis-sharelatex').createClient(
const rclientProjectHistory = require('redis-sharelatex').createClient(
Settings.redis.project_history
)
const ProjectHistoryKeys = Settings.redis.project_history.key_schema
@ -28,46 +15,46 @@ const DocUpdaterApp = require('./helpers/DocUpdaterApp')
describe("Applying updates to a project's structure", function () {
before(function () {
this.user_id = 'user-id-123'
return (this.version = 1234)
this.version = 1234
})
describe('renaming a file', function () {
before(function (done) {
this.project_id = DocUpdaterClient.randomId()
this.fileUpdate = {
type: 'rename-file',
id: DocUpdaterClient.randomId(),
pathname: '/file-path',
newPathname: '/new-file-path'
}
this.fileUpdates = [this.fileUpdate]
return DocUpdaterApp.ensureRunning((error) => {
if (error != null) {
throw error
this.updates = [this.fileUpdate]
DocUpdaterApp.ensureRunning((error) => {
if (error) {
return done(error)
}
return DocUpdaterClient.sendProjectUpdate(
DocUpdaterClient.sendProjectUpdate(
this.project_id,
this.user_id,
[],
this.fileUpdates,
this.updates,
this.version,
(error) => {
if (error != null) {
throw error
if (error) {
return done(error)
}
return setTimeout(done, 200)
setTimeout(done, 200)
}
)
})
})
return it('should push the applied file renames to the project history api', function (done) {
rclient_project_history.lrange(
it('should push the applied file renames to the project history api', function (done) {
rclientProjectHistory.lrange(
ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
0,
-1,
(error, updates) => {
if (error != null) {
throw error
if (error) {
return done(error)
}
const update = JSON.parse(updates[0])
@ -78,21 +65,21 @@ describe("Applying updates to a project's structure", function () {
update.meta.ts.should.be.a('string')
update.version.should.equal(`${this.version}.0`)
return done()
done()
}
)
return null
})
})
describe('renaming a document', function () {
before(function () {
this.docUpdate = {
this.update = {
type: 'rename-doc',
id: DocUpdaterClient.randomId(),
pathname: '/doc-path',
newPathname: '/new-doc-path'
}
return (this.docUpdates = [this.docUpdate])
this.updates = [this.update]
})
describe('when the document is not loaded', function () {
@ -101,112 +88,108 @@ describe("Applying updates to a project's structure", function () {
DocUpdaterClient.sendProjectUpdate(
this.project_id,
this.user_id,
this.docUpdates,
[],
this.updates,
this.version,
(error) => {
if (error != null) {
throw error
if (error) {
return done(error)
}
return setTimeout(done, 200)
setTimeout(done, 200)
}
)
return null
})
return it('should push the applied doc renames to the project history api', function (done) {
rclient_project_history.lrange(
it('should push the applied doc renames to the project history api', function (done) {
rclientProjectHistory.lrange(
ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
0,
-1,
(error, updates) => {
if (error != null) {
throw error
if (error) {
return done(error)
}
const update = JSON.parse(updates[0])
update.doc.should.equal(this.docUpdate.id)
update.doc.should.equal(this.update.id)
update.pathname.should.equal('/doc-path')
update.new_pathname.should.equal('/new-doc-path')
update.meta.user_id.should.equal(this.user_id)
update.meta.ts.should.be.a('string')
update.version.should.equal(`${this.version}.0`)
return done()
done()
}
)
return null
})
})
return describe('when the document is loaded', function () {
describe('when the document is loaded', function () {
before(function (done) {
this.project_id = DocUpdaterClient.randomId()
MockWebApi.insertDoc(this.project_id, this.docUpdate.id, {})
MockWebApi.insertDoc(this.project_id, this.update.id, {})
DocUpdaterClient.preloadDoc(
this.project_id,
this.docUpdate.id,
this.update.id,
(error) => {
if (error != null) {
throw error
if (error) {
return done(error)
}
sinon.spy(MockWebApi, 'getDocument')
return DocUpdaterClient.sendProjectUpdate(
DocUpdaterClient.sendProjectUpdate(
this.project_id,
this.user_id,
this.docUpdates,
[],
this.updates,
this.version,
(error) => {
if (error != null) {
throw error
if (error) {
return done(error)
}
return setTimeout(done, 200)
setTimeout(done, 200)
}
)
}
)
return null
})
after(function () {
return MockWebApi.getDocument.restore()
MockWebApi.getDocument.restore()
})
it('should update the doc', function (done) {
DocUpdaterClient.getDoc(
this.project_id,
this.docUpdate.id,
this.update.id,
(error, res, doc) => {
doc.pathname.should.equal(this.docUpdate.newPathname)
return done()
if (error) {
return done(error)
}
doc.pathname.should.equal(this.update.newPathname)
done()
}
)
return null
})
return it('should push the applied doc renames to the project history api', function (done) {
rclient_project_history.lrange(
it('should push the applied doc renames to the project history api', function (done) {
rclientProjectHistory.lrange(
ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
0,
-1,
(error, updates) => {
if (error != null) {
throw error
if (error) {
return done(error)
}
const update = JSON.parse(updates[0])
update.doc.should.equal(this.docUpdate.id)
update.doc.should.equal(this.update.id)
update.pathname.should.equal('/doc-path')
update.new_pathname.should.equal('/new-doc-path')
update.meta.user_id.should.equal(this.user_id)
update.meta.ts.should.be.a('string')
update.version.should.equal(`${this.version}.0`)
return done()
done()
}
)
return null
})
})
})
@ -214,56 +197,62 @@ describe("Applying updates to a project's structure", function () {
describe('renaming multiple documents and files', function () {
before(function () {
this.docUpdate0 = {
type: 'rename-doc',
id: DocUpdaterClient.randomId(),
pathname: '/doc-path0',
newPathname: '/new-doc-path0'
}
this.docUpdate1 = {
type: 'rename-doc',
id: DocUpdaterClient.randomId(),
pathname: '/doc-path1',
newPathname: '/new-doc-path1'
}
this.docUpdates = [this.docUpdate0, this.docUpdate1]
this.fileUpdate0 = {
type: 'rename-file',
id: DocUpdaterClient.randomId(),
pathname: '/file-path0',
newPathname: '/new-file-path0'
}
this.fileUpdate1 = {
type: 'rename-file',
id: DocUpdaterClient.randomId(),
pathname: '/file-path1',
newPathname: '/new-file-path1'
}
return (this.fileUpdates = [this.fileUpdate0, this.fileUpdate1])
this.updates = [
this.docUpdate0,
this.docUpdate1,
this.fileUpdate0,
this.fileUpdate1
]
})
return describe('when the documents are not loaded', function () {
describe('when the documents are not loaded', function () {
before(function (done) {
this.project_id = DocUpdaterClient.randomId()
DocUpdaterClient.sendProjectUpdate(
this.project_id,
this.user_id,
this.docUpdates,
this.fileUpdates,
this.updates,
this.version,
(error) => {
if (error != null) {
throw error
if (error) {
return done(error)
}
return setTimeout(done, 200)
setTimeout(done, 200)
}
)
return null
})
return it('should push the applied doc renames to the project history api', function (done) {
rclient_project_history.lrange(
it('should push the applied doc renames to the project history api', function (done) {
rclientProjectHistory.lrange(
ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
0,
-1,
(error, updates) => {
if (error != null) {
throw error
if (error) {
return done(error)
}
let update = JSON.parse(updates[0])
@ -298,10 +287,9 @@ describe("Applying updates to a project's structure", function () {
update.meta.ts.should.be.a('string')
update.version.should.equal(`${this.version}.3`)
return done()
done()
}
)
return null
})
})
})
@ -310,35 +298,34 @@ describe("Applying updates to a project's structure", function () {
before(function (done) {
this.project_id = DocUpdaterClient.randomId()
this.fileUpdate = {
type: 'add-file',
id: DocUpdaterClient.randomId(),
pathname: '/file-path',
url: 'filestore.example.com'
}
this.fileUpdates = [this.fileUpdate]
this.updates = [this.fileUpdate]
DocUpdaterClient.sendProjectUpdate(
this.project_id,
this.user_id,
[],
this.fileUpdates,
this.updates,
this.version,
(error) => {
if (error != null) {
throw error
if (error) {
return done(error)
}
return setTimeout(done, 200)
setTimeout(done, 200)
}
)
return null
})
return it('should push the file addition to the project history api', function (done) {
rclient_project_history.lrange(
it('should push the file addition to the project history api', function (done) {
rclientProjectHistory.lrange(
ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
0,
-1,
(error, updates) => {
if (error != null) {
throw error
if (error) {
return done(error)
}
const update = JSON.parse(updates[0])
@ -349,10 +336,9 @@ describe("Applying updates to a project's structure", function () {
update.meta.ts.should.be.a('string')
update.version.should.equal(`${this.version}.0`)
return done()
done()
}
)
return null
})
})
@ -360,35 +346,34 @@ describe("Applying updates to a project's structure", function () {
before(function (done) {
this.project_id = DocUpdaterClient.randomId()
this.docUpdate = {
type: 'add-doc',
id: DocUpdaterClient.randomId(),
pathname: '/file-path',
docLines: 'a\nb'
}
this.docUpdates = [this.docUpdate]
this.updates = [this.docUpdate]
DocUpdaterClient.sendProjectUpdate(
this.project_id,
this.user_id,
this.docUpdates,
[],
this.updates,
this.version,
(error) => {
if (error != null) {
throw error
if (error) {
return done(error)
}
return setTimeout(done, 200)
setTimeout(done, 200)
}
)
return null
})
return it('should push the doc addition to the project history api', function (done) {
rclient_project_history.lrange(
it('should push the doc addition to the project history api', function (done) {
rclientProjectHistory.lrange(
ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
0,
-1,
(error, updates) => {
if (error != null) {
throw error
if (error) {
return done(error)
}
const update = JSON.parse(updates[0])
@ -399,10 +384,9 @@ describe("Applying updates to a project's structure", function () {
update.meta.ts.should.be.a('string')
update.version.should.equal(`${this.version}.0`)
return done()
done()
}
)
return null
})
})
@ -416,6 +400,7 @@ describe("Applying updates to a project's structure", function () {
for (let v = 0; v <= 599; v++) {
// Should flush after 500 ops
updates.push({
type: 'add-doc',
id: DocUpdaterClient.randomId(),
pathname: '/file-' + v,
docLines: 'a\nb'
@ -431,42 +416,39 @@ describe("Applying updates to a project's structure", function () {
projectId,
userId,
updates.slice(0, 250),
[],
this.version0,
function (error) {
if (error != null) {
throw error
if (error) {
return done(error)
}
return DocUpdaterClient.sendProjectUpdate(
DocUpdaterClient.sendProjectUpdate(
projectId,
userId,
updates.slice(250),
[],
this.version1,
(error) => {
if (error != null) {
throw error
if (error) {
return done(error)
}
return setTimeout(done, 2000)
setTimeout(done, 2000)
}
)
}
)
return null
})
after(function () {
return MockProjectHistoryApi.flushProject.restore()
MockProjectHistoryApi.flushProject.restore()
})
return it('should flush project history', function () {
return MockProjectHistoryApi.flushProject
it('should flush project history', function () {
MockProjectHistoryApi.flushProject
.calledWith(this.project_id)
.should.equal(true)
})
})
return describe('with too few updates to flush to the history service', function () {
describe('with too few updates to flush to the history service', function () {
before(function (done) {
this.project_id = DocUpdaterClient.randomId()
this.user_id = DocUpdaterClient.randomId()
@ -477,6 +459,7 @@ describe("Applying updates to a project's structure", function () {
for (let v = 0; v <= 42; v++) {
// Should flush after 500 ops
updates.push({
type: 'add-doc',
id: DocUpdaterClient.randomId(),
pathname: '/file-' + v,
docLines: 'a\nb'
@ -492,36 +475,33 @@ describe("Applying updates to a project's structure", function () {
projectId,
userId,
updates.slice(0, 10),
[],
this.version0,
function (error) {
if (error != null) {
throw error
if (error) {
return done(error)
}
return DocUpdaterClient.sendProjectUpdate(
DocUpdaterClient.sendProjectUpdate(
projectId,
userId,
updates.slice(10),
[],
this.version1,
(error) => {
if (error != null) {
throw error
if (error) {
return done(error)
}
return setTimeout(done, 2000)
setTimeout(done, 2000)
}
)
}
)
return null
})
after(function () {
return MockProjectHistoryApi.flushProject.restore()
MockProjectHistoryApi.flushProject.restore()
})
return it('should not flush project history', function () {
return MockProjectHistoryApi.flushProject
it('should not flush project history', function () {
MockProjectHistoryApi.flushProject
.calledWith(this.project_id)
.should.equal(false)
})

View file

@ -1,16 +1,3 @@
/* eslint-disable
camelcase,
handle-callback-err,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
let DocUpdaterClient
const Settings = require('settings-sharelatex')
const rclient = require('redis-sharelatex').createClient(
@ -20,143 +7,122 @@ const keys = Settings.redis.documentupdater.key_schema
const request = require('request').defaults({ jar: false })
const async = require('async')
const rclient_sub = require('redis-sharelatex').createClient(
const rclientSub = require('redis-sharelatex').createClient(
Settings.redis.pubsub
)
rclient_sub.subscribe('applied-ops')
rclient_sub.setMaxListeners(0)
rclientSub.subscribe('applied-ops')
rclientSub.setMaxListeners(0)
module.exports = DocUpdaterClient = {
randomId() {
const chars = __range__(1, 24, true).map(
(i) => Math.random().toString(16)[2]
)
return chars.join('')
let str = ''
for (let i = 0; i < 24; i++) {
str += Math.floor(Math.random() * 16).toString(16)
}
return str
},
subscribeToAppliedOps(callback) {
if (callback == null) {
callback = function (message) {}
}
return rclient_sub.on('message', callback)
rclientSub.on('message', callback)
},
sendUpdate(project_id, doc_id, update, callback) {
if (callback == null) {
callback = function (error) {}
}
return rclient.rpush(
keys.pendingUpdates({ doc_id }),
sendUpdate(projectId, docId, update, callback) {
rclient.rpush(
keys.pendingUpdates({ doc_id: docId }),
JSON.stringify(update),
(error) => {
if (error != null) {
if (error) {
return callback(error)
}
const doc_key = `${project_id}:${doc_id}`
return rclient.sadd('DocsWithPendingUpdates', doc_key, (error) => {
if (error != null) {
const docKey = `${projectId}:${docId}`
rclient.sadd('DocsWithPendingUpdates', docKey, (error) => {
if (error) {
return callback(error)
}
return rclient.rpush('pending-updates-list', doc_key, callback)
rclient.rpush('pending-updates-list', docKey, callback)
})
}
)
},
sendUpdates(project_id, doc_id, updates, callback) {
if (callback == null) {
callback = function (error) {}
}
return DocUpdaterClient.preloadDoc(project_id, doc_id, (error) => {
if (error != null) {
sendUpdates(projectId, docId, updates, callback) {
DocUpdaterClient.preloadDoc(projectId, docId, (error) => {
if (error) {
return callback(error)
}
const jobs = []
for (const update of Array.from(updates)) {
;((update) =>
jobs.push((callback) =>
DocUpdaterClient.sendUpdate(project_id, doc_id, update, callback)
))(update)
}
return async.series(jobs, (err) =>
DocUpdaterClient.waitForPendingUpdates(project_id, doc_id, callback)
)
const jobs = updates.map((update) => (callback) => {
DocUpdaterClient.sendUpdate(projectId, docId, update, callback)
})
async.series(jobs, (err) => {
if (err) {
return callback(err)
}
DocUpdaterClient.waitForPendingUpdates(projectId, docId, callback)
})
})
},
waitForPendingUpdates(project_id, doc_id, callback) {
return async.retry(
waitForPendingUpdates(projectId, docId, callback) {
async.retry(
{ times: 30, interval: 100 },
(cb) =>
rclient.llen(keys.pendingUpdates({ doc_id }), (err, length) => {
rclient.llen(keys.pendingUpdates({ doc_id: docId }), (err, length) => {
if (err) {
return cb(err)
}
if (length > 0) {
return cb(new Error('updates still pending'))
cb(new Error('updates still pending'))
} else {
return cb()
cb()
}
}),
callback
)
},
getDoc(project_id, doc_id, callback) {
if (callback == null) {
callback = function (error, res, body) {}
}
return request.get(
`http://localhost:3003/project/${project_id}/doc/${doc_id}`,
getDoc(projectId, docId, callback) {
request.get(
`http://localhost:3003/project/${projectId}/doc/${docId}`,
(error, res, body) => {
if (body != null && res.statusCode >= 200 && res.statusCode < 300) {
body = JSON.parse(body)
}
return callback(error, res, body)
callback(error, res, body)
}
)
},
getDocAndRecentOps(project_id, doc_id, fromVersion, callback) {
if (callback == null) {
callback = function (error, res, body) {}
}
return request.get(
`http://localhost:3003/project/${project_id}/doc/${doc_id}?fromVersion=${fromVersion}`,
getDocAndRecentOps(projectId, docId, fromVersion, callback) {
request.get(
`http://localhost:3003/project/${projectId}/doc/${docId}?fromVersion=${fromVersion}`,
(error, res, body) => {
if (body != null && res.statusCode >= 200 && res.statusCode < 300) {
body = JSON.parse(body)
}
return callback(error, res, body)
callback(error, res, body)
}
)
},
preloadDoc(project_id, doc_id, callback) {
if (callback == null) {
callback = function (error) {}
}
return DocUpdaterClient.getDoc(project_id, doc_id, callback)
preloadDoc(projectId, docId, callback) {
DocUpdaterClient.getDoc(projectId, docId, callback)
},
flushDoc(project_id, doc_id, callback) {
if (callback == null) {
callback = function (error) {}
}
return request.post(
`http://localhost:3003/project/${project_id}/doc/${doc_id}/flush`,
flushDoc(projectId, docId, callback) {
request.post(
`http://localhost:3003/project/${projectId}/doc/${docId}/flush`,
(error, res, body) => callback(error, res, body)
)
},
setDocLines(project_id, doc_id, lines, source, user_id, undoing, callback) {
if (callback == null) {
callback = function (error) {}
}
return request.post(
setDocLines(projectId, docId, lines, source, userId, undoing, callback) {
request.post(
{
url: `http://localhost:3003/project/${project_id}/doc/${doc_id}`,
url: `http://localhost:3003/project/${projectId}/doc/${docId}`,
json: {
lines,
source,
user_id,
user_id: userId,
undoing
}
},
@ -164,115 +130,68 @@ module.exports = DocUpdaterClient = {
)
},
deleteDoc(project_id, doc_id, callback) {
if (callback == null) {
callback = function (error) {}
}
return request.del(
`http://localhost:3003/project/${project_id}/doc/${doc_id}`,
deleteDoc(projectId, docId, callback) {
request.del(
`http://localhost:3003/project/${projectId}/doc/${docId}`,
(error, res, body) => callback(error, res, body)
)
},
flushProject(project_id, callback) {
if (callback == null) {
callback = function () {}
}
return request.post(
`http://localhost:3003/project/${project_id}/flush`,
callback
)
flushProject(projectId, callback) {
request.post(`http://localhost:3003/project/${projectId}/flush`, callback)
},
deleteProject(project_id, callback) {
if (callback == null) {
callback = function () {}
}
return request.del(`http://localhost:3003/project/${project_id}`, callback)
deleteProject(projectId, callback) {
request.del(`http://localhost:3003/project/${projectId}`, callback)
},
deleteProjectOnShutdown(project_id, callback) {
if (callback == null) {
callback = function () {}
}
return request.del(
`http://localhost:3003/project/${project_id}?background=true&shutdown=true`,
deleteProjectOnShutdown(projectId, callback) {
request.del(
`http://localhost:3003/project/${projectId}?background=true&shutdown=true`,
callback
)
},
flushOldProjects(callback) {
if (callback == null) {
callback = function () {}
}
return request.get(
request.get(
'http://localhost:3003/flush_queued_projects?min_delete_age=1',
callback
)
},
acceptChange(project_id, doc_id, change_id, callback) {
if (callback == null) {
callback = function () {}
}
return request.post(
`http://localhost:3003/project/${project_id}/doc/${doc_id}/change/${change_id}/accept`,
acceptChange(projectId, docId, changeId, callback) {
request.post(
`http://localhost:3003/project/${projectId}/doc/${docId}/change/${changeId}/accept`,
callback
)
},
removeComment(project_id, doc_id, comment, callback) {
if (callback == null) {
callback = function () {}
}
return request.del(
`http://localhost:3003/project/${project_id}/doc/${doc_id}/comment/${comment}`,
removeComment(projectId, docId, comment, callback) {
request.del(
`http://localhost:3003/project/${projectId}/doc/${docId}/comment/${comment}`,
callback
)
},
getProjectDocs(project_id, projectStateHash, callback) {
if (callback == null) {
callback = function () {}
}
return request.get(
`http://localhost:3003/project/${project_id}/doc?state=${projectStateHash}`,
getProjectDocs(projectId, projectStateHash, callback) {
request.get(
`http://localhost:3003/project/${projectId}/doc?state=${projectStateHash}`,
(error, res, body) => {
if (body != null && res.statusCode >= 200 && res.statusCode < 300) {
body = JSON.parse(body)
}
return callback(error, res, body)
callback(error, res, body)
}
)
},
sendProjectUpdate(
project_id,
userId,
docUpdates,
fileUpdates,
version,
callback
) {
if (callback == null) {
callback = function (error) {}
}
return request.post(
sendProjectUpdate(projectId, userId, updates, version, callback) {
request.post(
{
url: `http://localhost:3003/project/${project_id}`,
json: { userId, docUpdates, fileUpdates, version }
url: `http://localhost:3003/project/${projectId}`,
json: { userId, updates, version }
},
(error, res, body) => callback(error, res, body)
)
}
}
function __range__(left, right, inclusive) {
const range = []
const ascending = left < right
const end = !inclusive ? right : ascending ? right + 1 : right - 1
for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) {
range.push(i)
}
return range
}

View file

@ -809,92 +809,7 @@ describe('HttpController', function () {
})
})
describe('updateProject (split doc and file updates)', function () {
beforeEach(function () {
this.projectHistoryId = 'history-id-123'
this.userId = 'user-id-123'
this.docUpdates = [
{ id: 1, pathname: 'thesis.tex', newPathname: 'book.tex' },
{ id: 2, pathname: 'article.tex', docLines: 'hello' }
]
this.fileUpdates = [
{ id: 3, pathname: 'apple.png', newPathname: 'banana.png' },
{ id: 4, url: 'filestore.example.com/4' }
]
this.expectedUpdates = [
{
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,
docUpdates: this.docUpdates,
fileUpdates: this.fileUpdates,
version: this.version
},
params: {
project_id: this.project_id
}
}
})
describe('successfully', function () {
beforeEach(function () {
this.ProjectManager.updateProjectWithLocks = sinon.stub().yields()
this.HttpController.updateProject(this.req, this.res, this.next)
})
it('should accept the change', function () {
this.ProjectManager.updateProjectWithLocks
.calledWith(
this.project_id,
this.projectHistoryId,
this.userId,
this.expectedUpdates,
this.version
)
.should.equal(true)
})
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(function () {
this.ProjectManager.updateProjectWithLocks = sinon
.stub()
.yields(new Error('oops'))
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('updateProject (single updates parameter)', function () {
describe('updateProject', function () {
beforeEach(function () {
this.projectHistoryId = 'history-id-123'
this.userId = 'user-id-123'