#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"
}
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
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,

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

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]. */
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. */
fun update(dto: D): D
@ -50,6 +53,9 @@ abstract class BaseLogic<D : EntityDto, S : Service<D, *, *>>(
override fun save(dto: D) =
service.save(dto)
override fun saveAll(dtos: Collection<D>) =
dtos.map(::save)
override fun update(dto: D): D {
if (!existsById(dto.id)) {
throw notFoundException(value = dto.id)

View File

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

View File

@ -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<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.
* 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<MixMaterial>)
fun MixMaterial.toOutput(): MixMaterialOutputDto
fun validateMixMaterials(mixMaterials: Set<MixMaterialDto>)
}
@Service
@Profile("!emergency")
class DefaultMixMaterialLogic(
mixMaterialRepository: MixMaterialRepository,
@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>) {
@LogicComponent
class DefaultMixMaterialLogic(service: MixMaterialService) :
BaseLogic<MixMaterialDto, MixMaterialService>(service, MixMaterial::class.simpleName!!), MixMaterialLogic {
override fun validateMixMaterials(mixMaterials: Set<MixMaterialDto>) {
if (mixMaterials.isEmpty()) return
val sortedMixMaterials = mixMaterials.sortedBy { it.position }
val firstMixMaterial = sortedMixMaterials[0]
val errors = mutableSetOf<InvalidMixMaterialsPositionsError>()
// 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<InvalidMixMaterialsPositionsError>
val errors: Set<InvalidPositionError>
) : 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"
)
)

View File

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

View File

@ -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<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)
/**
* 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
@ -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<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"
)
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<InvalidStepsPositionsError>
) : 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<InvalidStepsPositionsError>
val errors: Set<InvalidPositionError>
get() = exception.errors
}

View File

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

View File

@ -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<MixMaterialOutputDto>
val mixMaterials: Set<MixMaterialDto>
)
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
)

View File

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

View File

@ -14,9 +14,4 @@ interface EntityDto<out E> {
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"
}

View File

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

View File

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

View File

@ -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<MixMaterial, Long> {
/** 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
}

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) =
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 <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) =
this.map(keySelector)
.toIntArray()
@ -33,6 +39,12 @@ inline fun <T> Iterable<T>.hasGaps(keySelector: (T) -> Int) =
.filterIndexed { index, it -> it != index + 1 }
.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]. */
fun <T> MutableCollection<T>.setAll(elements: Collection<T>) {
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
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<RecipeStepService>()
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<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
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<Mix, MixSaveDto, MixUpdateDto, MixLogic, MixRepository>() {
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<Mix>())
val found = logic.save(entitySaveDto)
verify(logic).save(argThat<Mix> { 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<Mix>())
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<Set<MixMaterialDto>>())).doAnswer {
(it.arguments[0] as Set<MixMaterialDto>).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<Mix, MixSaveDto, MixUpdateDto, MixLogic, MixRepository>() {
// 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>()))
// 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<Mix>())
////
//// val found = logic.save(entitySaveDto)
////
//// verify(logic).save(argThat<Mix> { 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<Mix>())
////
//// 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<Set<MixMaterialDto>>())).doAnswer {
//// (it.arguments[0] as Set<MixMaterialDto>).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()
// )
// )
//)

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