diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt new file mode 100644 index 0000000..6694fed --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt @@ -0,0 +1,14 @@ +package dev.fyloz.colorrecipesexplorer + +object Constants { + object ControllerPaths { + const val file = "/api/file" + const val material = "/api/material" + } + + object FilePaths { + const val pdfs = "pdf" + + const val simdut = "$pdfs/simdut" + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/CompanyDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/CompanyDto.kt index 3596d65..520f738 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/CompanyDto.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/CompanyDto.kt @@ -5,6 +5,6 @@ import javax.validation.constraints.NotBlank data class CompanyDto( override val id: Long = 0L, - @NotBlank + @field:NotBlank val name: String ) : EntityDto \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt new file mode 100644 index 0000000..45c6b15 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt @@ -0,0 +1,34 @@ +package dev.fyloz.colorrecipesexplorer.dtos + +import dev.fyloz.colorrecipesexplorer.model.MaterialType +import org.springframework.web.multipart.MultipartFile +import javax.validation.constraints.Min +import javax.validation.constraints.NotBlank + +data class MaterialDto( + override val id: Long = 0L, + + val name: String, + + val inventoryQuantity: Float, + + val isMixType: Boolean, + + val materialType: MaterialType, + + val simdutUrl: String? = null +) : EntityDto + +data class MaterialSaveDto( + override val id: Long = 0L, + + @field:NotBlank + val name: String, + + @field:Min(0) + val inventoryQuantity: Float, + + val materialTypeId: Long, + + val simdutFile: MultipartFile? +) : EntityDto diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogic.kt index fe2b04e..ea323ad 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogic.kt @@ -23,7 +23,7 @@ class DefaultCompanyLogic(service: CompanyService) : } override fun deleteById(id: Long) { - if (service.recipesDependsOnCompanyById(id)) { + if (service.isUsedByRecipe(id)) { throw cannotDeleteException("Cannot delete the company with the id '$id' because one or more recipes depends on it") } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogic.kt index ea7bafe..f43f1e6 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogic.kt @@ -1,6 +1,7 @@ package dev.fyloz.colorrecipesexplorer.logic import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase +import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto import dev.fyloz.colorrecipesexplorer.exception.RestException import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.utils.mapMayThrow @@ -89,7 +90,7 @@ class DefaultInventoryLogic( } } -class NotEnoughInventoryException(quantity: Float, material: Material) : +class NotEnoughInventoryException(quantity: Float, material: MaterialDto) : RestException( "notenoughinventory", "Not enough inventory", diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogic.kt index 5f877cb..7f3027f 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogic.kt @@ -1,145 +1,120 @@ package dev.fyloz.colorrecipesexplorer.logic -import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase -import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic +import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent +import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto +import dev.fyloz.colorrecipesexplorer.dtos.MaterialSaveDto import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic -import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.repository.MaterialRepository -import dev.fyloz.colorrecipesexplorer.rest.FILE_CONTROLLER_PATH -import io.jsonwebtoken.lang.Assert -import org.springframework.context.annotation.Lazy -import org.springframework.stereotype.Service -import java.net.URLEncoder -import java.nio.charset.StandardCharsets +import dev.fyloz.colorrecipesexplorer.model.Material +import dev.fyloz.colorrecipesexplorer.service.MaterialService -interface MaterialLogic : - ExternalNamedModelService { - /** Checks if a material with the given [materialType] exists. */ - fun existsByMaterialType(materialType: MaterialType): Boolean - - /** Checks if the given [material] has a SIMDUT file. */ - fun hasSimdut(material: Material): Boolean +interface MaterialLogic : Logic { + /** Checks if a material with the given [name] exists. */ + fun existsByName(name: String): Boolean /** Gets all materials that are not a mix type. */ - fun getAllNotMixType(): Collection + fun getAllNotMixType(): Collection - /** Gets all materials available for the creation of a mix for the recipe with the given [recipeId], including normal materials and materials from [MixType]s included in the said recipe. */ - fun getAllForMixCreation(recipeId: Long): Collection + /** + * Gets all materials available for the creation of a mix for the recipe with the given [recipeId], + * including normal materials and materials from mix types included in the said recipe. + */ + fun getAllForMixCreation(recipeId: Long): Collection - /** Gets all materials available for updating the mix with the given [mixId], including normal materials and materials from [MixType]s included in the mix recipe, excluding the material of the [MixType] of the said mix. */ - fun getAllForMixUpdate(mixId: Long): Collection + /** + * Gets all materials available for updating the mix with the given [mixId], + * including normal materials and materials from mix types included in the mix recipe + * and excluding the material of the mix type of the said mix. + */ + fun getAllForMixUpdate(mixId: Long): Collection + + /** Saves the given [dto]. */ + fun save(dto: MaterialSaveDto): MaterialDto + + /** Updates the given [dto]. */ + fun update(dto: MaterialSaveDto): MaterialDto /** Updates the quantity of the given [material] with the given [factor] and returns the updated quantity. */ - fun updateQuantity(material: Material, factor: Float): Float + fun updateQuantity(material: MaterialDto, factor: Float): Float } -@Service -@RequireDatabase +@LogicComponent class DefaultMaterialLogic( - materialRepository: MaterialRepository, + service: MaterialService, val recipeLogic: RecipeLogic, val mixLogic: MixLogic, - @Lazy val materialTypeLogic: MaterialTypeLogic, - val fileService: WriteableFileLogic, - val configService: ConfigurationLogic -) : - AbstractExternalNamedModelService( - materialRepository - ), - MaterialLogic { - override fun idNotFoundException(id: Long) = materialIdNotFoundException(id) - override fun idAlreadyExistsException(id: Long) = materialIdAlreadyExistsException(id) - override fun nameNotFoundException(name: String) = materialNameNotFoundException(name) - override fun nameAlreadyExistsException(name: String) = materialNameAlreadyExistsException(name) + val materialTypeLogic: MaterialTypeLogic, + val fileLogic: WriteableFileLogic +) : BaseLogic(service, Material::class.simpleName!!), MaterialLogic { + override fun existsByName(name: String) = service.existsByName(name, null) + override fun getAllNotMixType() = service.getAllNotMixType() - override fun Material.toOutput(): MaterialOutputDto = - MaterialOutputDto( - id = this.id!!, - name = this.name, - inventoryQuantity = this.inventoryQuantity, - isMixType = this.isMixType, - materialType = this.materialType!!, - simdutUrl = if (fileService.exists(this.simdutFilePath)) - "${configService.getContent(ConfigurationType.INSTANCE_URL)}$FILE_CONTROLLER_PATH?path=${ - URLEncoder.encode( - this.simdutFilePath, - StandardCharsets.UTF_8 - ) - }" - else null - ) - - override fun existsByMaterialType(materialType: MaterialType): Boolean = - repository.existsByMaterialType(materialType) - - override fun hasSimdut(material: Material): Boolean = fileService.exists(material.simdutFilePath) - override fun getAllNotMixType(): Collection = getAllForOutput().filter { !it.isMixType } - - override fun save(entity: MaterialSaveDto): Material = - save(with(entity) { - material( - name = entity.name, - inventoryQuantity = entity.inventoryQuantity, - materialType = materialTypeLogic.getById(materialTypeId), - isMixType = false - ) - }).apply { - if (entity.simdutFile != null && !entity.simdutFile.isEmpty) fileService.write( - entity.simdutFile, - this.simdutFilePath, - false - ) - } - - override fun update(entity: MaterialUpdateDto): Material { - val persistedMaterial by lazy { - getById(entity.id).apply { assertPersistedMaterial(this) } - } - - return update(with(entity) { - material( - id = id, - name = if (name != null && name.isNotBlank()) name else persistedMaterial.name, - inventoryQuantity = if (inventoryQuantity != null && inventoryQuantity != Float.MIN_VALUE) inventoryQuantity else persistedMaterial.inventoryQuantity, - isMixType = persistedMaterial.isMixType, - materialType = if (materialTypeId != null) materialTypeLogic.getById(materialTypeId) else persistedMaterial.materialType - ) - }).apply { - if (entity.simdutFile != null && !entity.simdutFile.isEmpty) fileService.write( - entity.simdutFile, - this.simdutFilePath, - true - ) - } - } - - override fun updateQuantity(material: Material, factor: Float) = with(material) { - val updatedQuantity = this.inventoryQuantity + factor - repository.updateInventoryQuantityById(this.id!!, updatedQuantity) - updatedQuantity - } - - override fun getAllForMixCreation(recipeId: Long): Collection { + override fun getAllForMixCreation(recipeId: Long): Collection { val recipesMixTypes = recipeLogic.getById(recipeId).mixTypes - return getAllForOutput() - .filter { !it.isMixType || recipesMixTypes.any { mixType -> mixType.material.id == it.id } } + + return getAll().filter { !it.isMixType || recipesMixTypes.any { mixType -> mixType.material.id == it.id } } } - override fun getAllForMixUpdate(mixId: Long): Collection { + override fun getAllForMixUpdate(mixId: Long): Collection { val mix = mixLogic.getById(mixId) val recipesMixTypes = mix.recipe.mixTypes - return getAllForOutput() - .filter { !it.isMixType || recipesMixTypes.any { mixType -> mixType.material.id == it.id } } + + return getAll().filter { !it.isMixType || recipesMixTypes.any { mixType -> mixType.material.id == it.id } } .filter { it.id != mix.mixType.material.id } } - private fun assertPersistedMaterial(material: Material) { - Assert.notNull(material.name, "The persisted material with the id ${material.id} has a null name") + override fun save(dto: MaterialSaveDto) = save(saveDtoToDto(dto, false)).also { saveSimdutFile(dto, false) } + override fun save(dto: MaterialDto): MaterialDto { + throwIfNameAlreadyExists(dto.name) + + return super.save(dto) } - override fun delete(entity: Material) { - if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteMaterialException(entity) - if (fileService.exists(entity.simdutFilePath)) fileService.delete(entity.simdutFilePath) - super.delete(entity) + override fun update(dto: MaterialSaveDto) = update(saveDtoToDto(dto, true)).also { saveSimdutFile(dto, true) } + override fun update(dto: MaterialDto): MaterialDto { + throwIfNameAlreadyExists(dto.name, dto.id) + + return super.update(dto) } -} + + override fun updateQuantity(material: MaterialDto, factor: Float): Float { + val updatedQuantity = material.inventoryQuantity + factor + service.updateInventoryQuantityById(material.id, updatedQuantity) + + return updatedQuantity + } + + override fun deleteById(id: Long) { + if (service.isUsedByMixMaterialOrMixType(id)) { + throw cannotDeleteException("Cannot delete the material with the id '$id' because mix types and/or recipes depends on it") + } + + val material = getById(id) + val simdutPath = Material.getSimdutFilePath(material.name) + if (fileLogic.exists(simdutPath)) { + fileLogic.delete(simdutPath) + } + + super.deleteById(id) + } + + private fun saveDtoToDto(saveDto: MaterialSaveDto, updating: Boolean): MaterialDto { + val isMixType = !updating || getById(saveDto.id).isMixType + val materialType = materialTypeLogic.getById(saveDto.materialTypeId) + + return MaterialDto(saveDto.id, saveDto.name, saveDto.inventoryQuantity, isMixType, materialType, null) + } + + private fun saveSimdutFile(dto: MaterialSaveDto, updating: Boolean) { + val file = dto.simdutFile + + if (file != null && !file.isEmpty) { + fileLogic.write(file, Material.getSimdutFilePath(dto.name), updating) + } + } + + private fun throwIfNameAlreadyExists(name: String, id: Long? = null) { + if (service.existsByName(name, id)) { + throw alreadyExistsException(value = name) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogic.kt index b3cb0eb..833afc6 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogic.kt @@ -11,9 +11,6 @@ interface MaterialTypeLogic : /** Checks if a material type with the given [prefix] exists. */ fun existsByPrefix(prefix: String): Boolean - /** Checks if the given [materialType] is used by one or more materials. */ - fun isUsedByMaterial(materialType: MaterialType): Boolean - /** Gets all system material types. */ fun getAllSystemTypes(): Collection @@ -26,7 +23,7 @@ interface MaterialTypeLogic : @Service @RequireDatabase -class DefaultMaterialTypeLogic(repository: MaterialTypeRepository, private val materialLogic: MaterialLogic) : +class DefaultMaterialTypeLogic(repository: MaterialTypeRepository) : AbstractExternalNamedModelService( repository ), MaterialTypeLogic { @@ -38,8 +35,6 @@ class DefaultMaterialTypeLogic(repository: MaterialTypeRepository, private val m override fun MaterialType.toOutput() = this override fun existsByPrefix(prefix: String): Boolean = repository.existsByPrefix(prefix) - override fun isUsedByMaterial(materialType: MaterialType): Boolean = - materialLogic.existsByMaterialType(materialType) override fun getAllSystemTypes(): Collection = repository.findAllBySystemTypeIs(true) override fun getAllNonSystemType(): Collection = repository.findAllBySystemTypeIs(false) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt index 3aea9df..913016a 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt @@ -44,7 +44,7 @@ class DefaultMixMaterialLogic( override fun MixMaterial.toOutput() = MixMaterialOutputDto( this.id!!, - with(materialLogic) { this@toOutput.material.toOutput() }, + this.material, this.quantity, this.position ) @@ -55,7 +55,7 @@ class DefaultMixMaterialLogic( override fun create(mixMaterial: MixMaterialDto): MixMaterial = mixMaterial( - material = materialLogic.getById(mixMaterial.materialId), + material = material(materialLogic.getById(mixMaterial.materialId)), quantity = mixMaterial.quantity, position = mixMaterial.position ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt index bbc241a..3ec9b08 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt @@ -32,27 +32,27 @@ class DefaultMixTypeLogic( mixTypeRepository: MixTypeRepository, @Lazy val materialLogic: MaterialLogic ) : - AbstractNamedModelService(mixTypeRepository), MixTypeLogic { + AbstractNamedModelService(mixTypeRepository), MixTypeLogic { override fun idNotFoundException(id: Long) = mixTypeIdNotFoundException(id) override fun idAlreadyExistsException(id: Long) = mixTypeIdAlreadyExistsException(id) override fun nameNotFoundException(name: String) = mixTypeNameNotFoundException(name) override fun nameAlreadyExistsException(name: String) = mixTypeNameAlreadyExistsException(name) override fun existsByNameAndMaterialType(name: String, materialType: MaterialType): Boolean = - repository.existsByNameAndMaterialType(name, materialType) + repository.existsByNameAndMaterialType(name, materialType) override fun getByMaterial(material: Material): MixType = - repository.findByMaterial(material) ?: throw nameNotFoundException(material.name) + repository.findByMaterial(material) ?: throw nameNotFoundException(material.name) override fun getByNameAndMaterialType(name: String, materialType: MaterialType): MixType = - repository.findByNameAndMaterialType(name, materialType) - ?: throw MixTypeNameAndMaterialTypeNotFoundException(name, materialType) + repository.findByNameAndMaterialType(name, materialType) + ?: throw MixTypeNameAndMaterialTypeNotFoundException(name, materialType) override fun getOrCreateForNameAndMaterialType(name: String, materialType: MaterialType): MixType = - if (existsByNameAndMaterialType(name, materialType)) - getByNameAndMaterialType(name, materialType) - else - saveForNameAndMaterialType(name, materialType) + if (existsByNameAndMaterialType(name, materialType)) + getByNameAndMaterialType(name, materialType) + else + saveForNameAndMaterialType(name, materialType) override fun save(entity: MixType): MixType { if (materialLogic.existsByName(entity.name)) @@ -61,24 +61,20 @@ class DefaultMixTypeLogic( } override fun saveForNameAndMaterialType(name: String, materialType: MaterialType): MixType = - save( - mixType( - name = name, - material = material( - name = name, - inventoryQuantity = Float.MIN_VALUE, - isMixType = true, - materialType = materialType - ) + save( + mixType( + name = name, + material = material( + name = name, + inventoryQuantity = Float.MIN_VALUE, + isMixType = true, + materialType = materialType + ) + ) ) - ) override fun updateForNameAndMaterialType(mixType: MixType, name: String, materialType: MaterialType): MixType = - update(mixType.apply { - this.name = name - material.name = name - material.materialType = materialType - }) + update(mixType.copy(material = mixType.material.copy(name = name, materialType = materialType))) override fun delete(entity: MixType) { if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteMixTypeException(entity) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt index 3513570..bab6b2c 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt @@ -1,13 +1,12 @@ package dev.fyloz.colorrecipesexplorer.model -import com.fasterxml.jackson.annotation.JsonIgnore +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 org.springframework.web.multipart.MultipartFile import javax.persistence.* import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank const val SIMDUT_FILES_PATH = "pdf/simdut" @@ -19,59 +18,24 @@ data class Material( override val id: Long?, @Column(unique = true) - override var name: String, + val name: String, @Column(name = "inventory_quantity") - var inventoryQuantity: Float, + val inventoryQuantity: Float, @Column(name = "mix_type") val isMixType: Boolean, @ManyToOne @JoinColumn(name = "material_type_id") - var materialType: MaterialType? -) : NamedModelEntity { - val simdutFilePath - @JsonIgnore - @Transient - get() = "$SIMDUT_FILES_PATH/$name.pdf" + val materialType: MaterialType? +) : ModelEntity { + companion object { + fun getSimdutFilePath(name: String) = + "${Constants.FilePaths.simdut}/$name.pdf" + } } -open class MaterialSaveDto( - @field:NotBlank - val name: String, - - @field:Min(0, message = VALIDATION_SIZE_GE_ZERO) - val inventoryQuantity: Float, - - val materialTypeId: Long, - - val simdutFile: MultipartFile? = null -) : EntityDto - -open class MaterialUpdateDto( - val id: Long, - - @field:NotBlank - val name: String?, - - @field:Min(0, message = VALIDATION_SIZE_GE_ZERO) - val inventoryQuantity: Float?, - - val materialTypeId: Long?, - - val simdutFile: MultipartFile? = null -) : EntityDto - -data class MaterialOutputDto( - override val id: Long, - val name: String, - val inventoryQuantity: Float, - val isMixType: Boolean, - val materialType: MaterialType, - val simdutUrl: String? -) : ModelEntity - data class MaterialQuantityDto( val material: Long, @@ -99,22 +63,15 @@ fun material( ?: material.name, material.inventoryQuantity, material.isMixType, material.materialType ) -fun materialSaveDto( - name: String = "name", - inventoryQuantity: Float = 0f, - materialTypeId: Long = 0L, - simdutFile: MultipartFile? = null, - op: MaterialSaveDto.() -> Unit = {} -) = MaterialSaveDto(name, inventoryQuantity, materialTypeId, simdutFile).apply(op) +@Deprecated("Temporary DSL for transition") +fun material( + dto: MaterialDto +) = Material(dto.id, dto.name, dto.inventoryQuantity, dto.isMixType, dto.materialType) -fun materialUpdateDto( - id: Long = 0L, - name: String? = "name", - inventoryQuantity: Float? = 0f, - materialTypeId: Long? = 0L, - simdutFile: MultipartFile? = null, - op: MaterialUpdateDto.() -> Unit = {} -) = MaterialUpdateDto(id, name, inventoryQuantity, materialTypeId, simdutFile).apply(op) +@Deprecated("Temporary DSL for transition") +fun materialDto( + entity: Material +) = MaterialDto(entity.id!!, entity.name, entity.inventoryQuantity, entity.isMixType, entity.materialType!!) fun materialQuantityDto( materialId: Long, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt index afe56b2..7a91003 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt @@ -32,7 +32,7 @@ data class MixMaterialDto( data class MixMaterialOutputDto( val id: Long, - val material: MaterialOutputDto, + val material: Material, // TODO move to MaterialDto val quantity: Float, val position: Int ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt index 2e56962..dfdee70 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt @@ -1,16 +1,19 @@ package dev.fyloz.colorrecipesexplorer.model import com.fasterxml.jackson.annotation.JsonIgnore +import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.model.account.Group import dev.fyloz.colorrecipesexplorer.model.account.group -import dev.fyloz.colorrecipesexplorer.rest.FILE_CONTROLLER_PATH import java.net.URLEncoder import java.nio.charset.StandardCharsets import java.time.LocalDate import javax.persistence.* -import javax.validation.constraints.* +import javax.validation.constraints.Max +import javax.validation.constraints.Min +import javax.validation.constraints.NotBlank +import javax.validation.constraints.Pattern private const val VALIDATION_COLOR_PATTERN = "^#([0-9a-f]{6})$" @@ -67,7 +70,7 @@ data class Recipe( groupsInformation.firstOrNull { it.group.id == groupId } fun imageUrl(deploymentUrl: String, name: String) = - "$deploymentUrl$FILE_CONTROLLER_PATH?path=${ + "$deploymentUrl${Constants.ControllerPaths.file}?path=${ URLEncoder.encode( "${this.imagesDirectoryPath}/$name", StandardCharsets.UTF_8 diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/CompanyRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/CompanyRepository.kt index 963e80d..484bd4d 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/CompanyRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/CompanyRepository.kt @@ -7,7 +7,7 @@ import org.springframework.stereotype.Repository @Repository interface CompanyRepository : JpaRepository { - /** Checks if a company with the given [name] and an id different from the given [id] exists. */ + /** Checks if a company with the given [name] and a different [id] exists. */ fun existsByNameAndIdNot(name: String, id: Long): Boolean /** Checks if a recipe depends on the company with the given [id]. */ @@ -17,5 +17,5 @@ interface CompanyRepository : JpaRepository { from Recipe r where r.company.id = :id """ ) - fun recipesDependsOnCompanyById(id: Long): Boolean + fun isUsedByRecipe(id: Long): Boolean } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MaterialRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MaterialRepository.kt index 03ed9c3..5125087 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MaterialRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MaterialRepository.kt @@ -1,29 +1,33 @@ package dev.fyloz.colorrecipesexplorer.repository import dev.fyloz.colorrecipesexplorer.model.Material -import dev.fyloz.colorrecipesexplorer.model.MaterialType +import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository @Repository -interface MaterialRepository : NamedJpaRepository { - /** Checks if one or more materials have the given [materialType]. */ - fun existsByMaterialType(materialType: MaterialType): Boolean +interface MaterialRepository : JpaRepository { + /** Checks if a material with the given [name] and a different [id] exists. */ + fun existsByNameAndIdNot(name: String, id: Long): Boolean + + /** Gets all non mix type materials. */ + fun getAllByIsMixTypeIsFalse(): Collection /** Updates the [inventoryQuantity] of the [Material] with the given [id]. */ @Modifying @Query("UPDATE Material m SET m.inventoryQuantity = :inventoryQuantity WHERE m.id = :id") fun updateInventoryQuantityById(id: Long, inventoryQuantity: Float) + /** Checks if a mix material or a mix type depends on the material with the given [id]. */ @Query( - """ + """ select case when(count(mm.id) + count(mt.id) > 0) then false else true end from Material m - left join MixMaterial mm on m.id = mm.material.id - left join MixType mt on m.id = mt.material.id + left join MixMaterial mm on mm.material.id = m.id + left join MixType mt on mt.material.id = m.id where m.id = :id - """ + """ ) - fun canBeDeleted(id: Long): Boolean + fun isUsedByMixMaterialOrMixType(id: Long): Boolean } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/FileController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/FileController.kt index 5d6aa84..346b195 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/FileController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/FileController.kt @@ -1,5 +1,6 @@ package dev.fyloz.colorrecipesexplorer.rest +import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic import dev.fyloz.colorrecipesexplorer.model.ConfigurationType @@ -10,10 +11,8 @@ import org.springframework.web.bind.annotation.* import org.springframework.web.multipart.MultipartFile import java.net.URI -const val FILE_CONTROLLER_PATH = "/api/file" - @RestController -@RequestMapping(FILE_CONTROLLER_PATH) +@RequestMapping(Constants.ControllerPaths.file) class FileController( private val fileLogic: WriteableFileLogic, private val configurationLogic: ConfigurationLogic @@ -44,6 +43,6 @@ class FileController( private fun created(path: String): ResponseEntity = ResponseEntity - .created(URI.create("${configurationLogic.get(ConfigurationType.INSTANCE_URL)}$FILE_CONTROLLER_PATH?path=$path")) + .created(URI.create("${configurationLogic.get(ConfigurationType.INSTANCE_URL)}${Constants.ControllerPaths.file}?path=$path")) .build() } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt index 9401f28..eb29623 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt @@ -1,8 +1,10 @@ package dev.fyloz.colorrecipesexplorer.rest +import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewCatalog +import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto +import dev.fyloz.colorrecipesexplorer.dtos.MaterialSaveDto import dev.fyloz.colorrecipesexplorer.logic.MaterialLogic -import dev.fyloz.colorrecipesexplorer.model.* import org.springframework.context.annotation.Profile import org.springframework.http.MediaType import org.springframework.security.access.prepost.PreAuthorize @@ -10,10 +12,8 @@ import org.springframework.web.bind.annotation.* import org.springframework.web.multipart.MultipartFile import javax.validation.Valid -private const val MATERIAL_CONTROLLER_PATH = "api/material" - @RestController -@RequestMapping(MATERIAL_CONTROLLER_PATH) +@RequestMapping(Constants.ControllerPaths.material) @Profile("!emergency") @PreAuthorizeViewCatalog class MaterialController( @@ -21,7 +21,7 @@ class MaterialController( ) { @GetMapping fun getAll() = - ok(materialLogic.getAllForOutput()) + ok(materialLogic.getAll()) @GetMapping("notmixtype") fun getAllNotMixType() = @@ -29,37 +29,20 @@ class MaterialController( @GetMapping("{id}") fun getById(@PathVariable id: Long) = - ok(materialLogic.getByIdForOutput(id)) + ok(materialLogic.getById(id)) @PostMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) @PreAuthorize("hasAuthority('EDIT_MATERIALS')") fun save(@Valid material: MaterialSaveDto, simdutFile: MultipartFile?) = - created(MATERIAL_CONTROLLER_PATH) { - with(materialLogic) { - save( - materialSaveDto( - name = material.name, - inventoryQuantity = material.inventoryQuantity, - materialTypeId = material.materialTypeId, - simdutFile = simdutFile - ) - ).toOutput() - } + created(Constants.ControllerPaths.material) { + materialLogic.save(material.copy(simdutFile = simdutFile)) } @PutMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) @PreAuthorize("hasAuthority('EDIT_MATERIALS')") - fun update(@Valid material: MaterialUpdateDto, simdutFile: MultipartFile?) = + fun update(@Valid material: MaterialSaveDto, simdutFile: MultipartFile?) = noContent { - materialLogic.update( - materialUpdateDto( - id = material.id, - name = material.name, - inventoryQuantity = material.inventoryQuantity, - materialTypeId = material.materialTypeId, - simdutFile = simdutFile - ) - ) + materialLogic.update(material.copy(simdutFile = simdutFile)) } @DeleteMapping("{id}") diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt index c65b91c..70057e4 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt @@ -10,14 +10,14 @@ interface CompanyService : Service { fun existsByName(name: String, id: Long?): Boolean /** Checks if a recipe depends on the company with the given [id]. */ - fun recipesDependsOnCompanyById(id: Long): Boolean + fun isUsedByRecipe(id: Long): Boolean } @ServiceComponent class DefaultCompanyService(repository: CompanyRepository) : BaseService(repository), CompanyService { override fun existsByName(name: String, id: Long?) = repository.existsByNameAndIdNot(name, id ?: 0) - override fun recipesDependsOnCompanyById(id: Long) = repository.recipesDependsOnCompanyById(id) + override fun isUsedByRecipe(id: Long) = repository.isUsedByRecipe(id) override fun toDto(entity: Company) = CompanyDto(entity.id!!, entity.name) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt new file mode 100644 index 0000000..1f3af3b --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt @@ -0,0 +1,58 @@ +package dev.fyloz.colorrecipesexplorer.service + +import dev.fyloz.colorrecipesexplorer.Constants +import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent +import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto +import dev.fyloz.colorrecipesexplorer.logic.files.FileLogic +import dev.fyloz.colorrecipesexplorer.model.Material +import dev.fyloz.colorrecipesexplorer.repository.MaterialRepository +import org.springframework.beans.factory.annotation.Qualifier +import java.net.URLEncoder +import java.nio.charset.StandardCharsets + +interface MaterialService : Service { + /** Checks if a material with the given [name] and a different [id] exists. */ + fun existsByName(name: String, id: Long?): Boolean + + /** Gets all non mix type materials. */ + fun getAllNotMixType(): Collection + + /** Updates the [inventoryQuantity] of the [Material] with the given [id]. */ + fun updateInventoryQuantityById(id: Long, inventoryQuantity: Float) + + /** Checks if a mix material or a mix type depends on the material with the given [id]. */ + fun isUsedByMixMaterialOrMixType(id: Long): Boolean +} + +@ServiceComponent +class DefaultMaterialService(repository: MaterialRepository, @Qualifier("defaultFileLogic") val fileLogic: FileLogic) : + BaseService(repository), MaterialService { + override fun existsByName(name: String, id: Long?) = repository.existsByNameAndIdNot(name, id ?: 0) + override fun getAllNotMixType() = repository.getAllByIsMixTypeIsFalse().map(this::toDto) + override fun updateInventoryQuantityById(id: Long, inventoryQuantity: Float) = repository.updateInventoryQuantityById(id, inventoryQuantity) + override fun isUsedByMixMaterialOrMixType(id: Long) = repository.isUsedByMixMaterialOrMixType(id) + + override fun toDto(entity: Material) = + MaterialDto( + entity.id!!, + entity.name, + entity.inventoryQuantity, + entity.isMixType, + entity.materialType!!, + getSimdutUrl(entity) + ) + + override fun toEntity(dto: MaterialDto) = + Material(dto.id, dto.name, dto.inventoryQuantity, dto.isMixType, dto.materialType) + + private fun getSimdutUrl(material: Material): String? { + val filePath = "${Constants.FilePaths.simdut}/${material.name}.pdf" + + if (!fileLogic.exists(filePath)) { + return null + } + + val encodedPath = URLEncoder.encode(filePath, StandardCharsets.UTF_8) + return "${Constants.ControllerPaths.file}?path=$encodedPath" + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultCompanyLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultCompanyLogicTest.kt index 6049ae9..0cde4a3 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultCompanyLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultCompanyLogicTest.kt @@ -46,7 +46,7 @@ class DefaultCompanyLogicTest { @Test fun deleteById_recipesDependsOnCompany_throwsCannotDeleteException() { // Arrange - every { companyServiceMock.recipesDependsOnCompanyById(company.id) } returns true + every { companyServiceMock.isUsedByRecipe(company.id) } returns true // Act // Assert diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialLogicTest.kt new file mode 100644 index 0000000..7cba942 --- /dev/null +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialLogicTest.kt @@ -0,0 +1,382 @@ +package dev.fyloz.colorrecipesexplorer.logic + +import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto +import dev.fyloz.colorrecipesexplorer.dtos.MaterialSaveDto +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.service.MaterialService +import io.mockk.* +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.mock.web.MockMultipartFile +import org.springframework.web.multipart.MultipartFile +import kotlin.test.assertContains +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class DefaultMaterialLogicTest { + private val materialServiceMock = mockk() + private val recipeLogicMock = mockk() + private val mixLogicMock = mockk() + private val materialTypeLogicMock = mockk() + private val fileLogicMock = mockk() + + private val materialLogic = spyk( + DefaultMaterialLogic( + materialServiceMock, recipeLogicMock, mixLogicMock, materialTypeLogicMock, fileLogicMock + ) + ) + + private val materialType = MaterialType( + 1L, "Unit test material type", "UNT", usePercentages = false, systemType = false + ) // TODO move to DTO + private val material = MaterialDto(1L, "Unit test material", 1000f, false, materialType) + private val materialMixType = material.copy(id = 2L, isMixType = true) + private val materialMixType2 = material.copy(id = 3L, isMixType = true) + private val company = Company(1L, "Unit test company") + private val recipe = Recipe( + 1L, + "Unit test recipe", + "Unit test recipe", + "#FFFFFF", + 0, + 123, + null, + "A remark", + company, + mutableListOf(), + setOf() + ) + private val mix = Mix( + 1L, "location", recipe, mixType = MixType(1L, "Unit test mix type", material(materialMixType)), mutableSetOf() + ) + private val mix2 = mix.copy(id = 2L, mixType = mix.mixType.copy(id = 2L, material = material(materialMixType2))) + + private val simdutFileMock = MockMultipartFile( + "Unit test SIMDUT", + byteArrayOf(1, 2, 3, 4) + ) // Put some content in the mock file so it is not ignored + private val materialSaveDto = MaterialSaveDto(1L, "Unit test material", 1000f, materialType.id!!, simdutFileMock) + + init { + recipe.mixes.addAll(listOf(mix, mix2)) + } + + @AfterEach + internal fun afterEach() { + clearAllMocks() + } + + @Test + fun existsByName_normalBehavior_returnsTrue() { + // Arrange + every { materialServiceMock.existsByName(any(), any()) } returns true + + // Act + val exists = materialLogic.existsByName(material.name) + + // Assert + assertTrue(exists) + } + + @Test + fun existsByName_notFound_returnsFalse() { + // Arrange + every { materialServiceMock.existsByName(any(), any()) } returns false + + // Act + val exists = materialLogic.existsByName(material.name) + + // Assert + assertFalse(exists) + } + + @Test + fun getAllNotMixType_normalBehavior_returnsMaterialsFromService() { + // Arrange + every { materialServiceMock.getAllNotMixType() } returns listOf(material) + + // Act + val materials = materialLogic.getAllNotMixType() + + // Assert + assertContains(materials, material) + } + + @Test + fun getAllForMixCreation_normalBehavior_returnsNonMixTypeMaterials() { + // Arrange + every { materialLogic.getAll() } returns listOf(material, materialMixType2) + every { recipeLogicMock.getById(any()) } returns recipe + + // Act + val materials = materialLogic.getAllForMixCreation(recipe.id!!) + + // Assert + assertContains(materials, material) + } + + @Test + fun getAllForMixCreation_normalBehavior_returnsRecipeMixTypesMaterials() { + // Arrange + every { materialLogic.getAll() } returns listOf(material, materialMixType2) + every { recipeLogicMock.getById(any()) } returns recipe + + // Act + val materials = materialLogic.getAllForMixCreation(recipe.id!!) + + // Assert + assertContains(materials, materialMixType2) + } + + @Test + fun getAllForMixUpdate_normalBehavior_returnsNonMixTypeMaterials() { + // Arrange + every { materialLogic.getAll() } returns listOf(material, materialMixType, materialMixType2) + every { mixLogicMock.getById(any()) } returns mix + + // Act + val materials = materialLogic.getAllForMixUpdate(mix.id!!) + + // Assert + assertContains(materials, material) + } + + @Test + fun getAllForMixUpdate_normalBehavior_returnsRecipeMixTypesMaterials() { + // Arrange + every { materialLogic.getAll() } returns listOf(material, materialMixType, materialMixType2) + every { mixLogicMock.getById(any()) } returns mix + + // Act + val materials = materialLogic.getAllForMixUpdate(mix.id!!) + + // Assert + assertContains(materials, materialMixType2) + } + + @Test + fun getAllForMixUpdate_normalBehavior_excludesGivenMixTypeMaterial() { + // Arrange + every { materialLogic.getAll() } returns listOf(material, materialMixType, materialMixType2) + every { mixLogicMock.getById(any()) } returns mix + + // Act + val materials = materialLogic.getAllForMixUpdate(mix.id!!) + + // Assert + assertFalse { materialMixType in materials } + } + + @Test + fun save_materialSaveDto_normalBehavior_callsSave() { + // Arrange + every { materialLogic.save(any()) } returns material + every { materialTypeLogicMock.getById(any()) } returns materialType + every { fileLogicMock.write(any(), any(), any()) } just runs + + // Act + materialLogic.save(materialSaveDto) + + // Assert + verify { + materialLogic.save(any()) + } + } + + @Test + fun save_materialSaveDto_normalBehavior_callsWriteInFileService() { + // Arrange + every { materialLogic.save(any()) } returns material + every { materialTypeLogicMock.getById(any()) } returns materialType + every { fileLogicMock.write(any(), any(), any()) } just runs + + // Act + materialLogic.save(materialSaveDto) + + // Assert + verify { + fileLogicMock.write(simdutFileMock, any(), false) + } + confirmVerified(fileLogicMock) + } + + @Test + fun save_materialSaveDto_noSimdutFile_doesNotCallWriteInFileService() { + // Arrange + every { materialLogic.save(any()) } returns material + every { materialTypeLogicMock.getById(any()) } returns materialType + every { fileLogicMock.write(any(), any(), any()) } just runs + + val saveDto = materialSaveDto.copy(simdutFile = null) + + // Act + materialLogic.save(saveDto) + + // Assert + verify(exactly = 0) { + fileLogicMock.write(simdutFileMock, any(), false) + } + confirmVerified(fileLogicMock) + } + + @Test + fun save_nameExists_throwsNameAlreadyExists() { + // Arrange + every { materialServiceMock.existsByName(any(), any()) } returns true + + // Act + // Assert + assertThrows { materialLogic.save(material) } + } + + @Test + fun update_saveDto_normalBehavior_callsUpdate() { + // Arrange + every { materialLogic.getById(any()) } returns material + every { materialLogic.update(any()) } returns material + every { materialTypeLogicMock.getById(any()) } returns materialType + every { fileLogicMock.write(any(), any(), any()) } just runs + + // Act + materialLogic.update(materialSaveDto) + + // Assert + verify { + materialLogic.update(any()) + } + } + + @Test + fun update_materialSaveDto_normalBehavior_callsWriteInFileService() { + // Arrange + every { materialLogic.getById(any()) } returns material + every { materialLogic.update(any()) } returns material + every { materialTypeLogicMock.getById(any()) } returns materialType + every { fileLogicMock.write(any(), any(), any()) } just runs + + // Act + materialLogic.update(materialSaveDto) + + // Assert + verify { + fileLogicMock.write(simdutFileMock, any(), true) + } + confirmVerified(fileLogicMock) + } + + @Test + fun update_materialSaveDto_noSimdutFile_doesNotCallWriteInFileService() { + // Arrange + every { materialLogic.getById(any()) } returns material + every { materialLogic.update(any()) } returns material + every { materialTypeLogicMock.getById(any()) } returns materialType + every { fileLogicMock.write(any(), any(), any()) } just runs + + val saveDto = materialSaveDto.copy(simdutFile = null) + + // Act + materialLogic.update(saveDto) + + // Assert + verify(exactly = 0) { + fileLogicMock.write(simdutFileMock, any(), true) + } + confirmVerified(fileLogicMock) + } + + @Test + fun updateQuantity_normalBehavior_callsUpdateInventoryQuantityByIdInService() { + // Arrange + every { materialServiceMock.updateInventoryQuantityById(any(), any()) } just runs + + val factor = 3f + + // Act + materialLogic.updateQuantity(material, factor) + + // Assert + verify { + materialServiceMock.updateInventoryQuantityById(material.id, material.inventoryQuantity + factor) + } + confirmVerified(materialServiceMock) + } + + @Test + fun updateQuantity_normalBehavior_returnsUpdatedQuantity() { + // Arrange + every { materialServiceMock.updateInventoryQuantityById(any(), any()) } just runs + + val factor = 3f + + // Act + val updatedQuantity = materialLogic.updateQuantity(material, factor) + + // Assert + assertEquals(material.inventoryQuantity + factor, updatedQuantity) + } + + @Test + fun deleteById_normalBehavior_callsDeleteInFileLogic() { + // Arrange + every { materialLogic.getById(any()) } returns material + every { materialServiceMock.isUsedByMixMaterialOrMixType(any()) } returns false + every { materialServiceMock.deleteById(any()) } just runs + every { fileLogicMock.exists(any()) } returns true + every { fileLogicMock.delete(any()) } just runs + + val simdutPath = Material.getSimdutFilePath(material.name) + + // Act + materialLogic.deleteById(material.id) + + // Assert + verify { + fileLogicMock.exists(simdutPath) + fileLogicMock.delete(simdutPath) + } + confirmVerified(fileLogicMock) + } + + @Test + fun deleteById_simdutFileNotExists_doesNotCallDeleteInFileLogic() { + // Arrange + every { materialLogic.getById(any()) } returns material + every { materialServiceMock.isUsedByMixMaterialOrMixType(any()) } returns false + every { materialServiceMock.deleteById(any()) } just runs + every { fileLogicMock.exists(any()) } returns false + every { fileLogicMock.delete(any()) } just runs + + val simdutPath = Material.getSimdutFilePath(material.name) + + // Act + materialLogic.deleteById(material.id) + + // Assert + verify { + fileLogicMock.exists(simdutPath) + } + verify(exactly = 0) { + fileLogicMock.delete(simdutPath) + } + confirmVerified(fileLogicMock) + } + + @Test + fun deleteById_usedByMixMaterialOrMixType_throwsCannotDeleteException() { + // Arrange + every { materialLogic.getById(any()) } returns material + every { materialServiceMock.isUsedByMixMaterialOrMixType(any()) } returns true + every { materialServiceMock.deleteById(any()) } just runs + every { fileLogicMock.exists(any()) } returns false + every { fileLogicMock.delete(any()) } just runs + + // Act + // Assert + assertThrows { materialLogic.deleteById(material.id) } + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogicTest.kt index be94bf5..96a416c 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogicTest.kt @@ -175,7 +175,7 @@ class InventoryLogicTest { ) { val material = material(id = materialQuantity.material, inventoryQuantity = stored) - whenever(materialLogic.getById(material.id!!)).doReturn(material) + whenever(materialLogic.getById(material.id!!)).doReturn(materialDto(material)) materialQuantity.test(stored) } diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogicTest.kt deleted file mode 100644 index ee326cb..0000000 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogicTest.kt +++ /dev/null @@ -1,249 +0,0 @@ -package dev.fyloz.colorrecipesexplorer.logic - -import com.nhaarman.mockitokotlin2.* -import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException -import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic -import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.repository.MaterialRepository -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 org.springframework.mock.web.MockMultipartFile -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class MaterialLogicTest : - AbstractExternalNamedModelServiceTest() { - override val repository: MaterialRepository = mock() - private val recipeService: RecipeLogic = mock() - private val mixService: MixLogic = mock() - private val materialTypeService: MaterialTypeLogic = mock() - private val fileService: WriteableFileLogic = mock() - override val logic: MaterialLogic = - spy(DefaultMaterialLogic(repository, recipeService, mixService, materialTypeService, fileService, mock())) - - override val entity: Material = material(id = 0L, name = "material") - private val entityOutput = materialOutputDto(entity) - override val anotherEntity: Material = material(id = 1L, name = "another material") - override val entityWithEntityName: Material = material(id = 2L, name = "material") - override val entitySaveDto: MaterialSaveDto = spy(materialSaveDto()) - override val entityUpdateDto: MaterialUpdateDto = spy(materialUpdateDto(id = 0L)) - - private val materialType = materialType() - - @AfterEach - override fun afterEach() { - reset(recipeService, mixService, materialTypeService, fileService) - super.afterEach() - } - - // existsByMaterialType - - @Test - fun `existsByMaterialType() returns true when a material with the given material type exists in the repository`() { - whenever(repository.existsByMaterialType(materialType)).doReturn(true) - - val found = logic.existsByMaterialType(materialType) - - assertTrue(found) - } - - @Test - fun `existsByMaterialType() returns false when no material with the given material type exists in the repository`() { - whenever(repository.existsByMaterialType(materialType)).doReturn(false) - - val found = logic.existsByMaterialType(materialType) - - assertFalse(found) - } - - // hasSimdut() - - @Test - fun `hasSimdut() returns false when simdutService_exists() returns false`() { - whenever(fileService.exists(any())).doReturn(false) - doReturn(entity).whenever(logic).getById(entity.id!!) - - val found = logic.hasSimdut(entity) - - assertFalse(found) - } - - @Test - fun `hasSimdut() returns true when simdutService_exists() returns true`() { - whenever(fileService.exists(any())).doReturn(true) - doReturn(entity).whenever(logic).getById(entity.id!!) - - val found = logic.hasSimdut(entity) - - assertTrue(found) - } - - // getAllNotMixType() - - @Test - fun `getAllNotMixType() returns a list containing every material that are not a mix type`() { - val mixTypeMaterial = material(id = 1L, name = "mix type material", isMixType = true) - val mixTypeMaterialOutput = materialOutputDto(mixTypeMaterial) - val materialList = listOf(entity, mixTypeMaterial) - - doReturn(materialList).whenever(logic).getAll() - - val found = logic.getAllNotMixType() - - assertTrue(found.contains(entityOutput)) - assertFalse(found.contains(mixTypeMaterialOutput)) - } - - // save() - - @Test - fun `save() throws AlreadyExistsException when a material with the given name exists in the repository`() { - doReturn(true).whenever(logic).existsByName(entity.name) - - assertThrows { logic.save(entity) } - .assertErrorCode("name") - } - - @Test - override fun `save(dto) calls and returns save() with the created entity`() { - withBaseSaveDtoTest(entity, entitySaveDto, logic, { any() }) - } - - @Test - fun `save(dto) calls simdutService_write() with the saved entity`() { - val mockMultipartFile = spy(MockMultipartFile("simdut", byteArrayOf())) - val materialSaveDto = spy(materialSaveDto(simdutFile = mockMultipartFile)) - - doReturn(false).whenever(mockMultipartFile).isEmpty - doReturn(entity).whenever(logic).save(any()) - - logic.save(materialSaveDto) - - verify(fileService).write(mockMultipartFile, entity.simdutFilePath, false) - } - - // update() - - @Test - fun `update() throws AlreadyExistsException when another material with the updated name exists in the repository`() { - val material = material(id = 0L, name = "name") - val anotherMaterial = material(id = 1L, name = "name") - - whenever(repository.findByName(material.name)).doReturn(anotherMaterial) - doReturn(entity).whenever(logic).getById(material.id!!) - - assertThrows { logic.update(material) } - .assertErrorCode("name") - } - - @Test - override fun `update(dto) calls and returns update() with the created entity`() { - val mockSimdutFile = MockMultipartFile("simdut", byteArrayOf(1, 2, 3, 4, 5)) - val materialUpdateDto = spy(materialUpdateDto(id = 0L, simdutFile = mockSimdutFile)) - - doReturn(entity).whenever(logic).getById(any()) - doReturn(entity).whenever(logic).update(any()) - doReturn(entity).whenever(materialUpdateDto).toEntity() - - logic.update(materialUpdateDto) - - verify(fileService).write(mockSimdutFile, entity.simdutFilePath, true) - } - - // updateQuantity() - - @Test - fun `updateQuantity() updates the quantity of the the given material in the repository`() { - val material = material(id = 0L, inventoryQuantity = 4321f) - val quantity = 1234f - val totalQuantity = material.inventoryQuantity + quantity - - val found = logic.updateQuantity(material, quantity) - - verify(repository).updateInventoryQuantityById(material.id!!, totalQuantity) - assertEquals(totalQuantity, found) - } - - // getAllForMixCreation() - - @Test - fun `getAllForMixCreation() returns all normal materials and all mix type materials for the given recipe`() { - val normalMaterial = material(id = 0L, isMixType = false) - val mixTypeMaterial = material(id = 1L, isMixType = true) - val anotherMixTypeMaterial = material(id = 2L, isMixType = true) - val materials = listOf(normalMaterial, mixTypeMaterial, anotherMixTypeMaterial) - val recipe = - recipe(id = 0L, mixes = mutableListOf(mix(mixType = mixType(id = 0L, material = mixTypeMaterial)))) - - whenever(recipeService.getById(recipe.id!!)).doReturn(recipe) - doReturn(materials).whenever(logic).getAll() - - val found = logic.getAllForMixCreation(recipe.id!!) - - assertTrue(materialOutputDto(normalMaterial) in found) - assertTrue(materialOutputDto(mixTypeMaterial) in found) - assertFalse(materialOutputDto(anotherMixTypeMaterial) in found) - } - - // getAllForMixUpdate() - - @Test - fun `getAllForMixUpdate() returns all normal materials and all mix type materials for the recipe of the given mix without the mix type of the said mix`() { - val normalMaterial = material(id = 0L, isMixType = false) - val mixTypeMaterial = material(id = 1L, isMixType = true) - val anotherMixTypeMaterial = material(id = 2L, isMixType = true) - val materials = listOf(normalMaterial, mixTypeMaterial, anotherMixTypeMaterial) - val recipe = recipe(id = 0L, mixes = mutableListOf(mix(mixType = mixType(material = mixTypeMaterial)))) - val mix = mix(id = 1L, recipe = recipe, mixType = mixType(material = anotherMixTypeMaterial)) - recipe.mixes.add(mix) - - whenever(mixService.getById(mix.id!!)).doReturn(mix) - doReturn(materials).whenever(logic).getAll() - - val found = logic.getAllForMixUpdate(mix.id!!) - - assertTrue(materialOutputDto(normalMaterial) in found) - assertTrue(materialOutputDto(mixTypeMaterial) in found) - assertFalse(materialOutputDto(anotherMixTypeMaterial) in found) - } - - - // delete() - - override fun `delete() deletes in the repository`() { - whenCanBeDeleted { - super.`delete() deletes in the repository`() - } - } - - // deleteById() - - 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`() - } - } - - /** Utility property to check if the identifier of the given [Material] is even. */ - private val Material.evenId: Boolean - get() = this.id!! % 2 == 0L - - private fun whenCanBeDeleted(id: Long = any(), test: () -> Unit) { - whenever(repository.canBeDeleted(id)).doReturn(true) - - test() - } - - private fun materialOutputDto(material: Material) = MaterialOutputDto( - id = material.id!!, - name = material.name, - inventoryQuantity = material.inventoryQuantity, - isMixType = material.isMixType, - materialType = material.materialType!!, - simdutUrl = null - ) -} diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogicTest.kt index 18611a6..b1e84d3 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogicTest.kt @@ -20,7 +20,7 @@ class MaterialTypeLogicTest : AbstractExternalNamedModelServiceTest() { override val repository: MaterialTypeRepository = mock() private val materialService: MaterialLogic = mock() - override val logic: MaterialTypeLogic = spy(DefaultMaterialTypeLogic(repository, materialService)) + 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") @@ -56,26 +56,6 @@ class MaterialTypeLogicTest : assertFalse(found) } - // isUsedByMaterial() - - @Test - fun `isUsedByMaterial() returns true when materialService_existsByMaterialType() returns true`() { - whenever(materialService.existsByMaterialType(entity)).doReturn(true) - - val found = logic.isUsedByMaterial(entity) - - assertTrue(found) - } - - @Test - fun `isUsedByMaterial() returns false when materialService_existsByMaterialType() returns false`() { - whenever(materialService.existsByMaterialType(entity)).doReturn(false) - - val found = logic.isUsedByMaterial(entity) - - assertFalse(found) - } - // getAllSystemTypes() @Test diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogicTest.kt index 88ee333..2b54a53 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogicTest.kt @@ -78,7 +78,7 @@ class MixMaterialLogicTest : AbstractModelServiceTest