develop #29

Merged
william merged 33 commits from develop into master 2022-04-20 22:55:05 -04:00
30 changed files with 861 additions and 841 deletions
Showing only changes of commit efac09a76b - Show all commits

View File

@ -5,6 +5,7 @@ object Constants {
const val FILE = "/api/file"
const val MATERIAL = "/api/material"
const val MATERIAL_TYPE = "/api/materialtype"
const val MIX = "/api/recipe/mix"
}
object FilePaths {

View File

@ -1,9 +1,9 @@
package dev.fyloz.colorrecipesexplorer.config.initializers
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
import dev.fyloz.colorrecipesexplorer.dtos.MixDto
import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto
import dev.fyloz.colorrecipesexplorer.logic.MixLogic
import dev.fyloz.colorrecipesexplorer.model.Mix
import dev.fyloz.colorrecipesexplorer.model.MixMaterial
import dev.fyloz.colorrecipesexplorer.utils.merge
import mu.KotlinLogging
import org.springframework.context.annotation.Configuration
@ -31,12 +31,12 @@ class MixInitializer(
logger.debug("Mix materials positions are valid!")
}
private fun fixMixPositions(mix: Mix) {
private fun fixMixPositions(mix: MixDto) {
val maxPosition = mix.mixMaterials.maxOf { it.position }
logger.warn("Mix ${mix.id} (${mix.mixType.name}, ${mix.recipe.name}) has invalid positions:")
val invalidMixMaterials: Collection<MixMaterial> = with(mix.mixMaterials.filter { it.position == 0 }) {
val invalidMixMaterials: Collection<MixMaterialDto> = with(mix.mixMaterials.filter { it.position == 0 }) {
if (maxPosition == 0 && this.size > 1) {
orderMixMaterials(this)
} else {
@ -52,16 +52,16 @@ class MixInitializer(
}
}
private fun increaseMixMaterialsPosition(mixMaterials: Iterable<MixMaterial>, firstPosition: Int) =
private fun increaseMixMaterialsPosition(mixMaterials: Iterable<MixMaterialDto>, firstPosition: Int) =
mixMaterials
.mapIndexed { index, mixMaterial -> mixMaterial.copy(position = firstPosition + index) }
.onEach {
logger.info("\tPosition of material ${it.material.id} (${it.material.name}) has been set to ${it.position}")
}
private fun orderMixMaterials(mixMaterials: Collection<MixMaterial>) =
private fun orderMixMaterials(mixMaterials: Collection<MixMaterialDto>) =
LinkedList(mixMaterials).apply {
while (this.peek().material.materialType?.usePercentages == true) {
while (this.peek().material.materialType.usePercentages) {
// The first mix material can't use percents, so move it to the end of the queue
val pop = this.pop()
this.add(pop)

View File

@ -36,7 +36,7 @@ class RecipeInitializer(
.filter { groupInfo -> groupInfo.steps!!.any { it.position == 0 } }
.map { fixGroupInformationPositions(recipe, it) }
val updatedGroupInformation = recipe.groupsInformation.merge(fixedGroupInformation)
val updatedGroupInformation = recipe.groupsInformation.merge(fixedGroupInformation) { it.id }
with(recipe.copy(groupsInformation = updatedGroupInformation.toMutableSet())) {
recipeLogic.update(this)
@ -54,7 +54,7 @@ class RecipeInitializer(
val invalidRecipeSteps = steps.filter { it.position == 0 }
val fixedRecipeSteps = increaseRecipeStepsPosition(groupInformation, invalidRecipeSteps, maxPosition + 1)
val updatedRecipeSteps = steps.merge(fixedRecipeSteps)
val updatedRecipeSteps = steps.merge(fixedRecipeSteps) { it.id }
return groupInformation.copy(steps = updatedRecipeSteps.toMutableSet())
}

View File

@ -32,3 +32,10 @@ data class MaterialSaveDto(
val simdutFile: MultipartFile?
) : EntityDto
data class MaterialQuantityDto(
val materialId: Long,
@field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO)
val quantity: Float
)

View File

@ -0,0 +1,46 @@
package dev.fyloz.colorrecipesexplorer.dtos
import com.fasterxml.jackson.annotation.JsonIgnore
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.model.Recipe
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank
data class MixDto(
override val id: Long = 0L,
val location: String? = null,
@JsonIgnore
val recipe: Recipe, // TODO change to dto
val mixType: MixTypeDto,
val mixMaterials: Set<MixMaterialDto>
) : EntityDto
data class MixSaveDto(
val id: Long = 0L,
@field:NotBlank
val name: String,
val recipeId: Long = 0L,
val materialTypeId: Long,
val mixMaterials: Set<MixMaterialSaveDto>
)
data class MixDeductDto(
val id: Long,
@field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO)
val ratio: Float
)
data class MixLocationDto(
val mixId: Long,
val location: String?
)

View File

@ -2,8 +2,11 @@ package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto
import dev.fyloz.colorrecipesexplorer.dtos.MaterialQuantityDto
import dev.fyloz.colorrecipesexplorer.dtos.MixDeductDto
import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto
import dev.fyloz.colorrecipesexplorer.exception.RestException
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.model.Material
import dev.fyloz.colorrecipesexplorer.utils.mapMayThrow
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
@ -34,83 +37,87 @@ class DefaultInventoryLogic(
) : InventoryLogic {
@Transactional
override fun add(materialQuantities: Collection<MaterialQuantityDto>) =
materialQuantities.map {
materialQuantityDto(materialId = it.material, quantity = add(it))
}
materialQuantities.map { MaterialQuantityDto(it.materialId, add(it)) }
override fun add(materialQuantity: MaterialQuantityDto) =
materialLogic.updateQuantity(
materialLogic.getById(materialQuantity.material),
materialQuantity.quantity
)
materialLogic.updateQuantity(
materialLogic.getById(materialQuantity.materialId),
materialQuantity.quantity
)
@Transactional
override fun deductMix(mixRatio: MixDeductDto): Collection<MaterialQuantityDto> {
val mix = mixLogic.getById(mixRatio.id)
val firstMixMaterial = mix.mixMaterials.first()
val adjustedFirstMaterialQuantity = firstMixMaterial.quantity * mixRatio.ratio
fun adjustQuantity(mixMaterial: MixMaterial): Float =
if (!mixMaterial.material.materialType!!.usePercentages)
mixMaterial.quantity * mixRatio.ratio
else
(mixMaterial.quantity * adjustedFirstMaterialQuantity) / 100f
return deduct(mix.mixMaterials.map {
materialQuantityDto(
materialId = it.material.id!!,
quantity = adjustQuantity(it)
)
})
return deduct(getMaterialsWithAdjustedQuantities(mix.mixMaterials, mixRatio))
}
@Transactional
override fun deduct(materialQuantities: Collection<MaterialQuantityDto>): Collection<MaterialQuantityDto> {
val thrown = mutableListOf<NotEnoughInventoryException>()
val updatedQuantities =
materialQuantities.mapMayThrow<MaterialQuantityDto, MaterialQuantityDto, NotEnoughInventoryException>(
{ thrown.add(it) }
) {
materialQuantityDto(materialId = it.material, quantity = deduct(it))
}
materialQuantities.mapMayThrow<MaterialQuantityDto, MaterialQuantityDto, NotEnoughInventoryException>(
{ thrown.add(it) }
) {
MaterialQuantityDto(it.materialId, deduct(it))
}
if (thrown.isNotEmpty()) {
throw MultiplesNotEnoughInventoryException(thrown)
}
return updatedQuantities
}
override fun deduct(materialQuantity: MaterialQuantityDto): Float =
with(materialLogic.getById(materialQuantity.material)) {
if (this.inventoryQuantity >= materialQuantity.quantity) {
materialLogic.updateQuantity(this, -materialQuantity.quantity)
} else {
throw NotEnoughInventoryException(materialQuantity.quantity, this)
}
}
with(materialLogic.getById(materialQuantity.materialId)) {
if (this.inventoryQuantity >= materialQuantity.quantity) {
materialLogic.updateQuantity(this, -materialQuantity.quantity)
} else {
throw NotEnoughInventoryException(materialQuantity.quantity, this)
}
}
private fun getMaterialsWithAdjustedQuantities(
mixMaterials: Collection<MixMaterialDto>,
mixRatio: MixDeductDto
): Collection<MaterialQuantityDto> {
val adjustedFirstMaterialQuantity = mixMaterials.first().quantity * mixRatio.ratio
fun getAdjustedQuantity(material: MaterialDto, quantity: Float) =
if (!material.materialType.usePercentages)
quantity * mixRatio.ratio // Simply multiply the quantity by the ratio
else
(quantity * adjustedFirstMaterialQuantity) / 100f // Percents quantities are a ratio of the first material
return mixMaterials.associate { it.material to it.quantity }
.mapValues { getAdjustedQuantity(it.key, it.value) }
.map { MaterialQuantityDto(it.key.id, it.value) }
}
}
class NotEnoughInventoryException(quantity: Float, material: MaterialDto) :
RestException(
"notenoughinventory",
"Not enough inventory",
HttpStatus.BAD_REQUEST,
"Cannot deduct ${quantity}mL of ${material.name} because there is only ${material.inventoryQuantity}mL in inventory",
mapOf(
"material" to material.name,
"materialId" to material.id.toString(),
"requestQuantity" to quantity,
"availableQuantity" to material.inventoryQuantity
RestException(
"notenoughinventory",
"Not enough inventory",
HttpStatus.BAD_REQUEST,
"Cannot deduct ${quantity}mL of ${material.name} because there is only ${material.inventoryQuantity}mL in inventory",
mapOf(
"material" to material.name,
"materialId" to material.id.toString(),
"requestQuantity" to quantity,
"availableQuantity" to material.inventoryQuantity
)
)
)
class MultiplesNotEnoughInventoryException(exceptions: List<NotEnoughInventoryException>) :
RestException(
"notenoughinventory-multiple",
"Not enough inventory",
HttpStatus.BAD_REQUEST,
"Cannot deduct requested quantities because there is no enough of them in inventory",
mapOf(
"lowQuantities" to exceptions.map { it.extensions }
RestException(
"notenoughinventory-multiple",
"Not enough inventory",
HttpStatus.BAD_REQUEST,
"Cannot deduct requested quantities because there is no enough of them in inventory",
mapOf(
"lowQuantities" to exceptions.map { it.extensions }
)
)
)

View File

@ -1,102 +1,66 @@
package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.MixRepository
import dev.fyloz.colorrecipesexplorer.utils.setAll
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
import dev.fyloz.colorrecipesexplorer.dtos.MixDto
import dev.fyloz.colorrecipesexplorer.dtos.MixLocationDto
import dev.fyloz.colorrecipesexplorer.dtos.MixSaveDto
import dev.fyloz.colorrecipesexplorer.model.Mix
import dev.fyloz.colorrecipesexplorer.service.MixService
import org.springframework.context.annotation.Lazy
import org.springframework.stereotype.Service
import javax.transaction.Transactional
import org.springframework.transaction.annotation.Transactional
interface MixLogic : ExternalModelService<Mix, MixSaveDto, MixUpdateDto, MixOutputDto, MixRepository> {
/** Gets all mixes with the given [mixType]. */
fun getAllByMixType(mixType: MixType): Collection<Mix>
interface MixLogic : Logic<MixDto, MixService> {
/** Saves the given [dto]. */
fun save(dto: MixSaveDto): MixDto
/** Checks if a [MixType] is shared by several [Mix]es or not. */
fun mixTypeIsShared(mixType: MixType): Boolean
/** Updates the given [dto]. */
fun update(dto: MixSaveDto): MixDto
/** Updates the location of each [Mix] in the given [MixLocationDto]s. */
/** Updates the location of each mix in the given [updatedLocations]. */
fun updateLocations(updatedLocations: Collection<MixLocationDto>)
/** Updates the location of a given [Mix] to the given [MixLocationDto]. */
fun updateLocation(updatedLocation: MixLocationDto)
}
@Service
@RequireDatabase
@LogicComponent
class DefaultMixLogic(
mixRepository: MixRepository,
@Lazy val recipeLogic: RecipeLogic,
@Lazy val materialTypeLogic: MaterialTypeLogic,
val mixMaterialLogic: MixMaterialLogic,
val mixTypeLogic: MixTypeLogic
) : AbstractExternalModelService<Mix, MixSaveDto, MixUpdateDto, MixOutputDto, MixRepository>(mixRepository),
MixLogic {
override fun idNotFoundException(id: Long) = mixIdNotFoundException(id)
override fun idAlreadyExistsException(id: Long) = mixIdAlreadyExistsException(id)
override fun getAllByMixType(mixType: MixType): Collection<Mix> = repository.findAllByMixType(mixType)
override fun mixTypeIsShared(mixType: MixType): Boolean = getAllByMixType(mixType).count() > 1
override fun Mix.toOutput() = MixOutputDto(
this.id!!,
this.location,
this.mixType,
this.mixMaterials.map { mixMaterialDto(it) }.toSet()
)
service: MixService,
@Lazy private val recipeLogic: RecipeLogic,
@Lazy private val materialTypeLogic: MaterialTypeLogic,
private val mixTypeLogic: MixTypeLogic,
private val mixMaterialLogic: MixMaterialLogic
) : BaseLogic<MixDto, MixService>(service, Mix::class.simpleName!!), MixLogic {
@Transactional
override fun save(entity: MixSaveDto): Mix {
val recipe = recipeLogic.getById(entity.recipeId)
val materialType = materialTypeLogic.getById(entity.materialTypeId)
val mixType = mixTypeLogic.getOrCreateForNameAndMaterialType(entity.name, materialType)
override fun save(dto: MixSaveDto): MixDto {
val recipe = recipeLogic.getById(dto.recipeId)
val materialType = materialTypeLogic.getById(dto.materialTypeId)
val mixMaterials =
if (entity.mixMaterials != null) mixMaterialLogic.saveAll(entity.mixMaterials).toSet() else setOf()
mixMaterialLogic.validateMixMaterials(mixMaterials)
val mix = MixDto(
recipe = recipe,
mixType = mixTypeLogic.getOrCreateForNameAndMaterialType(dto.name, materialType),
mixMaterials = mixMaterialLogic.validateAndSaveAll(dto.mixMaterials).toSet()
)
var mix = mix(recipe = recipe, mixType = mixType(mixType), mixMaterials = mixMaterials.map(::mixMaterial).toMutableSet())
mix = save(mix)
recipeLogic.addMix(recipe, mix)
return mix
return save(mix)
}
@Transactional
override fun update(entity: MixUpdateDto): Mix {
val mix = getById(entity.id)
if (entity.name != null || entity.materialTypeId != null) {
val name = entity.name ?: mix.mixType.name
val materialType = if (entity.materialTypeId != null)
materialType(materialTypeLogic.getById(entity.materialTypeId))
else
mix.mixType.material.materialType!!
override fun update(dto: MixSaveDto): MixDto {
val materialType = materialTypeLogic.getById(dto.materialTypeId)
val mix = getById(dto.id)
mix.mixType = if (mixTypeIsShared(mix.mixType)) {
mixType(mixTypeLogic.saveForNameAndMaterialType(name, materialTypeDto(materialType)))
} else {
mixType(mixTypeLogic.updateForNameAndMaterialType(mixTypeDto(mix.mixType), name, materialTypeDto(materialType)))
}
}
if (entity.mixMaterials != null) {
mix.mixMaterials.setAll(mixMaterialLogic.saveAll(entity.mixMaterials!!).map(::mixMaterial).toMutableSet())
}
return update(mix)
return update(
MixDto(
id = dto.id,
recipe = recipeLogic.getById(dto.recipeId),
mixType = mixTypeLogic.updateOrCreateForNameAndMaterialType(mix.mixType, dto.name, materialType),
mixMaterials = mixMaterialLogic.validateAndSaveAll(dto.mixMaterials).toSet()
)
)
}
override fun updateLocations(updatedLocations: Collection<MixLocationDto>) {
override fun updateLocations(updatedLocations: Collection<MixLocationDto>) =
updatedLocations.forEach(::updateLocation)
}
override fun updateLocation(updatedLocation: MixLocationDto) {
repository.updateLocationById(updatedLocation.mixId, updatedLocation.location)
private fun updateLocation(updatedLocation: MixLocationDto) {
service.updateLocationById(updatedLocation.mixId, updatedLocation.location)
}
@Transactional
override fun delete(entity: Mix) {
if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteMixException(entity)
recipeLogic.removeMix(entity)
super.delete(entity)
}
}
}

View File

@ -2,12 +2,14 @@ package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto
import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialSaveDto
import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError
import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException
import dev.fyloz.colorrecipesexplorer.exception.RestException
import dev.fyloz.colorrecipesexplorer.model.MixMaterial
import dev.fyloz.colorrecipesexplorer.service.MixMaterialService
import dev.fyloz.colorrecipesexplorer.utils.PositionUtils
import org.springframework.context.annotation.Lazy
import org.springframework.http.HttpStatus
interface MixMaterialLogic : Logic<MixMaterialDto, MixMaterialService> {
@ -17,10 +19,13 @@ interface MixMaterialLogic : Logic<MixMaterialDto, MixMaterialService> {
* If any of those criteria are not met, an [InvalidGroupStepsPositionsException] will be thrown.
*/
fun validateMixMaterials(mixMaterials: Set<MixMaterialDto>)
/** Validates the given mix materials [dtos] and save them. */
fun validateAndSaveAll(dtos: Collection<MixMaterialSaveDto>): Collection<MixMaterialDto>
}
@LogicComponent
class DefaultMixMaterialLogic(service: MixMaterialService) :
class DefaultMixMaterialLogic(service: MixMaterialService, @Lazy private val materialLogic: MaterialLogic) :
BaseLogic<MixMaterialDto, MixMaterialService>(service, MixMaterial::class.simpleName!!), MixMaterialLogic {
override fun validateMixMaterials(mixMaterials: Set<MixMaterialDto>) {
if (mixMaterials.isEmpty()) return
@ -37,6 +42,21 @@ class DefaultMixMaterialLogic(service: MixMaterialService) :
throw InvalidFirstMixMaterialException(sortedMixMaterials[0])
}
}
override fun validateAndSaveAll(dtos: Collection<MixMaterialSaveDto>): Collection<MixMaterialDto> {
val dtosWithMaterials = dtos.map {
MixMaterialDto(
id = it.id,
material = materialLogic.getById(it.materialId),
quantity = it.quantity,
position = it.position
)
}.toSet()
validateMixMaterials(dtosWithMaterials)
return dtosWithMaterials.map(::save)
}
}
// TODO check if required

View File

@ -6,27 +6,50 @@ import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto
import dev.fyloz.colorrecipesexplorer.dtos.MixTypeDto
import dev.fyloz.colorrecipesexplorer.model.MixType
import dev.fyloz.colorrecipesexplorer.service.MixTypeService
import org.springframework.context.annotation.Lazy
import org.springframework.transaction.annotation.Transactional
interface MixTypeLogic : Logic<MixTypeDto, MixTypeService> {
/** Returns a [MixType] for the given [name] and [materialType]. If this mix type does not already exist, it will be created. */
/** Returns a mix type for the given [name] and [materialType]. If this mix type does not already exist, it will be created. */
fun getOrCreateForNameAndMaterialType(name: String, materialType: MaterialTypeDto): MixTypeDto
/** Returns a new and persisted [MixType] with the given [name] and [materialType]. */
fun saveForNameAndMaterialType(name: String, materialType: MaterialTypeDto): MixTypeDto
/** Returns the given [mixType] updated with the given [name] and [materialType]. */
fun updateForNameAndMaterialType(mixType: MixTypeDto, name: String, materialType: MaterialTypeDto): MixTypeDto
/** Updates the [mixType] with the given [name] and [materialType], or create a new one if it is shared with other mixes. */
fun updateOrCreateForNameAndMaterialType(
mixType: MixTypeDto,
name: String,
materialType: MaterialTypeDto
): MixTypeDto
}
@LogicComponent
class DefaultMixTypeLogic(service: MixTypeService, private val materialLogic: MaterialLogic) :
class DefaultMixTypeLogic(
service: MixTypeService,
@Lazy private val materialLogic: MaterialLogic
) :
BaseLogic<MixTypeDto, MixTypeService>(service, MixType::class.simpleName!!), MixTypeLogic {
@Transactional
override fun getOrCreateForNameAndMaterialType(name: String, materialType: MaterialTypeDto) =
service.getByNameAndMaterialType(name, materialType.id) ?: saveForNameAndMaterialType(name, materialType)
@Transactional
override fun saveForNameAndMaterialType(name: String, materialType: MaterialTypeDto): MixTypeDto {
override fun updateOrCreateForNameAndMaterialType(
mixType: MixTypeDto,
name: String,
materialType: MaterialTypeDto
) = if (service.isShared(mixType.id)) {
saveForNameAndMaterialType(name, materialType)
} else {
updateForNameAndMaterialType(mixType, name, materialType)
}
override fun deleteById(id: Long) {
if (service.isUsedByMixes(id)) {
throw cannotDeleteException("Cannot delete the mix type with the id '$id' because one or more mixes depends on it")
}
super.deleteById(id)
}
private fun saveForNameAndMaterialType(name: String, materialType: MaterialTypeDto): MixTypeDto {
val material = materialLogic.save(
MaterialDto(
name = name,
@ -39,7 +62,7 @@ class DefaultMixTypeLogic(service: MixTypeService, private val materialLogic: Ma
return save(MixTypeDto(name = name, material = material))
}
override fun updateForNameAndMaterialType(
private fun updateForNameAndMaterialType(
mixType: MixTypeDto,
name: String,
materialType: MaterialTypeDto
@ -47,12 +70,4 @@ class DefaultMixTypeLogic(service: MixTypeService, private val materialLogic: Ma
val material = materialLogic.update(mixType.material.copy(name = name, materialType = materialType))
return update(mixType.copy(name = name, material = material))
}
override fun deleteById(id: Long) {
if (service.isUsedByMixes(id)) {
throw cannotDeleteException("Cannot delete the mix type with the id '$id' because one or more mixes depends on it")
}
super.deleteById(id)
}
}

View File

@ -72,11 +72,7 @@ class DefaultRecipeLogic(
isApprobationExpired(this),
this.remark,
this.company,
this.mixes.map {
with(mixLogic) {
it.toOutput()
}
}.toSet(),
this.mixes.map { mix(it) }.toSet(),
this.groupsInformation,
recipeImageLogic.getAllImages(this)
.map { this.imageUrl(configService.getContent(ConfigurationType.INSTANCE_URL), it) }

View File

@ -2,13 +2,7 @@ package dev.fyloz.colorrecipesexplorer.model
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import javax.persistence.*
import javax.validation.constraints.Min
const val SIMDUT_FILES_PATH = "pdf/simdut"
@Entity
@Table(name = "material")
@ -36,13 +30,6 @@ data class Material(
}
}
data class MaterialQuantityDto(
val material: Long,
@field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO)
val quantity: Float
)
// === DSL ===
fun material(
@ -71,10 +58,4 @@ fun material(
@Deprecated("Temporary DSL for transition")
fun materialDto(
entity: Material
) = MaterialDto(entity.id!!, entity.name, entity.inventoryQuantity, entity.isMixType, materialTypeDto(entity.materialType!!))
fun materialQuantityDto(
materialId: Long,
quantity: Float,
op: MaterialQuantityDto.() -> Unit = {}
) = MaterialQuantityDto(materialId, quantity).apply(op)
) = MaterialDto(entity.id!!, entity.name, entity.inventoryQuantity, entity.isMixType, materialTypeDto(entity.materialType!!))

View File

@ -1,15 +1,11 @@
package dev.fyloz.colorrecipesexplorer.model
import com.fasterxml.jackson.annotation.JsonIgnore
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.dtos.MixDto
import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import javax.persistence.*
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank
@Entity
@Table(name = "mix")
@ -20,7 +16,6 @@ data class Mix(
var location: String?,
@JsonIgnore
@ManyToOne
@JoinColumn(name = "recipe_id")
val recipe: Recipe,
@ -31,53 +26,9 @@ data class Mix(
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
@JoinColumn(name = "mix_id")
var mixMaterials: MutableSet<MixMaterial>,
var mixMaterials: Set<MixMaterial>,
) : ModelEntity
open class MixSaveDto(
@field:NotBlank
val name: String,
val recipeId: Long,
val materialTypeId: Long,
val mixMaterials: Set<MixMaterialDto>?
) : EntityDto<Mix>
open class MixUpdateDto(
val id: Long,
@field:NotBlank
val name: String?,
val materialTypeId: Long?,
var mixMaterials: Set<MixMaterialDto>?
) : EntityDto<Mix>
data class MixOutputDto(
val id: Long,
val location: String?,
val mixType: MixType,
val mixMaterials: Set<MixMaterialDto>
)
data class MixDeductDto(
val id: Long,
@field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO)
val ratio: Float
)
data class MixLocationDto(
val mixId: Long,
val location: String?
)
//fun Mix.toOutput() =
// ==== DSL ====
fun mix(
id: Long? = null,
@ -88,59 +39,12 @@ fun mix(
op: Mix.() -> Unit = {}
) = Mix(id, location, recipe, mixType, mixMaterials).apply(op)
fun mixSaveDto(
name: String = "name",
recipeId: Long = 0L,
materialTypeId: Long = 0L,
mixMaterials: Set<MixMaterialDto>? = setOf(),
op: MixSaveDto.() -> Unit = {}
) = MixSaveDto(name, recipeId, materialTypeId, mixMaterials).apply(op)
@Deprecated("Temporary DSL for transition")
fun mix(
dto: MixDto
) = Mix(dto.id, dto.location, dto.recipe, mixType(dto.mixType), dto.mixMaterials.map(::mixMaterial).toSet())
fun mixUpdateDto(
id: Long = 0L,
name: String? = "name",
materialTypeId: Long? = 0L,
mixMaterials: Set<MixMaterialDto>? = setOf(),
op: MixUpdateDto.() -> Unit = {}
) = MixUpdateDto(id, name, materialTypeId, mixMaterials).apply(op)
fun mixRatio(
id: Long = 0L,
ratio: Float = 1f,
op: MixDeductDto.() -> Unit = {}
) = MixDeductDto(id, ratio).apply(op)
fun mixLocationDto(
mixId: Long = 0L,
location: String? = "location",
op: MixLocationDto.() -> Unit = {}
) = MixLocationDto(mixId, location).apply(op)
// ==== Exceptions ====
private const val MIX_NOT_FOUND_EXCEPTION_TITLE = "Mix not found"
private const val MIX_ALREADY_EXISTS_EXCEPTION_TITLE = "Mix already exists"
private const val MIX_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete mix"
private const val MIX_EXCEPTION_ERROR_CODE = "mix"
fun mixIdNotFoundException(id: Long) =
NotFoundException(
MIX_EXCEPTION_ERROR_CODE,
MIX_NOT_FOUND_EXCEPTION_TITLE,
"A mix with the id $id could not be found",
id
)
fun mixIdAlreadyExistsException(id: Long) =
AlreadyExistsException(
MIX_EXCEPTION_ERROR_CODE,
MIX_ALREADY_EXISTS_EXCEPTION_TITLE,
"A mix with the id $id already exists",
id
)
fun cannotDeleteMixException(mix: Mix) =
CannotDeleteException(
MIX_EXCEPTION_ERROR_CODE,
MIX_CANNOT_DELETE_EXCEPTION_TITLE,
"Cannot delete the mix ${mix.mixType.name} because one or more mixes depends on it"
)
@Deprecated("Temporary DSL for transition")
fun mix(
entity: Mix
) = MixDto(entity.id!!, entity.location, entity.recipe, mixTypeDto(entity.mixType), entity.mixMaterials.map(::mixMaterialDto).toSet())

View File

@ -2,6 +2,8 @@ package dev.fyloz.colorrecipesexplorer.model
import com.fasterxml.jackson.annotation.JsonIgnore
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.dtos.MixDto
import dev.fyloz.colorrecipesexplorer.dtos.MixLocationDto
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.account.Group
@ -150,7 +152,7 @@ data class RecipeOutputDto(
val approbationExpired: Boolean?,
val remark: String?,
val company: Company,
val mixes: Set<MixOutputDto>,
val mixes: Set<MixDto>,
val groupsInformation: Set<RecipeGroupInformation>,
var imagesUrls: Set<String>
) : ModelEntity

View File

@ -7,21 +7,11 @@ import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query
interface MixRepository : JpaRepository<Mix, Long> {
/** Finds all mixes with the given [mixType]. */
fun findAllByMixType(mixType: MixType): Collection<Mix>
/** Finds all mixes with the mix type with the given [mixTypeId]. */
fun findAllByMixTypeId(mixTypeId: Long): Collection<Mix>
/** Updates the [location] of the [Mix] with the given [id]. */
@Modifying
@Query("UPDATE Mix m SET m.location = :location WHERE m.id = :id")
@Query("update Mix m set m.location = :location where m.id = :id")
fun updateLocationById(id: Long, location: String?)
@Query(
"""
select case when(count(mm.id) > 0) then false else true end
from Mix m
left join MixMaterial mm on m.mixType.material.id = mm.material.id
where m.id = :id
"""
)
fun canBeDeleted(id: Long): Boolean
}

View File

@ -1,6 +1,5 @@
package dev.fyloz.colorrecipesexplorer.repository
import dev.fyloz.colorrecipesexplorer.model.MaterialType
import dev.fyloz.colorrecipesexplorer.model.MixType
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
@ -24,4 +23,13 @@ interface MixTypeRepository : JpaRepository<MixType, Long> {
"""
)
fun isUsedByMixes(id: Long): Boolean
/** Checks if the mix type with the given [id] is used by more than one mix. */
@Query(
"""
select case when(count(m.id) > 1) then false else true end
from Mix m where m.mixType.id = :id
"""
)
fun isShared(id: Long): Boolean
}

View File

@ -1,8 +1,8 @@
package dev.fyloz.colorrecipesexplorer.rest
import dev.fyloz.colorrecipesexplorer.dtos.MaterialQuantityDto
import dev.fyloz.colorrecipesexplorer.dtos.MixDeductDto
import dev.fyloz.colorrecipesexplorer.logic.InventoryLogic
import dev.fyloz.colorrecipesexplorer.model.MaterialQuantityDto
import dev.fyloz.colorrecipesexplorer.model.MixDeductDto
import org.springframework.context.annotation.Profile
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.PutMapping

View File

@ -0,0 +1,42 @@
package dev.fyloz.colorrecipesexplorer.rest
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditRecipes
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewRecipes
import dev.fyloz.colorrecipesexplorer.dtos.MixDto
import dev.fyloz.colorrecipesexplorer.dtos.MixSaveDto
import dev.fyloz.colorrecipesexplorer.logic.MixLogic
import org.springframework.context.annotation.Profile
import org.springframework.web.bind.annotation.*
import javax.validation.Valid
@RestController
@RequestMapping(Constants.ControllerPaths.MIX)
@Profile("!emergency")
@PreAuthorizeViewRecipes
class MixController(private val mixLogic: MixLogic) {
@GetMapping("{id}")
fun getById(@PathVariable id: Long) =
ok(mixLogic.getById(id))
@PostMapping
@PreAuthorizeEditRecipes
fun save(@Valid @RequestBody mix: MixSaveDto) =
created<MixDto>(Constants.ControllerPaths.MIX) {
mixLogic.save(mix)
}
@PutMapping
@PreAuthorizeEditRecipes
fun update(@Valid @RequestBody mix: MixSaveDto) =
noContent {
mixLogic.update(mix)
}
@DeleteMapping("{id}")
@PreAuthorizeEditRecipes
fun deleteById(@PathVariable id: Long) =
noContent {
mixLogic.deleteById(id)
}
}

View File

@ -16,7 +16,6 @@ import javax.validation.Valid
private const val RECIPE_CONTROLLER_PATH = "api/recipe"
private const val MIX_CONTROLLER_PATH = "api/recipe/mix"
@RestController
@RequestMapping(RECIPE_CONTROLLER_PATH)
@ -83,34 +82,3 @@ class RecipeController(
recipeImageLogic.delete(recipeLogic.getById(recipeId), name)
}
}
@RestController
@RequestMapping(MIX_CONTROLLER_PATH)
@Profile("!emergency")
@PreAuthorizeViewRecipes
class MixController(private val mixLogic: MixLogic) {
@GetMapping("{id}")
fun getById(@PathVariable id: Long) =
ok(mixLogic.getByIdForOutput(id))
@PostMapping
@PreAuthorizeEditRecipes
fun save(@Valid @RequestBody mix: MixSaveDto) =
created<Mix>(MIX_CONTROLLER_PATH) {
mixLogic.save(mix)
}
@PutMapping
@PreAuthorizeEditRecipes
fun update(@Valid @RequestBody mix: MixUpdateDto) =
noContent {
mixLogic.update(mix)
}
@DeleteMapping("{id}")
@PreAuthorizeEditRecipes
fun deleteById(@PathVariable id: Long) =
noContent {
mixLogic.deleteById(id)
}
}

View File

@ -0,0 +1,42 @@
package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent
import dev.fyloz.colorrecipesexplorer.dtos.MixDto
import dev.fyloz.colorrecipesexplorer.model.Mix
import dev.fyloz.colorrecipesexplorer.repository.MixRepository
interface MixService : Service<MixDto, Mix, MixRepository> {
/** Gets all mixes with the mix type with the given [mixTypeId]. */
fun getAllByMixTypeId(mixTypeId: Long): Collection<MixDto>
/** Updates the [location] of the mix with the given [id]. */
fun updateLocationById(id: Long, location: String?)
}
@ServiceComponent
class DefaultMixService(
repository: MixRepository,
private val mixTypeService: MixTypeService,
private val mixMaterialService: MixMaterialService
) : BaseService<MixDto, Mix, MixRepository>(repository), MixService {
override fun getAllByMixTypeId(mixTypeId: Long) = repository.findAllByMixTypeId(mixTypeId).map(::toDto)
override fun updateLocationById(id: Long, location: String?) = repository.updateLocationById(id, location)
override fun toDto(entity: Mix) =
MixDto(
entity.id!!,
entity.location,
entity.recipe,
mixTypeService.toDto(entity.mixType),
entity.mixMaterials.map(mixMaterialService::toDto).toSet()
)
override fun toEntity(dto: MixDto) =
Mix(
dto.id,
dto.location,
dto.recipe,
mixTypeService.toEntity(dto.mixType),
dto.mixMaterials.map(mixMaterialService::toEntity).toSet()
)
}

View File

@ -14,6 +14,9 @@ interface MixTypeService : Service<MixTypeDto, MixType, MixTypeRepository> {
/** Checks if a mix depends on the mix type with the given [id]. */
fun isUsedByMixes(id: Long): Boolean
/** Checks if the mix type with the given [id] is used by more than one mix. */
fun isShared(id: Long): Boolean
}
@ServiceComponent
@ -25,8 +28,8 @@ class DefaultMixTypeService(repository: MixTypeRepository, val materialService:
override fun getByNameAndMaterialType(name: String, materialTypeId: Long) =
repository.findByNameAndMaterialType(name, materialTypeId)?.let(::toDto)
override fun isUsedByMixes(id: Long) =
repository.isUsedByMixes(id)
override fun isUsedByMixes(id: Long) = repository.isUsedByMixes(id)
override fun isShared(id: Long) = repository.isShared(id)
override fun toDto(entity: MixType) =
MixTypeDto(entity.id!!, entity.name, materialService.toDto(entity.material))

View File

@ -1,6 +1,6 @@
package dev.fyloz.colorrecipesexplorer.utils
import dev.fyloz.colorrecipesexplorer.model.ModelEntity
import dev.fyloz.colorrecipesexplorer.dtos.EntityDto
/** Returns a list containing the result of the given [transform] applied to each item of the [Iterable]. If the given [transform] throws, the [Throwable] will be passed to the given [throwableConsumer]. */
inline fun <T, R, reified E : Throwable> Iterable<T>.mapMayThrow(
@ -19,26 +19,12 @@ inline fun <T, R, reified E : Throwable> Iterable<T>.mapMayThrow(
}
}
/** 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 }
/** 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()
.sorted()
.filterIndexed { index, it -> it != index + 1 }
.isNotEmpty()
/** Check if the given [Int] [Iterable] has gaps between each element. */
fun Iterable<Int>.hasGaps() =
this.sorted()
@ -58,8 +44,20 @@ inline fun <T> MutableCollection<T>.excludeAll(predicate: (T) -> Boolean): Itera
return matching
}
/** Merge to [ModelEntity] [Iterable]s and prevent id duplication. */
fun <T : ModelEntity> Iterable<T>.merge(other: Iterable<T>) =
this
.filter { model -> other.all { it.id != model.id } }
/**
* Merge two [EntityDto] [Iterable]s and prevent duplication of their ids.
* In case of collision, the items from the [other] iterable will be taken.
*/
@JvmName("mergeDto")
fun <T : EntityDto> Iterable<T>.merge(other: Iterable<T>) =
this.merge(other) { it.id }
/**
* Merge two [Iterable]s and prevent duplication of the keys determined by the given [keyMapper].
* In case of collision, the items from the [other] iterable will be taken.
*/
fun <T, K> Iterable<T>.merge(other: Iterable<T>, keyMapper: (T) -> K) =
this.associateBy { keyMapper(it) }
.filter { pair -> other.all { keyMapper(it) != pair.key } }
.map { it.value }
.plus(other)

View File

@ -0,0 +1 @@
spring.jpa.show-sql=true

View File

@ -0,0 +1,247 @@
package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.dtos.*
import dev.fyloz.colorrecipesexplorer.model.Recipe
import dev.fyloz.colorrecipesexplorer.model.company
import io.mockk.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import kotlin.test.assertEquals
class DefaultInventoryLogicTest {
private val materialLogicMock = mockk<MaterialLogic>()
private val mixLogicMock = mockk<MixLogic>()
private val inventoryLogic = spyk(DefaultInventoryLogic(materialLogicMock, mixLogicMock))
private val defaultInventoryQuantity = 5000f
private val materialType = MaterialTypeDto(1L, "Unit test material type", "UTMT", false)
private val material = MaterialDto(1L, "Unit test material", defaultInventoryQuantity, false, materialType)
@AfterEach
internal fun afterEach() {
clearAllMocks()
}
@Test
fun add_collection_normalBehavior_callsAddSingleForEachQuantity() {
// Arrange
val quantities = setOf(
MaterialQuantityDto(1L, 1000f),
MaterialQuantityDto(2L, 2000f)
)
val expectedQuantities = quantities.map { it.copy(quantity = it.quantity + defaultInventoryQuantity) }
every { inventoryLogic.add(any<MaterialQuantityDto>()) } answers { expectedQuantities[this.nArgs].quantity }
// Act
inventoryLogic.add(quantities)
// Assert
verify {
quantities.forEach {
inventoryLogic.add(it)
}
}
}
@Test
fun add_collection_normalBehavior_returnsFromAddSingle() {
// Arrange
val quantities = setOf(
MaterialQuantityDto(1L, 1000f),
MaterialQuantityDto(2L, 2000f)
)
val expectedQuantities = quantities.map { it.copy(quantity = it.quantity + defaultInventoryQuantity) }
every { inventoryLogic.add(any<MaterialQuantityDto>()) } returnsMany (expectedQuantities.map { it.quantity })
// Act
val actualQuantities = inventoryLogic.add(quantities)
// Assert
assertEquals(expectedQuantities, actualQuantities)
}
@Test
fun add_single_normalBehavior_callsUpdateQuantityInMaterialLogic() {
// Arrange
every { materialLogicMock.getById(any()) } returns material
every { materialLogicMock.updateQuantity(any(), any()) } answers {
this.secondArg<Float>() + defaultInventoryQuantity
}
val quantity = MaterialQuantityDto(1L, 1000f)
// Act
inventoryLogic.add(quantity)
// Assert
verify {
materialLogicMock.updateQuantity(material, quantity.quantity)
}
}
@Test
fun deductMix_normalBehavior_callsDeductWithMixMaterials() {
// Arrange
val company = CompanyDto(1L, "Unit test company")
val recipe = Recipe(
1L,
"Unit test recipe",
"Unit test recipe",
"FFFFFF",
0xf,
null,
null,
"Remark",
company(company),
mutableListOf(),
setOf()
)
val mixType = MixTypeDto(1L, "Unit test mix type", material)
val mixMaterial = MixMaterialDto(1L, material, 1000f, 1)
val mix = MixDto(1L, null, recipe, mixType, setOf(mixMaterial))
val dto = MixDeductDto(mix.id, 2f)
val expectedQuantities = listOf(MaterialQuantityDto(material.id, mixMaterial.quantity * dto.ratio))
every { mixLogicMock.getById(any()) } returns mix
every { inventoryLogic.deduct(any<Collection<MaterialQuantityDto>>()) } returns listOf()
// Act
inventoryLogic.deductMix(dto)
// Assert
verify {
inventoryLogic.deduct(expectedQuantities)
}
}
@Test
fun deductMix_normalBehavior_returnsFromDeduct() {
// Arrange
val company = CompanyDto(1L, "Unit test company")
val recipe = Recipe(
1L,
"Unit test recipe",
"Unit test recipe",
"FFFFFF",
0xf,
null,
null,
"Remark",
company(company),
mutableListOf(),
setOf()
)
val mixType = MixTypeDto(1L, "Unit test mix type", material)
val mixMaterial = MixMaterialDto(1L, material, 1000f, 1)
val mix = MixDto(1L, null, recipe, mixType, setOf(mixMaterial))
val dto = MixDeductDto(mix.id, 2f)
val expectedQuantities = listOf(MaterialQuantityDto(material.id, mixMaterial.quantity * dto.ratio))
every { mixLogicMock.getById(any()) } returns mix
every { inventoryLogic.deduct(any<Collection<MaterialQuantityDto>>()) } returns expectedQuantities
// Act
val actualQuantities = inventoryLogic.deductMix(dto)
// Assert
assertEquals(expectedQuantities, actualQuantities)
}
@Test
fun deduct_collection_normalBehavior_callsDeductForEachQuantity() {
// Arrange
val quantities = setOf(
MaterialQuantityDto(1L, 1000f),
MaterialQuantityDto(2L, 2000f)
)
every { inventoryLogic.deduct(any<MaterialQuantityDto>()) } answers {
defaultInventoryQuantity - firstArg<MaterialQuantityDto>().quantity
}
// Act
inventoryLogic.deduct(quantities)
// Assert
verify {
quantities.forEach {
inventoryLogic.deduct(it)
}
}
}
@Test
fun deduct_collection_normalBehavior_returnsFromDeduct() {
// Arrange
val quantities = setOf(
MaterialQuantityDto(1L, 1000f),
MaterialQuantityDto(2L, 2000f)
)
every { inventoryLogic.deduct(any<MaterialQuantityDto>()) } answers {
defaultInventoryQuantity - firstArg<MaterialQuantityDto>().quantity
}
val expectedQuantities =
quantities.map { MaterialQuantityDto(it.materialId, defaultInventoryQuantity - it.quantity) }
// Act
val actualQuantities = inventoryLogic.deduct(quantities)
// Assert
assertEquals(expectedQuantities, actualQuantities)
}
@Test
fun deduct_collection_notEnoughInventory_throwsMultiplesNotEnoughInventoryException() {
// Arrange
val quantities = setOf(
MaterialQuantityDto(1L, 1000f),
MaterialQuantityDto(2L, 2000f)
)
every { inventoryLogic.deduct(any<MaterialQuantityDto>()) } throws NotEnoughInventoryException(1000f, material)
// Act
// Assert
assertThrows<MultiplesNotEnoughInventoryException> { inventoryLogic.deduct(quantities) }
}
@Test
fun deduct_single_normalBehavior_callsUpdateQuantityInMaterialLogic() {
// Arrange
val quantity = MaterialQuantityDto(material.id, 1000f)
every { materialLogicMock.getById(any()) } returns material
every { materialLogicMock.updateQuantity(any(), any()) } returns defaultInventoryQuantity - quantity.quantity
// Act
inventoryLogic.deduct(quantity)
// Assert
verify {
materialLogicMock.getById(quantity.materialId)
materialLogicMock.updateQuantity(material, -quantity.quantity)
}
confirmVerified(materialLogicMock)
}
@Test
fun deduct_single_notEnoughInventory_throwsNotEnoughInventoryException() {
// Arrange
val quantity = MaterialQuantityDto(material.id, material.inventoryQuantity + 1000f)
every { materialLogicMock.getById(any()) } returns material
every { materialLogicMock.updateQuantity(any(), any()) } returns defaultInventoryQuantity - quantity.quantity
// Act
// Assert
assertThrows<NotEnoughInventoryException> { inventoryLogic.deduct(quantity) }
}
}

View File

@ -1,12 +1,13 @@
package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto
import dev.fyloz.colorrecipesexplorer.dtos.MaterialSaveDto
import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto
import dev.fyloz.colorrecipesexplorer.dtos.*
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.model.Company
import dev.fyloz.colorrecipesexplorer.model.Material
import dev.fyloz.colorrecipesexplorer.model.Recipe
import dev.fyloz.colorrecipesexplorer.model.mix
import dev.fyloz.colorrecipesexplorer.service.MaterialService
import io.mockk.*
import org.junit.jupiter.api.AfterEach
@ -50,10 +51,10 @@ class DefaultMaterialLogicTest {
mutableListOf(),
setOf()
)
private val mix = Mix(
1L, "location", recipe, mixType = MixType(1L, "Unit test mix type", material(materialMixType)), mutableSetOf()
private val mix = MixDto(
1L, "location", recipe, mixType = MixTypeDto(1L, "Unit test mix type", materialMixType), mutableSetOf()
)
private val mix2 = mix.copy(id = 2L, mixType = mix.mixType.copy(id = 2L, material = material(materialMixType2)))
private val mix2 = mix.copy(id = 2L, mixType = mix.mixType.copy(id = 2L, material = materialMixType2))
private val simdutFileMock = MockMultipartFile(
"Unit test SIMDUT",
@ -62,7 +63,7 @@ class DefaultMaterialLogicTest {
private val materialSaveDto = MaterialSaveDto(1L, "Unit test material", 1000f, materialType.id, simdutFileMock)
init {
recipe.mixes.addAll(listOf(mix, mix2))
recipe.mixes.addAll(listOf(mix(mix), mix(mix2)))
}
@AfterEach
@ -139,7 +140,7 @@ class DefaultMaterialLogicTest {
every { mixLogicMock.getById(any()) } returns mix
// Act
val materials = materialLogic.getAllForMixUpdate(mix.id!!)
val materials = materialLogic.getAllForMixUpdate(mix.id)
// Assert
assertContains(materials, material)
@ -152,7 +153,7 @@ class DefaultMaterialLogicTest {
every { mixLogicMock.getById(any()) } returns mix
// Act
val materials = materialLogic.getAllForMixUpdate(mix.id!!)
val materials = materialLogic.getAllForMixUpdate(mix.id)
// Assert
assertContains(materials, materialMixType2)

View File

@ -0,0 +1,180 @@
package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.dtos.*
import dev.fyloz.colorrecipesexplorer.model.Company
import dev.fyloz.colorrecipesexplorer.model.Recipe
import dev.fyloz.colorrecipesexplorer.service.MixService
import io.mockk.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
class DefaultMixLogicTest {
private val mixServiceMock = mockk<MixService>()
private val recipeLogicMock = mockk<RecipeLogic>()
private val materialTypeLogicMock = mockk<MaterialTypeLogic>()
private val mixTypeLogicMock = mockk<MixTypeLogic>()
private val mixMaterialLogicMock = mockk<MixMaterialLogic>()
private val mixLogic = spyk(
DefaultMixLogic(
mixServiceMock,
recipeLogicMock,
materialTypeLogicMock,
mixTypeLogicMock,
mixMaterialLogicMock
)
)
private val company = Company(1L, "Unit test company")
private val recipe = Recipe(
1L,
"Unit test recipe",
"Unit test recipe",
"FFFFFF",
0xf,
null,
null,
"A remark",
company,
mutableListOf(),
setOf()
)
private val materialType = MaterialTypeDto(1L, "Unit test material type", "UTMT", false)
private val mixType =
MixTypeDto(1L, "Unit test mix type", MaterialDto(1L, "Unit test mix type material", 1000f, true, materialType))
private val mixMaterial =
MixMaterialDto(1L, MaterialDto(2L, "Unit test material", 1000f, false, materialType), 50f, 1)
private val mix = MixDto(recipe = recipe, mixType = mixType, mixMaterials = setOf(mixMaterial))
@AfterEach
internal fun afterEach() {
clearAllMocks()
}
private fun setup_save_normalBehavior() {
every { recipeLogicMock.getById(any()) } returns recipe
every { materialTypeLogicMock.getById(any()) } returns materialType
every { mixTypeLogicMock.getOrCreateForNameAndMaterialType(any(), any()) } returns mixType
every { mixMaterialLogicMock.validateAndSaveAll(any()) } returns listOf(mixMaterial)
every { recipeLogicMock.addMix(any(), any()) } returns recipe
every { mixLogic.save(any<MixDto>()) } returnsArgument 0
}
private fun setup_update_normalBehavior() {
every { recipeLogicMock.getById(any()) } returns recipe
every { materialTypeLogicMock.getById(any()) } returns materialType
every { mixTypeLogicMock.updateOrCreateForNameAndMaterialType(any(), any(), any()) } returns mixType
every { mixMaterialLogicMock.validateAndSaveAll(any()) } returns listOf(mixMaterial)
every { mixLogic.getById(any()) } returns mix
every { mixLogic.update(any<MixDto>()) } returnsArgument 0
}
@Test
fun save_dto_normalBehavior_callsSave() {
// Arrange
setup_save_normalBehavior()
val mixMaterialDto =
MixMaterialSaveDto(mixMaterial.id, mixMaterial.material.id, mixMaterial.quantity, mixMaterial.position)
val saveDto = MixSaveDto(0L, mixType.name, recipe.id!!, materialType.id, setOf(mixMaterialDto))
// Act
mixLogic.save(saveDto)
// Assert
verify {
mixLogic.save(mix)
}
}
@Test
fun save_dto_normalBehavior_callsValidateAndSaveAllInMixMaterialLogic() {
// Arrange
setup_save_normalBehavior()
val mixMaterialDtos =
setOf(
MixMaterialSaveDto(
mixMaterial.id,
mixMaterial.material.id,
mixMaterial.quantity,
mixMaterial.position
)
)
val saveDto = MixSaveDto(0L, mixType.name, recipe.id!!, materialType.id, mixMaterialDtos)
// Act
mixLogic.save(saveDto)
// Assert
verify {
mixMaterialLogicMock.validateAndSaveAll(mixMaterialDtos)
}
confirmVerified(mixMaterialLogicMock)
}
@Test
fun update_dto_normalBehavior_callsUpdate() {
// Arrange
setup_update_normalBehavior()
val mixMaterialDto =
MixMaterialSaveDto(mixMaterial.id, mixMaterial.material.id, mixMaterial.quantity, mixMaterial.position)
val saveDto = MixSaveDto(mix.id, mixType.name, recipe.id!!, materialType.id, setOf(mixMaterialDto))
// Act
mixLogic.update(saveDto)
// Assert
verify {
mixLogic.update(mix)
}
}
@Test
fun update_dto_normalBehavior_callsValidateAndSaveAllInMixMaterialLogic() {
// Arrange
setup_update_normalBehavior()
val mixMaterialDtos = setOf(
MixMaterialSaveDto(
mixMaterial.id,
mixMaterial.material.id,
mixMaterial.quantity,
mixMaterial.position
)
)
val saveDto = MixSaveDto(mix.id, mixType.name, recipe.id!!, materialType.id, mixMaterialDtos)
// Act
mixLogic.update(saveDto)
// Assert
verify {
mixMaterialLogicMock.validateAndSaveAll(mixMaterialDtos)
}
confirmVerified(mixMaterialLogicMock)
}
@Test
fun updateLocations_normalBehavior_callsUpdateLocationByIdInServiceForEachUpdatedLocation() {
// Arrange
every { mixServiceMock.updateLocationById(any(), any()) } just runs
val updatedLocations = listOf(
MixLocationDto(1L, "A location"),
MixLocationDto(2L, "Another location")
)
// Act
mixLogic.updateLocations(updatedLocations)
// Assert
updatedLocations.forEach {
verify {
mixServiceMock.updateLocationById(it.mixId, it.location)
}
}
confirmVerified(mixServiceMock)
}
}

View File

@ -14,8 +14,9 @@ import org.junit.jupiter.api.assertThrows
class DefaultMixMaterialLogicTest {
private val mixMaterialServiceMock = mockk<MixMaterialService>()
private val materialLogicMock = mockk<MaterialLogic>()
private val mixMaterialLogic = DefaultMixMaterialLogic(mixMaterialServiceMock)
private val mixMaterialLogic = DefaultMixMaterialLogic(mixMaterialServiceMock, materialLogicMock)
@AfterEach
internal fun afterEach() {

View File

@ -9,6 +9,7 @@ import io.mockk.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import kotlin.math.exp
import kotlin.test.assertEquals
class DefaultMixTypeLogicTest {
@ -39,84 +40,105 @@ class DefaultMixTypeLogicTest {
}
@Test
fun getOrCreateForNameAndMaterialType_notFound_returnsFromSaveForNameAndMaterialType() {
fun getOrCreateForNameAndMaterialType_notFound_callsSave() {
// Arrange
every { mixTypeServiceMock.getByNameAndMaterialType(any(), any()) } returns null
every { mixTypeLogic.saveForNameAndMaterialType(any(), any()) } returns mixType
every { materialLogicMock.save(any<MaterialDto>()) } returns material
every { mixTypeLogic.save(any()) } returnsArgument 0
val expectedMixType = mixType.copy(id = 0L)
// Act
mixTypeLogic.getOrCreateForNameAndMaterialType(mixType.name, materialType)
// Assert
verify {
mixTypeLogic.save(expectedMixType)
}
}
@Test
fun getOrCreateForNameAndMaterialType_notFound_returnsFromSave() {
// Arrange
every { mixTypeServiceMock.getByNameAndMaterialType(any(), any()) } returns null
every { materialLogicMock.save(any<MaterialDto>()) } returns material
every { mixTypeLogic.save(any()) } returnsArgument 0
val expectedMixType = mixType.copy(id = 0L)
// Act
val actualMixType = mixTypeLogic.getOrCreateForNameAndMaterialType(mixType.name, materialType)
// Assert
assertEquals(mixType, actualMixType)
assertEquals(expectedMixType, actualMixType)
}
@Test
fun saveForNameAndMaterialType_normalBehavior_callsSavesInMaterialLogic() {
fun updateOrCreateForNameAndMaterialType_mixTypeShared_callsSave() {
// Arrange
every { materialLogicMock.save(any<MaterialDto>()) } returnsArgument 0
every { mixTypeServiceMock.isShared(any()) } returns true
every { materialLogicMock.save(any<MaterialDto>()) } returns material
every { mixTypeLogic.save(any()) } returnsArgument 0
val expectedMixType = mixType.copy(id = 0L, name = "${mixType.name} updated")
// Act
mixTypeLogic.saveForNameAndMaterialType(mixType.name, materialType)
mixTypeLogic.updateOrCreateForNameAndMaterialType(mixType, expectedMixType.name, materialType)
// Assert
verify {
materialLogicMock.save(match<MaterialDto> { it.name == mixType.name && it.materialType == materialType })
mixTypeLogic.save(expectedMixType)
}
confirmVerified(materialLogicMock)
}
@Test
fun saveForNameAndMaterialType_normalBehavior_callsSave() {
fun updateOrCreateForNameAndMaterialType_mixTypeShared_returnsFromSave() {
// Arrange
every { materialLogicMock.save(any<MaterialDto>()) } returnsArgument 0
every { mixTypeServiceMock.isShared(any()) } returns true
every { materialLogicMock.save(any<MaterialDto>()) } returns material
every { mixTypeLogic.save(any()) } returnsArgument 0
val expectedMixType = mixType.copy(id = 0L, name = "${mixType.name} updated")
// Act
mixTypeLogic.saveForNameAndMaterialType(mixType.name, materialType)
val actualMixType = mixTypeLogic.updateOrCreateForNameAndMaterialType(mixType, expectedMixType.name, materialType)
// Assert
assertEquals(expectedMixType, actualMixType)
}
@Test
fun updateOrCreateForNameAndMaterialType_mixTypeNotShared_callsUpdate() {
// Arrange
every { mixTypeServiceMock.isShared(any()) } returns false
every { materialLogicMock.update(any<MaterialDto>()) } returns material
every { mixTypeLogic.update(any()) } returnsArgument 0
val expectedMixType = mixType.copy(name = "${mixType.name} updated")
// Act
mixTypeLogic.updateOrCreateForNameAndMaterialType(mixType, expectedMixType.name, materialType)
// Assert
verify {
mixTypeLogic.save(match { it.name == mixType.name && it.material.name == mixType.name && it.material.materialType == materialType })
mixTypeLogic.update(expectedMixType)
}
}
@Test
fun updateForNameAndMaterialType_normalBehavior_callsSavesInMaterialLogic() {
fun updateOrCreateForNameAndMaterialType_mixTypeNotShared_returnsFromUpdate() {
// Arrange
val updatedName = mixType.name + " updated"
val updatedMaterialType = materialType.copy(id = 2L)
every { materialLogicMock.update(any<MaterialDto>()) } returnsArgument 0
every { mixTypeServiceMock.isShared(any()) } returns false
every { materialLogicMock.update(any<MaterialDto>()) } returns material
every { mixTypeLogic.update(any()) } returnsArgument 0
// Act
mixTypeLogic.updateForNameAndMaterialType(mixType, updatedName, updatedMaterialType)
// Assert
verify {
materialLogicMock.update(match<MaterialDto> { it.name == updatedName && it.materialType == updatedMaterialType })
}
confirmVerified(materialLogicMock)
}
@Test
fun updateForNameAndMaterialType_normalBehavior_callsSave() {
// Arrange
val updatedName = mixType.name + " updated"
val updatedMaterialType = materialType.copy(id = 2L)
every { materialLogicMock.update(any<MaterialDto>()) } returnsArgument 0
every { mixTypeLogic.update(any()) } returnsArgument 0
val expectedMixType = mixType.copy(name = "${mixType.name} updated")
// Act
mixTypeLogic.updateForNameAndMaterialType(mixType, updatedName, updatedMaterialType)
val actualMixType = mixTypeLogic.updateOrCreateForNameAndMaterialType(mixType, expectedMixType.name, materialType)
// Assert
verify {
mixTypeLogic.update(match { it.name == updatedName && it.material.name == updatedName && it.material.materialType == updatedMaterialType })
}
assertEquals(expectedMixType, actualMixType)
}
@Test

View File

@ -1,182 +0,0 @@
package dev.fyloz.colorrecipesexplorer.logic
import com.nhaarman.mockitokotlin2.*
import dev.fyloz.colorrecipesexplorer.model.*
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.assertTrue
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class InventoryLogicTest {
private val materialLogic: MaterialLogic = mock()
private val mixLogic: MixLogic = mock()
private val logic = spy(DefaultInventoryLogic(materialLogic, mixLogic))
@AfterEach
fun afterEach() {
reset(materialLogic, logic)
}
// add()
@Test
fun `add(materialQuantities) calls add() for each MaterialQuantityDto`() {
val materialQuantities = listOf(
materialQuantityDto(materialId = 1, quantity = 1234f),
materialQuantityDto(materialId = 2, quantity = 2345f),
materialQuantityDto(materialId = 3, quantity = 3456f),
materialQuantityDto(materialId = 4, quantity = 4567f)
)
val storedQuantity = 2000f
doAnswer { storedQuantity + (it.arguments[0] as MaterialQuantityDto).quantity }.whenever(logic)
.add(any<MaterialQuantityDto>())
val found = logic.add(materialQuantities)
materialQuantities.forEach {
verify(logic).add(it)
assertTrue { found.any { updated -> updated.material == it.material && updated.quantity == storedQuantity + it.quantity } }
}
}
@Test
fun `add(materialQuantity) updates material's quantity`() {
withGivenQuantities(0f, 1000f) {
val updatedQuantity = it + this.quantity
whenever(materialLogic.updateQuantity(any(), eq(this.quantity))).doReturn(updatedQuantity)
val found = logic.add(this)
verify(materialLogic).updateQuantity(
argThat { this.id == this@withGivenQuantities.material },
eq(this.quantity)
)
assertEquals(updatedQuantity, found)
}
}
// deductMix()
@Test
fun `deductMix() calls deduct() with a collection of MaterialQuantityDto with adjusted quantities`() {
val material = material(id = 0L, materialType = materialType(usePercentages = false))
val materialPercents = material(id = 1L, materialType = materialType(usePercentages = true))
val mixRatio = mixRatio(ratio = 1.5f)
val mix = mix(
id = mixRatio.id,
mixMaterials = mutableSetOf(
mixMaterial(id = 0L, material = material, quantity = 1000f, position = 0),
mixMaterial(id = 1L, material = materialPercents, quantity = 50f, position = 1)
)
)
val expectedQuantities = mapOf(
0L to 1500f,
1L to 750f
)
whenever(mixLogic.getById(mix.id!!)).doReturn(mix)
doAnswer {
(it.arguments[0] as Collection<MaterialQuantityDto>).map { materialQuantity ->
materialQuantityDto(materialId = materialQuantity.material, quantity = 0f)
}
}.whenever(logic).deduct(any<Collection<MaterialQuantityDto>>())
val found = logic.deductMix(mixRatio)
verify(logic).deduct(argThat<Collection<MaterialQuantityDto>> {
this.all { it.quantity == expectedQuantities[it.material] }
})
assertEquals(expectedQuantities.size, found.size)
}
// deduct()
@Test
fun `deduct(materialQuantities) calls deduct() for each MaterialQuantityDto`() {
val materialQuantities = listOf(
materialQuantityDto(materialId = 1, quantity = 1234f),
materialQuantityDto(materialId = 2, quantity = 2345f),
materialQuantityDto(materialId = 3, quantity = 3456f),
materialQuantityDto(materialId = 4, quantity = 4567f)
)
val storedQuantity = 5000f
doAnswer { storedQuantity - (it.arguments[0] as MaterialQuantityDto).quantity }.whenever(logic)
.deduct(any<MaterialQuantityDto>())
val found = logic.deduct(materialQuantities)
materialQuantities.forEach {
verify(logic).deduct(it)
assertTrue { found.any { updated -> updated.material == it.material && updated.quantity == storedQuantity - it.quantity } }
}
}
@Test
fun `deduct(materialQuantities) throws MultiplesNotEnoughInventoryException when there is not enough inventory of a given material`() {
val materialQuantities = listOf(
materialQuantityDto(materialId = 1, quantity = 1234f),
materialQuantityDto(materialId = 2, quantity = 2345f),
materialQuantityDto(materialId = 3, quantity = 3456f),
materialQuantityDto(materialId = 4, quantity = 4567f)
)
val inventoryQuantity = 3000f
materialQuantities.forEach {
withGivenQuantities(inventoryQuantity, it)
}
assertThrows<MultiplesNotEnoughInventoryException> { logic.deduct(materialQuantities) }
}
@Test
fun `deduct(materialQuantity) updates material's quantity`() {
withGivenQuantities(5000f, 1000f) {
val updatedQuantity = it - this.quantity
whenever(materialLogic.updateQuantity(any(), eq(-this.quantity))).doReturn(updatedQuantity)
val found = logic.deduct(this)
verify(materialLogic).updateQuantity(
argThat { this.id == this@withGivenQuantities.material },
eq(-this.quantity)
)
assertEquals(updatedQuantity, found)
}
}
@Test
fun `deduct(materialQuantity) throws NotEnoughInventoryException when there is not enough inventory of the given material`() {
withGivenQuantities(0f, 1000f) {
assertThrows<NotEnoughInventoryException> { logic.deduct(this) }
}
}
private fun withGivenQuantities(
stored: Float,
quantity: Float,
materialId: Long = 0L,
test: MaterialQuantityDto.(Float) -> Unit = {}
) {
val materialQuantity = materialQuantityDto(materialId = materialId, quantity = quantity)
withGivenQuantities(stored, materialQuantity, test)
}
private fun withGivenQuantities(
stored: Float,
materialQuantity: MaterialQuantityDto,
test: MaterialQuantityDto.(Float) -> Unit = {}
) {
val material = material(id = materialQuantity.material, inventoryQuantity = stored)
whenever(materialLogic.getById(material.id!!)).doReturn(materialDto(material))
materialQuantity.test(stored)
}
}

View File

@ -1,245 +0,0 @@
package dev.fyloz.colorrecipesexplorer.logic
//@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,6 +1,7 @@
package dev.fyloz.colorrecipesexplorer.logic
import com.nhaarman.mockitokotlin2.*
import dev.fyloz.colorrecipesexplorer.dtos.MixLocationDto
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic
import dev.fyloz.colorrecipesexplorer.logic.files.CachedFile
@ -228,8 +229,8 @@ class RecipeLogicTest :
fun `updatePublicData() update the location of a recipe mixes in the mix logic according to the RecipePublicDataDto`() {
val publicData = recipePublicDataDto(
mixesLocation = setOf(
mixLocationDto(mixId = 0L, location = "Loc 1"),
mixLocationDto(mixId = 1L, location = "Loc 2")
MixLocationDto(mixId = 0L, location = "Loc 1"),
MixLocationDto(mixId = 1L, location = "Loc 2")
)
)