From a59bad7a7abe6f1f4dae9ad95909faf6bfe9c69b Mon Sep 17 00:00:00 2001 From: FyloZ Date: Tue, 4 May 2021 12:04:02 -0400 Subject: [PATCH] =?UTF-8?q?Ajout=20des=20conflits=20de=20nom=20entre=20les?= =?UTF-8?q?=20recettes=20d'une=20banni=C3=A8re=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/RestException.kt | 99 +++++++++---------- .../colorrecipesexplorer/model/Recipe.kt | 15 +++ .../repository/RecipeRepository.kt | 3 + .../service/RecipeService.kt | 28 +++++- .../service/RecipeServiceTest.kt | 43 +++++++- 5 files changed, 133 insertions(+), 55 deletions(-) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/exception/RestException.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/exception/RestException.kt index 2732f61..1bc62b7 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/exception/RestException.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/exception/RestException.kt @@ -12,63 +12,62 @@ import org.springframework.web.context.request.WebRequest import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler abstract class RestException( - val errorCode: String, - val title: String, - val status: HttpStatus, - val details: String, - val extensions: Map = mapOf() + val errorCode: String, + val title: String, + val status: HttpStatus, + val details: String, + val extensions: Map = mapOf() ) : RuntimeException(details) { fun buildExceptionBody() = mapOf( - "type" to errorCode, - "title" to title, - "status" to status.value(), - "detail" to details, + "type" to errorCode, + "title" to title, + "status" to status.value(), + "detail" to details, - *extensions.map { it.key to it.value }.toTypedArray() + *extensions.map { it.key to it.value }.toTypedArray() ) } class NotFoundException( - errorCode: String, - title: String, - details: String, - identifierValue: Any, - identifierName: String = "id" + errorCode: String, + title: String, + details: String, + identifierValue: Any, + identifierName: String = "id" ) : RestException( - errorCode = "notfound-$errorCode-$identifierName", - title = title, - status = HttpStatus.NOT_FOUND, - details = details, - extensions = mapOf( - identifierName to identifierValue - ) + errorCode = "notfound-$errorCode-$identifierName", + title = title, + status = HttpStatus.NOT_FOUND, + details = details, + extensions = mapOf( + identifierName to identifierValue + ) ) class AlreadyExistsException( - errorCode: String, - title: String, - details: String, - identifierValue: Any, - identifierName: String = "id" + errorCode: String, + title: String, + details: String, + identifierValue: Any, + identifierName: String = "id", + extensions: MutableMap = mutableMapOf() ) : RestException( - errorCode = "exists-$errorCode-$identifierName", - title = title, - status = HttpStatus.CONFLICT, - details = details, - extensions = mapOf( - identifierName to identifierValue - ) + errorCode = "exists-$errorCode-$identifierName", + title = title, + status = HttpStatus.CONFLICT, + details = details, + extensions = extensions.apply { this[identifierName] = identifierValue }.toMap() ) class CannotDeleteException( - errorCode: String, - title: String, - details: String + errorCode: String, + title: String, + details: String ) : RestException( - errorCode = "cannotdelete-$errorCode", - title = title, - status = HttpStatus.CONFLICT, - details = details + errorCode = "cannotdelete-$errorCode", + title = title, + status = HttpStatus.CONFLICT, + details = details ) @ControllerAdvice @@ -79,19 +78,19 @@ class RestResponseEntityExceptionHandler : ResponseEntityExceptionHandler() { finalBody["instance"] = (request as ServletWebRequest).request.requestURI return handleExceptionInternal( - exception, - finalBody, - HttpHeaders(), - exception.status, - request + exception, + finalBody, + HttpHeaders(), + exception.status, + request ) } override fun handleMethodArgumentNotValid( - ex: MethodArgumentNotValidException, - headers: HttpHeaders, - status: HttpStatus, - request: WebRequest + ex: MethodArgumentNotValidException, + headers: HttpHeaders, + status: HttpStatus, + request: WebRequest ): ResponseEntity { val errors = hashMapOf() ex.bindingResult.allErrors.forEach { diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt index 367375a..75c0287 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt @@ -288,6 +288,8 @@ private const val RECIPE_NOT_FOUND_EXCEPTION_TITLE = "Recipe not found" private const val RECIPE_ALREADY_EXISTS_EXCEPTION_TITLE = "Recipe already exists" private const val RECIPE_EXCEPTION_ERROR_CODE = "recipe" +sealed class RecipeException + fun recipeIdNotFoundException(id: Long) = NotFoundException( RECIPE_EXCEPTION_ERROR_CODE, @@ -303,3 +305,16 @@ fun recipeIdAlreadyExistsException(id: Long) = "A recipe with the id $id already exists", id ) + +fun recipeNameAlreadyExistsForCompanyException(name: String, company: Company) = + AlreadyExistsException( + "${RECIPE_EXCEPTION_ERROR_CODE}-company", + RECIPE_ALREADY_EXISTS_EXCEPTION_TITLE, + "A recipe with the name $name already exists for the company ${company.name}", + name, + "name", + mutableMapOf( + "company" to company.name, + "companyId" to company.id!! + ) + ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/RecipeRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/RecipeRepository.kt index c3b9e4b..a7284f7 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/RecipeRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/RecipeRepository.kt @@ -8,6 +8,9 @@ interface RecipeRepository : JpaRepository { /** Checks if one or more recipes have the given [company]. */ fun existsByCompany(company: Company): Boolean + /** Checks if a recipe exists with the given [name] and [company]. */ + fun existsByNameAndCompany(name: String, company: Company): Boolean + /** Gets all recipes with the given [company]. */ fun findAllByCompany(company: Company): Collection } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt index 4ace03b..b170e81 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt @@ -16,6 +16,9 @@ interface RecipeService : /** Checks if one or more recipes have the given [company]. */ fun existsByCompany(company: Company): Boolean + /** Checks if a recipe exists with the given [name] and [company]. */ + fun existsByNameAndCompany(name: String, company: Company): Boolean + /** Gets all recipes with the given [company]. */ fun getAllByCompany(company: Company): Collection @@ -67,10 +70,18 @@ class RecipeServiceImpl( ) override fun existsByCompany(company: Company): Boolean = repository.existsByCompany(company) + override fun existsByNameAndCompany(name: String, company: Company) = + repository.existsByNameAndCompany(name, company) + override fun getAllByCompany(company: Company): Collection = repository.findAllByCompany(company) override fun save(entity: RecipeSaveDto): Recipe { - // TODO checks if name is unique in the scope of the [company] + val company = companyService.getById(entity.companyId) + + if (existsByNameAndCompany(entity.name, company)) { + throw recipeNameAlreadyExistsForCompanyException(entity.name, company) + } + return save(with(entity) { recipe( name = name, @@ -80,14 +91,23 @@ class RecipeServiceImpl( sample = sample, approbationDate = approbationDate, remark = remark ?: "", - company = companyService.getById(companyId) + company = company ) }) } @Transactional override fun update(entity: RecipeUpdateDto): Recipe { - val persistedRecipe by lazy { getById(entity.id) } + val persistedRecipe = getById(entity.id) + val name = entity.name + val company = persistedRecipe.company + + if (name != null && + name != persistedRecipe.name && + existsByNameAndCompany(name, company) + ) { + throw recipeNameAlreadyExistsForCompanyException(name, company) + } return update(with(entity) { recipe( @@ -99,7 +119,7 @@ class RecipeServiceImpl( sample = sample ?: persistedRecipe.sample, approbationDate = approbationDate ?: persistedRecipe.approbationDate, remark = remark or persistedRecipe.remark, - company = persistedRecipe.company, + company = company, mixes = persistedRecipe.mixes, groupsInformation = updateGroupsInformation(persistedRecipe, entity) ) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt index 98dd0f4..859f4d9 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt @@ -1,12 +1,14 @@ package dev.fyloz.colorrecipesexplorer.service import com.nhaarman.mockitokotlin2.* +import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository import dev.fyloz.colorrecipesexplorer.service.files.FileService import io.mockk.* import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import org.springframework.mock.web.MockMultipartFile import org.springframework.web.multipart.MultipartFile import java.io.File @@ -56,6 +58,19 @@ class RecipeServiceTest : assertFalse(found) } + // existsByNameAndCompany() + + @Test + fun `existsByNameAndCompany() returns if a recipe exists for the given name and company in the repository`() { + setOf(true, false).forEach { + whenever(repository.existsByNameAndCompany(entity.name, company)).doReturn(it) + + val exists = service.existsByNameAndCompany(entity.name, company) + + assertEquals(it, exists) + } + } + // getAllByCompany() @Test @@ -73,14 +88,40 @@ class RecipeServiceTest : @Test override fun `save(dto) calls and returns save() with the created entity`() { whenever(companyService.getById(company.id!!)).doReturn(company) + doReturn(false).whenever(service).existsByNameAndCompany(entity.name, company) withBaseSaveDtoTest(entity, entitySaveDto, service, { argThat { this.id == null && this.color == color } }) } + @Test + fun `save(dto) throw AlreadyExistsException when a recipe with the given name and company exists in the repository`() { + whenever(companyService.getById(company.id!!)).doReturn(company) + doReturn(true).whenever(service).existsByNameAndCompany(entity.name, company) + + with(assertThrows { service.save(entitySaveDto) }) { + this.assertErrorCode("company-name") + } + } + // update() @Test - override fun `update(dto) calls and returns update() with the created entity`() = + override fun `update(dto) calls and returns update() with the created entity`() { + doReturn(false).whenever(service).existsByNameAndCompany(entity.name, company) withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() }) + } + + @Test + fun `update(dto) throws AlreadyExistsException when a recipe exists for the given name and company`() { + val name = "another recipe" + + doReturn(entity).whenever(service).getById(entity.id!!) + doReturn(true).whenever(service).existsByNameAndCompany(name, company) + doReturn(name).whenever(entityUpdateDto).name + + with(assertThrows { service.update(entityUpdateDto) }) { + this.assertErrorCode("company-name") + } + } // updatePublicData()