Ajout de la vérification de l'ordre des étapes des recettes

This commit is contained in:
FyloZ 2021-04-17 19:55:00 -04:00
parent 2b1c8c2555
commit 26314af635
5 changed files with 200 additions and 44 deletions

View File

@ -1,3 +1,5 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
group = "dev.fyloz.colorrecipesexplorer"
plugins {
@ -79,15 +81,17 @@ tasks.test {
}
}
tasks.withType<JavaCompile> {
tasks.withType<JavaCompile>() {
options.compilerArgs.addAll(arrayOf("--release", "11"))
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
tasks.withType<KotlinCompile>().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"
)
}
}

View File

@ -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<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeRepository>(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<RecipeGroupInformation>()
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
}

View File

@ -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<RecipeStep, RecipeStepRepository> {
/**
* 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<RecipeStep>): 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<RecipeStep>): 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<InvalidStepsPositionsError>()
// 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<InvalidStepsPositionsError>
) :
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"
)

View File

@ -306,6 +306,10 @@ fun RestException.assertErrorCode(type: String, identifierName: String) {
}
}
fun RestException.assertErrorCode(errorCode: String) {
assertEquals(errorCode, this.errorCode)
}
fun <E : Model, N : EntityDto<E>> withBaseSaveDtoTest(
entity: E,
entitySaveDto: N,

View File

@ -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<RecipeStep, RecipeStepService, RecipeStepRepository>() {
@ -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<InvalidStepsPositionsException> {
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<InvalidStepsPositionsException> {
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<InvalidStepsPositionsException> {
service.validateGroupInformationSteps(this)
}
assertTrue { exception.errors.count() == 1 }
assertTrue { exception.errors.first().type == GAP_BETWEEN_STEPS_POSITIONS_ERROR_CODE }
}
}
private fun withSteps(steps: MutableSet<RecipeStep>, test: RecipeGroupInformation.() -> Unit) {
recipeGroupInformation(
group = employeeGroup(id = 0L),
steps = steps
) {
test()
}
}
}