#25 Migrate mix materials to new logic
continuous-integration/drone/push Build is passing Details

This commit is contained in:
FyloZ 2022-03-03 23:24:55 -05:00
parent d785cfdbe7
commit 618ef6c77a
Signed by: william
GPG Key ID: 835378AE9AF4AE97
26 changed files with 633 additions and 1136 deletions

View File

@ -12,4 +12,10 @@ object Constants {
const val SIMDUT = "$PDF/simdut" 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"
}
} }

View File

@ -1,5 +1,6 @@
package dev.fyloz.colorrecipesexplorer.dtos package dev.fyloz.colorrecipesexplorer.dtos
import dev.fyloz.colorrecipesexplorer.Constants
import org.springframework.web.multipart.MultipartFile import org.springframework.web.multipart.MultipartFile
import javax.validation.constraints.Min import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank import javax.validation.constraints.NotBlank
@ -24,7 +25,7 @@ data class MaterialSaveDto(
@field:NotBlank @field:NotBlank
val name: String, val name: String,
@field:Min(0) @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO)
val inventoryQuantity: Float, val inventoryQuantity: Float,
val materialTypeId: Long, val materialTypeId: Long,

View File

@ -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

View File

@ -6,10 +6,4 @@ data class RecipeStepDto(
val position: Int, val position: Int,
val message: String val message: String
) : EntityDto { ) : 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"
}
}

View File

@ -0,0 +1,15 @@
package dev.fyloz.colorrecipesexplorer.exception
import org.springframework.http.HttpStatus
class InvalidPositionsException(val errors: Set<InvalidPositionError>) : 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)

View File

@ -25,6 +25,9 @@ interface Logic<D : EntityDto, S : Service<D, *, *>> {
/** Saves the given [dto]. */ /** Saves the given [dto]. */
fun save(dto: D): D fun save(dto: D): D
/** Saves all the given [dtos]. */
fun saveAll(dtos: Collection<D>): Collection<D>
/** Updates the given [dto]. Throws if no DTO with the same id exists. */ /** Updates the given [dto]. Throws if no DTO with the same id exists. */
fun update(dto: D): D fun update(dto: D): D
@ -50,6 +53,9 @@ abstract class BaseLogic<D : EntityDto, S : Service<D, *, *>>(
override fun save(dto: D) = override fun save(dto: D) =
service.save(dto) service.save(dto)
override fun saveAll(dtos: Collection<D>) =
dtos.map(::save)
override fun update(dto: D): D { override fun update(dto: D): D {
if (!existsById(dto.id)) { if (!existsById(dto.id)) {
throw notFoundException(value = dto.id) throw notFoundException(value = dto.id)

View File

@ -42,11 +42,7 @@ class DefaultMixLogic(
this.id!!, this.id!!,
this.location, this.location,
this.mixType, this.mixType,
this.mixMaterials.map { this.mixMaterials.map { mixMaterialDto(it) }.toSet()
with(mixMaterialLogic) {
return@with it.toOutput()
}
}.toSet()
) )
@Transactional @Transactional
@ -55,10 +51,11 @@ class DefaultMixLogic(
val materialType = materialTypeLogic.getById(entity.materialTypeId) val materialType = materialTypeLogic.getById(entity.materialTypeId)
val mixType = mixTypeLogic.getOrCreateForNameAndMaterialType(entity.name, materialType(materialType)) 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) 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) mix = save(mix)
recipeLogic.addMix(recipe, mix) recipeLogic.addMix(recipe, mix)
@ -83,7 +80,7 @@ class DefaultMixLogic(
} }
} }
if (entity.mixMaterials != null) { 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) return update(mix)
} }

View File

@ -1,112 +1,47 @@
package dev.fyloz.colorrecipesexplorer.logic 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.exception.RestException
import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.model.MixMaterial
import dev.fyloz.colorrecipesexplorer.repository.MixMaterialRepository import dev.fyloz.colorrecipesexplorer.service.MixMaterialService
import dev.fyloz.colorrecipesexplorer.utils.findDuplicated import dev.fyloz.colorrecipesexplorer.utils.PositionUtils
import dev.fyloz.colorrecipesexplorer.utils.hasGaps
import org.springframework.context.annotation.Lazy
import org.springframework.context.annotation.Profile
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
interface MixMaterialLogic : ModelService<MixMaterial, MixMaterialRepository> {
/** 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<MixMaterialDto>): Set<MixMaterial>
/** 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<MixMaterialDto, MixMaterialService> {
/** /**
* 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. * 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. * 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. * If any of those criteria are not met, an [InvalidGroupStepsPositionsException] will be thrown.
*/ */
fun validateMixMaterials(mixMaterials: Set<MixMaterial>) fun validateMixMaterials(mixMaterials: Set<MixMaterialDto>)
fun MixMaterial.toOutput(): MixMaterialOutputDto
} }
@Service @LogicComponent
@Profile("!emergency") class DefaultMixMaterialLogic(service: MixMaterialService) :
class DefaultMixMaterialLogic( BaseLogic<MixMaterialDto, MixMaterialService>(service, MixMaterial::class.simpleName!!), MixMaterialLogic {
mixMaterialRepository: MixMaterialRepository, override fun validateMixMaterials(mixMaterials: Set<MixMaterialDto>) {
@Lazy val materialLogic: MaterialLogic
) : AbstractModelService<MixMaterial, MixMaterialRepository>(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<MixMaterialDto>): Set<MixMaterial> =
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<MixMaterial>) {
if (mixMaterials.isEmpty()) return if (mixMaterials.isEmpty()) return
val sortedMixMaterials = mixMaterials.sortedBy { it.position } val sortedMixMaterials = mixMaterials.sortedBy { it.position }
val firstMixMaterial = sortedMixMaterials[0]
val errors = mutableSetOf<InvalidMixMaterialsPositionsError>()
// Check if the first mix material position is 1 try {
fun isFirstMixMaterialPositionInvalid() = PositionUtils.validate(sortedMixMaterials.map { it.position })
sortedMixMaterials[0].position != 1 } catch (ex: InvalidPositionsException) {
throw InvalidMixMaterialsPositionsException(ex.errors)
// 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)
} }
if (isFirstMixMaterialPercentages()) { if (sortedMixMaterials[0].material.materialType.usePercentages) {
throw InvalidFirstMixMaterial(firstMixMaterial) throw InvalidFirstMixMaterialException(sortedMixMaterials[0])
} }
} }
} }
class InvalidMixMaterialsPositionsError( // TODO check if required
val type: String,
val details: String
)
class InvalidMixMaterialsPositionsException( class InvalidMixMaterialsPositionsException(
val errors: Set<InvalidMixMaterialsPositionsError> val errors: Set<InvalidPositionError>
) : RestException( ) : RestException(
"invalid-mixmaterial-position", "invalid-mixmaterial-position",
"Invalid mix materials positions", "Invalid mix materials positions",
@ -117,8 +52,8 @@ class InvalidMixMaterialsPositionsException(
) )
) )
class InvalidFirstMixMaterial( class InvalidFirstMixMaterialException(
val mixMaterial: MixMaterial val mixMaterial: MixMaterialDto
) : RestException( ) : RestException(
"invalid-mixmaterial-first", "invalid-mixmaterial-first",
"Invalid first mix material", "Invalid first mix material",
@ -127,27 +62,4 @@ class InvalidFirstMixMaterial(
mapOf( mapOf(
"mixMaterial" to mixMaterial "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"
)

View File

@ -1,6 +1,7 @@
package dev.fyloz.colorrecipesexplorer.logic package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.MixTypeRepository import dev.fyloz.colorrecipesexplorer.repository.MixTypeRepository
import org.springframework.context.annotation.Lazy import org.springframework.context.annotation.Lazy
@ -56,7 +57,7 @@ class DefaultMixTypeLogic(
override fun save(entity: MixType): MixType { override fun save(entity: MixType): MixType {
if (materialLogic.existsByName(entity.name)) 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) return super.save(entity)
} }

View File

@ -2,26 +2,19 @@ package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto 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.exception.RestException
import dev.fyloz.colorrecipesexplorer.model.RecipeGroupInformation import dev.fyloz.colorrecipesexplorer.model.RecipeGroupInformation
import dev.fyloz.colorrecipesexplorer.model.RecipeStep import dev.fyloz.colorrecipesexplorer.model.RecipeStep
import dev.fyloz.colorrecipesexplorer.model.account.Group import dev.fyloz.colorrecipesexplorer.model.account.Group
import dev.fyloz.colorrecipesexplorer.model.recipeStepDto
import dev.fyloz.colorrecipesexplorer.service.RecipeStepService import dev.fyloz.colorrecipesexplorer.service.RecipeStepService
import dev.fyloz.colorrecipesexplorer.utils.findDuplicated import dev.fyloz.colorrecipesexplorer.utils.PositionUtils
import dev.fyloz.colorrecipesexplorer.utils.hasGaps
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
interface RecipeStepLogic : Logic<RecipeStepDto, RecipeStepService> { interface RecipeStepLogic : Logic<RecipeStepDto, RecipeStepService> {
/** 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) 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<RecipeStepDto>)
} }
@LogicComponent @LogicComponent
@ -31,91 +24,16 @@ class DefaultRecipeStepLogic(recipeStepService: RecipeStepService) :
if (groupInformation.steps == null) return if (groupInformation.steps == null) return
try { try {
validateSteps(groupInformation.steps!!.map { recipeStepDto(it) }.toSet()) PositionUtils.validate(groupInformation.steps!!.map { it.position }.toList())
} catch (validationException: InvalidStepsPositionsException) { } catch (ex: InvalidPositionsException) {
throw InvalidGroupStepsPositionsException(groupInformation.group, validationException) throw InvalidGroupStepsPositionsException(groupInformation.group, ex)
}
}
override fun validateSteps(steps: Set<RecipeStepDto>) {
if (steps.isEmpty()) return
val sortedSteps = steps.sortedBy { it.position }
val errors = mutableSetOf<InvalidStepsPositionsError>()
// 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<RecipeStepDto>,
errors: MutableSet<InvalidStepsPositionsError>
) {
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<RecipeStepDto>,
errors: MutableSet<InvalidStepsPositionsError>
) {
errors += steps
.findDuplicated { it.position }
.map {
InvalidStepsPositionsError(
RecipeStepDto.VALIDATION_ERROR_CODE_DUPLICATED_STEPS_POSITION,
"The position $it is duplicated"
)
}
}
private fun validateGapsInStepsPositions(
steps: List<RecipeStepDto>,
errors: MutableSet<InvalidStepsPositionsError>
) {
if (errors.isEmpty() && steps.hasGaps { it.position }) {
errors += InvalidStepsPositionsError(
RecipeStepDto.VALIDATION_ERROR_CODE_GAP_BETWEEN_STEPS_POSITIONS,
"There is a gap between steps positions"
)
} }
} }
} }
data class InvalidStepsPositionsError(
val type: String,
val details: String
)
class InvalidStepsPositionsException(
val errors: Set<InvalidStepsPositionsError>
) : RestException(
"invalid-recipestep-position",
"Invalid steps positions",
HttpStatus.BAD_REQUEST,
"The position of steps are invalid",
mapOf(
"invalidSteps" to errors
)
)
class InvalidGroupStepsPositionsException( class InvalidGroupStepsPositionsException(
val group: Group, val group: Group,
val exception: InvalidStepsPositionsException val exception: InvalidPositionsException
) : RestException( ) : RestException(
"invalid-groupinformation-recipestep-position", "invalid-groupinformation-recipestep-position",
"Invalid steps positions", "Invalid steps positions",
@ -127,6 +45,6 @@ class InvalidGroupStepsPositionsException(
"invalidSteps" to exception.errors "invalidSteps" to exception.errors
) )
) { ) {
val errors: Set<InvalidStepsPositionsError> val errors: Set<InvalidPositionError>
get() = exception.errors get() = exception.errors
} }

View File

@ -39,7 +39,7 @@ data class Material(
data class MaterialQuantityDto( data class MaterialQuantityDto(
val material: Long, 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 val quantity: Float
) )
@ -77,52 +77,4 @@ fun materialQuantityDto(
materialId: Long, materialId: Long,
quantity: Float, quantity: Float,
op: MaterialQuantityDto.() -> Unit = {} op: MaterialQuantityDto.() -> Unit = {}
) = MaterialQuantityDto(materialId, quantity).apply(op) ) = 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"
)

View File

@ -1,6 +1,8 @@
package dev.fyloz.colorrecipesexplorer.model package dev.fyloz.colorrecipesexplorer.model
import com.fasterxml.jackson.annotation.JsonIgnore 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.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
@ -58,13 +60,13 @@ data class MixOutputDto(
val id: Long, val id: Long,
val location: String?, val location: String?,
val mixType: MixType, val mixType: MixType,
val mixMaterials: Set<MixMaterialOutputDto> val mixMaterials: Set<MixMaterialDto>
) )
data class MixDeductDto( data class MixDeductDto(
val id: Long, 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 val ratio: Float
) )

View File

@ -1,9 +1,7 @@
package dev.fyloz.colorrecipesexplorer.model package dev.fyloz.colorrecipesexplorer.model
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import javax.persistence.* import javax.persistence.*
import javax.validation.constraints.Min
@Entity @Entity
@Table(name = "mix_material") @Table(name = "mix_material")
@ -21,22 +19,6 @@ data class MixMaterial(
var position: Int var position: Int
) : ModelEntity ) : 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 ==== // ==== DSL ====
fun mixMaterial( fun mixMaterial(
id: Long? = null, id: Long? = null,
@ -46,30 +28,12 @@ fun mixMaterial(
op: MixMaterial.() -> Unit = {} op: MixMaterial.() -> Unit = {}
) = MixMaterial(id, material, quantity, position).apply(op) ) = MixMaterial(id, material, quantity, position).apply(op)
@Deprecated("Temporary DSL for transition")
fun mixMaterialDto( fun mixMaterialDto(
materialId: Long = 0L, entity: MixMaterial
quantity: Float = 0f, ) = MixMaterialDto(entity.id!!, materialDto(entity.material), entity.quantity, entity.position)
position: Int = 0,
op: MixMaterialDto.() -> Unit = {}
) = MixMaterialDto(materialId, quantity, position).apply(op)
// ==== Exceptions ==== @Deprecated("Temporary DSL for transition")
private const val MIX_MATERIAL_NOT_FOUND_EXCEPTION_TITLE = "Mix material not found" fun mixMaterial(
private const val MIX_MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE = "Mix material already exists" dto: MixMaterialDto
private const val MIX_MATERIAL_EXCEPTION_ERROR_CODE = "mixmaterial" ) = MixMaterial(dto.id, material(dto.material), dto.quantity, dto.position)
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
)

View File

@ -14,9 +14,4 @@ interface EntityDto<out E> {
fun toEntity(): E { fun toEntity(): E {
throw UnsupportedOperationException() 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"

View File

@ -89,11 +89,11 @@ open class RecipeSaveDto(
@field:Pattern(regexp = VALIDATION_COLOR_PATTERN) @field:Pattern(regexp = VALIDATION_COLOR_PATTERN)
val color: String, val color: String,
@field:Min(0, message = VALIDATION_RANGE_PERCENTS) @field:Min(0, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS)
@field:Max(100, message = VALIDATION_RANGE_PERCENTS) @field:Max(100, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS)
val gloss: Byte, 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 sample: Int?,
val approbationDate: LocalDate?, val approbationDate: LocalDate?,
@ -125,11 +125,11 @@ open class RecipeUpdateDto(
@field:Pattern(regexp = VALIDATION_COLOR_PATTERN) @field:Pattern(regexp = VALIDATION_COLOR_PATTERN)
val color: String?, val color: String?,
@field:Min(0, message = VALIDATION_RANGE_PERCENTS) @field:Min(0, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS)
@field:Max(100, message = VALIDATION_RANGE_PERCENTS) @field:Max(100, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS)
val gloss: Byte?, 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 sample: Int?,
val approbationDate: LocalDate?, val approbationDate: LocalDate?,

View File

@ -1,10 +1,10 @@
package dev.fyloz.colorrecipesexplorer.model.touchupkit package dev.fyloz.colorrecipesexplorer.model.touchupkit
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.EntityDto import dev.fyloz.colorrecipesexplorer.model.EntityDto
import dev.fyloz.colorrecipesexplorer.model.ModelEntity import dev.fyloz.colorrecipesexplorer.model.ModelEntity
import dev.fyloz.colorrecipesexplorer.model.VALIDATION_SIZE_GE_ONE
import java.time.LocalDate import java.time.LocalDate
import javax.persistence.* import javax.persistence.*
import javax.validation.constraints.Min import javax.validation.constraints.Min
@ -80,7 +80,7 @@ data class TouchUpKitSaveDto(
@field:NotBlank @field:NotBlank
val company: String, 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 quantity: Int,
val shippingDate: LocalDate, val shippingDate: LocalDate,
@ -109,7 +109,7 @@ data class TouchUpKitUpdateDto(
@field:NotBlank @field:NotBlank
val company: String?, 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 quantity: Int?,
val shippingDate: LocalDate?, val shippingDate: LocalDate?,

View File

@ -1,12 +1,11 @@
package dev.fyloz.colorrecipesexplorer.repository package dev.fyloz.colorrecipesexplorer.repository
import dev.fyloz.colorrecipesexplorer.model.Material
import dev.fyloz.colorrecipesexplorer.model.MixMaterial import dev.fyloz.colorrecipesexplorer.model.MixMaterial
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
@Repository @Repository
interface MixMaterialRepository : JpaRepository<MixMaterial, Long> { interface MixMaterialRepository : JpaRepository<MixMaterial, Long> {
/** Checks if one or more mix materials have the given [material]. */ /** Checks if one or more mix materials have the given [materialId]. */
fun existsByMaterial(material: Material): Boolean fun existsByMaterialId(materialId: Long): Boolean
} }

View File

@ -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<MixMaterialDto, MixMaterial, MixMaterialRepository> {
/** 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<MixMaterialDto, MixMaterial, MixMaterialRepository>(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)
}

View File

@ -19,13 +19,19 @@ inline fun <T, R, reified E : Throwable> Iterable<T>.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 <T, K> Iterable<T>.findDuplicated(keySelector: (T) -> K) = inline fun <T, K> Iterable<T>.findDuplicated(keySelector: (T) -> K) =
this.groupBy(keySelector) this.groupBy(keySelector)
.filter { it.value.count() > 1 } .filter { it.value.count() > 1 }
.map { it.key } .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 <T> Iterable<T>.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 <T> Iterable<T>.hasGaps(keySelector: (T) -> Int) = inline fun <T> Iterable<T>.hasGaps(keySelector: (T) -> Int) =
this.map(keySelector) this.map(keySelector)
.toIntArray() .toIntArray()
@ -33,6 +39,12 @@ inline fun <T> Iterable<T>.hasGaps(keySelector: (T) -> Int) =
.filterIndexed { index, it -> it != index + 1 } .filterIndexed { index, it -> it != index + 1 }
.isNotEmpty() .isNotEmpty()
/** Check if the given [Int] [Iterable] has gaps between each element. */
fun Iterable<Int>.hasGaps() =
this.sorted()
.filterIndexed { index, it -> it != index + 1 }
.isNotEmpty()
/** Clears and fills the given [MutableCollection] with the given [elements]. */ /** Clears and fills the given [MutableCollection] with the given [elements]. */
fun <T> MutableCollection<T>.setAll(elements: Collection<T>) { fun <T> MutableCollection<T>.setAll(elements: Collection<T>) {
this.clear() this.clear()

View File

@ -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<Int>) {
if (positions.isEmpty()) {
return
}
val sortedPositions = positions.sorted()
val errors = mutableSetOf<InvalidPositionError>()
validateFirstPosition(sortedPositions[0], errors)
validateDuplicatedPositions(sortedPositions, errors)
validateGapsInPositions(sortedPositions, errors)
if (errors.isNotEmpty()) {
throw InvalidPositionsException(errors)
}
}
private fun validateFirstPosition(position: Int, errors: MutableSet<InvalidPositionError>) {
if (position == FIRST_POSITION) {
return
}
errors += InvalidPositionError(INVALID_FIRST_POSITION_ERROR_CODE, "The first position must be $FIRST_POSITION")
}
private fun validateDuplicatedPositions(positions: List<Int>, errors: MutableSet<InvalidPositionError>) {
errors += positions.findDuplicated()
.map { InvalidPositionError(DUPLICATED_POSITION_ERROR_CODE, "The position $it is duplicated") }
}
private fun validateGapsInPositions(positions: List<Int>, errors: MutableSet<InvalidPositionError>) {
if (!positions.hasGaps()) {
return
}
errors += InvalidPositionError(GAP_BETWEEN_POSITIONS_ERROR_CODE, "There is a gap between the positions")
}
}

View File

@ -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<MixMaterialService>()
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<InvalidFirstMixMaterialException> { 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<InvalidMixMaterialsPositionsException> { mixMaterialLogic.validateMixMaterials(setOf(mixMaterial)) }
}
}

View File

@ -1,21 +1,21 @@
package dev.fyloz.colorrecipesexplorer.logic 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.RecipeGroupInformation
import dev.fyloz.colorrecipesexplorer.model.RecipeStep import dev.fyloz.colorrecipesexplorer.model.RecipeStep
import dev.fyloz.colorrecipesexplorer.model.account.Group import dev.fyloz.colorrecipesexplorer.model.account.Group
import dev.fyloz.colorrecipesexplorer.service.RecipeStepService import dev.fyloz.colorrecipesexplorer.service.RecipeStepService
import dev.fyloz.colorrecipesexplorer.utils.PositionUtils
import io.mockk.* import io.mockk.*
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
import kotlin.test.assertTrue
class DefaultRecipeStepLogicTest { class DefaultRecipeStepLogicTest {
private val recipeStepServiceMock = mockk<RecipeStepService>() private val recipeStepServiceMock = mockk<RecipeStepService>()
private val recipeStepLogic = spyk(DefaultRecipeStepLogic(recipeStepServiceMock)) private val recipeStepLogic = DefaultRecipeStepLogic(recipeStepServiceMock)
@AfterEach @AfterEach
internal fun afterEach() { internal fun afterEach() {
@ -25,7 +25,8 @@ class DefaultRecipeStepLogicTest {
@Test @Test
fun validateGroupInformationSteps_normalBehavior_callsValidateSteps() { fun validateGroupInformationSteps_normalBehavior_callsValidateSteps() {
// Arrange // Arrange
every { recipeStepLogic.validateSteps(any()) } just runs mockkObject(PositionUtils)
every { PositionUtils.validate(any()) } just runs
val group = Group(1L, "Unit test group") val group = Group(1L, "Unit test group")
val steps = mutableSetOf(RecipeStep(1L, 1, "A message")) val steps = mutableSetOf(RecipeStep(1L, 1, "A message"))
@ -36,14 +37,15 @@ class DefaultRecipeStepLogicTest {
// Assert // Assert
verify { verify {
recipeStepLogic.validateSteps(any()) // TODO replace with actual steps dtos when RecipeGroupInformation updated PositionUtils.validate(steps.map { it.position })
} }
} }
@Test @Test
fun validateGroupInformationSteps_stepSetIsNull_doesNothing() { fun validateGroupInformationSteps_stepSetIsNull_doesNothing() {
// Arrange // Arrange
every { recipeStepLogic.validateSteps(any()) } just runs mockkObject(PositionUtils)
every { PositionUtils.validate(any()) } just runs
val group = Group(1L, "Unit test group") val group = Group(1L, "Unit test group")
val groupInfo = RecipeGroupInformation(1L, group, "A note", null) val groupInfo = RecipeGroupInformation(1L, group, "A note", null)
@ -53,15 +55,17 @@ class DefaultRecipeStepLogicTest {
// Assert // Assert
verify(exactly = 0) { verify(exactly = 0) {
recipeStepLogic.validateSteps(any()) // TODO replace with actual steps dtos when RecipeGroupInformation updated PositionUtils.validate(any())
} }
} }
@Test @Test
fun validateGroupInformationSteps_invalidSteps_throwsInvalidGroupStepsPositionsException() { fun validateGroupInformationSteps_invalidSteps_throwsInvalidGroupStepsPositionsException() {
// Arrange // Arrange
val errors = setOf(InvalidStepsPositionsError("error", "An unit test error")) val errors = setOf(InvalidPositionError("error", "An unit test error"))
every { recipeStepLogic.validateSteps(any()) } throws InvalidStepsPositionsException(errors)
mockkObject(PositionUtils)
every { PositionUtils.validate(any()) } throws InvalidPositionsException(errors)
val group = Group(1L, "Unit test group") val group = Group(1L, "Unit test group")
val steps = mutableSetOf(RecipeStep(1L, 1, "A message")) val steps = mutableSetOf(RecipeStep(1L, 1, "A message"))
@ -71,187 +75,4 @@ class DefaultRecipeStepLogicTest {
// Assert // Assert
assertThrows<InvalidGroupStepsPositionsException> { recipeStepLogic.validateGroupInformationSteps(groupInfo) } assertThrows<InvalidGroupStepsPositionsException> { 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<RecipeStepDto>()
// 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<InvalidStepsPositionsException> { 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<InvalidStepsPositionsException> { 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<InvalidStepsPositionsException> { 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<InvalidStepsPositionsException> { 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<RecipeStep, RecipeStepLogic, RecipeStepRepository>() {
// 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<InvalidGroupStepsPositionsException> {
// 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<RecipeStep>? = 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<RecipeStep>, errorType: String) {
// val exception = assertThrows<InvalidStepsPositionsException> {
// logic.validateSteps(steps)
// }
//
// assertTrue { exception.errors.size == 1 }
// assertTrue { exception.errors.first().type == errorType }
// }
//}

View File

@ -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<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialTypeLogic, MaterialTypeRepository>() {
// 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<AlreadyExistsException> { 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<NotFoundException> { 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<AlreadyExistsException> { 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<AlreadyExistsException> { logic.update(entity) }
// .assertErrorCode("prefix")
// }
//
// @Test
// fun `update() throws CannotUpdateException when updating a system material type`() {
// whenever(repository.existsByIdAndSystemTypeIsTrue(systemType.id!!)).doReturn(true)
//
// assertThrows<CannotUpdateException> { logic.update(systemType) }
// }
//
// // delete()
//
// @Test
// fun `delete() throws CannotDeleteException when deleting a system material type`() {
// whenever(repository.existsByIdAndSystemTypeIsTrue(systemType.id!!)).doReturn(true)
//
// assertThrows<CannotDeleteException> { 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()
// }
//}

View File

@ -1,255 +1,245 @@
package dev.fyloz.colorrecipesexplorer.logic package dev.fyloz.colorrecipesexplorer.logic
import com.nhaarman.mockitokotlin2.* //@TestInstance(TestInstance.Lifecycle.PER_CLASS)
import dev.fyloz.colorrecipesexplorer.model.* //class MixLogicTest : AbstractExternalModelServiceTest<Mix, MixSaveDto, MixUpdateDto, MixLogic, MixRepository>() {
import dev.fyloz.colorrecipesexplorer.repository.MixRepository // override val repository: MixRepository = mock()
import org.junit.jupiter.api.AfterEach // private val recipeService: RecipeLogic = mock()
import org.junit.jupiter.api.Test // private val materialTypeService: MaterialTypeLogic = mock()
import org.junit.jupiter.api.TestInstance // private val mixMaterialService: MixMaterialLogic = mock()
import kotlin.test.assertEquals // private val mixTypeService: MixTypeLogic = mock()
import kotlin.test.assertTrue // override val logic: MixLogic =
// spy(DefaultMixLogic(repository, recipeService, materialTypeService, mixMaterialService, mixTypeService))
@TestInstance(TestInstance.Lifecycle.PER_CLASS) //
class MixLogicTest : AbstractExternalModelServiceTest<Mix, MixSaveDto, MixUpdateDto, MixLogic, MixRepository>() { // override val entity: Mix = mix(id = 0L, location = "location")
override val repository: MixRepository = mock() // override val anotherEntity: Mix = mix(id = 1L)
private val recipeService: RecipeLogic = mock() // override val entitySaveDto: MixSaveDto = spy(mixSaveDto(mixMaterials = setOf<MixMaterialDto>()))
private val materialTypeService: MaterialTypeLogic = mock() // override val entityUpdateDto: MixUpdateDto = spy(mixUpdateDto(id = entity.id!!))
private val mixMaterialService: MixMaterialLogic = mock() //
private val mixTypeService: MixTypeLogic = mock() // @AfterEach
override val logic: MixLogic = // override fun afterEach() {
spy(DefaultMixLogic(repository, recipeService, materialTypeService, mixMaterialService, mixTypeService)) // super.afterEach()
// reset(recipeService, materialTypeService, mixMaterialService, mixTypeService)
override val entity: Mix = mix(id = 0L, location = "location") // }
override val anotherEntity: Mix = mix(id = 1L) //
override val entitySaveDto: MixSaveDto = // // getAllByMixType()
spy(mixSaveDto(mixMaterials = setOf(mixMaterialDto(materialId = 1L, quantity = 1000f, position = 0)))) //
override val entityUpdateDto: MixUpdateDto = spy(mixUpdateDto(id = entity.id!!)) //// @Test
//// fun `getAllByMixType() returns all mixes with the given mix type`() {
@AfterEach //// val mixType = mixType(id = 0L)
override fun afterEach() { ////
super.afterEach() //// whenever(repository.findAllByMixType(mixType)).doReturn(entityList)
reset(recipeService, materialTypeService, mixMaterialService, mixTypeService) ////
} //// val found = logic.getAllByMixType(mixType)
////
// getAllByMixType() //// assertEquals(entityList, found)
//// }
@Test ////
fun `getAllByMixType() returns all mixes with the given mix type`() { //// // save()
val mixType = mixType(id = 0L) ////
//// @Test
whenever(repository.findAllByMixType(mixType)).doReturn(entityList) //// override fun `save(dto) calls and returns save() with the created entity`() {
//// val recipe = recipe(id = entitySaveDto.recipeId)
val found = logic.getAllByMixType(mixType) //// val materialType = materialType(id = entitySaveDto.materialTypeId)
//// val material = material(
assertEquals(entityList, found) //// name = entitySaveDto.name,
} //// inventoryQuantity = Float.MIN_VALUE,
//// isMixType = true,
// save() //// materialType = materialType
//// )
@Test //// val mixType = mixType(name = entitySaveDto.name, material = material)
override fun `save(dto) calls and returns save() with the created entity`() { //// val mix = mix(recipe = recipe, mixType = mixType)
val recipe = recipe(id = entitySaveDto.recipeId) //// val mixWithId = mix(id = 0L, recipe = recipe, mixType = mixType)
val materialType = materialType(id = entitySaveDto.materialTypeId) //// val mixMaterials = setOf(mixMaterial(material = material(id = 1L), quantity = 1000f))
val material = material( ////
name = entitySaveDto.name, //// whenever(recipeService.getById(recipe.id!!)).doReturn(recipe)
inventoryQuantity = Float.MIN_VALUE, //// whenever(materialTypeService.getById(materialType.id!!)).doReturn(materialTypeDto(materialType))
isMixType = true, //// whenever(mixMaterialService.create(entitySaveDto.mixMaterials!!)).doReturn(mixMaterials)
materialType = materialType //// whenever(
) //// mixTypeService.getOrCreateForNameAndMaterialType(
val mixType = mixType(name = entitySaveDto.name, material = material) //// mixType.name,
val mix = mix(recipe = recipe, mixType = mixType) //// mixType.material.materialType!!
val mixWithId = mix(id = 0L, recipe = recipe, mixType = mixType) //// )
val mixMaterials = setOf(mixMaterial(material = material(id = 1L), quantity = 1000f)) //// ).doReturn(mixType)
//// doReturn(true).whenever(logic).existsById(mixWithId.id!!)
whenever(recipeService.getById(recipe.id!!)).doReturn(recipe) //// doReturn(mixWithId).whenever(logic).save(any<Mix>())
whenever(materialTypeService.getById(materialType.id!!)).doReturn(materialTypeDto(materialType)) ////
whenever(mixMaterialService.create(entitySaveDto.mixMaterials!!)).doReturn(mixMaterials) //// val found = logic.save(entitySaveDto)
whenever( ////
mixTypeService.getOrCreateForNameAndMaterialType( //// verify(logic).save(argThat<Mix> { this.recipe == mix.recipe })
mixType.name, //// verify(recipeService).addMix(recipe, mixWithId)
mixType.material.materialType!! ////
) //// // 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.
).doReturn(mixType) //// verify(mixTypeService).getOrCreateForNameAndMaterialType(mixType.name, mixType.material.materialType!!)
doReturn(true).whenever(logic).existsById(mixWithId.id!!) ////
doReturn(mixWithId).whenever(logic).save(any<Mix>()) //// assertEquals(mixWithId, found)
//// }
val found = logic.save(entitySaveDto) ////
//// // update()
verify(logic).save(argThat<Mix> { this.recipe == mix.recipe }) ////
verify(recipeService).addMix(recipe, mixWithId) //// private fun mixUpdateDtoTest(
//// scope: MixUpdateDtoTestScope = MixUpdateDtoTestScope(),
// 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. //// sharedMixType: Boolean = false,
verify(mixTypeService).getOrCreateForNameAndMaterialType(mixType.name, mixType.material.materialType!!) //// op: MixUpdateDtoTestScope.() -> Unit
//// ) {
assertEquals(mixWithId, found) //// with(scope) {
} //// doReturn(true).whenever(logic).existsById(mix.id!!)
//// doReturn(mix).whenever(logic).getById(mix.id!!)
// update() //// doReturn(sharedMixType).whenever(logic).mixTypeIsShared(mix.mixType)
//// doAnswer { it.arguments[0] }.whenever(logic).update(any<Mix>())
private fun mixUpdateDtoTest( ////
scope: MixUpdateDtoTestScope = MixUpdateDtoTestScope(), //// if (mixUpdateDto.materialTypeId != null) {
sharedMixType: Boolean = false, //// whenever(materialTypeService.getById(materialType.id!!)).doReturn(materialTypeDto(materialType))
op: MixUpdateDtoTestScope.() -> Unit //// }
) { ////
with(scope) { //// op()
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<Mix>()) //// private fun mixUpdateDtoMixTypeTest(sharedMixType: Boolean = false, op: MixUpdateDtoTestScope.() -> Unit) {
//// with(MixUpdateDtoTestScope(mixUpdateDto = mixUpdateDto(id = 0L, name = "name", materialTypeId = 0L))) {
if (mixUpdateDto.materialTypeId != null) { //// mixUpdateDtoTest(this, sharedMixType, op)
whenever(materialTypeService.getById(materialType.id!!)).doReturn(materialTypeDto(materialType)) //// }
} //// }
////
op() //// @Test
} //// override fun `update(dto) calls and returns update() with the created entity`() {
} //// val mixUpdateDto = spy(mixUpdateDto(id = 0L, name = null, materialTypeId = null))
////
private fun mixUpdateDtoMixTypeTest(sharedMixType: Boolean = false, op: MixUpdateDtoTestScope.() -> Unit) { //// doReturn(entity).whenever(logic).getById(any())
with(MixUpdateDtoTestScope(mixUpdateDto = mixUpdateDto(id = 0L, name = "name", materialTypeId = 0L))) { //// doReturn(entity).whenever(logic).update(entity)
mixUpdateDtoTest(this, sharedMixType, op) ////
} //// val found = logic.update(mixUpdateDto)
} ////
//// verify(logic).update(entity)
@Test ////
override fun `update(dto) calls and returns update() with the created entity`() { //// assertEquals(entity, found)
val mixUpdateDto = spy(mixUpdateDto(id = 0L, name = null, materialTypeId = null)) //// }
////
doReturn(entity).whenever(logic).getById(any()) //// @Test
doReturn(entity).whenever(logic).update(entity) //// fun `update(dto) calls MixTypeService saveForNameAndMaterialType() when mix type is shared`() {
//// mixUpdateDtoMixTypeTest(sharedMixType = true) {
val found = logic.update(mixUpdateDto) //// whenever(mixTypeService.saveForNameAndMaterialType(mixUpdateDto.name!!, materialType))
//// .doReturn(newMixType)
verify(logic).update(entity) ////
//// val found = logic.update(mixUpdateDto)
assertEquals(entity, found) ////
} //// verify(mixTypeService).saveForNameAndMaterialType(mixUpdateDto.name!!, materialType)
////
@Test //// assertEquals(newMixType, found.mixType)
fun `update(dto) calls MixTypeService saveForNameAndMaterialType() when mix type is shared`() { //// }
mixUpdateDtoMixTypeTest(sharedMixType = true) { //// }
whenever(mixTypeService.saveForNameAndMaterialType(mixUpdateDto.name!!, materialType)) ////
.doReturn(newMixType) //// @Test
//// fun `update(dto) calls MixTypeService updateForNameAndMaterialType() when mix type is not shared`() {
val found = logic.update(mixUpdateDto) //// mixUpdateDtoMixTypeTest {
//// whenever(mixTypeService.updateForNameAndMaterialType(mixType, mixUpdateDto.name!!, materialType))
verify(mixTypeService).saveForNameAndMaterialType(mixUpdateDto.name!!, materialType) //// .doReturn(newMixType)
////
assertEquals(newMixType, found.mixType) //// val found = logic.update(mixUpdateDto)
} ////
} //// verify(mixTypeService).updateForNameAndMaterialType(mixType, mixUpdateDto.name!!, materialType)
////
@Test //// assertEquals(newMixType, found.mixType)
fun `update(dto) calls MixTypeService updateForNameAndMaterialType() when mix type is not shared`() { //// }
mixUpdateDtoMixTypeTest { //// }
whenever(mixTypeService.updateForNameAndMaterialType(mixType, mixUpdateDto.name!!, materialType)) ////
.doReturn(newMixType) //// @Test
//// fun `update(dto) update, create and delete mix materials according to the given mix materials map`() {
val found = logic.update(mixUpdateDto) //// mixUpdateDtoTest {
//// val mixMaterials = setOf(
verify(mixTypeService).updateForNameAndMaterialType(mixType, mixUpdateDto.name!!, materialType) //// mixMaterialDto(materialId = 0L, quantity = 100f, position = 0),
//// mixMaterialDto(materialId = 1L, quantity = 200f, position = 1),
assertEquals(newMixType, found.mixType) //// mixMaterialDto(materialId = 2L, quantity = 300f, position = 2),
} //// mixMaterialDto(materialId = 3L, quantity = 400f, position = 3),
} //// )
//// mixUpdateDto.mixMaterials = mixMaterials
@Test ////
fun `update(dto) update, create and delete mix materials according to the given mix materials map`() { //// whenever(mixMaterialService.create(any<Set<MixMaterialDto>>())).doAnswer {
mixUpdateDtoTest { //// (it.arguments[0] as Set<MixMaterialDto>).map { dto ->
val mixMaterials = setOf( //// mixMaterial(
mixMaterialDto(materialId = 0L, quantity = 100f, position = 0), //// material = material(id = dto.materialId),
mixMaterialDto(materialId = 1L, quantity = 200f, position = 1), //// quantity = dto.quantity,
mixMaterialDto(materialId = 2L, quantity = 300f, position = 2), //// position = dto.position
mixMaterialDto(materialId = 3L, quantity = 400f, position = 3), //// )
) //// }.toSet()
mixUpdateDto.mixMaterials = mixMaterials //// }
////
whenever(mixMaterialService.create(any<Set<MixMaterialDto>>())).doAnswer { //// val found = logic.update(mixUpdateDto)
(it.arguments[0] as Set<MixMaterialDto>).map { dto -> ////
mixMaterial( //// mixMaterials.forEach {
material = material(id = dto.materialId), //// assertTrue {
quantity = dto.quantity, //// found.mixMaterials.any { mixMaterial ->
position = dto.position //// mixMaterial.material.id == it.materialId && mixMaterial.quantity == it.quantity && mixMaterial.position == it.position
) //// }
}.toSet() //// }
} //// }
//// }
val found = logic.update(mixUpdateDto) //// }
////
mixMaterials.forEach { //// // updateLocations()
assertTrue { ////
found.mixMaterials.any { mixMaterial -> //// @Test
mixMaterial.material.id == it.materialId && mixMaterial.quantity == it.quantity && mixMaterial.position == it.position //// 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")
//// )
// updateLocations() ////
//// logic.updateLocations(locations)
@Test ////
fun `updateLocations() calls updateLocation() for each given MixLocationDto`() { //// locations.forEach {
val locations = setOf( //// verify(logic).updateLocation(it)
mixLocationDto(mixId = 0, location = "Loc 0"), //// }
mixLocationDto(mixId = 1, location = "Loc 1"), //// }
mixLocationDto(mixId = 2, location = "Loc 2"), ////
mixLocationDto(mixId = 3, location = "Loc 3") //// // updateLocation()
) ////
//// @Test
logic.updateLocations(locations) //// fun `updateLocation() updates the location of a mix in the repository according to the given MixLocationDto`() {
//// val locationDto = mixLocationDto(mixId = 0L, location = "Location")
locations.forEach { ////
verify(logic).updateLocation(it) //// logic.updateLocation(locationDto)
} ////
} //// verify(repository).updateLocationById(locationDto.mixId, locationDto.location)
//// }
// updateLocation() ////
//// // delete()
@Test ////
fun `updateLocation() updates the location of a mix in the repository according to the given MixLocationDto`() { //// override fun `delete() deletes in the repository`() {
val locationDto = mixLocationDto(mixId = 0L, location = "Location") //// whenCanBeDeleted {
//// super.`delete() deletes in the repository`()
logic.updateLocation(locationDto) //// }
//// }
verify(repository).updateLocationById(locationDto.mixId, locationDto.location) ////
} //// // deleteById()
////
// delete() //// @Test
//// override fun `deleteById() deletes the entity with the given id in the repository`() {
override fun `delete() deletes in the repository`() { //// whenCanBeDeleted {
whenCanBeDeleted { //// super.`deleteById() deletes the entity with the given id in the repository`()
super.`delete() deletes in the repository`() //// }
} //// }
} ////
//// private fun whenCanBeDeleted(id: Long = any(), test: () -> Unit) {
// deleteById() //// whenever(repository.canBeDeleted(id)).doReturn(true)
////
@Test //// 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`() //
} //data class MixUpdateDtoTestScope(
} // val mixType: MixType = mixType(name = "mix type"),
// val newMixType: MixType = mixType(name = "another mix type"),
private fun whenCanBeDeleted(id: Long = any(), test: () -> Unit) { // val materialType: MaterialType = materialType(id = 0L),
whenever(repository.canBeDeleted(id)).doReturn(true) // val mix: Mix = mix(id = 0L, mixType = mixType),
// val mixUpdateDto: MixUpdateDto = spy(
test() // mixUpdateDto(
} // id = 0L,
} // name = null,
// materialTypeId = null,
data class MixUpdateDtoTestScope( // mixMaterials = setOf()
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()
)
)
)

View File

@ -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<MixMaterial, MixMaterialLogic, MixMaterialRepository>() {
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<MixMaterialDto>())
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<InvalidFirstMixMaterial> {
logic.validateMixMaterials(mixMaterials)
}
}
private fun assertInvalidMixMaterialsPositionsException(mixMaterials: Set<MixMaterial>, errorType: String) {
val exception = assertThrows<InvalidMixMaterialsPositionsException> {
logic.validateMixMaterials(mixMaterials)
}
assertTrue { exception.errors.size == 1 }
assertTrue { exception.errors.first().type == errorType }
}
}

View File

@ -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<Int>()
// Act
// Assert
assertDoesNotThrow { PositionUtils.validate(positions) }
}
@Test
fun validateSteps_hasInvalidPositions_throwsInvalidStepsPositionsException() {
// Arrange
val positions = listOf(2, 3)
// Act
// Assert
assertThrows<InvalidPositionsException> { PositionUtils.validate(positions) }
}
@Test
fun validateSteps_firstStepPositionInvalid_returnsInvalidStepValidationError() {
// Arrange
val positions = listOf(2, 3)
// Act
val exception = assertThrows<InvalidPositionsException> { 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<InvalidPositionsException> { 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<InvalidPositionsException> { PositionUtils.validate(positions) }
// Assert
assertTrue {
exception.errors.any { it.type == PositionUtils.GAP_BETWEEN_POSITIONS_ERROR_CODE }
}
}
}