diff --git a/services/web/app/coffee/Features/Project/ProjectEntityMongoUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityMongoUpdateHandler.coffee index 251ff10577..eaf92df92a 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityMongoUpdateHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityMongoUpdateHandler.coffee @@ -6,15 +6,32 @@ settings = require('settings-sharelatex') CooldownManager = require '../Cooldown/CooldownManager' Errors = require '../Errors/Errors' Folder = require('../../models/Folder').Folder +LockManager = require('../../infrastructure/LockManager') Project = require('../../models/Project').Project ProjectEntityHandler = require('./ProjectEntityHandler') ProjectGetter = require('./ProjectGetter') ProjectLocator = require('./ProjectLocator') SafePath = require './SafePath' +LOCK_NAMESPACE = "mongoTransaction" + +wrapWithLock = (methodWithoutLock) -> + # This lock is used whenever we read or write to an existing project's + # structure. Some operations to project structure cannot be done atomically + # in mongo, this lock is used to prevent reading the structure between two + # parts of a staged update. + methodWithLock = (project_id, args..., callback) -> + LockManager.runWithLock LOCK_NAMESPACE, project_id, + (cb) -> methodWithoutLock project_id, args..., cb + callback + methodWithLock.withoutLock = methodWithoutLock + methodWithLock + module.exports = ProjectEntityMongoUpdateHandler = self = - addDoc: (project_id, folder_id, doc, callback = (err, result) ->) -> - ProjectGetter.getProject project_id, {rootFolder:true, name:true}, (err, project) -> + LOCK_NAMESPACE: LOCK_NAMESPACE + + addDoc: wrapWithLock (project_id, folder_id, doc, callback = (err, result) ->) -> + ProjectGetter.getProjectWithoutLock project_id, {rootFolder:true, name:true}, (err, project) -> if err? logger.err project_id:project_id, err:err, "error getting project for add doc" return callback(err) @@ -22,8 +39,8 @@ module.exports = ProjectEntityMongoUpdateHandler = self = self._confirmFolder project, folder_id, (folder_id) => self._putElement project, folder_id, doc, "doc", callback - addFile: (project_id, folder_id, fileRef, callback = (error, result, project) ->)-> - ProjectGetter.getProject project_id, {rootFolder:true, name:true}, (err, project) -> + addFile: wrapWithLock (project_id, folder_id, fileRef, callback = (error, result, project) ->)-> + ProjectGetter.getProjectWithoutLock project_id, {rootFolder:true, name:true}, (err, project) -> if err? logger.err project_id:project_id, err:err, "error getting project for add file" return callback(err) @@ -31,8 +48,8 @@ module.exports = ProjectEntityMongoUpdateHandler = self = self._confirmFolder project, folder_id, (folder_id)-> self._putElement project, folder_id, fileRef, "file", callback - replaceFile: (project_id, file_id, callback) -> - ProjectGetter.getProject project_id, {rootFolder: true, name:true}, (err, project) -> + replaceFile: wrapWithLock (project_id, file_id, callback) -> + ProjectGetter.getProjectWithoutLock project_id, {rootFolder: true, name:true}, (err, project) -> return callback(err) if err? ProjectLocator.findElement {project:project, element_id: file_id, type: 'file'}, (err, fileRef, path)=> return callback(err) if err? @@ -48,7 +65,7 @@ module.exports = ProjectEntityMongoUpdateHandler = self = return callback(err) if err? callback null, fileRef, project, path - mkdirp: (project_id, path, callback) -> + mkdirp: wrapWithLock (project_id, path, callback) -> folders = path.split('/') folders = _.select folders, (folder)-> return folder.length != 0 @@ -66,10 +83,10 @@ module.exports = ProjectEntityMongoUpdateHandler = self = if parentFolder? parentFolder_id = parentFolder._id builtUpPath = "#{builtUpPath}/#{folderName}" - ProjectLocator.findElementByPath project, builtUpPath, (err, foundFolder)=> + ProjectLocator.findElementByPath project: project, path: builtUpPath, (err, foundFolder)=> if !foundFolder? logger.log path:path, project_id:project._id, folderName:folderName, "making folder from mkdirp" - self.addFolder project_id, parentFolder_id, folderName, (err, newFolder, parentFolder_id)-> + self.addFolder.withoutLock project_id, parentFolder_id, folderName, (err, newFolder, parentFolder_id)-> return callback(err) if err? newFolder.parentFolder_id = parentFolder_id previousFolders.push newFolder @@ -86,8 +103,8 @@ module.exports = ProjectEntityMongoUpdateHandler = self = !folder.filterOut callback null, folders, lastFolder - moveEntity: (project_id, entity_id, destFolderId, entityType, callback = (error) ->) -> - ProjectGetter.getProject project_id, {rootFolder:true, name:true}, (err, project) -> + moveEntity: wrapWithLock (project_id, entity_id, destFolderId, entityType, callback = (error) ->) -> + ProjectGetter.getProjectWithoutLock project_id, {rootFolder:true, name:true}, (err, project) -> return callback(err) if err? ProjectLocator.findElement {project, element_id: entity_id, type: entityType}, (err, entity, entityPath)-> return callback(err) if err? @@ -106,8 +123,8 @@ module.exports = ProjectEntityMongoUpdateHandler = self = changes = {oldDocs, newDocs, oldFiles, newFiles} callback null, project.name, startPath, endPath, entity.rev, changes, callback - deleteEntity: (project_id, entity_id, entityType, callback) -> - ProjectGetter.getProject project_id, {name:true, rootFolder:true}, (error, project) -> + deleteEntity: wrapWithLock (project_id, entity_id, entityType, callback) -> + ProjectGetter.getProjectWithoutLock project_id, {name:true, rootFolder:true}, (error, project) -> return callback(error) if error? ProjectLocator.findElement {project: project, element_id: entity_id, type: entityType}, (error, entity, path) -> return callback(error) if error? @@ -115,8 +132,8 @@ module.exports = ProjectEntityMongoUpdateHandler = self = return callback(error) if error? callback null, entity, path, project - renameEntity: (project_id, entity_id, entityType, newName, callback) -> - ProjectGetter.getProject project_id, {rootFolder:true, name:true}, (error, project)=> + renameEntity: wrapWithLock (project_id, entity_id, entityType, newName, callback) -> + ProjectGetter.getProjectWithoutLock project_id, {rootFolder:true, name:true}, (error, project)=> return callback(error) if error? ProjectEntityHandler.getAllEntitiesFromProject project, (error, oldDocs, oldFiles) => return callback(error) if error? @@ -138,8 +155,8 @@ module.exports = ProjectEntityMongoUpdateHandler = self = changes = {oldDocs, newDocs, oldFiles, newFiles} callback null, project.name, startPath, endPath, entity.rev, changes, callback - addFolder: (project_id, parentFolder_id, folderName, callback) -> - ProjectGetter.getProject project_id, {rootFolder:true, name:true}, (err, project) -> + addFolder: wrapWithLock (project_id, parentFolder_id, folderName, callback) -> + ProjectGetter.getProjectWithoutLock project_id, {rootFolder:true, name:true}, (err, project) -> if err? logger.err project_id:project_id, err:err, "error getting project for add folder" return callback(err) diff --git a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee index 0d2864081c..1f5cf4da33 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee @@ -18,9 +18,14 @@ ProjectEntityMongoUpdateHandler = require('./ProjectEntityMongoUpdateHandler') SafePath = require './SafePath' TpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender') +LOCK_NAMESPACE = "sequentialProjectStructureUpdateLock" + wrapWithLock = (methodWithoutLock) -> + # This lock is used to make sure that the project structure updates are made + # sequentially. In particular the updates must be made in mongo and sent to + # the doc-updater in the same order. methodWithLock = (project_id, args..., callback) -> - LockManager.runWithLock project_id, + LockManager.runWithLock LOCK_NAMESPACE, project_id, (cb) -> methodWithoutLock project_id, args..., cb callback methodWithLock.withoutLock = methodWithoutLock @@ -267,7 +272,7 @@ module.exports = ProjectEntityUpdateHandler = self = callback null, entity_id deleteEntityWithPath: wrapWithLock (project_id, path, userId, callback) -> - ProjectLocator.findElementByPath project_id, path, (err, element, type)-> + ProjectLocator.findElementByPath project_id: project_id, path: path, (err, element, type)-> return callback(err) if err? return callback(new Errors.NotFoundError("project not found")) if !element? self.deleteEntity.withoutLock project_id, element._id, type, userId, callback diff --git a/services/web/app/coffee/Features/Project/ProjectGetter.coffee b/services/web/app/coffee/Features/Project/ProjectGetter.coffee index 4506292fbb..8a32556638 100644 --- a/services/web/app/coffee/Features/Project/ProjectGetter.coffee +++ b/services/web/app/coffee/Features/Project/ProjectGetter.coffee @@ -5,46 +5,66 @@ ObjectId = mongojs.ObjectId async = require "async" Project = require("../../models/Project").Project logger = require("logger-sharelatex") +LockManager = require("../../infrastructure/LockManager") module.exports = ProjectGetter = EXCLUDE_DEPTH: 8 - getProjectWithoutDocLines: (project_id, callback=(error, project) ->) -> excludes = {} for i in [1..ProjectGetter.EXCLUDE_DEPTH] excludes["rootFolder#{Array(i).join(".folders")}.docs.lines"] = 0 - db.projects.find _id: ObjectId(project_id.toString()), excludes, (error, projects = []) -> - callback error, projects[0] + ProjectGetter.getProject project_id, excludes, callback getProjectWithOnlyFolders: (project_id, callback=(error, project) ->) -> excludes = {} for i in [1..ProjectGetter.EXCLUDE_DEPTH] excludes["rootFolder#{Array(i).join(".folders")}.docs"] = 0 excludes["rootFolder#{Array(i).join(".folders")}.fileRefs"] = 0 - db.projects.find _id: ObjectId(project_id.toString()), excludes, (error, projects = []) -> - callback error, projects[0] + ProjectGetter.getProject project_id, excludes, callback + getProject: (project_id, projection, callback) -> + if !project_id? + return callback(new Error("no project_id provided")) - getProject: (query, projection, callback = (error, project) ->) -> - if !query? - return callback("no query provided") - - if typeof(projection) == "function" + if typeof(projection) == "function" && !callback? callback = projection + projection = {} - if typeof query == "string" - query = _id: ObjectId(query) - else if query instanceof ObjectId - query = _id: query - else if query?.toString().length == 24 # sometimes mongoose ids are hard to identify, this will catch them - query = _id: ObjectId(query.toString()) + if typeof(projection) != "object" + return callback(new Error("projection is not an object")) + + if projection?.rootFolder || Object.keys(projection).length == 0 + ProjectEntityMongoUpdateHandler = require './ProjectEntityMongoUpdateHandler' + LockManager.runWithLock ProjectEntityMongoUpdateHandler.LOCK_NAMESPACE, project_id, + (cb) -> ProjectGetter.getProjectWithoutLock project_id, projection, cb + callback + else + ProjectGetter.getProjectWithoutLock project_id, projection, callback + + getProjectWithoutLock: (project_id, projection, callback) -> + if !project_id? + return callback(new Error("no project_id provided")) + + if typeof(projection) == "function" && !callback? + callback = projection + projection = {} + + if typeof(projection) != "object" + return callback(new Error("projection is not an object")) + + if typeof project_id == "string" + query = _id: ObjectId(project_id) + else if project_id instanceof ObjectId + query = _id: project_id + else if project_id?.toString().length == 24 # sometimes mongoose ids are hard to identify, this will catch them + query = _id: ObjectId(project_id.toString()) else err = new Error("malformed get request") - logger.log query:query, err:err, type:typeof(query), "malformed get request" + logger.log project_id:project_id, err:err, type:typeof(project_id), "malformed get request" return callback(err) - db.projects.find query, projection, (err, project)-> + db.projects.find query, projection, (err, project) -> if err? logger.err err:err, query:query, projection:projection, "error getting project" return callback(err) diff --git a/services/web/app/coffee/Features/Project/ProjectLocator.coffee b/services/web/app/coffee/Features/Project/ProjectLocator.coffee index cb1bcf55ee..80de0cb253 100644 --- a/services/web/app/coffee/Features/Project/ProjectLocator.coffee +++ b/services/web/app/coffee/Features/Project/ProjectLocator.coffee @@ -4,7 +4,6 @@ Errors = require "../Errors/Errors" _ = require('underscore') logger = require('logger-sharelatex') async = require('async') -ProjectGetter = require "./ProjectGetter" module.exports = ProjectLocator = findElement: (options, _callback = (err, element, path, parentFolder)->)-> @@ -83,8 +82,19 @@ module.exports = ProjectLocator = else getRootDoc project - findElementByPath: (project_or_id, needlePath, callback = (err, foundEntity, type)->)-> + findElementByPath: (options, callback = (err, foundEntity, type)->)-> + {project, project_id, path} = options + if !path? + return new Error('no path provided for findElementByPath') + if project? + ProjectLocator._findElementByPathWithProject project, path, callback + else + ProjectGetter.getProject project_id, {rootFolder:true, rootDoc_id:true}, (err, project)-> + return callback(err) if err? + ProjectLocator._findElementByPathWithProject project, path, callback + + _findElementByPathWithProject: (project, needlePath, callback = (err, foundEntity, type)->)-> getParentFolder = (haystackFolder, foldersList, level, cb)-> if foldersList.length == 0 return cb null, haystackFolder @@ -98,7 +108,7 @@ module.exports = ProjectLocator = else return getParentFolder(folder, foldersList, level+1, cb) if !found - cb("not found project_or_id: #{project_or_id} search path: #{needlePath}, folder #{foldersList[level]} could not be found") + cb("not found project: #{project._id} search path: #{needlePath}, folder #{foldersList[level]} could not be found") getEntity = (folder, entityName, cb)-> if !entityName? @@ -119,35 +129,34 @@ module.exports = ProjectLocator = if result? cb null, result, type else - cb("not found project_or_id: #{project_or_id} search path: #{needlePath}, entity #{entityName} could not be found") + cb("not found project: #{project._id} search path: #{needlePath}, entity #{entityName} could not be found") - Project.getProject project_or_id, "", (err, project)-> - if err? - logger.err err:err, project_or_id:project_or_id, "error getting project for finding element" - return callback(err) - if !project? - return callback("project could not be found for finding a element #{project_or_id}") - if needlePath == '' || needlePath == '/' - return callback(null, project.rootFolder[0], "folder") + if err? + logger.err err:err, project_id:project._id, "error getting project for finding element" + return callback(err) + if !project? + return callback("project could not be found for finding a element #{project._id}") + if needlePath == '' || needlePath == '/' + return callback(null, project.rootFolder[0], "folder") - if needlePath.indexOf('/') == 0 - needlePath = needlePath.substring(1) - foldersList = needlePath.split('/') - needleName = foldersList.pop() - rootFolder = project.rootFolder[0] + if needlePath.indexOf('/') == 0 + needlePath = needlePath.substring(1) + foldersList = needlePath.split('/') + needleName = foldersList.pop() + rootFolder = project.rootFolder[0] - logger.log project_id:project._id, path:needlePath, foldersList:foldersList, "looking for element by path" - jobs = new Array() - jobs.push( - (cb)-> - getParentFolder rootFolder, foldersList, 0, cb - ) - jobs.push( - (folder, cb)-> - getEntity folder, needleName, cb - ) - async.waterfall jobs, callback + logger.log project_id:project._id, path:needlePath, foldersList:foldersList, "looking for element by path" + jobs = new Array() + jobs.push( + (cb)-> + getParentFolder rootFolder, foldersList, 0, cb + ) + jobs.push( + (folder, cb)-> + getEntity folder, needleName, cb + ) + async.waterfall jobs, callback findUsersProjectByName: (user_id, projectName, callback)-> ProjectGetter.findAllUsersProjects user_id, 'name archived', (err, allProjects)-> diff --git a/services/web/app/coffee/infrastructure/LockManager.coffee b/services/web/app/coffee/infrastructure/LockManager.coffee index 5532d5bd94..c96660ecf5 100644 --- a/services/web/app/coffee/infrastructure/LockManager.coffee +++ b/services/web/app/coffee/infrastructure/LockManager.coffee @@ -7,12 +7,42 @@ logger = require "logger-sharelatex" module.exports = LockManager = LOCK_TEST_INTERVAL: 50 # 50ms between each test of the lock MAX_LOCK_WAIT_TIME: 10000 # 10s maximum time to spend trying to get the lock - REDIS_LOCK_EXPIRY: 30 # seconds. Time until lock auto expires in redis. + REDIS_LOCK_EXPIRY: 30 # seconds. Time until lock auto expires in redis + SLOW_EXECUTION_THRESHOLD: 5000 # 5s, if execution takes longer than this then log - _blockingKey : (key)-> "lock:web:{#{key}}" + runWithLock: (namespace, id, runner = ( (releaseLock = (error) ->) -> ), callback = ( (error) -> )) -> - tryLock : (key, callback = (err, isFree)->)-> - rclient.set LockManager._blockingKey(key), "locked", "EX", LockManager.REDIS_LOCK_EXPIRY, "NX", (err, gotLock)-> + # The lock can expire in redis but the process carry on. This setTimout call + # is designed to log if this happens. + # + # error is defined here so we get a useful stacktrace + lockReleased = false + slowExecutionError = new Error "slow execution during lock" + countIfExceededLockTimeout = () -> + if !lockReleased + metrics.inc "lock.#{namespace}.exceeded_lock_timeout" + logger.log "exceeded lock timeout", { namespace, id, slowExecutionError } + + setTimeout countIfExceededLockTimeout, LockManager.REDIS_LOCK_EXPIRY + + timer = new metrics.Timer("lock.#{namespace}") + key = "lock:web:#{namespace}:#{id}" + LockManager._getLock key, (error) -> + return callback(error) if error? + runner (error1, values...) -> + LockManager._releaseLock key, (error2) -> + lockReleased = true + timeTaken = new Date - timer.start + if timeTaken > LockManager.SLOW_EXECUTION_THRESHOLD + logger.log "slow execution during lock", { namespace, id, timeTaken, slowExecutionError } + + timer.done() + error = error1 or error2 + return callback(error) if error? + callback null, values... + + _tryLock : (key, callback = (err, isFree)->)-> + rclient.set key, "locked", "EX", LockManager.REDIS_LOCK_EXPIRY, "NX", (err, gotLock)-> return callback(err) if err? if gotLock == "OK" metrics.inc "lock-not-blocking" @@ -22,22 +52,22 @@ module.exports = LockManager = logger.log key: key, redis_response: gotLock, "lock is locked" callback err, false - getLock: (key, callback = (error) ->) -> + _getLock: (key, callback = (error) ->) -> startTime = Date.now() do attempt = () -> if Date.now() - startTime > LockManager.MAX_LOCK_WAIT_TIME return callback(new Error("Timeout")) - LockManager.tryLock key, (error, gotLock) -> + LockManager._tryLock key, (error, gotLock) -> return callback(error) if error? if gotLock callback(null) else setTimeout attempt, LockManager.LOCK_TEST_INTERVAL - checkLock: (key, callback = (err, isFree)->)-> + _checkLock: (key, callback = (err, isFree)->)-> multi = rclient.multi() - multi.exists LockManager._blockingKey(key) + multi.exists key multi.exec (err, replys)-> return callback(err) if err? exists = parseInt replys[0] @@ -48,14 +78,5 @@ module.exports = LockManager = metrics.inc "lock-not-blocking" callback err, true - releaseLock: (key, callback)-> - rclient.del LockManager._blockingKey(key), callback - - runWithLock: (key, runner = ( (releaseLock = (error) ->) -> ), callback = ( (error) -> )) -> - LockManager.getLock key, (error) -> - return callback(error) if error? - runner (error1, values...) -> - LockManager.releaseLock key, (error2) -> - error = error1 or error2 - return callback(error) if error? - callback null, values... + _releaseLock: (key, callback)-> + rclient.del key, callback diff --git a/services/web/test/acceptance/coffee/ProjectStructureMongoLockTest.coffee b/services/web/test/acceptance/coffee/ProjectStructureMongoLockTest.coffee new file mode 100644 index 0000000000..7788e18fc1 --- /dev/null +++ b/services/web/test/acceptance/coffee/ProjectStructureMongoLockTest.coffee @@ -0,0 +1,83 @@ +APP_PATH = "../../../app/js" + +LockManager = require "#{APP_PATH}/infrastructure/LockManager" +ProjectCreationHandler = require "#{APP_PATH}/Features/Project/ProjectCreationHandler.js" +ProjectGetter = require "#{APP_PATH}/Features/Project/ProjectGetter.js" +ProjectEntityMongoUpdateHandler = require "#{APP_PATH}/Features/Project/ProjectEntityMongoUpdateHandler.js" +UserCreator = require "#{APP_PATH}/Features/User/UserCreator.js" + +expect = require("chai").expect +_ = require("lodash") + +# These tests are neither acceptance tests nor unit tests. It's difficult to +# test/verify that our locking is doing what we hope. +# These tests call methods in ProjectGetter and ProjectEntityMongoUpdateHandler +# to see that they DO NOT work when a lock has been taken. +# +# It is tested that these methods DO work when the lock has not been taken in +# other acceptance tests. + +describe "ProjectStructureMongoLock", -> + describe "whilst a project lock is taken", -> + before (done) -> + # We want to instantly fail if the lock is taken + LockManager.MAX_LOCK_WAIT_TIME = 1 + userDetails = + holdingAccount:false, + email: 'test@example.com' + UserCreator.createNewUser userDetails, (err, user) => + @user = user + throw err if err? + ProjectCreationHandler.createBlankProject user._id, 'locked-project', (err, project) => + throw err if err? + @locked_project = project + @lock_key = "lock:web:#{ProjectEntityMongoUpdateHandler.LOCK_NAMESPACE}:#{project._id}" + LockManager._getLock @lock_key, done + + after (done) -> + LockManager._releaseLock @lock_key, done + + describe 'interacting with the locked project', -> + LOCKING_UPDATE_METHODS = ['addDoc', 'addFile', 'mkdirp', 'moveEntity', 'renameEntity', 'addFolder'] + for methodName in LOCKING_UPDATE_METHODS + it "cannot call ProjectEntityMongoUpdateHandler.#{methodName}", (done) -> + method = ProjectEntityMongoUpdateHandler[methodName] + args = _.times(method.length - 2, _.constant(null)) + method @locked_project._id, args, (err) -> + expect(err).to.deep.equal new Error("Timeout") + done() + + it "cannot get the project without a projection", (done) -> + ProjectGetter.getProject @locked_project._id, (err) -> + expect(err).to.deep.equal new Error("Timeout") + done() + + it "cannot get the project if rootFolder is in the projection", (done) -> + ProjectGetter.getProject @locked_project._id, rootFolder: true, (err) -> + expect(err).to.deep.equal new Error("Timeout") + done() + + it "can get the project if rootFolder is not in the projection", (done) -> + ProjectGetter.getProject @locked_project._id, _id: true, (err, project) => + expect(err).to.equal(null) + expect(project._id).to.deep.equal(@locked_project._id) + done() + + describe 'interacting with other projects', -> + before (done) -> + ProjectCreationHandler.createBlankProject @user._id, 'unlocked-project', (err, project) => + throw err if err? + @unlocked_project = project + done() + + it "can add folders to other projects", (done) -> + ProjectEntityMongoUpdateHandler.addFolder @unlocked_project._id, @unlocked_project.rootFolder[0]._id, 'new folder', (err, folder) -> + expect(err).to.equal(null) + expect(folder).to.be.defined + done() + + it "can get other projects without a projection", (done) -> + ProjectGetter.getProject @unlocked_project._id, (err, project) => + expect(err).to.equal(null) + expect(project._id).to.deep.equal(@unlocked_project._id) + done() diff --git a/services/web/test/acceptance/coffee/ProjectStructureTests.coffee b/services/web/test/acceptance/coffee/ProjectStructureTests.coffee index 6a303f3fc7..7e9801176b 100644 --- a/services/web/test/acceptance/coffee/ProjectStructureTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectStructureTests.coffee @@ -80,13 +80,13 @@ describe "ProjectStructureChanges", -> before (done) -> MockDocUpdaterApi.clearProjectStructureUpdates() - ProjectGetter.getProject @example_project_id, (error, projects) => + ProjectGetter.getProject @example_project_id, (error, project) => throw error if error? @owner.request.post { uri: "project/#{@example_project_id}/doc", json: name: 'new.tex' - parent_folder_id: projects[0].rootFolder[0]._id + parent_folder_id: project.rootFolder[0]._id }, (error, res, body) => throw error if error? if res.statusCode < 200 || res.statusCode >= 300 @@ -137,9 +137,9 @@ describe "ProjectStructureChanges", -> describe "uploading a file", -> before (done) -> - ProjectGetter.getProject @example_project_id, (error, projects) => + ProjectGetter.getProject @example_project_id, (error, project) => throw error if error? - @root_folder_id = projects[0].rootFolder[0]._id.toString() + @root_folder_id = project.rootFolder[0]._id.toString() done() beforeEach () -> diff --git a/services/web/test/acceptance/coffee/TpdsUpdateTests.coffee b/services/web/test/acceptance/coffee/TpdsUpdateTests.coffee index 201d2fc3f5..3f300a55c3 100644 --- a/services/web/test/acceptance/coffee/TpdsUpdateTests.coffee +++ b/services/web/test/acceptance/coffee/TpdsUpdateTests.coffee @@ -28,10 +28,10 @@ describe "TpdsUpdateTests", -> done() it "should have deleted the file", (done) -> - ProjectGetter.getProject @project_id, (error, [project]) -> + ProjectGetter.getProject @project_id, (error, project) -> throw error if error? projectFolder = project.rootFolder[0] for doc in projectFolder.docs if doc.name == "main.tex" throw new Error("expected main.tex to have been deleted") - done() \ No newline at end of file + done() diff --git a/services/web/test/unit/coffee/Project/ProjectEntityMongoUpdateHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectEntityMongoUpdateHandlerTests.coffee index 6b45537465..d2a5a334b4 100644 --- a/services/web/test/unit/coffee/Project/ProjectEntityMongoUpdateHandlerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectEntityMongoUpdateHandlerTests.coffee @@ -35,11 +35,14 @@ describe 'ProjectEntityMongoUpdateHandler', -> } "../Cooldown/CooldownManager": @CooldownManager = {} '../../models/Folder': Folder:@FolderModel + "../../infrastructure/LockManager":@LockManager = + runWithLock: + sinon.spy((namespace, id, runner, callback) -> runner(callback)) '../../models/Project': Project:@ProjectModel = {} './ProjectEntityHandler': @ProjectEntityHandler = {} './ProjectLocator': @ProjectLocator = {} "./ProjectGetter": @ProjectGetter = - getProject: sinon.stub().yields(null, @project) + getProjectWithoutLock: sinon.stub().yields(null, @project) afterEach -> tk.reset() @@ -53,7 +56,7 @@ describe 'ProjectEntityMongoUpdateHandler', -> @subject.addDoc project_id, folder_id, @doc, @callback it 'gets the project', -> - @ProjectGetter.getProject + @ProjectGetter.getProjectWithoutLock .calledWith(project_id, {rootFolder:true, name: true}) .should.equal true @@ -76,7 +79,7 @@ describe 'ProjectEntityMongoUpdateHandler', -> @subject.addFile project_id, folder_id, @file, @callback it 'gets the project', -> - @ProjectGetter.getProject + @ProjectGetter.getProjectWithoutLock .calledWith(project_id, {rootFolder:true, name: true}) .should.equal true @@ -100,7 +103,7 @@ describe 'ProjectEntityMongoUpdateHandler', -> @subject.replaceFile project_id, file_id, @callback it 'gets the project', -> - @ProjectGetter.getProject + @ProjectGetter.getProjectWithoutLock .calledWith(project_id, {rootFolder:true, name: true}) .should.equal true @@ -134,15 +137,17 @@ describe 'ProjectEntityMongoUpdateHandler', -> @project = _id: project_id, rootFolder: [@rootFolder] @ProjectGetter.getProjectWithOnlyFolders = sinon.stub().yields(null, @project) - @ProjectLocator.findElementByPath = (project_id, path, cb) => + @ProjectLocator.findElementByPath = (options, cb) => + {path} = options @parentFolder = {_id:"parentFolder_id_here"} lastFolder = path.substring(path.lastIndexOf("/")) if lastFolder.indexOf("level1") == -1 cb "level1 is not the last foler " else cb null, @parentFolder - @subject.addFolder = (project_id, parentFolder_id, folderName, callback) => - callback null, {name:folderName}, @parentFolder_id + @subject.addFolder = + withoutLock: (project_id, parentFolder_id, folderName, callback) => + callback null, {name:folderName}, @parentFolder_id it 'should return the root folder if the path is just a slash', (done)-> path = "/" @@ -217,7 +222,7 @@ describe 'ProjectEntityMongoUpdateHandler', -> @subject.moveEntity project_id, doc_id, folder_id, "docs", @callback it 'should get the project', -> - @ProjectGetter.getProject + @ProjectGetter.getProjectWithoutLock .calledWith(project_id, {rootFolder:true, name:true}) .should.equal true @@ -256,7 +261,7 @@ describe 'ProjectEntityMongoUpdateHandler', -> @subject.deleteEntity project_id, doc_id, 'doc', @callback it "should get the project", -> - @ProjectGetter.getProject + @ProjectGetter.getProjectWithoutLock .calledWith(project_id, {name:true, rootFolder:true}) .should.equal true @@ -284,7 +289,7 @@ describe 'ProjectEntityMongoUpdateHandler', -> @doc = _id: doc_id, name: "old.tex", rev: 1 @folder = _id: folder_id - @ProjectGetter.getProject = sinon.stub().yields(null, @project) + @ProjectGetter.getProjectWithoutLock = sinon.stub().yields(null, @project) @ProjectEntityHandler.getAllEntitiesFromProject = sinon.stub() @ProjectEntityHandler.getAllEntitiesFromProject @@ -301,7 +306,7 @@ describe 'ProjectEntityMongoUpdateHandler', -> @subject.renameEntity project_id, doc_id, 'doc', @newName, @callback it 'should get the project', -> - @ProjectGetter.getProject + @ProjectGetter.getProjectWithoutLock .calledWith(project_id, {rootFolder:true, name:true}) .should.equal true @@ -339,7 +344,7 @@ describe 'ProjectEntityMongoUpdateHandler', -> @subject.addFolder project_id, folder_id, @folderName, @callback it 'gets the project', -> - @ProjectGetter.getProject + @ProjectGetter.getProjectWithoutLock .calledWith(project_id, {rootFolder:true, name: true}) .should.equal true diff --git a/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee index d1fca71bc1..ed4bbe8e33 100644 --- a/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee @@ -53,7 +53,7 @@ describe 'ProjectEntityUpdateHandler', -> '../FileStore/FileStoreHandler':@FileStoreHandler "../../infrastructure/LockManager":@LockManager = runWithLock: - sinon.spy((key, runner, callback) -> runner(callback)) + sinon.spy((namespace, id, runner, callback) -> runner(callback)) '../../models/Project': Project:@ProjectModel = {} "./ProjectGetter": @ProjectGetter = {} './ProjectLocator': @ProjectLocator = {} @@ -642,7 +642,7 @@ describe 'ProjectEntityUpdateHandler', -> it 'finds the entity', -> @ProjectLocator.findElementByPath - .calledWith(project_id, @path) + .calledWith({project_id, @path}) .should.equal true it 'deletes the entity', -> diff --git a/services/web/test/unit/coffee/Project/ProjectGetterTests.coffee b/services/web/test/unit/coffee/Project/ProjectGetterTests.coffee index f7dbbb6a62..eac6837d45 100644 --- a/services/web/test/unit/coffee/Project/ProjectGetterTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectGetterTests.coffee @@ -20,8 +20,9 @@ describe "ProjectGetter", -> "../../models/Project": Project: @Project = {} "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {} "../../infrastructure/LockManager": @LockManager = - mongoTransactionLock: - runWithLock : sinon.spy((key, runner, callback) -> runner(callback)) + runWithLock : sinon.spy((namespace, id, runner, callback) -> runner(callback)) + './ProjectEntityMongoUpdateHandler': + lockKey: (project_id) -> project_id "logger-sharelatex": err:-> log:-> @@ -30,16 +31,16 @@ describe "ProjectGetter", -> beforeEach -> @project = _id: @project_id = "56d46b0a1d3422b87c5ebcb1" - @db.projects.find = sinon.stub().callsArgWith(2, null, [@project]) + @ProjectGetter.getProject = sinon.stub().yields() describe "passing an id", -> beforeEach -> @ProjectGetter.getProjectWithoutDocLines @project_id, @callback it "should call find with the project id", -> - expect(@db.projects.find.lastCall.args[0]).to.deep.equal { - _id: ObjectId(@project_id) - } + @ProjectGetter.getProject + .calledWith(@project_id) + .should.equal true it "should exclude the doc lines", -> excludes = @@ -51,11 +52,13 @@ describe "ProjectGetter", -> "rootFolder.folders.folders.folders.folders.folders.docs.lines": 0 "rootFolder.folders.folders.folders.folders.folders.folders.docs.lines": 0 "rootFolder.folders.folders.folders.folders.folders.folders.folders.docs.lines": 0 - @db.projects.find.calledWith(sinon.match.any, excludes) + + @ProjectGetter.getProject + .calledWith(@project_id, excludes) .should.equal true - it "should call the callback with the project", -> - @callback.calledWith(null, @project).should.equal true + it "should call the callback", -> + @callback.called.should.equal true describe "getProjectWithOnlyFolders", -> @@ -63,16 +66,16 @@ describe "ProjectGetter", -> beforeEach ()-> @project = _id: @project_id = "56d46b0a1d3422b87c5ebcb1" - @db.projects.find = sinon.stub().callsArgWith(2, null, [@project]) + @ProjectGetter.getProject = sinon.stub().yields() describe "passing an id", -> beforeEach -> @ProjectGetter.getProjectWithOnlyFolders @project_id, @callback it "should call find with the project id", -> - expect(@db.projects.find.lastCall.args[0]).to.deep.equal { - _id: ObjectId(@project_id) - } + @ProjectGetter.getProject + .calledWith(@project_id) + .should.equal true it "should exclude the docs and files linesaaaa", -> excludes = @@ -92,11 +95,12 @@ describe "ProjectGetter", -> "rootFolder.folders.folders.folders.folders.folders.folders.fileRefs": 0 "rootFolder.folders.folders.folders.folders.folders.folders.folders.docs": 0 "rootFolder.folders.folders.folders.folders.folders.folders.folders.fileRefs": 0 - @db.projects.find.calledWith(sinon.match.any, excludes).should.equal true + @ProjectGetter.getProject + .calledWith(@project_id, excludes) + .should.equal true it "should call the callback with the project", -> - @callback.calledWith(null, @project).should.equal true - + @callback.called.should.equal true describe "getProject", -> diff --git a/services/web/test/unit/coffee/Project/ProjectLocatorTests.coffee b/services/web/test/unit/coffee/Project/ProjectLocatorTests.coffee index 3e94a4f23e..fe5464fc86 100644 --- a/services/web/test/unit/coffee/Project/ProjectLocatorTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectLocatorTests.coffee @@ -33,12 +33,9 @@ project.rootDoc_id = rootDoc._id describe 'ProjectLocator', -> beforeEach -> - Project.getProject = (project_id, fields, callback)=> - callback(null, project) - Project.findById = (project_id, callback)=> callback(null, project) - @ProjectGetter = + @ProjectGetter = getProject: sinon.stub().callsArgWith(2, null, project) @locator = SandboxedModule.require modulePath, requires: '../../models/Project':{Project:Project} @@ -162,14 +159,14 @@ describe 'ProjectLocator', -> assert !err? doc._id.should.equal rootDoc._id done() - + it 'should return null when the project has no rootDoc', (done) -> project.rootDoc_id = null @locator.findRootDoc project, (err, doc)-> assert !err? expect(doc).to.equal null done() - + it 'should return null when the rootDoc_id no longer exists', (done) -> project.rootDoc_id = "doesntexist" @locator.findRootDoc project, (err, doc)-> @@ -181,68 +178,71 @@ describe 'ProjectLocator', -> it 'should take a doc path and return the element for a root level document', (done)-> path = "#{doc1.name}" - @locator.findElementByPath project._id, path, (err, element, type)-> + @locator.findElementByPath {project, path}, (err, element, type)-> element.should.deep.equal doc1 expect(type).to.equal "doc" done() it 'should take a doc path and return the element for a root level document with a starting slash', (done)-> path = "/#{doc1.name}" - @locator.findElementByPath project._id, path, (err, element, type)-> + @locator.findElementByPath {project, path}, (err, element, type)-> element.should.deep.equal doc1 expect(type).to.equal "doc" done() - + it 'should take a doc path and return the element for a nested document', (done)-> path = "#{subFolder.name}/#{secondSubFolder.name}/#{subSubDoc.name}" - @locator.findElementByPath project._id, path, (err, element, type)-> + @locator.findElementByPath {project, path}, (err, element, type)-> element.should.deep.equal subSubDoc expect(type).to.equal "doc" done() it 'should take a file path and return the element for a root level document', (done)-> path = "#{file1.name}" - @locator.findElementByPath project._id, path, (err, element, type)-> + @locator.findElementByPath {project, path}, (err, element, type)-> element.should.deep.equal file1 expect(type).to.equal "file" done() it 'should take a file path and return the element for a nested document', (done)-> path = "#{subFolder.name}/#{secondSubFolder.name}/#{subSubFile.name}" - @locator.findElementByPath project._id, path, (err, element, type)-> + @locator.findElementByPath {project, path}, (err, element, type)-> element.should.deep.equal subSubFile expect(type).to.equal "file" done() it 'should take a file path and return the element for a nested document case insenstive', (done)-> path = "#{subFolder.name.toUpperCase()}/#{secondSubFolder.name.toUpperCase()}/#{subSubFile.name.toUpperCase()}" - @locator.findElementByPath project._id, path, (err, element, type)-> + @locator.findElementByPath {project, path}, (err, element, type)-> element.should.deep.equal subSubFile expect(type).to.equal "file" done() it 'should take a file path and return the element for a nested folder', (done)-> path = "#{subFolder.name}/#{secondSubFolder.name}" - @locator.findElementByPath project._id, path, (err, element, type)-> + @locator.findElementByPath {project, path}, (err, element, type)-> element.should.deep.equal secondSubFolder expect(type).to.equal "folder" done() it 'should take a file path and return the root folder', (done)-> - @locator.findElementByPath project._id, "/", (err, element, type)-> + path = "/" + @locator.findElementByPath {project, path}, (err, element, type)-> element.should.deep.equal rootFolder expect(type).to.equal "folder" done() it 'should return an error if the file can not be found inside know folder', (done)-> - @locator.findElementByPath project._id, "#{subFolder.name}/#{secondSubFolder.name}/exist.txt", (err, element, type)-> + path = "#{subFolder.name}/#{secondSubFolder.name}/exist.txt" + @locator.findElementByPath {project, path}, (err, element, type)-> err.should.not.equal undefined assert.equal element, undefined expect(type).to.be.undefined done() it 'should return an error if the file can not be found inside unknown folder', (done)-> - @locator.findElementByPath project._id, "this/does/not/exist.txt", (err, element, type)-> + path = "this/does/not/exist.txt" + @locator.findElementByPath {project, path}, (err, element, type)-> err.should.not.equal undefined assert.equal element, undefined expect(type).to.be.undefined @@ -250,7 +250,6 @@ describe 'ProjectLocator', -> describe "where duplicate folder exists", -> - beforeEach -> @duplicateFolder = {name:"duplicate1", _id:"1234", folders:[{ name: "1" @@ -264,17 +263,15 @@ describe 'ProjectLocator', -> fileRefs: [] docs: [] ] - Project.getProject = sinon.stub() - Project.getProject.callsArgWith(2, null, @project) - it "should not call the callback more than once", (done)-> - @locator.findElementByPath project._id, "#{@duplicateFolder.name}/#{@doc.name}", -> + path = "#{@duplicateFolder.name}/#{@doc.name}" + @locator.findElementByPath {@project, path}, -> done() #mocha will throw exception if done called multiple times - it "should not call the callback more than once when the path is longer than 1 level below the duplicate level", (done)-> - @locator.findElementByPath project._id, "#{@duplicateFolder.name}/1/main.tex", -> + path = "#{@duplicateFolder.name}/1/main.tex" + @locator.findElementByPath {@project, path}, -> done() #mocha will throw exception if done called multiple times describe "with a null doc", -> @@ -285,33 +282,34 @@ describe 'ProjectLocator', -> fileRefs: [] docs: [{name:"main.tex"}, null, {name:"other.tex"}] ] - Project.getProject = sinon.stub() - Project.getProject.callsArgWith(2, null, @project) it "should not crash with a null", (done)-> - callback = sinon.stub() - @locator.findElementByPath project._id, "/other.tex", (err, element)-> + path = "/other.tex" + @locator.findElementByPath {@project, path}, (err, element)-> element.name.should.equal "other.tex" done() - describe "with a null project", -> beforeEach -> - @project = - rootFolder:[ - folders: [] - fileRefs: [] - docs: [{name:"main.tex"}, null, {name:"other.tex"}] - ] - Project.getProject = sinon.stub() - Project.getProject.callsArgWith(2, null) + @ProjectGetter = + getProject: sinon.stub().callsArg(2) it "should not crash with a null", (done)-> - callback = sinon.stub() - @locator.findElementByPath project._id, "/other.tex", (err, element)-> + path = "/other.tex" + @locator.findElementByPath {project_id: @project._id, path}, (err, element)-> expect(err).to.exist - done() + done() + describe "with a project_id", -> + it 'should take a doc path and return the element for a root level document', (done)-> + path = "#{doc1.name}" + @locator.findElementByPath {project_id: project._id, path}, (err, element, type)=> + @ProjectGetter.getProject + .calledWith(project._id, {rootFolder:true, rootDoc_id: true}) + .should.equal true + element.should.deep.equal doc1 + expect(type).to.equal "doc" + done() describe 'findUsersProjectByName finding a project by user_id and project name', ()-> it 'should return the project from an array case insenstive', (done)-> diff --git a/services/web/test/unit/coffee/infrastructure/LockManager/CheckingTheLock.coffee b/services/web/test/unit/coffee/infrastructure/LockManager/CheckingTheLock.coffee index cf56778ec8..80fcdd1dc2 100644 --- a/services/web/test/unit/coffee/infrastructure/LockManager/CheckingTheLock.coffee +++ b/services/web/test/unit/coffee/infrastructure/LockManager/CheckingTheLock.coffee @@ -2,9 +2,7 @@ sinon = require('sinon') assert = require('assert') path = require('path') modulePath = path.join __dirname, '../../../../../app/js/infrastructure/LockManager.js' -project_id = 1234 -doc_id = 5678 -blockingKey = "lock:web:{#{doc_id}}" +lockKey = "lock:web:{#{5678}}" SandboxedModule = require('sandboxed-module') describe 'LockManager - checking the lock', ()-> @@ -29,20 +27,20 @@ describe 'LockManager - checking the lock', ()-> it 'should check if lock exists but not set or expire', (done)-> execStub.callsArgWith(0, null, ["1"]) - LockManager.checkLock doc_id, (err, docIsLocked)-> - existsStub.calledWith(blockingKey).should.equal true + LockManager._checkLock lockKey, (err, docIsLocked)-> + existsStub.calledWith(lockKey).should.equal true setStub.called.should.equal false exireStub.called.should.equal false done() it 'should return true if the key does not exists', (done)-> execStub.callsArgWith(0, null, "0") - LockManager.checkLock doc_id, (err, free)-> + LockManager._checkLock lockKey, (err, free)-> free.should.equal true done() it 'should return false if the key does exists', (done)-> execStub.callsArgWith(0, null, "1") - LockManager.checkLock doc_id, (err, free)-> + LockManager._checkLock lockKey, (err, free)-> free.should.equal false done() diff --git a/services/web/test/unit/coffee/infrastructure/LockManager/ReleasingTheLock.coffee b/services/web/test/unit/coffee/infrastructure/LockManager/ReleasingTheLock.coffee index 8ccab0a757..256f72f95f 100644 --- a/services/web/test/unit/coffee/infrastructure/LockManager/ReleasingTheLock.coffee +++ b/services/web/test/unit/coffee/infrastructure/LockManager/ReleasingTheLock.coffee @@ -2,8 +2,7 @@ sinon = require('sinon') assert = require('assert') path = require('path') modulePath = path.join __dirname, '../../../../../app/js/infrastructure/LockManager.js' -project_id = 1234 -doc_id = 5678 +lockKey = "lock:web:{#{5678}}" SandboxedModule = require('sandboxed-module') describe 'LockManager - releasing the lock', ()-> @@ -20,7 +19,7 @@ describe 'LockManager - releasing the lock', ()-> LockManager = SandboxedModule.require(modulePath, requires: mocks) it 'should put a all data into memory', (done)-> - LockManager.releaseLock doc_id, -> - deleteStub.calledWith("lock:web:{#{doc_id}}").should.equal true + LockManager._releaseLock lockKey, -> + deleteStub.calledWith(lockKey).should.equal true done() diff --git a/services/web/test/unit/coffee/infrastructure/LockManager/getLockTests.coffee b/services/web/test/unit/coffee/infrastructure/LockManager/getLockTests.coffee index 16ae0a8b8c..754d1f2a85 100644 --- a/services/web/test/unit/coffee/infrastructure/LockManager/getLockTests.coffee +++ b/services/web/test/unit/coffee/infrastructure/LockManager/getLockTests.coffee @@ -20,18 +20,18 @@ describe 'LockManager - getting the lock', -> describe "when the lock is not set", -> beforeEach (done) -> - @LockManager.tryLock = sinon.stub().callsArgWith(1, null, true) - @LockManager.getLock @doc_id, (args...) => + @LockManager._tryLock = sinon.stub().callsArgWith(1, null, true) + @LockManager._getLock @doc_id, (args...) => @callback(args...) done() it "should try to get the lock", -> - @LockManager.tryLock + @LockManager._tryLock .calledWith(@doc_id) .should.equal true it "should only need to try once", -> - @LockManager.tryLock.callCount.should.equal 1 + @LockManager._tryLock.callCount.should.equal 1 it "should return the callback", -> @callback.calledWith(null).should.equal true @@ -41,20 +41,20 @@ describe 'LockManager - getting the lock', -> startTime = Date.now() tries = 0 @LockManager.LOCK_TEST_INTERVAL = 5 - @LockManager.tryLock = (doc_id, callback = (error, isFree) ->) -> + @LockManager._tryLock = (doc_id, callback = (error, isFree) ->) -> if (Date.now() - startTime < 20) or (tries < 2) tries = tries + 1 callback null, false else callback null, true - sinon.spy @LockManager, "tryLock" + sinon.spy @LockManager, "_tryLock" - @LockManager.getLock @doc_id, (args...) => + @LockManager._getLock @doc_id, (args...) => @callback(args...) done() it "should call tryLock multiple times until free", -> - (@LockManager.tryLock.callCount > 1).should.equal true + (@LockManager._tryLock.callCount > 1).should.equal true it "should return the callback", -> @callback.calledWith(null).should.equal true @@ -63,8 +63,8 @@ describe 'LockManager - getting the lock', -> beforeEach (done) -> time = Date.now() @LockManager.MAX_LOCK_WAIT_TIME = 5 - @LockManager.tryLock = sinon.stub().callsArgWith(1, null, false) - @LockManager.getLock @doc_id, (args...) => + @LockManager._tryLock = sinon.stub().callsArgWith(1, null, false) + @LockManager._getLock @doc_id, (args...) => @callback(args...) done() diff --git a/services/web/test/unit/coffee/infrastructure/LockManager/tryLockTests.coffee b/services/web/test/unit/coffee/infrastructure/LockManager/tryLockTests.coffee index 5397e18cb2..3ea837df92 100644 --- a/services/web/test/unit/coffee/infrastructure/LockManager/tryLockTests.coffee +++ b/services/web/test/unit/coffee/infrastructure/LockManager/tryLockTests.coffee @@ -14,17 +14,18 @@ describe 'LockManager - trying the lock', -> auth:-> set: @set = sinon.stub() "settings-sharelatex":{redis:{}} - "metrics-sharelatex": inc:-> + "metrics-sharelatex": inc:-> @callback = sinon.stub() @doc_id = "doc-id-123" - + @key = "lock:web:{#{@doc_id}}" + describe "when the lock is not set", -> beforeEach -> @set.callsArgWith(5, null, "OK") - @LockManager.tryLock @doc_id, @callback + @LockManager._tryLock @key, @callback it "should set the lock key with an expiry if it is not set", -> - @set.calledWith("lock:web:{#{@doc_id}}", "locked", "EX", 30, "NX") + @set.calledWith(@key, "locked", "EX", 30, "NX") .should.equal true it "should return the callback with true", -> @@ -33,7 +34,7 @@ describe 'LockManager - trying the lock', -> describe "when the lock is already set", -> beforeEach -> @set.callsArgWith(5, null, null) - @LockManager.tryLock @doc_id, @callback + @LockManager._tryLock @key, @callback it "should return the callback with false", -> @callback.calledWith(null, false).should.equal true