From 26314af635a8104fa2b5b832b7365fe129d52c3a Mon Sep 17 00:00:00 2001 From: FyloZ Date: Sat, 17 Apr 2021 19:55:00 -0400 Subject: [PATCH] =?UTF-8?q?Ajout=20de=20la=20v=C3=A9rification=20de=20l'or?= =?UTF-8?q?dre=20des=20=C3=A9tapes=20des=20recettes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 14 ++- .../service/RecipeService.kt | 50 ++++----- .../service/RecipeStepService.kt | 102 +++++++++++++++--- .../service/AbstractServiceTest.kt | 4 + .../service/RecipeStepServiceTest.kt | 74 ++++++++++++- 5 files changed, 200 insertions(+), 44 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 4544a44..cfa2953 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + group = "dev.fyloz.colorrecipesexplorer" plugins { @@ -79,15 +81,17 @@ tasks.test { } } -tasks.withType { +tasks.withType() { options.compilerArgs.addAll(arrayOf("--release", "11")) } - -tasks.withType { +tasks.withType().all { kotlinOptions { - jvmTarget = "11" + jvmTarget = JavaVersion.VERSION_11.toString() useIR = true - freeCompilerArgs = freeCompilerArgs + "-Xopt-in=kotlin.contracts.ExperimentalContracts" + freeCompilerArgs = listOf( + "-Xopt-in=kotlin.contracts.ExperimentalContracts", + "-Xinline-classes" + ) } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt index 9fd386e..7df7296 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt @@ -1,6 +1,5 @@ package dev.fyloz.colorrecipesexplorer.service -import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.model.validation.or import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository @@ -35,6 +34,7 @@ class RecipeServiceImpl( recipeRepository: RecipeRepository, val companyService: CompanyService, val mixService: MixService, + val recipeStepService: RecipeStepService, @Lazy val groupService: EmployeeGroupService ) : AbstractExternalModelService(recipeRepository), @@ -67,17 +67,17 @@ class RecipeServiceImpl( return update(with(entity) { recipe( - id = id, - name = name or persistedRecipe.name, - description = description or persistedRecipe.description, - color = color or persistedRecipe.color, - gloss = gloss ?: persistedRecipe.gloss, - sample = sample ?: persistedRecipe.sample, - approbationDate = approbationDate ?: persistedRecipe.approbationDate, - remark = remark or persistedRecipe.remark, - company = persistedRecipe.company, - mixes = persistedRecipe.mixes, - groupsInformation = updateGroupsInformation(persistedRecipe, entity) + id = id, + name = name or persistedRecipe.name, + description = description or persistedRecipe.description, + color = color or persistedRecipe.color, + gloss = gloss ?: persistedRecipe.gloss, + sample = sample ?: persistedRecipe.sample, + approbationDate = approbationDate ?: persistedRecipe.approbationDate, + remark = remark or persistedRecipe.remark, + company = persistedRecipe.company, + mixes = persistedRecipe.mixes, + groupsInformation = updateGroupsInformation(persistedRecipe, entity) ) }) } @@ -88,21 +88,23 @@ class RecipeServiceImpl( val updatedGroupsInformation = mutableSetOf() steps.forEach { with(recipe.groupInformationForGroup(it.groupId)) { - updatedGroupsInformation.add( - // Set steps for the existing RecipeGroupInformation or create a new one - this?.apply { - if (this.steps != null) { - this.steps!!.setAll(it.steps) - } else { - this.steps = it.steps.toMutableSet() - } - } ?: recipeGroupInformation( - group = groupService.getById(it.groupId), - steps = it.steps.toMutableSet() - ) + // Set steps for the existing RecipeGroupInformation or create a new one + val updatedGroupInformation = this?.apply { + if (this.steps != null) { + this.steps!!.setAll(it.steps) + } else { + this.steps = it.steps.toMutableSet() + } + } ?: recipeGroupInformation( + group = groupService.getById(it.groupId), + steps = it.steps.toMutableSet() ) + + updatedGroupsInformation.add(updatedGroupInformation) + recipeStepService.validateGroupInformationSteps(updatedGroupInformation) } } + return updatedGroupsInformation } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt index b58539c..4cae4c9 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt @@ -1,21 +1,22 @@ package dev.fyloz.colorrecipesexplorer.service -import dev.fyloz.colorrecipesexplorer.model.RecipeStep -import dev.fyloz.colorrecipesexplorer.model.recipeStepIdAlreadyExistsException -import dev.fyloz.colorrecipesexplorer.model.recipeStepIdNotFoundException +import dev.fyloz.colorrecipesexplorer.exception.RestException +import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.repository.RecipeStepRepository +import org.springframework.http.HttpStatus import org.springframework.stereotype.Service interface RecipeStepService : ModelService { - /** - * Validates if the given [steps] obey the following criteria: + * Validates if the steps of the given [groupInformation] obey the following criteria: * * * The position of the steps is greater or equals to 1 * * Each position is unique in the collection * * There is no gap between positions + * + * If any of those criteria are not met, an [InvalidStepsPositionsException] will be thrown. */ - fun validateStepsCollection(steps: Collection): Boolean + fun validateGroupInformationSteps(groupInformation: RecipeGroupInformation) } @Service @@ -25,11 +26,86 @@ class RecipeStepServiceImpl(recipeStepRepository: RecipeStepRepository) : override fun idNotFoundException(id: Long) = recipeStepIdNotFoundException(id) override fun idAlreadyExistsException(id: Long) = recipeStepIdAlreadyExistsException(id) - // override fun validateStepsCollection(steps: Collection): Boolean { -// val sortedSteps = steps.sortedBy { it.position } -// -// fun validateStepPosition(step: RecipeStep) = -// step.position >= 1 -// -// } + override fun validateGroupInformationSteps(groupInformation: RecipeGroupInformation) { + val steps = groupInformation.steps + val group = groupInformation.group + + if (steps == null) return + + val sortedSteps = steps.sortedBy { it.position } + val errors = mutableSetOf() + + // Check if the first step position is 1 + fun isFirstStepPositionInvalid() = + sortedSteps[0].position != 1 + + // Check if any position is duplicated + fun getDuplicatedPositionsErrors() = + sortedSteps + .groupBy { it.position } + .filter { it.value.count() > 1 } + .map { duplicatedStepsPositions(it.key, group) } + + // Check if there is any gap between steps positions + // Knowing that steps are sorted by position, if the position of the step is not index + 1, there is a gap between them + fun hasGapBetweenPositions() = + sortedSteps + .filterIndexed { index, step -> step.position != index + 1 } + .isNotEmpty() + + // Find all errors and throw if there is any + if (isFirstStepPositionInvalid()) errors += invalidFirstStepPosition(sortedSteps[0]) + errors += getDuplicatedPositionsErrors() + if (errors.isEmpty() && hasGapBetweenPositions()) errors += gapBetweenStepsPositions(group) + if (errors.isNotEmpty()) { + throw InvalidStepsPositionsException(group, errors) + } + } } + +data class InvalidStepsPositionsError( + val type: String, + val details: String +) + +class InvalidStepsPositionsException( + val group: EmployeeGroup, + val errors: Set +) : + RestException( + "invalid-recipestep-position", + "Invalid steps position", + HttpStatus.BAD_REQUEST, + "The position of steps for the group ${group.name} are invalid", + mapOf( + "group" to group.name, + "groupId" to group.id!!, + "invalidSteps" to errors + ) + ) + +const val INVALID_FIRST_STEP_POSITION_ERROR_CODE = "first" +const val DUPLICATED_STEPS_POSITIONS_ERROR_CODE = "duplicated" +const val GAP_BETWEEN_STEPS_POSITIONS_ERROR_CODE = "gap" + +private fun invalidFirstStepPosition( + step: RecipeStep +) = InvalidStepsPositionsError( + INVALID_FIRST_STEP_POSITION_ERROR_CODE, + "The position ${step.position} is under the minimum of 1" +) + +private fun duplicatedStepsPositions( + position: Int, + group: EmployeeGroup +) = InvalidStepsPositionsError( + DUPLICATED_STEPS_POSITIONS_ERROR_CODE, + "The position $position is duplicated in the group ${group.name}" +) + +private fun gapBetweenStepsPositions( + group: EmployeeGroup +) = InvalidStepsPositionsError( + GAP_BETWEEN_STEPS_POSITIONS_ERROR_CODE, + "The positions for the steps of the group ${group.name} have gaps between them" +) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AbstractServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AbstractServiceTest.kt index 0710913..0574fee 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AbstractServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AbstractServiceTest.kt @@ -306,6 +306,10 @@ fun RestException.assertErrorCode(type: String, identifierName: String) { } } +fun RestException.assertErrorCode(errorCode: String) { + assertEquals(errorCode, this.errorCode) +} + fun > withBaseSaveDtoTest( entity: E, entitySaveDto: N, diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepServiceTest.kt index 714b4a7..acafb28 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepServiceTest.kt @@ -2,9 +2,11 @@ package dev.fyloz.colorrecipesexplorer.service import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.spy -import dev.fyloz.colorrecipesexplorer.model.RecipeStep -import dev.fyloz.colorrecipesexplorer.model.recipeStep +import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.repository.RecipeStepRepository +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertTrue class RecipeStepServiceTest : AbstractModelServiceTest() { @@ -13,4 +15,72 @@ class RecipeStepServiceTest : override val entity: RecipeStep = recipeStep(id = 0L, message = "message") override val anotherEntity: RecipeStep = recipeStep(id = 1L, message = "another message") + + // validateStepsCollection() + + @Test + fun `validateStepsCollection() throws an InvalidStepsPositionsException when the position of the first step of the given groupInformation is not 1`() { + withSteps( + mutableSetOf( + recipeStep(id = 0L, position = 0), + recipeStep(id = 1L, position = 1), + recipeStep(id = 2L, position = 2), + recipeStep(id = 3L, position = 3) + ) + ) { + val exception = assertThrows { + service.validateGroupInformationSteps(this) + } + + assertTrue { exception.errors.count() == 1 } + assertTrue { exception.errors.first().type == INVALID_FIRST_STEP_POSITION_ERROR_CODE } + } + } + + @Test + fun `validateStepsCollection() throws an InvalidStepsPositionsException when steps positions are duplicated in the given groupInformation`() { + withSteps( + mutableSetOf( + recipeStep(id = 0L, position = 1), + recipeStep(id = 1L, position = 2), + recipeStep(id = 2L, position = 2), + recipeStep(id = 3L, position = 3) + ) + ) { + val exception = assertThrows { + service.validateGroupInformationSteps(this) + } + + assertTrue { exception.errors.count() == 1 } + assertTrue { exception.errors.first().type == DUPLICATED_STEPS_POSITIONS_ERROR_CODE } + } + } + + @Test + fun `validateStepsCollection() throws an InvalidStepsPositionsException when there is a gap between steps positions in the given groupInformation`() { + withSteps( + mutableSetOf( + recipeStep(id = 0L, position = 1), + recipeStep(id = 1L, position = 2), + recipeStep(id = 2L, position = 4), + recipeStep(id = 3L, position = 5) + ) + ) { + val exception = assertThrows { + service.validateGroupInformationSteps(this) + } + + assertTrue { exception.errors.count() == 1 } + assertTrue { exception.errors.first().type == GAP_BETWEEN_STEPS_POSITIONS_ERROR_CODE } + } + } + + private fun withSteps(steps: MutableSet, test: RecipeGroupInformation.() -> Unit) { + recipeGroupInformation( + group = employeeGroup(id = 0L), + steps = steps + ) { + test() + } + } }