diff --git a/moove/api/src/main/kotlin/io/charlescd/moove/api/MooveExceptionHandler.kt b/moove/api/src/main/kotlin/io/charlescd/moove/api/MooveExceptionHandler.kt index 99519a5929..84268adb2d 100644 --- a/moove/api/src/main/kotlin/io/charlescd/moove/api/MooveExceptionHandler.kt +++ b/moove/api/src/main/kotlin/io/charlescd/moove/api/MooveExceptionHandler.kt @@ -59,7 +59,7 @@ class MooveExceptionHandler(private val messageSource: MessageSource) { @ResponseBody fun exceptions(ex: Exception): ErrorMessageResponse { this.logger.error(ex.message, ex) - return ErrorMessageResponse.of(MooveErrorCode.INTERNAL_SERVER_ERROR, ex.message!!) + return ErrorMessageResponse.of(MooveErrorCode.INTERNAL_SERVER_ERROR, ex.message?.also { ex.message } ?: "Internal Server Error") } @ExceptionHandler(BadRequestClientException::class) diff --git a/moove/application/src/main/kotlin/io/charlescd/moove/application/CircleService.kt b/moove/application/src/main/kotlin/io/charlescd/moove/application/CircleService.kt index bcfaf91ca5..6eb7362ef9 100644 --- a/moove/application/src/main/kotlin/io/charlescd/moove/application/CircleService.kt +++ b/moove/application/src/main/kotlin/io/charlescd/moove/application/CircleService.kt @@ -29,6 +29,9 @@ import javax.inject.Named class CircleService(private val circleRepository: CircleRepository) { fun save(circle: Circle): Circle { + if (this.circleRepository.existsByNameAndWorkspaceId(circle.name, circle.workspaceId)) { + throw BusinessException.of(MooveErrorCode.DUPLICATED_CIRCLE_NAME_ERROR) + } return this.circleRepository.save(circle) } diff --git a/moove/application/src/main/kotlin/io/charlescd/moove/application/WorkspaceService.kt b/moove/application/src/main/kotlin/io/charlescd/moove/application/WorkspaceService.kt index 706933159b..34e8694458 100644 --- a/moove/application/src/main/kotlin/io/charlescd/moove/application/WorkspaceService.kt +++ b/moove/application/src/main/kotlin/io/charlescd/moove/application/WorkspaceService.kt @@ -19,6 +19,7 @@ package io.charlescd.moove.application import io.charlescd.moove.domain.* +import io.charlescd.moove.domain.exceptions.BusinessException import io.charlescd.moove.domain.exceptions.NotFoundException import io.charlescd.moove.domain.repository.UserRepository import io.charlescd.moove.domain.repository.WorkspaceRepository @@ -46,6 +47,9 @@ class WorkspaceService( } fun save(workspace: Workspace): Workspace { + if (this.workspaceRepository.existsByName(workspace.name)) { + throw BusinessException.of(MooveErrorCode.DUPLICATED_WORKSPACE_NAME_ERROR) + } return this.workspaceRepository.save(workspace) } diff --git a/moove/application/src/main/kotlin/io/charlescd/moove/application/circle/request/CreateCircleRequest.kt b/moove/application/src/main/kotlin/io/charlescd/moove/application/circle/request/CreateCircleRequest.kt index adbfcb34a7..961fdf28bd 100644 --- a/moove/application/src/main/kotlin/io/charlescd/moove/application/circle/request/CreateCircleRequest.kt +++ b/moove/application/src/main/kotlin/io/charlescd/moove/application/circle/request/CreateCircleRequest.kt @@ -31,14 +31,11 @@ import javax.validation.constraints.NotNull import javax.validation.constraints.Size class CreateCircleRequest( - @field:NotBlank @field:Size(min = 1, max = 64, message = "Name minimum size is 1 and maximum is 64.") val name: String, - @field:Valid val rules: NodePart - ) { fun toDomain(user: User, workspaceId: String) = Circle( id = UUID.randomUUID().toString(), diff --git a/moove/application/src/test/groovy/io/charlescd/moove/application/workspace/impl/CreateWorkspaceInteractorImplTest.groovy b/moove/application/src/test/groovy/io/charlescd/moove/application/workspace/impl/CreateWorkspaceInteractorImplTest.groovy index 0e2f1e8ecc..b84ee36988 100644 --- a/moove/application/src/test/groovy/io/charlescd/moove/application/workspace/impl/CreateWorkspaceInteractorImplTest.groovy +++ b/moove/application/src/test/groovy/io/charlescd/moove/application/workspace/impl/CreateWorkspaceInteractorImplTest.groovy @@ -24,9 +24,10 @@ import io.charlescd.moove.application.WorkspaceService import io.charlescd.moove.application.workspace.CreateWorkspaceInteractor import io.charlescd.moove.application.workspace.request.CreateWorkspaceRequest import io.charlescd.moove.domain.Circle -import io.charlescd.moove.domain.User +import io.charlescd.moove.domain.MooveErrorCode import io.charlescd.moove.domain.Workspace import io.charlescd.moove.domain.WorkspaceStatusEnum +import io.charlescd.moove.domain.exceptions.BusinessException import io.charlescd.moove.domain.exceptions.NotFoundException import io.charlescd.moove.domain.repository.CircleRepository import io.charlescd.moove.domain.repository.SystemTokenRepository @@ -72,6 +73,27 @@ class CreateWorkspaceInteractorImplTest extends Specification { ex.resourceName == "user" } + def 'when workspace name exist, should throw exception'() { + given: + def authorization = TestUtils.authorization + def author = TestUtils.user + def expectedWorkspace = TestUtils.workspace + def createWorkspaceRequest = new CreateWorkspaceRequest(expectedWorkspace.name) + + when: + createWorkspaceInteractor.execute(createWorkspaceRequest, authorization, null) + + then: + 1 * managementUserSecurityService.getUserEmail(authorization) >> author.email + 1 * userRepository.findByEmail(author.email) >> Optional.of(author) + 1 * workspaceRepository.existsByName(expectedWorkspace.name) >> true + + + def ex = thrown(BusinessException) + ex.errorCode == MooveErrorCode.DUPLICATED_WORKSPACE_NAME_ERROR + + } + def 'should create workspace successfully using authorization'() { given: def authorization = TestUtils.authorization @@ -84,6 +106,7 @@ class CreateWorkspaceInteractorImplTest extends Specification { then: 1 * managementUserSecurityService.getUserEmail(authorization) >> author.email 1 * userRepository.findByEmail(author.email) >> Optional.of(author) + 1 * workspaceRepository.existsByName(expectedWorkspace.name) >> false 1 * workspaceRepository.save(_) >> { arguments -> def workspace = arguments[0] assert workspace instanceof Workspace diff --git a/moove/domain/src/main/kotlin/io/charlescd/moove/domain/MooveErrorCode.kt b/moove/domain/src/main/kotlin/io/charlescd/moove/domain/MooveErrorCode.kt index 0ddd293b6e..76d9738e97 100644 --- a/moove/domain/src/main/kotlin/io/charlescd/moove/domain/MooveErrorCode.kt +++ b/moove/domain/src/main/kotlin/io/charlescd/moove/domain/MooveErrorCode.kt @@ -65,5 +65,7 @@ enum class MooveErrorCode(val key: String) { IDM_UNEXPECTED_ERROR("idm.unexpected.error"), DUPLICATED_COMPONENT_NAME_ERROR("duplicated.component.name.error"), DEPLOYMENT_CONFIGURATION_ALREADY_REGISTERED("deployment.configuration.already.registered"), - ACTIVE_DEPLOYMENT_NAMESPACE_ERROR("active.deployment.namespace.error") + ACTIVE_DEPLOYMENT_NAMESPACE_ERROR("active.deployment.namespace.error"), + DUPLICATED_WORKSPACE_NAME_ERROR("duplicated.workspace.name.error"), + DUPLICATED_CIRCLE_NAME_ERROR("duplicated.circe.name.error") } diff --git a/moove/domain/src/main/kotlin/io/charlescd/moove/domain/repository/CircleRepository.kt b/moove/domain/src/main/kotlin/io/charlescd/moove/domain/repository/CircleRepository.kt index 1c94aa493f..c5b8044e36 100644 --- a/moove/domain/src/main/kotlin/io/charlescd/moove/domain/repository/CircleRepository.kt +++ b/moove/domain/src/main/kotlin/io/charlescd/moove/domain/repository/CircleRepository.kt @@ -57,4 +57,6 @@ interface CircleRepository { fun countPercentageByWorkspaceId(workspaceId: String): Int fun findCirclesPercentage(workspaceId: String, name: String?, active: Boolean, pageRequest: PageRequest?): Page + + fun existsByNameAndWorkspaceId(name: String, workspaceId: String): Boolean } diff --git a/moove/domain/src/main/kotlin/io/charlescd/moove/domain/repository/WorkspaceRepository.kt b/moove/domain/src/main/kotlin/io/charlescd/moove/domain/repository/WorkspaceRepository.kt index 420820fefb..cd712a40ec 100644 --- a/moove/domain/src/main/kotlin/io/charlescd/moove/domain/repository/WorkspaceRepository.kt +++ b/moove/domain/src/main/kotlin/io/charlescd/moove/domain/repository/WorkspaceRepository.kt @@ -36,4 +36,6 @@ interface WorkspaceRepository { fun associateUserGroupAndPermissions(workspaceId: String, userGroupId: String, permissions: List) fun disassociateUserGroupAndPermissions(workspaceId: String, userGroupId: String) + + fun existsByName(name: String): Boolean } diff --git a/moove/infrastructure/src/main/kotlin/io/charlescd/moove/infrastructure/repository/JdbcCircleRepository.kt b/moove/infrastructure/src/main/kotlin/io/charlescd/moove/infrastructure/repository/JdbcCircleRepository.kt index 98af217df0..8f0366628b 100644 --- a/moove/infrastructure/src/main/kotlin/io/charlescd/moove/infrastructure/repository/JdbcCircleRepository.kt +++ b/moove/infrastructure/src/main/kotlin/io/charlescd/moove/infrastructure/repository/JdbcCircleRepository.kt @@ -146,6 +146,28 @@ class JdbcCircleRepository( deleteCircleById(id) } + override fun existsByNameAndWorkspaceId(name: String, workspaceId: String): Boolean { + val baseCountQuery = """ + SELECT count(*) AS total + FROM circles + WHERE 1 = 1 + """ + val countStatement = StringBuilder(baseCountQuery) + .appendln("AND circles.workspace_id = ?") + .appendln("AND circles.name = ?") + .toString() + return applyCountQuery( + countStatement, arrayOf(workspaceId, name)) + } + + private fun applyCountQuery(statement: String, params: Array): Boolean { + val count = this.jdbcTemplate.queryForObject( + statement, + params + ) { rs, _ -> rs.getInt(1) } + return count != null && count >= 1 + } + private fun createParametersArray(name: String?, active: Boolean?, workspaceId: String, pageRequest: PageRequest? = null): Array { val parameters = ArrayList() if (active != null && !active) parameters.add(workspaceId) diff --git a/moove/infrastructure/src/main/kotlin/io/charlescd/moove/infrastructure/repository/JdbcWorkspaceRepository.kt b/moove/infrastructure/src/main/kotlin/io/charlescd/moove/infrastructure/repository/JdbcWorkspaceRepository.kt index 453a195890..eff5665a28 100644 --- a/moove/infrastructure/src/main/kotlin/io/charlescd/moove/infrastructure/repository/JdbcWorkspaceRepository.kt +++ b/moove/infrastructure/src/main/kotlin/io/charlescd/moove/infrastructure/repository/JdbcWorkspaceRepository.kt @@ -78,6 +78,12 @@ class JdbcWorkspaceRepository( LEFT JOIN users user_group_member ON user_groups_users.user_id = user_group_member.id WHERE 1 = 1 """ + + const val BASE_COUNT_QUERY_STATEMENT = """ + SELECT count(*) AS total + FROM workspaces + WHERE 1 = 1 + """ } override fun save(workspace: Workspace): Workspace { @@ -102,6 +108,14 @@ class JdbcWorkspaceRepository( return checkIfWorkspaceExists(id) } + override fun existsByName(name: String): Boolean { + val countStatement = StringBuilder(BASE_COUNT_QUERY_STATEMENT) + .appendln("AND workspaces.name= ?") + .toString() + return applyCountQuery( + countStatement, arrayOf(name)) + } + override fun associateUserGroupAndPermissions(workspaceId: String, userGroupId: String, permissions: List) { createAssociation(workspaceId, userGroupId, permissions) } @@ -179,16 +193,8 @@ class JdbcWorkspaceRepository( } private fun executeCountQuery(name: String?): Int? { - - val statement = StringBuilder( - """ - SELECT COUNT(*) - FROM workspaces w - WHERE 1 = 1 - """ - ) + val statement = StringBuilder(BASE_COUNT_QUERY_STATEMENT) name?.let { statement.appendln("AND w.name ILIKE ?") } - return this.jdbcTemplate.queryForObject( statement.toString(), createParametersArray(name) @@ -198,17 +204,18 @@ class JdbcWorkspaceRepository( } private fun checkIfWorkspaceExists(id: String): Boolean { - val countStatement = """ - SELECT count(*) AS total - FROM workspaces - WHERE workspaces.id = ? - """ + val countStatement = StringBuilder(BASE_COUNT_QUERY_STATEMENT) + .appendln("AND workspaces.id = ?") + .toString() + return applyCountQuery( + countStatement, arrayOf(id)) + } + private fun applyCountQuery(statement: String, params: Array): Boolean { val count = this.jdbcTemplate.queryForObject( - countStatement, - arrayOf(id) + statement, + params ) { rs, _ -> rs.getInt(1) } - return count != null && count >= 1 } @@ -246,7 +253,9 @@ class JdbcWorkspaceRepository( ) } - private fun createWorkspace(workspace: Workspace) { + private fun createWorkspace( + workspace: Workspace + ) { val statement = "INSERT INTO workspaces(" + "id, " + "name, " + diff --git a/moove/web/src/main/resources/locale/messages.properties b/moove/web/src/main/resources/locale/messages.properties index 9ddd41ff4f..f47171e63f 100644 --- a/moove/web/src/main/resources/locale/messages.properties +++ b/moove/web/src/main/resources/locale/messages.properties @@ -45,3 +45,5 @@ idm.unexpected.error=An unexpected error occurred when trying to reach your iden duplicated.component.name.error=You can not save two components with the same name. deployment.configuration.already.registered=Deployment configuration can not be registered, because it is already configuration on workspace. active.deployment.namespace.error=It is not possible to delete or edit the Deployment Configuration for this worskpace. There are active deployments for the current namespace. +duplicated.workspace.name.error=You can not save two workspaces with the same name. +duplicated.circle.name.error=You can not save two circles with the same name. diff --git a/moove/web/src/main/resources/locale/messages_pt_BR.properties b/moove/web/src/main/resources/locale/messages_pt_BR.properties index 24425a7ad1..8f862c5156 100644 --- a/moove/web/src/main/resources/locale/messages_pt_BR.properties +++ b/moove/web/src/main/resources/locale/messages_pt_BR.properties @@ -42,3 +42,5 @@ villager.unexpected.error=Um erro inesperado ocorreu. idm.unexpected.error=Um erro inesperado ocorreu ao tentar acessar o seu identity manager. duplicated.component.name.error=Não é possível salvar dois componentes com o mesmo nome. active.deployment.namespace.error=Não é possivel excluir ou editar a configuração de deploy para esse workspace. Há deploy ativos para o atual namespace. +duplicated.workspace.name.error=Não é possível salvar dois workspaces com o mesmo nome. +duplicated.circle.name.error=Não é possível salvar dois círculos com o mesmo nome.