diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee index 16b1a79e31..614781211c 100644 --- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee @@ -105,7 +105,7 @@ module.exports = EditorHttpController = else if error?.message == 'invalid element name' res.status(400).json(req.i18n.translate('invalid_file_name')) else if error? - res.status(500).json(req.i18n.translate('generic_something_went_wrong')) + next(error) else res.json doc diff --git a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee index c4992a29f8..b67180659e 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee @@ -170,7 +170,8 @@ module.exports = ProjectEntityHandler = if project_or_id._id? # project return cb(null, project_or_id) else # id - return ProjectGetter.getProjectWithOnlyFolders project_or_id, cb + # need to retrieve full project structure to check for duplicates + return ProjectGetter.getProject project_or_id, {rootFolder:true, name:true}, cb getProject (error, project) -> if err? logger.err project_id:project_id, err:err, "error getting project for add doc" @@ -207,7 +208,7 @@ module.exports = ProjectEntityHandler = ProjectEntityHandler.addDoc project_id, null, name, lines, callback addFileWithoutUpdatingHistory: (project_id, folder_id, fileName, path, userId, callback = (error, fileRef, folder_id, path, fileStoreUrl) ->)-> - ProjectGetter.getProjectWithOnlyFolders project_id, (err, project) -> + ProjectGetter.getProject 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) @@ -229,6 +230,7 @@ module.exports = ProjectEntityHandler = addFile: (project_id, folder_id, fileName, fsPath, userId, callback = (error, fileRef, folder_id) ->)-> ProjectEntityHandler.addFileWithoutUpdatingHistory project_id, folder_id, fileName, fsPath, userId, (error, fileRef, folder_id, path, fileStoreUrl) -> + return callback(error) if error? newFiles = [ file: fileRef path: path @@ -340,7 +342,7 @@ module.exports = ProjectEntityHandler = callback(null, folders, lastFolder) addFolder: (project_id, parentFolder_id, folderName, callback) -> - ProjectGetter.getProjectWithOnlyFolders project_id, (err, project)=> + ProjectGetter.getProject 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) @@ -394,7 +396,7 @@ module.exports = ProjectEntityHandler = return callback(err) if err? projectLocator.findElement {project, element_id: entity_id, type: entityType}, (err, entity, entityPath)-> return callback(err) if err? - self._checkValidMove project, entityType, entityPath, destFolderId, (error) -> + self._checkValidMove project, entityType, entity, entityPath, destFolderId, (error) -> return callback(error) if error? self.getAllEntitiesFromProject project, (error, oldDocs, oldFiles) => return callback(error) if error? @@ -413,19 +415,20 @@ module.exports = ProjectEntityHandler = return callback(error) if error? DocumentUpdaterHandler.updateProjectStructure project_id, userId, {oldDocs, newDocs, oldFiles, newFiles}, callback - _checkValidMove: (project, entityType, entityPath, destFolderId, callback = (error) ->) -> - return callback() if !entityType.match(/folder/) - + _checkValidMove: (project, entityType, entity, entityPath, destFolderId, callback = (error) ->) -> projectLocator.findElement { project, element_id: destFolderId, type:"folder"}, (err, destEntity, destFolderPath) -> return callback(err) if err? - logger.log destFolderPath: destFolderPath.fileSystem, folderPath: entityPath.fileSystem, "checking folder is not moving into child folder" - isNestedFolder = destFolderPath.fileSystem.slice(0, entityPath.fileSystem.length) == entityPath.fileSystem - if isNestedFolder - callback(new Error("destination folder is a child folder of me")) - else + # check if there is already a doc/file/folder with the same name + # in the destination folder + ProjectEntityHandler.checkValidElementName destEntity, entity.name, (err)-> + return callback(err) if err? + if entityType.match(/folder/) + logger.log destFolderPath: destFolderPath.fileSystem, folderPath: entityPath.fileSystem, "checking folder is not moving into child folder" + isNestedFolder = destFolderPath.fileSystem.slice(0, entityPath.fileSystem.length) == entityPath.fileSystem + if isNestedFolder + return callback(new Errors.InvalidNameError("destination folder is a child folder of me")) callback() - deleteEntity: (project_id, entity_id, entityType, userId, callback = (error) ->)-> self = @ logger.log entity_id:entity_id, entityType:entityType, project_id:project_id, "deleting project entity" @@ -456,19 +459,22 @@ module.exports = ProjectEntityHandler = return callback(error) if error? ProjectEntityHandler.getAllEntitiesFromProject project, (error, oldDocs, oldFiles) => return callback(error) if error? - projectLocator.findElement {project:project, element_id:entity_id, type:entityType}, (error, entity, entPath)=> + projectLocator.findElement {project:project, element_id:entity_id, type:entityType}, (error, entity, entPath, parentFolder)=> return callback(error) if error? - endPath = path.join(path.dirname(entPath.fileSystem), newName) - conditions = {_id:project_id} - update = "$set":{} - namePath = entPath.mongo+".name" - update["$set"][namePath] = newName - tpdsUpdateSender.moveEntity({project_id:project_id, startPath:entPath.fileSystem, endPath:endPath, project_name:project.name, rev:entity.rev}) - Project.findOneAndUpdate conditions, update, { "new": true}, (error, newProject) -> + # check if the new name already exists in the current folder + ProjectEntityHandler.checkValidElementName parentFolder, newName, (error) => return callback(error) if error? - ProjectEntityHandler.getAllEntitiesFromProject newProject, (error, newDocs, newFiles) => + endPath = path.join(path.dirname(entPath.fileSystem), newName) + conditions = {_id:project_id} + update = "$set":{} + namePath = entPath.mongo+".name" + update["$set"][namePath] = newName + tpdsUpdateSender.moveEntity({project_id:project_id, startPath:entPath.fileSystem, endPath:endPath, project_name:project.name, rev:entity.rev}) + Project.findOneAndUpdate conditions, update, { "new": true}, (error, newProject) -> return callback(error) if error? - DocumentUpdaterHandler.updateProjectStructure project_id, userId, {oldDocs, newDocs, oldFiles, newFiles}, callback + ProjectEntityHandler.getAllEntitiesFromProject newProject, (error, newDocs, newFiles) => + return callback(error) if error? + DocumentUpdaterHandler.updateProjectStructure project_id, userId, {oldDocs, newDocs, oldFiles, newFiles}, callback _cleanUpEntity: (project, entity, entityType, path, userId, callback = (error) ->) -> if(entityType.indexOf("file") != -1) @@ -606,19 +612,32 @@ module.exports = ProjectEntityHandler = newPath = fileSystem: "#{path.fileSystem}/#{element.name}" mongo: path.mongo - id = element._id+'' - element._id = require('mongoose').Types.ObjectId(id) - conditions = _id:project._id - mongopath = "#{path.mongo}.#{type}" - update = "$push":{} - update["$push"][mongopath] = element - logger.log project_id: project._id, element_id: element._id, fileType: type, folder_id: folder_id, mongopath:mongopath, "adding element to project" - Project.findOneAndUpdate conditions, update, {"new": true}, (err, project)-> - if err? - logger.err err: err, project_id: project._id, 'error saving in putElement project' - return callback(err) - callback(err, {path:newPath}, project) + ProjectEntityHandler.checkValidElementName folder, element.name, (err) => + return callback(err) if err? + id = element._id+'' + element._id = require('mongoose').Types.ObjectId(id) + conditions = _id:project._id + mongopath = "#{path.mongo}.#{type}" + update = "$push":{} + update["$push"][mongopath] = element + logger.log project_id: project._id, element_id: element._id, fileType: type, folder_id: folder_id, mongopath:mongopath, "adding element to project" + Project.findOneAndUpdate conditions, update, {"new": true}, (err, project)-> + if err? + logger.err err: err, project_id: project._id, 'error saving in putElement project' + return callback(err) + callback(err, {path:newPath}, project) + checkValidElementName: (folder, name, callback = (err) ->) -> + # check if the name is already taken by a doc, file or + # folder. If so, return an error "file already exists". + err = new Errors.InvalidNameError("file already exists") + for doc in folder?.docs or [] + return callback(err) if doc.name is name + for file in folder?.fileRefs or [] + return callback(err) if file.name is name + for folder in folder?.folders or [] + return callback(err) if folder.name is name + callback() confirmFolder = (project, folder_id, callback)-> logger.log folder_id:folder_id, project_id:project._id, "confirming folder in project" diff --git a/services/web/app/views/project/editor/file-tree.pug b/services/web/app/views/project/editor/file-tree.pug index cfbc9cedaf..fade8e0782 100644 --- a/services/web/app/views/project/editor/file-tree.pug +++ b/services/web/app/views/project/editor/file-tree.pug @@ -110,7 +110,7 @@ script(type='text/ng-template', id='entityListItemTemplate') i.fa.fa-fw(ng-if="entity.type != 'folder'", ng-class="'fa-' + iconTypeFromName(entity.name)") span( ng-hide="entity.renaming" - ) {{ entity.name }} + ) {{ entity.renamingToName || entity.name }} span.rename-input input( ng-if="permissions.write", @@ -198,7 +198,7 @@ script(type='text/ng-template', id='entityListItemTemplate') span( ng-hide="entity.renaming" - ) {{ entity.name }} + ) {{ entity.renamingToName || entity.name }} span.rename-input input( ng-if="permissions.write", diff --git a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee index 390b9dba79..9891bd8e74 100644 --- a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee +++ b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee @@ -321,7 +321,20 @@ define [ return null + existsInThisFolder: (folder, name) -> + for entity in folder?.children or [] + return true if entity.name is name + return false + + nameExistsError: (message = "already exists") -> + nameExists = @ide.$q.defer() + nameExists.reject({data: message}) + return nameExists.promise + createDoc: (name, parent_folder = @getCurrentFolder()) -> + # check if a doc/file/folder already exists with this name + if @existsInThisFolder parent_folder, name + return @nameExistsError() # We'll wait for the socket.io notification to actually # add the doc for us. @ide.$http.post "/project/#{@ide.project_id}/doc", { @@ -331,6 +344,9 @@ define [ } createFolder: (name, parent_folder = @getCurrentFolder()) -> + # check if a doc/file/folder already exists with this name + if @existsInThisFolder parent_folder, name + return @nameExistsError() # We'll wait for the socket.io notification to actually # add the folder for us. return @ide.$http.post "/project/#{@ide.project_id}/folder", { @@ -341,12 +357,20 @@ define [ renameEntity: (entity, name, callback = (error) ->) -> return if entity.name == name - if name.length < 150 - entity.name = name - return @ide.$http.post "/project/#{@ide.project_id}/#{entity.type}/#{entity.id}/rename", { - name: entity.name, + return if name.length >= 150 + # check if a doc/file/folder already exists with this name + parent_folder = @getCurrentFolder() + if @existsInThisFolder parent_folder, name + return @nameExistsError() + entity.renamingToName = name + @ide.$http.post("/project/#{@ide.project_id}/#{entity.type}/#{entity.id}/rename", { + name: name, _csrf: window.csrfToken - } + }) + .then () -> + entity.name = name + .finally () -> + entity.renamingToName = null deleteEntity: (entity, callback = (error) ->) -> # We'll wait for the socket.io notification to @@ -362,11 +386,15 @@ define [ # Abort move if the folder being moved (entity) has the parent_folder as child # since that would break the tree structure. return if @_isChildFolder(entity, parent_folder) - @_moveEntityInScope(entity, parent_folder) - return @ide.queuedHttp.post "/project/#{@ide.project_id}/#{entity.type}/#{entity.id}/move", { + # check if a doc/file/folder already exists with this name + if @existsInThisFolder parent_folder, entity.name + return @nameExistsError() + # Wait for the http response before doing the move + @ide.queuedHttp.post("/project/#{@ide.project_id}/#{entity.type}/#{entity.id}/move", { folder_id: parent_folder.id _csrf: window.csrfToken - } + }).then () => + @_moveEntityInScope(entity, parent_folder) _isChildFolder: (parent_folder, child_folder) -> parent_path = @getEntityPath(parent_folder) or "" # null if root folder diff --git a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee index 28964b9090..aee631f334 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee @@ -69,6 +69,7 @@ define [ .catch (response)-> { data } = response $scope.error = data + $scope.state.inflight = false $scope.cancel = () -> $modalInstance.dismiss('cancel') diff --git a/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee b/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee index e6573bbe57..55bf16a172 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee @@ -28,6 +28,9 @@ define [ invalidModalShowing = false $scope.finishRenaming = () -> + # avoid double events when blur and on-enter fire together + return if !$scope.entity.renaming + name = $scope.inputs.name if !name.match(new RegExp(ide.validFileRegex)) diff --git a/services/web/public/coffee/ide/services/ide.coffee b/services/web/public/coffee/ide/services/ide.coffee index 57e0bbef3d..6462859df2 100644 --- a/services/web/public/coffee/ide/services/ide.coffee +++ b/services/web/public/coffee/ide/services/ide.coffee @@ -3,10 +3,11 @@ define [ ], (App) -> # We create and provide this as service so that we can access the global ide # from within other parts of the angular app. - App.factory "ide", ["$http", "queuedHttp", "$modal", ($http, queuedHttp, $modal) -> + App.factory "ide", ["$http", "queuedHttp", "$modal", "$q", ($http, queuedHttp, $modal, $q) -> ide = {} ide.$http = $http ide.queuedHttp = queuedHttp + ide.$q = $q @recentEvents = [] ide.pushEvent = (type, meta = {}) => diff --git a/services/web/test/acceptance/coffee/ProjectDuplicateNameTests.coffee b/services/web/test/acceptance/coffee/ProjectDuplicateNameTests.coffee new file mode 100644 index 0000000000..5da94856fc --- /dev/null +++ b/services/web/test/acceptance/coffee/ProjectDuplicateNameTests.coffee @@ -0,0 +1,448 @@ +async = require "async" +expect = require("chai").expect +sinon = require "sinon" +mkdirp = require "mkdirp" +ObjectId = require("mongojs").ObjectId +Path = require "path" +fs = require "fs" +Settings = require "settings-sharelatex" +_ = require "underscore" + +ProjectGetter = require "../../../app/js/Features/Project/ProjectGetter.js" + +MockDocStoreApi = require './helpers/MockDocstoreApi' +MockFileStoreApi = require './helpers/MockFileStoreApi' +request = require "./helpers/request" +User = require "./helpers/User" + +describe "ProjectDuplicateNames", -> + before (done) -> + @owner = new User() + @owner.login done + @project = {} + @callback = sinon.stub() + + describe "creating a project from the example template", -> + before (done) -> + @owner.createProject "example-project", {template: "example"}, (error, project_id) => + throw error if error? + @example_project_id = project_id + @owner.getProject project_id, (error, project) => + @project = project + @mainTexDoc = _.find(project.rootFolder[0].docs, (doc) -> doc.name is 'main.tex') + @refBibDoc = _.find(project.rootFolder[0].docs, (doc) -> doc.name is 'references.bib') + @imageFile = _.find(project.rootFolder[0].fileRefs, (file) -> file.name is 'universe.jpg') + @rootFolderId = project.rootFolder[0]._id.toString() + # create a folder called 'testfolder' + @owner.request.post { + uri: "/project/#{@example_project_id}/folder" + json: + name: "testfolder" + parent_folder_id: @rootFolderId + }, (err, res, body) => + @testFolderId = body._id + done() + + it "should create a project", -> + expect(@project.rootFolder[0].docs.length).to.equal(2) + expect(@project.rootFolder[0].fileRefs.length).to.equal(1) + + it "should create two docs in the docstore", -> + docs = MockDocStoreApi.docs[@example_project_id] + expect(Object.keys(docs).length).to.equal(2) + + it "should create one file in the filestore", -> + files = MockFileStoreApi.files[@example_project_id] + expect(Object.keys(files).length).to.equal(1) + + describe "for an existing doc", -> + describe "trying to add a doc with the same name", -> + before (done) -> + @owner.request.post { + uri: "/project/#{@example_project_id}/doc" + json: + name: "main.tex" + parent_folder_id: @rootFolderId + }, (err, res, body) => + @res = res + done() + + it "should respond with 400 error status", -> + expect(@res.statusCode).to.equal 400 + + describe "trying to add a folder with the same name", -> + before (done) -> + @owner.request.post { + uri: "/project/#{@example_project_id}/folder" + json: + name: "main.tex" + parent_folder_id: @rootFolderId + }, (err, res, body) => + @res = res + done() + + it "should respond with 400 error status", -> + expect(@res.statusCode).to.equal 400 + + describe "trying to upload a file with the same name", -> + before (done) -> + @owner.request.post + uri: "/project/#{@example_project_id}/upload" + json: true + qs: + folder_id: @rootFolderId + qqfilename: "main.tex" + formData: + qqfile: + value: fs.createReadStream Path.resolve(__dirname + '/../files/1pixel.png') + options: + filename: 'main.tex', + contentType: 'image/png' + , (err, res, body) => + @body = body + done() + + it "should respond with failure status", -> + expect(@body.success).to.equal false + + describe "trying to add a folder with the same name", -> + before (done) -> + @owner.request.post { + uri: "/project/#{@example_project_id}/folder" + json: + name: "main.tex" + parent_folder_id: @rootFolderId + }, (err, res, body) => + @res = res + done() + + it "should respond with 400 error status", -> + expect(@res.statusCode).to.equal 400 + + describe "trying to upload a file with the same name", -> + before (done) -> + @owner.request.post + uri: "/project/#{@example_project_id}/upload" + json: true + qs: + folder_id: @rootFolderId + qqfilename: "main.tex" + formData: + qqfile: + value: fs.createReadStream Path.resolve(__dirname + '/../files/1pixel.png') + options: + filename: 'main.tex', + contentType: 'image/png' + , (err, res, body) => + @body = body + done() + + it "should respond with failure status", -> + expect(@body.success).to.equal false + + + describe "for an existing file", -> + describe "trying to add a doc with the same name", -> + before (done) -> + @owner.request.post { + uri: "/project/#{@example_project_id}/doc" + json: + name: "universe.jpg" + parent_folder_id: @rootFolderId + }, (err, res, body) => + @res = res + done() + + it "should respond with 400 error status", -> + expect(@res.statusCode).to.equal 400 + + describe "trying to add a folder with the same name", -> + before (done) -> + @owner.request.post { + uri: "/project/#{@example_project_id}/folder" + json: + name: "universe.jpg" + parent_folder_id: @rootFolderId + }, (err, res, body) => + @res = res + done() + + it "should respond with 400 error status", -> + expect(@res.statusCode).to.equal 400 + + describe "trying to upload a file with the same name", -> + before (done) -> + @owner.request.post + uri: "/project/#{@example_project_id}/upload" + json: true + qs: + folder_id: @rootFolderId + qqfilename: "universe.jpg" + formData: + qqfile: + value: fs.createReadStream Path.resolve(__dirname + '/../files/1pixel.png') + options: + filename: 'universe.jpg', + contentType: 'image/jpeg' + , (err, res, body) => + @body = body + done() + + it "should succeed (overwriting the file)", -> + expect(@body.success).to.equal true + + describe "for an existing folder", -> + describe "trying to add a doc with the same name", -> + before (done) -> + @owner.request.post { + uri: "/project/#{@example_project_id}/doc" + json: + name: "testfolder" + parent_folder_id: @rootFolderId + }, (err, res, body) => + @res = res + done() + + it "should respond with 400 error status", -> + expect(@res.statusCode).to.equal 400 + + describe "trying to add a folder with the same name", -> + before (done) -> + @owner.request.post { + uri: "/project/#{@example_project_id}/folder" + json: + name: "testfolder" + parent_folder_id: @rootFolderId + }, (err, res, body) => + @res = res + done() + + it "should respond with 400 error status", -> + expect(@res.statusCode).to.equal 400 + + describe "trying to upload a file with the same name", -> + before (done) -> + @owner.request.post + uri: "/project/#{@example_project_id}/upload" + json: true + qs: + folder_id: @rootFolderId + qqfilename: "universe.jpg" + formData: + qqfile: + value: fs.createReadStream Path.resolve(__dirname + '/../files/1pixel.png') + options: + filename: 'testfolder', + contentType: 'image/jpeg' + , (err, res, body) => + @body = body + done() + + it "should respond with failure status", -> + expect(@body.success).to.equal false + + + describe "for an existing doc", -> + describe "trying to rename a doc to the same name", -> + before (done) -> + @owner.request.post { + uri: "/project/#{@example_project_id}/doc/#{@refBibDoc._id}/rename" + json: + name: "main.tex" + }, (err, res, body) => + @res = res + done() + + it "should respond with 400 error status", -> + expect(@res.statusCode).to.equal 400 + + describe "trying to rename a folder to the same name", -> + before (done) -> + @owner.request.post { + uri: "/project/#{@example_project_id}/folder/#{@testFolderId}/rename" + json: + name: "main.tex" + }, (err, res, body) => + @res = res + done() + + it "should respond with 400 error status", -> + expect(@res.statusCode).to.equal 400 + + describe "trying to rename a file to the same name", -> + before (done) -> + @owner.request.post { + uri: "/project/#{@example_project_id}/file/#{@imageFile._id}/rename" + json: + name: "main.tex" + }, (err, res, body) => + @res = res + done() + + it "should respond with failure status", -> + expect(@res.statusCode).to.equal 400 + + + describe "for an existing file", -> + describe "trying to rename a doc to the same name", -> + before (done) -> + @owner.request.post { + uri: "/project/#{@example_project_id}/doc/#{@refBibDoc._id}/rename" + json: + name: "universe.jpg" + }, (err, res, body) => + @res = res + done() + + it "should respond with 400 error status", -> + expect(@res.statusCode).to.equal 400 + + describe "trying to rename a folder to the same name", -> + before (done) -> + @owner.request.post { + uri: "/project/#{@example_project_id}/folder/#{@testFolderId}/rename" + json: + name: "universe.jpg" + }, (err, res, body) => + @res = res + done() + + it "should respond with 400 error status", -> + expect(@res.statusCode).to.equal 400 + + describe "trying to rename a file to the same name", -> + before (done) -> + @owner.request.post { + uri: "/project/#{@example_project_id}/file/#{@imageFile._id}/rename" + json: + name: "universe.jpg" + }, (err, res, body) => + @res = res + done() + + it "should respond with failure status", -> + expect(@res.statusCode).to.equal 400 + + + describe "for an existing folder", -> + describe "trying to rename a doc to the same name", -> + before (done) -> + @owner.request.post { + uri: "/project/#{@example_project_id}/doc/#{@refBibDoc._id}/rename" + json: + name: "testfolder" + }, (err, res, body) => + @res = res + done() + + it "should respond with 400 error status", -> + expect(@res.statusCode).to.equal 400 + + describe "trying to rename a folder to the same name", -> + before (done) -> + @owner.request.post { + uri: "/project/#{@example_project_id}/folder/#{@testFolderId}/rename" + json: + name: "testfolder" + }, (err, res, body) => + @res = res + done() + + it "should respond with 400 error status", -> + expect(@res.statusCode).to.equal 400 + + describe "trying to rename a file to the same name", -> + before (done) -> + @owner.request.post { + uri: "/project/#{@example_project_id}/file/#{@imageFile._id}/rename" + json: + name: "testfolder" + }, (err, res, body) => + @res = res + done() + + it "should respond with failure status", -> + expect(@res.statusCode).to.equal 400 + + + describe "for an existing folder with a file with the same name", -> + before (done) -> + @owner.request.post { + uri: "/project/#{@example_project_id}/doc" + json: + name: "main.tex" + parent_folder_id: @testFolderId + }, (err, res, body) => + @owner.request.post { + uri: "/project/#{@example_project_id}/doc" + json: + name: "universe.jpg" + parent_folder_id: @testFolderId + }, (err, res, body) => + @owner.request.post { + uri: "/project/#{@example_project_id}/folder" + json: + name: "otherFolder" + parent_folder_id: @testFolderId + }, (err, res, body) => + @subFolderId = body._id + @owner.request.post { + uri: "/project/#{@example_project_id}/folder" + json: + name: "otherFolder" + parent_folder_id: @rootFolderId + }, (err, res, body) => + @otherFolderId = body._id + done() + + describe "trying to move a doc into the folder", -> + before (done) -> + @owner.request.post { + uri: "/project/#{@example_project_id}/doc/#{@mainTexDoc._id}/move" + json: + folder_id: @testFolderId + }, (err, res, body) => + @res = res + done() + + it "should respond with 400 error status", -> + expect(@res.statusCode).to.equal 400 + + describe "trying to move a file into the folder", -> + before (done) -> + @owner.request.post { + uri: "/project/#{@example_project_id}/file/#{@imageFile._id}/move" + json: + folder_id: @testFolderId + }, (err, res, body) => + @res = res + done() + + it "should respond with 400 error status", -> + expect(@res.statusCode).to.equal 400 + + describe "trying to move a folder into the folder", -> + before (done) -> + @owner.request.post { + uri: "/project/#{@example_project_id}/folder/#{@otherFolderId}/move" + json: + folder_id: @testFolderId + }, (err, res, body) => + @res = res + done() + + it "should respond with 400 error status", -> + expect(@res.statusCode).to.equal 400 + + describe "trying to move a folder into a subfolder of itself", -> + before (done) -> + @owner.request.post { + uri: "/project/#{@example_project_id}/folder/#{@testFolderId}/move" + json: + folder_id: @subFolderId + }, (err, res, body) => + @res = res + done() + + it "should respond with 400 error status", -> + expect(@res.statusCode).to.equal 400 diff --git a/services/web/test/acceptance/coffee/helpers/MockFileStoreApi.coffee b/services/web/test/acceptance/coffee/helpers/MockFileStoreApi.coffee index f3022302f4..f5fa79b641 100644 --- a/services/web/test/acceptance/coffee/helpers/MockFileStoreApi.coffee +++ b/services/web/test/acceptance/coffee/helpers/MockFileStoreApi.coffee @@ -8,8 +8,11 @@ module.exports = MockFileStoreApi = app.post "/project/:project_id/file/:file_id", (req, res, next) => req.on 'data', -> - req.on 'end', -> - res.send 200 + req.on 'end', => + {project_id, file_id} = req.params + @files[project_id] ?= {} + @files[project_id][file_id] = { content : "test-file-content" } + res.sendStatus 200 app.listen 3009, (error) -> throw error if error? diff --git a/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee index 3b6a6e8626..39c1eba75d 100644 --- a/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee @@ -62,7 +62,7 @@ describe 'ProjectEntityHandler', -> @ProjectGetter = getProjectWithOnlyFolders : (project_id, callback)=> callback(null, @project) getProjectWithoutDocLines : (project_id, callback)=> callback(null, @project) - getProject:sinon.stub() + getProject: sinon.stub().callsArgWith(2, null, @project) @projectUpdater = markAsUpdated:sinon.stub() @projectLocator = findElement : sinon.stub() @@ -287,6 +287,8 @@ describe 'ProjectEntityHandler', -> @projectLocator.findElement = sinon.stub() @projectLocator.findElement.withArgs({project: @project, element_id: @docId, type: 'docs'}) .callsArgWith(1, null, @doc, @path) + @projectLocator.findElement.withArgs({project: @project, element_id: folder_id, type:"folder"},) + .callsArgWith(1, null, @destFolder, @destFolderPath) @ProjectEntityHandler.moveEntity project_id, @docId, folder_id, "docs", userId, done it 'should find the doc to move', -> @@ -315,6 +317,46 @@ describe 'ProjectEntityHandler', -> }) .should.equal true + describe "moving a doc when another with the same name already exists", -> + beforeEach () -> + @docId = "4eecaffcbffa66588e000009" + @doc = { name: "another-doc.tex", lines:["1234","312343d"], rev: "1234"} + @path = { + mongo:"folders[0]" + fileSystem:"/old_folder/somewhere.txt" + } + @destFolder = { name: "folder", docs: [ {name:"another-doc.tex"} ] } + @destFolderPath = { + mongo: "folders[0]" + fileSystem: "/dest_folder" + } + @projectLocator.findElement = sinon.stub() + @projectLocator.findElement.withArgs({project: @project, element_id: @docId, type: 'docs'}) + .callsArgWith(1, null, @doc, @path) + @projectLocator.findElement.withArgs({project: @project, element_id: folder_id, type:"folder"},) + .callsArgWith(1, null, @destFolder, @destFolderPath) + @callback = sinon.stub() + @ProjectEntityHandler.moveEntity project_id, @docId, folder_id, "docs", userId, @callback + + it 'should return an error', -> + @callback.calledWith(new Errors.InvalidNameError("file already exists")).should.equal true + + it "should should not send the update to the doc updater", -> + @documentUpdaterHandler.updateProjectStructure + .called.should.equal false + + it 'should not remove the element from its current position', -> + @ProjectEntityHandler._removeElementFromMongoArray + .called.should.equal false + + it "should not put the element back in the new folder", -> + @ProjectEntityHandler._putElement.called.should.equal false + + it 'should not tell the third party data store', -> + @tpdsUpdateSender.moveEntity + .called.should.equal false + + describe "moving a folder", -> beforeEach -> @folder_id = "folder-to-move" @@ -379,10 +421,37 @@ describe 'ProjectEntityHandler', -> }) .should.equal true + describe "when the destination folder contains a file with the same name", -> + beforeEach -> + @path.fileSystem = "/one/src_dir" + @pathToMoveTo.fileSystem = "/two/dest_dir" + @folder_to_move_to = { name: "folder to move to", fileRefs: [ {name: "folder"}] } + @projectLocator.findElement.withArgs({project: @project, element_id: @move_to_folder_id, type: 'folder'}) + .callsArgWith(1, null, @folder_to_move_to, @pathToMoveTo) + @callback = sinon.stub() + @ProjectEntityHandler.moveEntity project_id, @folder_id, @move_to_folder_id, "folder", userId, @callback + + it 'should find the folder we are moving to element', -> + @projectLocator.findElement + .calledWith({ + element_id: @move_to_folder_id, + type: "folder", + project: @project + }) + .should.equal true + + it "should return an error", -> + @callback + .calledWith(new Errors.InvalidNameError("file already exists")) + .should.equal true + describe "when the destination folder is inside the moving folder", -> beforeEach -> @path.fileSystem = "/one/two" @pathToMoveTo.fileSystem = "/one/two/three" + + @projectLocator.findElement.withArgs({project: @project, element_id: @move_to_folder_id, type: 'folder'}) + .callsArgWith(1, null, @folder_to_move_to, @pathToMoveTo) @callback = sinon.stub() @ProjectEntityHandler.moveEntity project_id, @folder_id, @move_to_folder_id, "folder", userId, @callback @@ -472,6 +541,7 @@ describe 'ProjectEntityHandler', -> @lines = ['1234','abc'] @path = "/path/to/doc" + @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @project) @ProjectEntityHandler._putElement = sinon.stub().callsArgWith(4, null, {path:{fileSystem:@path}}) @callback = sinon.stub() @tpdsUpdateSender.addDoc = sinon.stub().callsArg(1) @@ -522,6 +592,7 @@ describe 'ProjectEntityHandler', -> @lines = ['1234','abc'] @path = "/path/to/doc" + @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @project) @ProjectEntityHandler._putElement = sinon.stub().callsArgWith(4, null, {path:{fileSystem:@path}}) @callback = sinon.stub() @tpdsUpdateSender.addDoc = sinon.stub().callsArg(1) @@ -1123,6 +1194,20 @@ describe 'ProjectEntityHandler', -> @newName = "new.tex" @path = mongo: "mongo.path", fileSystem: "/oldnamepath/oldname" + @project_id = project_id + @project = + _id: ObjectId(project_id) + rootFolder: [_id:ObjectId()] + @folder = + _id: ObjectId() + name: "someFolder" + docs: [ {name: "another-doc.tex"} ] + fileRefs: [ {name: "another-file.tex"} ] + folders: [ {name: "another-folder"} ] + @doc = + _id: ObjectId() + name: "new.tex" + @ProjectGetter.getProject.callsArgWith(2, null, @project) @ProjectEntityHandler.getAllEntitiesFromProject = sinon.stub() @ProjectEntityHandler.getAllEntitiesFromProject @@ -1132,7 +1217,7 @@ describe 'ProjectEntityHandler', -> .onSecondCall() .callsArgWith(1, null, @newDocs = ['new-doc'], @newFiles = ['new-file']) - @projectLocator.findElement = sinon.stub().callsArgWith(1, null, @entity = { _id: @entity_id, name:"oldname", rev:4 }, @path) + @projectLocator.findElement = sinon.stub().callsArgWith(1, null, @entity = { _id: @entity_id, name:"oldname", rev:4 }, @path, @folder) @tpdsUpdateSender.moveEntity = sinon.stub() @ProjectModel.findOneAndUpdate = sinon.stub().callsArgWith(3, null, @project) @documentUpdaterHandler.updateProjectStructure = sinon.stub().yields() @@ -1154,6 +1239,27 @@ describe 'ProjectEntityHandler', -> @tpdsUpdateSender.moveEntity.calledWith({project_id:project_id, startPath:@path.fileSystem, endPath:"/oldnamepath/new.tex", project_name:@project.name, rev:4}).should.equal true done() + describe "when a document already exists with the same name", -> + beforeEach -> + @project = + _id: ObjectId(project_id) + rootFolder: [_id:ObjectId()] + @folder = + _id: ObjectId() + name: "someFolder" + docs: [ {name: "another-doc.tex"} ] + fileRefs: [ {name: "another-file.tex"} ] + folders: [ {name: "another-folder"} ] + @doc = + _id: ObjectId() + name: "new.tex" + @newName = "another-doc.tex" + + it "should return an error", (done)-> + @ProjectEntityHandler.renameEntity project_id, @entity_id, @entityType, @newName, userId, (err)=> + err.should.deep.equal new Errors.InvalidNameError("file already exists") + done() + describe "_insertDeletedDocReference", -> beforeEach -> @doc = @@ -1248,6 +1354,9 @@ describe 'ProjectEntityHandler', -> @folder = _id: ObjectId() name: "someFolder" + docs: [ {name: "another-doc.tex"} ] + fileRefs: [ {name: "another-file.tex"} ] + folders: [ {name: "another-folder"} ] @doc = _id: ObjectId() name: "new.tex" @@ -1290,6 +1399,33 @@ describe 'ProjectEntityHandler', -> @ProjectModel.findOneAndUpdate.called.should.equal false done() + it "should error if a document already exists with the same name", (done)-> + doc = + _id: ObjectId() + name: "another-doc.tex" + @ProjectEntityHandler._putElement @project, @folder, doc, "doc", (err)=> + @ProjectModel.findOneAndUpdate.called.should.equal false + err.should.deep.equal new Errors.InvalidNameError("file already exists") + done() + + it "should error if a file already exists with the same name", (done)-> + doc = + _id: ObjectId() + name: "another-file.tex" + @ProjectEntityHandler._putElement @project, @folder, doc, "doc", (err)=> + @ProjectModel.findOneAndUpdate.called.should.equal false + err.should.deep.equal new Errors.InvalidNameError("file already exists") + done() + + it "should error if a folder already exists with the same name", (done)-> + doc = + _id: ObjectId() + name: "another-folder" + @ProjectEntityHandler._putElement @project, @folder, doc, "doc", (err)=> + @ProjectModel.findOneAndUpdate.called.should.equal false + err.should.deep.equal new Errors.InvalidNameError("file already exists") + done() + describe "_countElements", -> beforeEach ->