Просмотр исходного кода

Merge pull request #286 from sharelatex/bg-prevent-duplicate-filenames

prevent duplicate filenames
Brian Gough 8 лет назад
Родитель
Сommit
5e52ade1ff

+ 1 - 1
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
 

+ 55 - 36
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"

+ 2 - 2
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",

+ 36 - 8
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

+ 1 - 0
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')

+ 3 - 0
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))

+ 2 - 1
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 = {}) =>

+ 448 - 0
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

+ 5 - 2
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?

+ 138 - 2
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 ->