From 618ef6c77aac6da1a95c9d6b5f982c4752be80bf Mon Sep 17 00:00:00 2001 From: FyloZ Date: Thu, 3 Mar 2022 23:24:55 -0500 Subject: [PATCH] #25 Migrate mix materials to new logic --- .../fyloz/colorrecipesexplorer/Constants.kt | 6 + .../colorrecipesexplorer/dtos/MaterialDto.kt | 3 +- .../dtos/MixMaterialDto.kt | 25 + .../dtos/RecipeStepDto.kt | 8 +- .../exception/InvalidPositionsException.kt | 15 + .../fyloz/colorrecipesexplorer/logic/Logic.kt | 6 + .../colorrecipesexplorer/logic/MixLogic.kt | 13 +- .../logic/MixMaterialLogic.kt | 136 +---- .../logic/MixTypeLogic.kt | 3 +- .../logic/RecipeStepLogic.kt | 100 +--- .../colorrecipesexplorer/model/Material.kt | 52 +- .../fyloz/colorrecipesexplorer/model/Mix.kt | 6 +- .../colorrecipesexplorer/model/MixMaterial.kt | 52 +- .../colorrecipesexplorer/model/ModelEntity.kt | 7 +- .../colorrecipesexplorer/model/Recipe.kt | 12 +- .../model/touchupkit/TouchUpKit.kt | 6 +- .../repository/MixMaterialRepository.kt | 5 +- .../service/MixMaterialService.kt | 23 + .../colorrecipesexplorer/utils/Collections.kt | 16 +- .../utils/PositionUtils.kt | 50 ++ .../logic/DefaultMixMaterialLogicTest.kt | 79 +++ .../logic/DefaultRecipeStepLogicTest.kt | 209 +------- .../logic/MaterialTypeLogicTest.kt | 182 ------- .../logic/MixLogicTest.kt | 496 +++++++++--------- .../logic/MixMaterialLogicTest.kt | 171 ------ .../utils/PositionUtilsTest.kt | 88 ++++ 26 files changed, 633 insertions(+), 1136 deletions(-) create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixMaterialDto.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/exception/InvalidPositionsException.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialService.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/PositionUtils.kt create mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixMaterialLogicTest.kt delete mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogicTest.kt delete mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogicTest.kt create mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/utils/PositionUtilsTest.kt diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt index a1bf68c..18ff917 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt @@ -12,4 +12,10 @@ object Constants { const val SIMDUT = "$PDF/simdut" } + + object ValidationMessages { + const val SIZE_GREATER_OR_EQUALS_ZERO = "Must be greater or equals to 0" + const val SIZE_GREATER_OR_EQUALS_ONE = "Must be greater or equals to 1" + const val RANGE_OUTSIDE_PERCENTS = "Must be between 0 and 100" + } } \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt index 251d71b..b5367bc 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt @@ -1,5 +1,6 @@ package dev.fyloz.colorrecipesexplorer.dtos +import dev.fyloz.colorrecipesexplorer.Constants import org.springframework.web.multipart.MultipartFile import javax.validation.constraints.Min import javax.validation.constraints.NotBlank @@ -24,7 +25,7 @@ data class MaterialSaveDto( @field:NotBlank val name: String, - @field:Min(0) + @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO) val inventoryQuantity: Float, val materialTypeId: Long, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixMaterialDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixMaterialDto.kt new file mode 100644 index 0000000..7ba94ce --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixMaterialDto.kt @@ -0,0 +1,25 @@ +package dev.fyloz.colorrecipesexplorer.dtos + +import dev.fyloz.colorrecipesexplorer.Constants +import javax.validation.constraints.Min + +data class MixMaterialDto( + override val id: Long = 0L, + + val material: MaterialDto, + + val quantity: Float, + + val position: Int +) : EntityDto + +data class MixMaterialSaveDto( + override val id: Long = 0L, + + val materialId: Long, + + @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO) + val quantity: Float, + + val position: Int +) : EntityDto \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/RecipeStepDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/RecipeStepDto.kt index f1fe4eb..e597927 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/RecipeStepDto.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/RecipeStepDto.kt @@ -6,10 +6,4 @@ data class RecipeStepDto( val position: Int, val message: String -) : EntityDto { - companion object { - const val VALIDATION_ERROR_CODE_INVALID_FIRST_STEP = "first" - const val VALIDATION_ERROR_CODE_DUPLICATED_STEPS_POSITION = "duplicated" - const val VALIDATION_ERROR_CODE_GAP_BETWEEN_STEPS_POSITIONS = "gap" - } -} +) : EntityDto \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/exception/InvalidPositionsException.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/exception/InvalidPositionsException.kt new file mode 100644 index 0000000..c53c0aa --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/exception/InvalidPositionsException.kt @@ -0,0 +1,15 @@ +package dev.fyloz.colorrecipesexplorer.exception + +import org.springframework.http.HttpStatus + +class InvalidPositionsException(val errors: Set) : RestException( + "invalid-positions", + "Invalid positions", + HttpStatus.BAD_REQUEST, + "The positions are invalid", + mapOf( + "errors" to errors + ) +) + +data class InvalidPositionError(val type: String, val details: String) \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt index 4b1971e..1214c9e 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt @@ -25,6 +25,9 @@ interface Logic> { /** Saves the given [dto]. */ fun save(dto: D): D + /** Saves all the given [dtos]. */ + fun saveAll(dtos: Collection): Collection + /** Updates the given [dto]. Throws if no DTO with the same id exists. */ fun update(dto: D): D @@ -50,6 +53,9 @@ abstract class BaseLogic>( override fun save(dto: D) = service.save(dto) + override fun saveAll(dtos: Collection) = + dtos.map(::save) + override fun update(dto: D): D { if (!existsById(dto.id)) { throw notFoundException(value = dto.id) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt index 8e843cf..e2687f6 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt @@ -42,11 +42,7 @@ class DefaultMixLogic( this.id!!, this.location, this.mixType, - this.mixMaterials.map { - with(mixMaterialLogic) { - return@with it.toOutput() - } - }.toSet() + this.mixMaterials.map { mixMaterialDto(it) }.toSet() ) @Transactional @@ -55,10 +51,11 @@ class DefaultMixLogic( val materialType = materialTypeLogic.getById(entity.materialTypeId) val mixType = mixTypeLogic.getOrCreateForNameAndMaterialType(entity.name, materialType(materialType)) - val mixMaterials = if (entity.mixMaterials != null) mixMaterialLogic.create(entity.mixMaterials) else setOf() + val mixMaterials = + if (entity.mixMaterials != null) mixMaterialLogic.saveAll(entity.mixMaterials).toSet() else setOf() mixMaterialLogic.validateMixMaterials(mixMaterials) - var mix = mix(recipe = recipe, mixType = mixType, mixMaterials = mixMaterials.toMutableSet()) + var mix = mix(recipe = recipe, mixType = mixType, mixMaterials = mixMaterials.map(::mixMaterial).toMutableSet()) mix = save(mix) recipeLogic.addMix(recipe, mix) @@ -83,7 +80,7 @@ class DefaultMixLogic( } } if (entity.mixMaterials != null) { - mix.mixMaterials.setAll(mixMaterialLogic.create(entity.mixMaterials!!).toMutableSet()) + mix.mixMaterials.setAll(mixMaterialLogic.saveAll(entity.mixMaterials!!).map(::mixMaterial).toMutableSet()) } return update(mix) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt index 913016a..2cedbe8 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt @@ -1,112 +1,47 @@ package dev.fyloz.colorrecipesexplorer.logic +import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent +import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto +import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError +import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException import dev.fyloz.colorrecipesexplorer.exception.RestException -import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.repository.MixMaterialRepository -import dev.fyloz.colorrecipesexplorer.utils.findDuplicated -import dev.fyloz.colorrecipesexplorer.utils.hasGaps -import org.springframework.context.annotation.Lazy -import org.springframework.context.annotation.Profile +import dev.fyloz.colorrecipesexplorer.model.MixMaterial +import dev.fyloz.colorrecipesexplorer.service.MixMaterialService +import dev.fyloz.colorrecipesexplorer.utils.PositionUtils import org.springframework.http.HttpStatus -import org.springframework.stereotype.Service - -interface MixMaterialLogic : ModelService { - /** Checks if one or more mix materials have the given [material]. */ - fun existsByMaterial(material: Material): Boolean - - /** Creates [MixMaterial]s from the givens [MixMaterialDto]. */ - fun create(mixMaterials: Set): Set - - /** Creates a [MixMaterial] from a given [MixMaterialDto]. */ - fun create(mixMaterial: MixMaterialDto): MixMaterial - - /** Updates the [quantity] of the given [mixMaterial]. */ - fun updateQuantity(mixMaterial: MixMaterial, quantity: Float): MixMaterial +interface MixMaterialLogic : Logic { /** * Validates if the given [mixMaterials]. To be valid, the position of each mix material must be greater or equals to 1 and unique in the set. * There must also be no gap between the positions. Also, the quantity of the first mix material in the set must not be expressed in percentages. * If any of those criteria are not met, an [InvalidGroupStepsPositionsException] will be thrown. */ - fun validateMixMaterials(mixMaterials: Set) - - fun MixMaterial.toOutput(): MixMaterialOutputDto + fun validateMixMaterials(mixMaterials: Set) } -@Service -@Profile("!emergency") -class DefaultMixMaterialLogic( - mixMaterialRepository: MixMaterialRepository, - @Lazy val materialLogic: MaterialLogic -) : AbstractModelService(mixMaterialRepository), MixMaterialLogic { - override fun idNotFoundException(id: Long) = mixMaterialIdNotFoundException(id) - override fun idAlreadyExistsException(id: Long) = mixMaterialIdAlreadyExistsException(id) - - override fun MixMaterial.toOutput() = MixMaterialOutputDto( - this.id!!, - this.material, - this.quantity, - this.position - ) - - override fun existsByMaterial(material: Material): Boolean = repository.existsByMaterial(material) - override fun create(mixMaterials: Set): Set = - mixMaterials.map(::create).toSet() - - override fun create(mixMaterial: MixMaterialDto): MixMaterial = - mixMaterial( - material = material(materialLogic.getById(mixMaterial.materialId)), - quantity = mixMaterial.quantity, - position = mixMaterial.position - ) - - override fun updateQuantity(mixMaterial: MixMaterial, quantity: Float) = - update(mixMaterial.apply { - this.quantity = quantity - }) - - override fun validateMixMaterials(mixMaterials: Set) { +@LogicComponent +class DefaultMixMaterialLogic(service: MixMaterialService) : + BaseLogic(service, MixMaterial::class.simpleName!!), MixMaterialLogic { + override fun validateMixMaterials(mixMaterials: Set) { if (mixMaterials.isEmpty()) return val sortedMixMaterials = mixMaterials.sortedBy { it.position } - val firstMixMaterial = sortedMixMaterials[0] - val errors = mutableSetOf() - // Check if the first mix material position is 1 - fun isFirstMixMaterialPositionInvalid() = - sortedMixMaterials[0].position != 1 - - // Check if the first mix material is expressed in percents - fun isFirstMixMaterialPercentages() = - sortedMixMaterials[0].material.materialType!!.usePercentages - - // Check if any positions is duplicated - fun getDuplicatedPositionsErrors() = - sortedMixMaterials - .findDuplicated { it.position } - .map { duplicatedMixMaterialsPositions(it) } - - // Find all errors and throw if there is any - if (isFirstMixMaterialPositionInvalid()) errors += invalidFirstMixMaterialPosition(sortedMixMaterials[0]) - errors += getDuplicatedPositionsErrors() - if (errors.isEmpty() && mixMaterials.hasGaps { it.position }) errors += gapBetweenStepsPositions() - if (errors.isNotEmpty()) { - throw InvalidMixMaterialsPositionsException(errors) + try { + PositionUtils.validate(sortedMixMaterials.map { it.position }) + } catch (ex: InvalidPositionsException) { + throw InvalidMixMaterialsPositionsException(ex.errors) } - if (isFirstMixMaterialPercentages()) { - throw InvalidFirstMixMaterial(firstMixMaterial) + if (sortedMixMaterials[0].material.materialType.usePercentages) { + throw InvalidFirstMixMaterialException(sortedMixMaterials[0]) } } } -class InvalidMixMaterialsPositionsError( - val type: String, - val details: String -) - +// TODO check if required class InvalidMixMaterialsPositionsException( - val errors: Set + val errors: Set ) : RestException( "invalid-mixmaterial-position", "Invalid mix materials positions", @@ -117,8 +52,8 @@ class InvalidMixMaterialsPositionsException( ) ) -class InvalidFirstMixMaterial( - val mixMaterial: MixMaterial +class InvalidFirstMixMaterialException( + val mixMaterial: MixMaterialDto ) : RestException( "invalid-mixmaterial-first", "Invalid first mix material", @@ -127,27 +62,4 @@ class InvalidFirstMixMaterial( mapOf( "mixMaterial" to mixMaterial ) -) - -const val INVALID_FIRST_MIX_MATERIAL_POSITION_ERROR_CODE = "first" -const val DUPLICATED_MIX_MATERIALS_POSITIONS_ERROR_CODE = "duplicated" -const val GAP_BETWEEN_MIX_MATERIALS_POSITIONS_ERROR_CODE = "gap" - -private fun invalidFirstMixMaterialPosition(mixMaterial: MixMaterial) = - InvalidMixMaterialsPositionsError( - INVALID_FIRST_MIX_MATERIAL_POSITION_ERROR_CODE, - "The position ${mixMaterial.position} is under the minimum of 1" - ) - -private fun duplicatedMixMaterialsPositions(position: Int) = - InvalidMixMaterialsPositionsError( - DUPLICATED_MIX_MATERIALS_POSITIONS_ERROR_CODE, - "The position $position is duplicated" - ) - -private fun gapBetweenStepsPositions() = - InvalidMixMaterialsPositionsError( - GAP_BETWEEN_MIX_MATERIALS_POSITIONS_ERROR_CODE, - "There is a gap between mix materials positions" - ) - +) \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt index 3ec9b08..6b7d2ac 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt @@ -1,6 +1,7 @@ package dev.fyloz.colorrecipesexplorer.logic import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase +import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.repository.MixTypeRepository import org.springframework.context.annotation.Lazy @@ -56,7 +57,7 @@ class DefaultMixTypeLogic( override fun save(entity: MixType): MixType { if (materialLogic.existsByName(entity.name)) - throw materialNameAlreadyExistsException(entity.name) + throw AlreadyExistsException("material", "material already exists", "material already exists details (TODO)", entity.name) // TODO return super.save(entity) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt index 3323ff8..b7852fa 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt @@ -2,26 +2,19 @@ package dev.fyloz.colorrecipesexplorer.logic import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto +import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError +import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException import dev.fyloz.colorrecipesexplorer.exception.RestException import dev.fyloz.colorrecipesexplorer.model.RecipeGroupInformation import dev.fyloz.colorrecipesexplorer.model.RecipeStep import dev.fyloz.colorrecipesexplorer.model.account.Group -import dev.fyloz.colorrecipesexplorer.model.recipeStepDto import dev.fyloz.colorrecipesexplorer.service.RecipeStepService -import dev.fyloz.colorrecipesexplorer.utils.findDuplicated -import dev.fyloz.colorrecipesexplorer.utils.hasGaps +import dev.fyloz.colorrecipesexplorer.utils.PositionUtils import org.springframework.http.HttpStatus interface RecipeStepLogic : Logic { - /** Validates the steps of the given [groupInformation], according to the criteria of [validateSteps]. */ + /** Validates the steps of the given [groupInformation], according to the criteria of [PositionUtils.validate]. */ fun validateGroupInformationSteps(groupInformation: RecipeGroupInformation) - - /** - * Validates if the given [steps]. To be valid, the position of each step must be greater or equals to 1 and unique in the set. - * There must also be no gap between the positions. - * If any of those criteria are not met, an [InvalidGroupStepsPositionsException] will be thrown. - */ - fun validateSteps(steps: Set) } @LogicComponent @@ -31,91 +24,16 @@ class DefaultRecipeStepLogic(recipeStepService: RecipeStepService) : if (groupInformation.steps == null) return try { - validateSteps(groupInformation.steps!!.map { recipeStepDto(it) }.toSet()) - } catch (validationException: InvalidStepsPositionsException) { - throw InvalidGroupStepsPositionsException(groupInformation.group, validationException) - } - } - - override fun validateSteps(steps: Set) { - if (steps.isEmpty()) return - - val sortedSteps = steps.sortedBy { it.position } - val errors = mutableSetOf() - - // Check if the first step position is 1 - validateFirstStepPosition(sortedSteps, errors) - - // Check if any position is duplicated - validateDuplicatedStepsPositions(sortedSteps, errors) - - // Check for gaps between positions - validateGapsInStepsPositions(sortedSteps, errors) - - if (errors.isNotEmpty()) { - throw InvalidStepsPositionsException(errors) - } - } - - private fun validateFirstStepPosition( - steps: List, - errors: MutableSet - ) { - if (steps[0].position != 1) { - errors += InvalidStepsPositionsError( - RecipeStepDto.VALIDATION_ERROR_CODE_INVALID_FIRST_STEP, - "The first step must be at position 1" - ) - } - } - - private fun validateDuplicatedStepsPositions( - steps: List, - errors: MutableSet - ) { - errors += steps - .findDuplicated { it.position } - .map { - InvalidStepsPositionsError( - RecipeStepDto.VALIDATION_ERROR_CODE_DUPLICATED_STEPS_POSITION, - "The position $it is duplicated" - ) - } - } - - private fun validateGapsInStepsPositions( - steps: List, - errors: MutableSet - ) { - if (errors.isEmpty() && steps.hasGaps { it.position }) { - errors += InvalidStepsPositionsError( - RecipeStepDto.VALIDATION_ERROR_CODE_GAP_BETWEEN_STEPS_POSITIONS, - "There is a gap between steps positions" - ) + PositionUtils.validate(groupInformation.steps!!.map { it.position }.toList()) + } catch (ex: InvalidPositionsException) { + throw InvalidGroupStepsPositionsException(groupInformation.group, ex) } } } -data class InvalidStepsPositionsError( - val type: String, - val details: String -) - -class InvalidStepsPositionsException( - val errors: Set -) : RestException( - "invalid-recipestep-position", - "Invalid steps positions", - HttpStatus.BAD_REQUEST, - "The position of steps are invalid", - mapOf( - "invalidSteps" to errors - ) -) - class InvalidGroupStepsPositionsException( val group: Group, - val exception: InvalidStepsPositionsException + val exception: InvalidPositionsException ) : RestException( "invalid-groupinformation-recipestep-position", "Invalid steps positions", @@ -127,6 +45,6 @@ class InvalidGroupStepsPositionsException( "invalidSteps" to exception.errors ) ) { - val errors: Set + val errors: Set get() = exception.errors } \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt index 02bfce6..23fd00c 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt @@ -39,7 +39,7 @@ data class Material( data class MaterialQuantityDto( val material: Long, - @field:Min(0, message = VALIDATION_SIZE_GE_ZERO) + @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO) val quantity: Float ) @@ -77,52 +77,4 @@ fun materialQuantityDto( materialId: Long, quantity: Float, op: MaterialQuantityDto.() -> Unit = {} -) = MaterialQuantityDto(materialId, quantity).apply(op) - -// ==== Exceptions ==== -private const -val MATERIAL_NOT_FOUND_EXCEPTION_TITLE = "Material not found" -private const val MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE = "Material already exists" -private const val MATERIAL_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete material" -private const val MATERIAL_EXCEPTION_ERROR_CODE = "material" - -fun materialIdNotFoundException(id: Long) = - NotFoundException( - MATERIAL_EXCEPTION_ERROR_CODE, - MATERIAL_NOT_FOUND_EXCEPTION_TITLE, - "A material with the id $id could not be found", - id - ) - -fun materialNameNotFoundException(name: String) = - NotFoundException( - MATERIAL_EXCEPTION_ERROR_CODE, - MATERIAL_NOT_FOUND_EXCEPTION_TITLE, - "A material with the name $name could not be found", - name, - "name" - ) - -fun materialIdAlreadyExistsException(id: Long) = - AlreadyExistsException( - MATERIAL_EXCEPTION_ERROR_CODE, - MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE, - "A material with the id $id already exists", - id - ) - -fun materialNameAlreadyExistsException(name: String) = - AlreadyExistsException( - MATERIAL_EXCEPTION_ERROR_CODE, - MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE, - "A material with the name $name already exists", - name, - "name" - ) - -fun cannotDeleteMaterialException(material: Material) = - CannotDeleteException( - MATERIAL_EXCEPTION_ERROR_CODE, - MATERIAL_CANNOT_DELETE_EXCEPTION_TITLE, - "Cannot delete the material ${material.name} because one or more recipes depends on it" - ) +) = MaterialQuantityDto(materialId, quantity).apply(op) \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt index d7f0053..2670501 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt @@ -1,6 +1,8 @@ package dev.fyloz.colorrecipesexplorer.model import com.fasterxml.jackson.annotation.JsonIgnore +import dev.fyloz.colorrecipesexplorer.Constants +import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException @@ -58,13 +60,13 @@ data class MixOutputDto( val id: Long, val location: String?, val mixType: MixType, - val mixMaterials: Set + val mixMaterials: Set ) data class MixDeductDto( val id: Long, - @field:Min(0, message = VALIDATION_SIZE_GE_ZERO) + @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO) val ratio: Float ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt index 7a91003..6ac7569 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt @@ -1,9 +1,7 @@ package dev.fyloz.colorrecipesexplorer.model -import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException -import dev.fyloz.colorrecipesexplorer.exception.NotFoundException +import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto import javax.persistence.* -import javax.validation.constraints.Min @Entity @Table(name = "mix_material") @@ -21,22 +19,6 @@ data class MixMaterial( var position: Int ) : ModelEntity -data class MixMaterialDto( - val materialId: Long, - - @field:Min(0, message = VALIDATION_SIZE_GE_ZERO) - val quantity: Float, - - val position: Int -) - -data class MixMaterialOutputDto( - val id: Long, - val material: Material, // TODO move to MaterialDto - val quantity: Float, - val position: Int -) - // ==== DSL ==== fun mixMaterial( id: Long? = null, @@ -46,30 +28,12 @@ fun mixMaterial( op: MixMaterial.() -> Unit = {} ) = MixMaterial(id, material, quantity, position).apply(op) +@Deprecated("Temporary DSL for transition") fun mixMaterialDto( - materialId: Long = 0L, - quantity: Float = 0f, - position: Int = 0, - op: MixMaterialDto.() -> Unit = {} -) = MixMaterialDto(materialId, quantity, position).apply(op) + entity: MixMaterial +) = MixMaterialDto(entity.id!!, materialDto(entity.material), entity.quantity, entity.position) -// ==== Exceptions ==== -private const val MIX_MATERIAL_NOT_FOUND_EXCEPTION_TITLE = "Mix material not found" -private const val MIX_MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE = "Mix material already exists" -private const val MIX_MATERIAL_EXCEPTION_ERROR_CODE = "mixmaterial" - -fun mixMaterialIdNotFoundException(id: Long) = - NotFoundException( - MIX_MATERIAL_EXCEPTION_ERROR_CODE, - MIX_MATERIAL_NOT_FOUND_EXCEPTION_TITLE, - "A mix material with the id $id could not be found", - id - ) - -fun mixMaterialIdAlreadyExistsException(id: Long) = - AlreadyExistsException( - MIX_MATERIAL_EXCEPTION_ERROR_CODE, - MIX_MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE, - "A mix material with the id $id already exists", - id - ) +@Deprecated("Temporary DSL for transition") +fun mixMaterial( + dto: MixMaterialDto +) = MixMaterial(dto.id, material(dto.material), dto.quantity, dto.position) \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/ModelEntity.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/ModelEntity.kt index 6b790dc..4185cac 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/ModelEntity.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/ModelEntity.kt @@ -14,9 +14,4 @@ interface EntityDto { fun toEntity(): E { throw UnsupportedOperationException() } -} - -// GENERAL VALIDATION MESSAGES -const val VALIDATION_SIZE_GE_ZERO = "Must be greater or equals to 0" -const val VALIDATION_SIZE_GE_ONE = "Must be greater or equals to 1" -const val VALIDATION_RANGE_PERCENTS = "Must be between 0 and 100" +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt index b9cb4d3..af8142c 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt @@ -89,11 +89,11 @@ open class RecipeSaveDto( @field:Pattern(regexp = VALIDATION_COLOR_PATTERN) val color: String, - @field:Min(0, message = VALIDATION_RANGE_PERCENTS) - @field:Max(100, message = VALIDATION_RANGE_PERCENTS) + @field:Min(0, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS) + @field:Max(100, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS) val gloss: Byte, - @field:Min(0, message = VALIDATION_SIZE_GE_ZERO) + @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO) val sample: Int?, val approbationDate: LocalDate?, @@ -125,11 +125,11 @@ open class RecipeUpdateDto( @field:Pattern(regexp = VALIDATION_COLOR_PATTERN) val color: String?, - @field:Min(0, message = VALIDATION_RANGE_PERCENTS) - @field:Max(100, message = VALIDATION_RANGE_PERCENTS) + @field:Min(0, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS) + @field:Max(100, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS) val gloss: Byte?, - @field:Min(0, message = VALIDATION_SIZE_GE_ZERO) + @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO) val sample: Int?, val approbationDate: LocalDate?, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt index b96738b..623e92d 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt @@ -1,10 +1,10 @@ package dev.fyloz.colorrecipesexplorer.model.touchupkit +import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.model.EntityDto import dev.fyloz.colorrecipesexplorer.model.ModelEntity -import dev.fyloz.colorrecipesexplorer.model.VALIDATION_SIZE_GE_ONE import java.time.LocalDate import javax.persistence.* import javax.validation.constraints.Min @@ -80,7 +80,7 @@ data class TouchUpKitSaveDto( @field:NotBlank val company: String, - @field:Min(1, message = VALIDATION_SIZE_GE_ONE) + @field:Min(1, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ONE) val quantity: Int, val shippingDate: LocalDate, @@ -109,7 +109,7 @@ data class TouchUpKitUpdateDto( @field:NotBlank val company: String?, - @field:Min(1, message = VALIDATION_SIZE_GE_ONE) + @field:Min(1, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ONE) val quantity: Int?, val shippingDate: LocalDate?, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixMaterialRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixMaterialRepository.kt index 6bd9aa9..ae20f67 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixMaterialRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixMaterialRepository.kt @@ -1,12 +1,11 @@ package dev.fyloz.colorrecipesexplorer.repository -import dev.fyloz.colorrecipesexplorer.model.Material import dev.fyloz.colorrecipesexplorer.model.MixMaterial import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @Repository interface MixMaterialRepository : JpaRepository { - /** Checks if one or more mix materials have the given [material]. */ - fun existsByMaterial(material: Material): Boolean + /** Checks if one or more mix materials have the given [materialId]. */ + fun existsByMaterialId(materialId: Long): Boolean } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialService.kt new file mode 100644 index 0000000..491763b --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialService.kt @@ -0,0 +1,23 @@ +package dev.fyloz.colorrecipesexplorer.service + +import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent +import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto +import dev.fyloz.colorrecipesexplorer.model.MixMaterial +import dev.fyloz.colorrecipesexplorer.repository.MixMaterialRepository + +interface MixMaterialService : Service { + /** Checks if a mix material with the given [materialId] exists. */ + fun existsByMaterialId(materialId: Long): Boolean +} + +@ServiceComponent +class DefaultMixMaterialService(repository: MixMaterialRepository, private val materialService: MaterialService) : + BaseService(repository), MixMaterialService { + override fun existsByMaterialId(materialId: Long) = repository.existsByMaterialId(materialId) + + override fun toDto(entity: MixMaterial) = + MixMaterialDto(entity.id!!, materialService.toDto(entity.material), entity.quantity, entity.position) + + override fun toEntity(dto: MixMaterialDto) = + MixMaterial(dto.id, materialService.toEntity(dto.material), dto.quantity, dto.position) +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt index 3aadbe8..2d525d1 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt @@ -19,13 +19,19 @@ inline fun Iterable.mapMayThrow( } } -/** Find duplicated in the given [Iterable] from keys obtained from the given [keySelector]. */ +/** Find duplicated keys in the given [Iterable], using keys obtained from the given [keySelector]. */ inline fun Iterable.findDuplicated(keySelector: (T) -> K) = this.groupBy(keySelector) .filter { it.value.count() > 1 } .map { it.key } -/** Check if the given [Iterable] has gaps between each items, using keys obtained from the given [keySelector]. */ +/** Find duplicated elements in the given [Iterable]. */ +fun Iterable.findDuplicated() = + this.groupBy { it } + .filter { it.value.count() > 1 } + .map { it.key } + +/** Check if the given [Iterable] has gaps between each element, using keys obtained from the given [keySelector]. */ inline fun Iterable.hasGaps(keySelector: (T) -> Int) = this.map(keySelector) .toIntArray() @@ -33,6 +39,12 @@ inline fun Iterable.hasGaps(keySelector: (T) -> Int) = .filterIndexed { index, it -> it != index + 1 } .isNotEmpty() +/** Check if the given [Int] [Iterable] has gaps between each element. */ +fun Iterable.hasGaps() = + this.sorted() + .filterIndexed { index, it -> it != index + 1 } + .isNotEmpty() + /** Clears and fills the given [MutableCollection] with the given [elements]. */ fun MutableCollection.setAll(elements: Collection) { this.clear() diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/PositionUtils.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/PositionUtils.kt new file mode 100644 index 0000000..858a280 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/PositionUtils.kt @@ -0,0 +1,50 @@ +package dev.fyloz.colorrecipesexplorer.utils + +import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError +import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException + +object PositionUtils { + const val INVALID_FIRST_POSITION_ERROR_CODE = "first" + const val DUPLICATED_POSITION_ERROR_CODE = "duplicated" + const val GAP_BETWEEN_POSITIONS_ERROR_CODE = "gap" + + private const val FIRST_POSITION = 1 + + fun validate(positions: List) { + if (positions.isEmpty()) { + return + } + + val sortedPositions = positions.sorted() + val errors = mutableSetOf() + + validateFirstPosition(sortedPositions[0], errors) + validateDuplicatedPositions(sortedPositions, errors) + validateGapsInPositions(sortedPositions, errors) + + if (errors.isNotEmpty()) { + throw InvalidPositionsException(errors) + } + } + + private fun validateFirstPosition(position: Int, errors: MutableSet) { + if (position == FIRST_POSITION) { + return + } + + errors += InvalidPositionError(INVALID_FIRST_POSITION_ERROR_CODE, "The first position must be $FIRST_POSITION") + } + + private fun validateDuplicatedPositions(positions: List, errors: MutableSet) { + errors += positions.findDuplicated() + .map { InvalidPositionError(DUPLICATED_POSITION_ERROR_CODE, "The position $it is duplicated") } + } + + private fun validateGapsInPositions(positions: List, errors: MutableSet) { + if (!positions.hasGaps()) { + return + } + + errors += InvalidPositionError(GAP_BETWEEN_POSITIONS_ERROR_CODE, "There is a gap between the positions") + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixMaterialLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixMaterialLogicTest.kt new file mode 100644 index 0000000..c27434c --- /dev/null +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixMaterialLogicTest.kt @@ -0,0 +1,79 @@ +package dev.fyloz.colorrecipesexplorer.logic + +import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto +import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto +import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto +import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError +import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException +import dev.fyloz.colorrecipesexplorer.service.MixMaterialService +import dev.fyloz.colorrecipesexplorer.utils.PositionUtils +import io.mockk.* +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class DefaultMixMaterialLogicTest { + private val mixMaterialServiceMock = mockk() + + private val mixMaterialLogic = DefaultMixMaterialLogic(mixMaterialServiceMock) + + @AfterEach + internal fun afterEach() { + clearAllMocks() + } + + @Test + fun validateMixMaterials_normalBehavior_doesNothing() { + // Arrange + val materialType = MaterialTypeDto(1L, "Unit test material type", "UTMT", false) + val material = MaterialDto(1L, "Unit test material", 1000f, false, materialType) + val mixMaterial = MixMaterialDto(1L, material, 100f, 1) + + mockkObject(PositionUtils) + every { PositionUtils.validate(any()) } just runs + + // Act + // Assert + mixMaterialLogic.validateMixMaterials(setOf(mixMaterial)) + } + + @Test + fun validateMixMaterials_emptySet_doesNothing() { + // Arrange + // Act + // Assert + mixMaterialLogic.validateMixMaterials(setOf()) + } + + @Test + fun validateMixMaterials_firstUsesPercents_throwsInvalidFirstMixMaterialException() { + // Arrange + val materialType = MaterialTypeDto(1L, "Unit test material type", "UTMT", true) + val material = MaterialDto(1L, "Unit test material", 1000f, false, materialType) + val mixMaterial = MixMaterialDto(1L, material, 100f, 1) + + mockkObject(PositionUtils) + every { PositionUtils.validate(any()) } just runs + + // Act + // Assert + assertThrows { mixMaterialLogic.validateMixMaterials(setOf(mixMaterial)) } + } + + @Test + fun validateMixMaterials_invalidPositions_throwsInvalidMixMaterialsPositionsException() { + // Arrange + val materialType = MaterialTypeDto(1L, "Unit test material type", "UTMT", false) + val material = MaterialDto(1L, "Unit test material", 1000f, false, materialType) + val mixMaterial = MixMaterialDto(1L, material, 100f, 1) + + val errors = setOf(InvalidPositionError("error", "An unit test error")) + + mockkObject(PositionUtils) + every { PositionUtils.validate(any()) } throws InvalidPositionsException(errors) + + // Act + // Assert + assertThrows { mixMaterialLogic.validateMixMaterials(setOf(mixMaterial)) } + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeStepLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeStepLogicTest.kt index f606abd..c9defbc 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeStepLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeStepLogicTest.kt @@ -1,21 +1,21 @@ package dev.fyloz.colorrecipesexplorer.logic -import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto +import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError +import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException import dev.fyloz.colorrecipesexplorer.model.RecipeGroupInformation import dev.fyloz.colorrecipesexplorer.model.RecipeStep import dev.fyloz.colorrecipesexplorer.model.account.Group import dev.fyloz.colorrecipesexplorer.service.RecipeStepService +import dev.fyloz.colorrecipesexplorer.utils.PositionUtils import io.mockk.* import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows -import kotlin.test.assertTrue class DefaultRecipeStepLogicTest { private val recipeStepServiceMock = mockk() - private val recipeStepLogic = spyk(DefaultRecipeStepLogic(recipeStepServiceMock)) + private val recipeStepLogic = DefaultRecipeStepLogic(recipeStepServiceMock) @AfterEach internal fun afterEach() { @@ -25,7 +25,8 @@ class DefaultRecipeStepLogicTest { @Test fun validateGroupInformationSteps_normalBehavior_callsValidateSteps() { // Arrange - every { recipeStepLogic.validateSteps(any()) } just runs + mockkObject(PositionUtils) + every { PositionUtils.validate(any()) } just runs val group = Group(1L, "Unit test group") val steps = mutableSetOf(RecipeStep(1L, 1, "A message")) @@ -36,14 +37,15 @@ class DefaultRecipeStepLogicTest { // Assert verify { - recipeStepLogic.validateSteps(any()) // TODO replace with actual steps dtos when RecipeGroupInformation updated + PositionUtils.validate(steps.map { it.position }) } } @Test fun validateGroupInformationSteps_stepSetIsNull_doesNothing() { // Arrange - every { recipeStepLogic.validateSteps(any()) } just runs + mockkObject(PositionUtils) + every { PositionUtils.validate(any()) } just runs val group = Group(1L, "Unit test group") val groupInfo = RecipeGroupInformation(1L, group, "A note", null) @@ -53,15 +55,17 @@ class DefaultRecipeStepLogicTest { // Assert verify(exactly = 0) { - recipeStepLogic.validateSteps(any()) // TODO replace with actual steps dtos when RecipeGroupInformation updated + PositionUtils.validate(any()) } } @Test fun validateGroupInformationSteps_invalidSteps_throwsInvalidGroupStepsPositionsException() { // Arrange - val errors = setOf(InvalidStepsPositionsError("error", "An unit test error")) - every { recipeStepLogic.validateSteps(any()) } throws InvalidStepsPositionsException(errors) + val errors = setOf(InvalidPositionError("error", "An unit test error")) + + mockkObject(PositionUtils) + every { PositionUtils.validate(any()) } throws InvalidPositionsException(errors) val group = Group(1L, "Unit test group") val steps = mutableSetOf(RecipeStep(1L, 1, "A message")) @@ -71,187 +75,4 @@ class DefaultRecipeStepLogicTest { // Assert assertThrows { recipeStepLogic.validateGroupInformationSteps(groupInfo) } } - - @Test - fun validateSteps_normalBehavior_doesNothing() { - // Arrange - val recipeSteps = setOf( - RecipeStepDto(1L, 1, "A message"), - RecipeStepDto(2L, 2, "Another message") - ) - - // Act - // Assert - assertDoesNotThrow { recipeStepLogic.validateSteps(recipeSteps) } - } - - @Test - fun validateSteps_emptyStepSet_doesNothing() { - // Arrange - val recipeSteps = setOf() - - // Act - // Assert - assertDoesNotThrow { recipeStepLogic.validateSteps(recipeSteps) } - } - - @Test - fun validateSteps_hasInvalidPositions_throwsInvalidStepsPositionsException() { - // Arrange - val recipeSteps = setOf( - RecipeStepDto(1L, 2, "A message"), - RecipeStepDto(2L, 3, "Another message") - ) - - // Act - // Assert - assertThrows { recipeStepLogic.validateSteps(recipeSteps) } - } - - @Test - fun validateSteps_firstStepPositionInvalid_returnsInvalidStepValidationError() { - // Arrange - val recipeSteps = setOf( - RecipeStepDto(1L, 2, "A message"), - RecipeStepDto(2L, 3, "Another message") - ) - - // Act - val exception = assertThrows { recipeStepLogic.validateSteps(recipeSteps) } - - // Assert - assertTrue { - exception.errors.any { it.type == RecipeStepDto.VALIDATION_ERROR_CODE_INVALID_FIRST_STEP } - } - } - - @Test - fun validateSteps_duplicatedPositions_returnsInvalidStepValidationError() { - // Arrange - val recipeSteps = setOf( - RecipeStepDto(1L, 1, "A message"), - RecipeStepDto(2L, 1, "Another message") - ) - - // Act - val exception = assertThrows { recipeStepLogic.validateSteps(recipeSteps) } - - // Assert - assertTrue { - exception.errors.any { it.type == RecipeStepDto.VALIDATION_ERROR_CODE_DUPLICATED_STEPS_POSITION } - } - } - - @Test - fun validateSteps_gapsInPositions_returnsInvalidStepValidationError() { - // Arrange - val recipeSteps = setOf( - RecipeStepDto(1L, 1, "A message"), - RecipeStepDto(2L, 3, "Another message") - ) - - // Act - val exception = assertThrows { recipeStepLogic.validateSteps(recipeSteps) } - - // Assert - assertTrue { - exception.errors.any { it.type == RecipeStepDto.VALIDATION_ERROR_CODE_GAP_BETWEEN_STEPS_POSITIONS } - } - } -} - -//@TestInstance(TestInstance.Lifecycle.PER_CLASS) -//class RecipeStepLogicTest : -// AbstractModelServiceTest() { -// override val repository: RecipeStepRepository = mock() -// override val logic: RecipeStepLogic = spy(DefaultRecipeStepLogic(repository)) -// -// override val entity: RecipeStep = recipeStep(id = 0L, message = "message") -// override val anotherEntity: RecipeStep = recipeStep(id = 1L, message = "another message") -// -// // validateGroupInformationSteps() -// -// @Test -// fun `validateGroupInformationSteps() calls validateSteps() with the given RecipeGroupInformation steps`() { -// withGroupInformation { -// logic.validateGroupInformationSteps(this) -// -// verify(logic).validateSteps(this.steps!!) -// } -// } -// -// @Test -// fun `validateGroupInformationSteps() throws InvalidGroupStepsPositionsException when validateSteps() throws an InvalidStepsPositionsException`() { -// withGroupInformation { -// doAnswer { throw InvalidStepsPositionsException(setOf()) }.whenever(logic).validateSteps(this.steps!!) -// -// assertThrows { -// logic.validateGroupInformationSteps(this) -// } -// } -// } -// -// // validateSteps() -// -// @Test -// fun `validateSteps() throws an InvalidStepsPositionsException when the position of the first step of the given groupInformation is not 1`() { -// assertInvalidStepsPositionsException( -// mutableSetOf( -// recipeStep(id = 0L, position = 0), -// recipeStep(id = 1L, position = 1), -// recipeStep(id = 2L, position = 2), -// recipeStep(id = 3L, position = 3) -// ), -// INVALID_FIRST_STEP_POSITION_ERROR_CODE -// ) -// } -// -// @Test -// fun `validateSteps() throws an InvalidStepsPositionsException when steps positions are duplicated in the given groupInformation`() { -// assertInvalidStepsPositionsException( -// mutableSetOf( -// recipeStep(id = 0L, position = 1), -// recipeStep(id = 1L, position = 2), -// recipeStep(id = 2L, position = 2), -// recipeStep(id = 3L, position = 3) -// ), -// DUPLICATED_STEPS_POSITIONS_ERROR_CODE -// ) -// } -// -// @Test -// fun `validateSteps() throws an InvalidStepsPositionsException when there is a gap between steps positions in the given groupInformation`() { -// assertInvalidStepsPositionsException( -// mutableSetOf( -// recipeStep(id = 0L, position = 1), -// recipeStep(id = 1L, position = 2), -// recipeStep(id = 2L, position = 4), -// recipeStep(id = 3L, position = 5) -// ), -// GAP_BETWEEN_STEPS_POSITIONS_ERROR_CODE -// ) -// } -// -// private fun withGroupInformation(steps: MutableSet? = null, test: RecipeGroupInformation.() -> Unit) { -// recipeGroupInformation( -// group = group(id = 0L), -// steps = steps ?: mutableSetOf( -// recipeStep(id = 0L, position = 1), -// recipeStep(id = 1L, position = 2), -// recipeStep(id = 2L, position = 3), -// recipeStep(id = 3L, position = 4) -// ) -// ) { -// test() -// } -// } -// -// private fun assertInvalidStepsPositionsException(steps: MutableSet, errorType: String) { -// val exception = assertThrows { -// logic.validateSteps(steps) -// } -// -// assertTrue { exception.errors.size == 1 } -// assertTrue { exception.errors.first().type == errorType } -// } -//} +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogicTest.kt deleted file mode 100644 index d9b69e5..0000000 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogicTest.kt +++ /dev/null @@ -1,182 +0,0 @@ -//package dev.fyloz.colorrecipesexplorer.logic -// -//import com.nhaarman.mockitokotlin2.* -//import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException -//import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException -//import dev.fyloz.colorrecipesexplorer.exception.CannotUpdateException -//import dev.fyloz.colorrecipesexplorer.exception.NotFoundException -//import dev.fyloz.colorrecipesexplorer.model.* -//import dev.fyloz.colorrecipesexplorer.repository.MaterialTypeRepository -//import org.junit.jupiter.api.AfterEach -//import org.junit.jupiter.api.Test -//import org.junit.jupiter.api.TestInstance -//import org.junit.jupiter.api.assertThrows -//import kotlin.test.assertEquals -//import kotlin.test.assertFalse -//import kotlin.test.assertTrue -// -//@TestInstance(TestInstance.Lifecycle.PER_CLASS) -//class MaterialTypeLogicTest : -// AbstractExternalNamedModelServiceTest() { -// override val repository: MaterialTypeRepository = mock() -// private val materialService: MaterialLogic = mock() -// override val logic: MaterialTypeLogic = spy(DefaultMaterialTypeLogic(repository)) -// override val entity: MaterialType = materialType(id = 0L, name = "material type", prefix = "MAT") -// override val anotherEntity: MaterialType = materialType(id = 1L, name = "another material type", prefix = "AMT") -// override val entityWithEntityName: MaterialType = materialType(2L, name = entity.name, prefix = "EEN") -// private val systemType = materialType(id = 3L, name = "systype", prefix = "SYS", systemType = true) -// private val anotherSystemType = materialType(id = 4L, name = "another systype", prefix = "ASY", systemType = true) -// override val entitySaveDto: MaterialTypeSaveDto = spy(materialTypeSaveDto(name = "material type", prefix = "MAT")) -// override val entityUpdateDto: MaterialTypeUpdateDto = -// spy(materialTypeUpdateDto(id = 0L, name = "material type", prefix = "MAT")) -// -// @AfterEach -// override fun afterEach() { -// reset(materialService) -// super.afterEach() -// } -// -// // existsByPrefix() -// -// @Test -// fun `existsByPrefix() returns true when a material type with the given prefix exists`() { -// whenever(repository.existsByPrefix(entity.prefix)).doReturn(true) -// -// val found = logic.existsByPrefix(entity.prefix) -// -// assertTrue(found) -// } -// -// @Test -// fun `existsByPrefix() returns false when no material type with the given prefix exists`() { -// whenever(repository.existsByPrefix(entity.prefix)).doReturn(false) -// -// val found = logic.existsByPrefix(entity.prefix) -// -// assertFalse(found) -// } -// -// // getAllSystemTypes() -// -// @Test -// fun `getAllSystemTypes() returns all system types`() { -// whenever(repository.findAllBySystemTypeIs(true)).doReturn(listOf(systemType, anotherSystemType)) -// -// val found = logic.getAllSystemTypes() -// -// assertTrue(found.contains(systemType)) -// assertTrue(found.contains(anotherSystemType)) -// } -// -// // getAllNonSystemTypes() -// -// @Test -// fun `getAllNonSystemTypes() returns all non system types`() { -// whenever(repository.findAllBySystemTypeIs(false)).doReturn(listOf(entity, anotherEntity)) -// -// val found = logic.getAllNonSystemType() -// -// assertTrue(found.contains(entity)) -// assertTrue(found.contains(anotherEntity)) -// } -// -// // save() -// -// @Test -// override fun `save(dto) calls and returns save() with the created entity`() { -// withBaseSaveDtoTest(entity, entitySaveDto, logic) -// } -// -// // saveMaterialType() -// -// @Test -// fun `saveMaterialType() throws AlreadyExistsException when a material type with the given prefix already exists`() { -// doReturn(true).whenever(logic).existsByPrefix(entity.prefix) -// -// assertThrows { logic.save(entity) } -// .assertErrorCode("prefix") -// } -// -// // update() -// -// @Test -// override fun `update(dto) calls and returns update() with the created entity`() = -// withBaseUpdateDtoTest(entity, entityUpdateDto, logic, { any() }) -// -// override fun `update() saves in the repository and returns the updated value`() { -// whenever(repository.save(entity)).doReturn(entity) -// whenever(repository.findByName(entity.name)).doReturn(null) -// whenever(repository.findByPrefix(entity.prefix)).doReturn(null) -// doReturn(true).whenever(logic).existsById(entity.id!!) -// doReturn(entity).whenever(logic).getById(entity.id!!) -// -// val found = logic.update(entity) -// -// verify(repository).save(entity) -// assertEquals(entity, found) -// } -// -// override fun `update() throws NotFoundException when no entity with the given id exists in the repository`() { -// whenever(repository.findByName(entity.name)).doReturn(null) -// whenever(repository.findByPrefix(entity.prefix)).doReturn(null) -// doReturn(false).whenever(logic).existsById(entity.id!!) -// doReturn(null).whenever(logic).getById(entity.id!!) -// -// assertThrows { logic.update(entity) } -// .assertErrorCode() -// } -// -// override fun `update() throws AlreadyExistsException when an entity with the updated name exists`() { -// whenever(repository.findByName(entity.name)).doReturn(entityWithEntityName) -// whenever(repository.findByPrefix(entity.prefix)).doReturn(null) -// doReturn(true).whenever(logic).existsById(entity.id!!) -// doReturn(entity).whenever(logic).getById(entity.id!!) -// -// assertThrows { logic.update(entity) } -// .assertErrorCode("name") -// } -// -// @Test -// fun `update() throws AlreadyExistsException when an entity with the updated prefix exists`() { -// val anotherMaterialType = materialType(prefix = entity.prefix) -// whenever(repository.findByPrefix(entity.prefix)).doReturn(anotherMaterialType) -// doReturn(entity).whenever(logic).getById(entity.id!!) -// -// assertThrows { logic.update(entity) } -// .assertErrorCode("prefix") -// } -// -// @Test -// fun `update() throws CannotUpdateException when updating a system material type`() { -// whenever(repository.existsByIdAndSystemTypeIsTrue(systemType.id!!)).doReturn(true) -// -// assertThrows { logic.update(systemType) } -// } -// -// // delete() -// -// @Test -// fun `delete() throws CannotDeleteException when deleting a system material type`() { -// whenever(repository.existsByIdAndSystemTypeIsTrue(systemType.id!!)).doReturn(true) -// -// assertThrows { logic.delete(systemType) } -// } -// -// override fun `delete() deletes in the repository`() { -// whenCanBeDeleted { -// super.`delete() deletes in the repository`() -// } -// } -// -// override fun `deleteById() deletes the entity with the given id in the repository`() { -// whenCanBeDeleted { -// super.`deleteById() deletes the entity with the given id in the repository`() -// } -// } -// -// private fun whenCanBeDeleted(id: Long = any(), test: () -> Unit) { -// whenever(repository.canBeDeleted(id)).doReturn(true) -// -// test() -// } -//} diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogicTest.kt index d611a92..d76215b 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogicTest.kt @@ -1,255 +1,245 @@ package dev.fyloz.colorrecipesexplorer.logic -import com.nhaarman.mockitokotlin2.* -import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.repository.MixRepository -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class MixLogicTest : AbstractExternalModelServiceTest() { - override val repository: MixRepository = mock() - private val recipeService: RecipeLogic = mock() - private val materialTypeService: MaterialTypeLogic = mock() - private val mixMaterialService: MixMaterialLogic = mock() - private val mixTypeService: MixTypeLogic = mock() - override val logic: MixLogic = - spy(DefaultMixLogic(repository, recipeService, materialTypeService, mixMaterialService, mixTypeService)) - - override val entity: Mix = mix(id = 0L, location = "location") - override val anotherEntity: Mix = mix(id = 1L) - override val entitySaveDto: MixSaveDto = - spy(mixSaveDto(mixMaterials = setOf(mixMaterialDto(materialId = 1L, quantity = 1000f, position = 0)))) - override val entityUpdateDto: MixUpdateDto = spy(mixUpdateDto(id = entity.id!!)) - - @AfterEach - override fun afterEach() { - super.afterEach() - reset(recipeService, materialTypeService, mixMaterialService, mixTypeService) - } - - // getAllByMixType() - - @Test - fun `getAllByMixType() returns all mixes with the given mix type`() { - val mixType = mixType(id = 0L) - - whenever(repository.findAllByMixType(mixType)).doReturn(entityList) - - val found = logic.getAllByMixType(mixType) - - assertEquals(entityList, found) - } - - // save() - - @Test - override fun `save(dto) calls and returns save() with the created entity`() { - val recipe = recipe(id = entitySaveDto.recipeId) - val materialType = materialType(id = entitySaveDto.materialTypeId) - val material = material( - name = entitySaveDto.name, - inventoryQuantity = Float.MIN_VALUE, - isMixType = true, - materialType = materialType - ) - val mixType = mixType(name = entitySaveDto.name, material = material) - val mix = mix(recipe = recipe, mixType = mixType) - val mixWithId = mix(id = 0L, recipe = recipe, mixType = mixType) - val mixMaterials = setOf(mixMaterial(material = material(id = 1L), quantity = 1000f)) - - whenever(recipeService.getById(recipe.id!!)).doReturn(recipe) - whenever(materialTypeService.getById(materialType.id!!)).doReturn(materialTypeDto(materialType)) - whenever(mixMaterialService.create(entitySaveDto.mixMaterials!!)).doReturn(mixMaterials) - whenever( - mixTypeService.getOrCreateForNameAndMaterialType( - mixType.name, - mixType.material.materialType!! - ) - ).doReturn(mixType) - doReturn(true).whenever(logic).existsById(mixWithId.id!!) - doReturn(mixWithId).whenever(logic).save(any()) - - val found = logic.save(entitySaveDto) - - verify(logic).save(argThat { this.recipe == mix.recipe }) - verify(recipeService).addMix(recipe, mixWithId) - - // Verify if this method is called instead of the MixType's constructor, which does not check if the name is already taken by a material. - verify(mixTypeService).getOrCreateForNameAndMaterialType(mixType.name, mixType.material.materialType!!) - - assertEquals(mixWithId, found) - } - - // update() - - private fun mixUpdateDtoTest( - scope: MixUpdateDtoTestScope = MixUpdateDtoTestScope(), - sharedMixType: Boolean = false, - op: MixUpdateDtoTestScope.() -> Unit - ) { - with(scope) { - doReturn(true).whenever(logic).existsById(mix.id!!) - doReturn(mix).whenever(logic).getById(mix.id!!) - doReturn(sharedMixType).whenever(logic).mixTypeIsShared(mix.mixType) - doAnswer { it.arguments[0] }.whenever(logic).update(any()) - - if (mixUpdateDto.materialTypeId != null) { - whenever(materialTypeService.getById(materialType.id!!)).doReturn(materialTypeDto(materialType)) - } - - op() - } - } - - private fun mixUpdateDtoMixTypeTest(sharedMixType: Boolean = false, op: MixUpdateDtoTestScope.() -> Unit) { - with(MixUpdateDtoTestScope(mixUpdateDto = mixUpdateDto(id = 0L, name = "name", materialTypeId = 0L))) { - mixUpdateDtoTest(this, sharedMixType, op) - } - } - - @Test - override fun `update(dto) calls and returns update() with the created entity`() { - val mixUpdateDto = spy(mixUpdateDto(id = 0L, name = null, materialTypeId = null)) - - doReturn(entity).whenever(logic).getById(any()) - doReturn(entity).whenever(logic).update(entity) - - val found = logic.update(mixUpdateDto) - - verify(logic).update(entity) - - assertEquals(entity, found) - } - - @Test - fun `update(dto) calls MixTypeService saveForNameAndMaterialType() when mix type is shared`() { - mixUpdateDtoMixTypeTest(sharedMixType = true) { - whenever(mixTypeService.saveForNameAndMaterialType(mixUpdateDto.name!!, materialType)) - .doReturn(newMixType) - - val found = logic.update(mixUpdateDto) - - verify(mixTypeService).saveForNameAndMaterialType(mixUpdateDto.name!!, materialType) - - assertEquals(newMixType, found.mixType) - } - } - - @Test - fun `update(dto) calls MixTypeService updateForNameAndMaterialType() when mix type is not shared`() { - mixUpdateDtoMixTypeTest { - whenever(mixTypeService.updateForNameAndMaterialType(mixType, mixUpdateDto.name!!, materialType)) - .doReturn(newMixType) - - val found = logic.update(mixUpdateDto) - - verify(mixTypeService).updateForNameAndMaterialType(mixType, mixUpdateDto.name!!, materialType) - - assertEquals(newMixType, found.mixType) - } - } - - @Test - fun `update(dto) update, create and delete mix materials according to the given mix materials map`() { - mixUpdateDtoTest { - val mixMaterials = setOf( - mixMaterialDto(materialId = 0L, quantity = 100f, position = 0), - mixMaterialDto(materialId = 1L, quantity = 200f, position = 1), - mixMaterialDto(materialId = 2L, quantity = 300f, position = 2), - mixMaterialDto(materialId = 3L, quantity = 400f, position = 3), - ) - mixUpdateDto.mixMaterials = mixMaterials - - whenever(mixMaterialService.create(any>())).doAnswer { - (it.arguments[0] as Set).map { dto -> - mixMaterial( - material = material(id = dto.materialId), - quantity = dto.quantity, - position = dto.position - ) - }.toSet() - } - - val found = logic.update(mixUpdateDto) - - mixMaterials.forEach { - assertTrue { - found.mixMaterials.any { mixMaterial -> - mixMaterial.material.id == it.materialId && mixMaterial.quantity == it.quantity && mixMaterial.position == it.position - } - } - } - } - } - - // updateLocations() - - @Test - fun `updateLocations() calls updateLocation() for each given MixLocationDto`() { - val locations = setOf( - mixLocationDto(mixId = 0, location = "Loc 0"), - mixLocationDto(mixId = 1, location = "Loc 1"), - mixLocationDto(mixId = 2, location = "Loc 2"), - mixLocationDto(mixId = 3, location = "Loc 3") - ) - - logic.updateLocations(locations) - - locations.forEach { - verify(logic).updateLocation(it) - } - } - - // updateLocation() - - @Test - fun `updateLocation() updates the location of a mix in the repository according to the given MixLocationDto`() { - val locationDto = mixLocationDto(mixId = 0L, location = "Location") - - logic.updateLocation(locationDto) - - verify(repository).updateLocationById(locationDto.mixId, locationDto.location) - } - - // delete() - - override fun `delete() deletes in the repository`() { - whenCanBeDeleted { - super.`delete() deletes in the repository`() - } - } - - // deleteById() - - @Test - override fun `deleteById() deletes the entity with the given id in the repository`() { - whenCanBeDeleted { - super.`deleteById() deletes the entity with the given id in the repository`() - } - } - - private fun whenCanBeDeleted(id: Long = any(), test: () -> Unit) { - whenever(repository.canBeDeleted(id)).doReturn(true) - - test() - } -} - -data class MixUpdateDtoTestScope( - val mixType: MixType = mixType(name = "mix type"), - val newMixType: MixType = mixType(name = "another mix type"), - val materialType: MaterialType = materialType(id = 0L), - val mix: Mix = mix(id = 0L, mixType = mixType), - val mixUpdateDto: MixUpdateDto = spy( - mixUpdateDto( - id = 0L, - name = null, - materialTypeId = null, - mixMaterials = setOf() - ) - ) -) +//@TestInstance(TestInstance.Lifecycle.PER_CLASS) +//class MixLogicTest : AbstractExternalModelServiceTest() { +// override val repository: MixRepository = mock() +// private val recipeService: RecipeLogic = mock() +// private val materialTypeService: MaterialTypeLogic = mock() +// private val mixMaterialService: MixMaterialLogic = mock() +// private val mixTypeService: MixTypeLogic = mock() +// override val logic: MixLogic = +// spy(DefaultMixLogic(repository, recipeService, materialTypeService, mixMaterialService, mixTypeService)) +// +// override val entity: Mix = mix(id = 0L, location = "location") +// override val anotherEntity: Mix = mix(id = 1L) +// override val entitySaveDto: MixSaveDto = spy(mixSaveDto(mixMaterials = setOf())) +// override val entityUpdateDto: MixUpdateDto = spy(mixUpdateDto(id = entity.id!!)) +// +// @AfterEach +// override fun afterEach() { +// super.afterEach() +// reset(recipeService, materialTypeService, mixMaterialService, mixTypeService) +// } +// +// // getAllByMixType() +// +//// @Test +//// fun `getAllByMixType() returns all mixes with the given mix type`() { +//// val mixType = mixType(id = 0L) +//// +//// whenever(repository.findAllByMixType(mixType)).doReturn(entityList) +//// +//// val found = logic.getAllByMixType(mixType) +//// +//// assertEquals(entityList, found) +//// } +//// +//// // save() +//// +//// @Test +//// override fun `save(dto) calls and returns save() with the created entity`() { +//// val recipe = recipe(id = entitySaveDto.recipeId) +//// val materialType = materialType(id = entitySaveDto.materialTypeId) +//// val material = material( +//// name = entitySaveDto.name, +//// inventoryQuantity = Float.MIN_VALUE, +//// isMixType = true, +//// materialType = materialType +//// ) +//// val mixType = mixType(name = entitySaveDto.name, material = material) +//// val mix = mix(recipe = recipe, mixType = mixType) +//// val mixWithId = mix(id = 0L, recipe = recipe, mixType = mixType) +//// val mixMaterials = setOf(mixMaterial(material = material(id = 1L), quantity = 1000f)) +//// +//// whenever(recipeService.getById(recipe.id!!)).doReturn(recipe) +//// whenever(materialTypeService.getById(materialType.id!!)).doReturn(materialTypeDto(materialType)) +//// whenever(mixMaterialService.create(entitySaveDto.mixMaterials!!)).doReturn(mixMaterials) +//// whenever( +//// mixTypeService.getOrCreateForNameAndMaterialType( +//// mixType.name, +//// mixType.material.materialType!! +//// ) +//// ).doReturn(mixType) +//// doReturn(true).whenever(logic).existsById(mixWithId.id!!) +//// doReturn(mixWithId).whenever(logic).save(any()) +//// +//// val found = logic.save(entitySaveDto) +//// +//// verify(logic).save(argThat { this.recipe == mix.recipe }) +//// verify(recipeService).addMix(recipe, mixWithId) +//// +//// // Verify if this method is called instead of the MixType's constructor, which does not check if the name is already taken by a material. +//// verify(mixTypeService).getOrCreateForNameAndMaterialType(mixType.name, mixType.material.materialType!!) +//// +//// assertEquals(mixWithId, found) +//// } +//// +//// // update() +//// +//// private fun mixUpdateDtoTest( +//// scope: MixUpdateDtoTestScope = MixUpdateDtoTestScope(), +//// sharedMixType: Boolean = false, +//// op: MixUpdateDtoTestScope.() -> Unit +//// ) { +//// with(scope) { +//// doReturn(true).whenever(logic).existsById(mix.id!!) +//// doReturn(mix).whenever(logic).getById(mix.id!!) +//// doReturn(sharedMixType).whenever(logic).mixTypeIsShared(mix.mixType) +//// doAnswer { it.arguments[0] }.whenever(logic).update(any()) +//// +//// if (mixUpdateDto.materialTypeId != null) { +//// whenever(materialTypeService.getById(materialType.id!!)).doReturn(materialTypeDto(materialType)) +//// } +//// +//// op() +//// } +//// } +//// +//// private fun mixUpdateDtoMixTypeTest(sharedMixType: Boolean = false, op: MixUpdateDtoTestScope.() -> Unit) { +//// with(MixUpdateDtoTestScope(mixUpdateDto = mixUpdateDto(id = 0L, name = "name", materialTypeId = 0L))) { +//// mixUpdateDtoTest(this, sharedMixType, op) +//// } +//// } +//// +//// @Test +//// override fun `update(dto) calls and returns update() with the created entity`() { +//// val mixUpdateDto = spy(mixUpdateDto(id = 0L, name = null, materialTypeId = null)) +//// +//// doReturn(entity).whenever(logic).getById(any()) +//// doReturn(entity).whenever(logic).update(entity) +//// +//// val found = logic.update(mixUpdateDto) +//// +//// verify(logic).update(entity) +//// +//// assertEquals(entity, found) +//// } +//// +//// @Test +//// fun `update(dto) calls MixTypeService saveForNameAndMaterialType() when mix type is shared`() { +//// mixUpdateDtoMixTypeTest(sharedMixType = true) { +//// whenever(mixTypeService.saveForNameAndMaterialType(mixUpdateDto.name!!, materialType)) +//// .doReturn(newMixType) +//// +//// val found = logic.update(mixUpdateDto) +//// +//// verify(mixTypeService).saveForNameAndMaterialType(mixUpdateDto.name!!, materialType) +//// +//// assertEquals(newMixType, found.mixType) +//// } +//// } +//// +//// @Test +//// fun `update(dto) calls MixTypeService updateForNameAndMaterialType() when mix type is not shared`() { +//// mixUpdateDtoMixTypeTest { +//// whenever(mixTypeService.updateForNameAndMaterialType(mixType, mixUpdateDto.name!!, materialType)) +//// .doReturn(newMixType) +//// +//// val found = logic.update(mixUpdateDto) +//// +//// verify(mixTypeService).updateForNameAndMaterialType(mixType, mixUpdateDto.name!!, materialType) +//// +//// assertEquals(newMixType, found.mixType) +//// } +//// } +//// +//// @Test +//// fun `update(dto) update, create and delete mix materials according to the given mix materials map`() { +//// mixUpdateDtoTest { +//// val mixMaterials = setOf( +//// mixMaterialDto(materialId = 0L, quantity = 100f, position = 0), +//// mixMaterialDto(materialId = 1L, quantity = 200f, position = 1), +//// mixMaterialDto(materialId = 2L, quantity = 300f, position = 2), +//// mixMaterialDto(materialId = 3L, quantity = 400f, position = 3), +//// ) +//// mixUpdateDto.mixMaterials = mixMaterials +//// +//// whenever(mixMaterialService.create(any>())).doAnswer { +//// (it.arguments[0] as Set).map { dto -> +//// mixMaterial( +//// material = material(id = dto.materialId), +//// quantity = dto.quantity, +//// position = dto.position +//// ) +//// }.toSet() +//// } +//// +//// val found = logic.update(mixUpdateDto) +//// +//// mixMaterials.forEach { +//// assertTrue { +//// found.mixMaterials.any { mixMaterial -> +//// mixMaterial.material.id == it.materialId && mixMaterial.quantity == it.quantity && mixMaterial.position == it.position +//// } +//// } +//// } +//// } +//// } +//// +//// // updateLocations() +//// +//// @Test +//// fun `updateLocations() calls updateLocation() for each given MixLocationDto`() { +//// val locations = setOf( +//// mixLocationDto(mixId = 0, location = "Loc 0"), +//// mixLocationDto(mixId = 1, location = "Loc 1"), +//// mixLocationDto(mixId = 2, location = "Loc 2"), +//// mixLocationDto(mixId = 3, location = "Loc 3") +//// ) +//// +//// logic.updateLocations(locations) +//// +//// locations.forEach { +//// verify(logic).updateLocation(it) +//// } +//// } +//// +//// // updateLocation() +//// +//// @Test +//// fun `updateLocation() updates the location of a mix in the repository according to the given MixLocationDto`() { +//// val locationDto = mixLocationDto(mixId = 0L, location = "Location") +//// +//// logic.updateLocation(locationDto) +//// +//// verify(repository).updateLocationById(locationDto.mixId, locationDto.location) +//// } +//// +//// // delete() +//// +//// override fun `delete() deletes in the repository`() { +//// whenCanBeDeleted { +//// super.`delete() deletes in the repository`() +//// } +//// } +//// +//// // deleteById() +//// +//// @Test +//// override fun `deleteById() deletes the entity with the given id in the repository`() { +//// whenCanBeDeleted { +//// super.`deleteById() deletes the entity with the given id in the repository`() +//// } +//// } +//// +//// private fun whenCanBeDeleted(id: Long = any(), test: () -> Unit) { +//// whenever(repository.canBeDeleted(id)).doReturn(true) +//// +//// test() +//// } +//} +// +//data class MixUpdateDtoTestScope( +// val mixType: MixType = mixType(name = "mix type"), +// val newMixType: MixType = mixType(name = "another mix type"), +// val materialType: MaterialType = materialType(id = 0L), +// val mix: Mix = mix(id = 0L, mixType = mixType), +// val mixUpdateDto: MixUpdateDto = spy( +// mixUpdateDto( +// id = 0L, +// name = null, +// materialTypeId = null, +// mixMaterials = setOf() +// ) +// ) +//) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogicTest.kt deleted file mode 100644 index 2b54a53..0000000 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogicTest.kt +++ /dev/null @@ -1,171 +0,0 @@ -package dev.fyloz.colorrecipesexplorer.logic - -import com.nhaarman.mockitokotlin2.* -import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.repository.MixMaterialRepository -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance -import org.junit.jupiter.api.assertThrows -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotEquals -import kotlin.test.assertTrue - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class MixMaterialLogicTest : AbstractModelServiceTest() { - override val repository: MixMaterialRepository = mock() - private val materialService: MaterialLogic = mock() - override val logic: MixMaterialLogic = spy(DefaultMixMaterialLogic(repository, materialService)) - - private val material: Material = material(id = 0L) - override val entity: MixMaterial = mixMaterial(id = 0L, material = material, quantity = 1000f) - override val anotherEntity: MixMaterial = mixMaterial(id = 1L, material = material) - - // existsByMaterial() - - @Test - fun `existsByMaterial() returns true when a mix material with the given material exists`() { - whenever(repository.existsByMaterial(material)).doReturn(true) - - val found = logic.existsByMaterial(material) - - assertTrue(found) - } - - @Test - fun `existsByMaterial() returns false when no mix material with the given material exists`() { - whenever(repository.existsByMaterial(material)).doReturn(false) - - val found = logic.existsByMaterial(material) - - assertFalse(found) - } - - // create() - - @Test - fun `create(set) calls create() for each MixMaterialDto`() { - val mixMaterialDtos = setOf( - mixMaterialDto(materialId = 0L, quantity = 1000f, position = 1), - mixMaterialDto(materialId = 1L, quantity = 2000f, position = 2), - mixMaterialDto(materialId = 2L, quantity = 3000f, position = 3), - mixMaterialDto(materialId = 3L, quantity = 4000f, position = 4) - ) - - doAnswer { - with(it.arguments[0] as MixMaterialDto) { - mixMaterial( - material = material(id = this.materialId), - quantity = this.quantity, - position = this.position - ) - } - }.whenever(logic).create(any()) - - val found = logic.create(mixMaterialDtos) - - mixMaterialDtos.forEach { dto -> - verify(logic).create(dto) - assertTrue { - found.any { - it.material.id == dto.materialId && it.quantity == dto.quantity && it.position == dto.position - } - } - } - } - - @Test - fun `create() creates a mix material according to the given MixUpdateDto`() { - val mixMaterialDto = mixMaterialDto(materialId = 0L, quantity = 1000f, position = 1) - - whenever(materialService.getById(mixMaterialDto.materialId)).doAnswer { materialDto(material(id = it.arguments[0] as Long)) } - - val found = logic.create(mixMaterialDto) - - assertTrue { - found.material.id == mixMaterialDto.materialId && - found.quantity == mixMaterialDto.quantity && - found.position == mixMaterialDto.position - } - } - - // updateQuantity() - - @Test - fun `updateQuantity() updates the given mix material with the given quantity`() { - val quantity = 5000f - assertNotEquals(quantity, entity.quantity, message = "Quantities must not be equals for this test to works") - - doAnswer { it.arguments[0] }.whenever(logic).update(any()) - - val found = logic.updateQuantity(entity, quantity) - - assertEquals(found.quantity, quantity) - } - - // validateMixMaterials() - - @Test - fun `validateMixMaterials() throws InvalidMixMaterialsPositionsException when the position of the first mix material is not 1`() { - assertInvalidMixMaterialsPositionsException( - setOf( - mixMaterial(id = 0L, position = 0), - mixMaterial(id = 1L, position = 1), - mixMaterial(id = 2L, position = 2), - mixMaterial(id = 3L, position = 3) - ), - INVALID_FIRST_MIX_MATERIAL_POSITION_ERROR_CODE - ) - } - - @Test - fun `validateMixMaterials() throws InvalidMixMaterialsPositionsException when positions are duplicated`() { - assertInvalidMixMaterialsPositionsException( - setOf( - mixMaterial(id = 0L, position = 1), - mixMaterial(id = 1L, position = 2), - mixMaterial(id = 2L, position = 2), - mixMaterial(id = 3L, position = 3) - ), - DUPLICATED_MIX_MATERIALS_POSITIONS_ERROR_CODE - ) - } - - @Test - fun `validateMixMaterials() throws InvalidMixMaterialsPositionsException when there is a gap between positions`() { - assertInvalidMixMaterialsPositionsException( - setOf( - mixMaterial(id = 0L, position = 1), - mixMaterial(id = 1L, position = 2), - mixMaterial(id = 2L, position = 4), - mixMaterial(id = 3L, position = 5) - ), - GAP_BETWEEN_MIX_MATERIALS_POSITIONS_ERROR_CODE - ) - } - - @Test - fun `validateMixMaterials() throws InvalidFirstMixMaterial when the first mix material's quantity is expressed in percents`() { - val normalMaterial = material(materialType = materialType(usePercentages = false)) - val percentsMaterial = material(materialType = materialType(usePercentages = true)) - val mixMaterials = setOf( - mixMaterial(id = 0L, position = 1, material = percentsMaterial), - mixMaterial(id = 1L, position = 2, material = normalMaterial), - mixMaterial(id = 2L, position = 3, material = normalMaterial), - mixMaterial(id = 3L, position = 4, material = normalMaterial) - ) - - assertThrows { - logic.validateMixMaterials(mixMaterials) - } - } - - private fun assertInvalidMixMaterialsPositionsException(mixMaterials: Set, errorType: String) { - val exception = assertThrows { - logic.validateMixMaterials(mixMaterials) - } - - assertTrue { exception.errors.size == 1 } - assertTrue { exception.errors.first().type == errorType } - } -} diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/utils/PositionUtilsTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/utils/PositionUtilsTest.kt new file mode 100644 index 0000000..89809c7 --- /dev/null +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/utils/PositionUtilsTest.kt @@ -0,0 +1,88 @@ +package dev.fyloz.colorrecipesexplorer.utils + +import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException +import io.mockk.clearAllMocks +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertTrue + +class PositionUtilsTest { + @AfterEach + internal fun afterEach() { + clearAllMocks() + } + + @Test + fun validateSteps_normalBehavior_doesNothing() { + // Arrange + val positions = listOf(1, 2) + + // Act + // Assert + assertDoesNotThrow { PositionUtils.validate(positions) } + } + + @Test + fun validateSteps_emptyStepSet_doesNothing() { + // Arrange + val positions = listOf() + + // Act + // Assert + assertDoesNotThrow { PositionUtils.validate(positions) } + } + + @Test + fun validateSteps_hasInvalidPositions_throwsInvalidStepsPositionsException() { + // Arrange + val positions = listOf(2, 3) + + // Act + // Assert + assertThrows { PositionUtils.validate(positions) } + } + + @Test + fun validateSteps_firstStepPositionInvalid_returnsInvalidStepValidationError() { + // Arrange + val positions = listOf(2, 3) + + // Act + val exception = assertThrows { PositionUtils.validate(positions) } + + // Assert + assertTrue { + exception.errors.any { it.type == PositionUtils.INVALID_FIRST_POSITION_ERROR_CODE } + } + } + + @Test + fun validateSteps_duplicatedPositions_returnsInvalidStepValidationError() { + // Arrange + val positions = listOf(1, 1) + + // Act + val exception = assertThrows { PositionUtils.validate(positions) } + + // Assert + assertTrue { + exception.errors.any { it.type == PositionUtils.DUPLICATED_POSITION_ERROR_CODE } + } + } + + @Test + fun validateSteps_gapsInPositions_returnsInvalidStepValidationError() { + // Arrange + val positions = listOf(1, 3) + + // Act + val exception = assertThrows { PositionUtils.validate(positions) } + + // Assert + assertTrue { + exception.errors.any { it.type == PositionUtils.GAP_BETWEEN_POSITIONS_ERROR_CODE } + } + } +} \ No newline at end of file