diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/annotations/Components.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/annotations/Components.kt new file mode 100644 index 0000000..c7d0927 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/annotations/Components.kt @@ -0,0 +1,15 @@ +package dev.fyloz.colorrecipesexplorer.config.annotations + +import org.springframework.stereotype.Service + +@Service +@RequireDatabase +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class ServiceComponent + +@Service +@RequireDatabase +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class LogicComponent \ 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 new file mode 100644 index 0000000..3596d65 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/CompanyDto.kt @@ -0,0 +1,10 @@ +package dev.fyloz.colorrecipesexplorer.dtos + +import javax.validation.constraints.NotBlank + +data class CompanyDto( + override val id: Long = 0L, + + @NotBlank + val name: String +) : EntityDto \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/EntityDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/EntityDto.kt new file mode 100644 index 0000000..43fbb19 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/EntityDto.kt @@ -0,0 +1,5 @@ +package dev.fyloz.colorrecipesexplorer.dtos + +interface EntityDto { + val id: Long +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogic.kt index 0bb2e2b..fe2b04e 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogic.kt @@ -1,50 +1,38 @@ package dev.fyloz.colorrecipesexplorer.logic -import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase -import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.repository.CompanyRepository -import org.springframework.context.annotation.Lazy -import org.springframework.stereotype.Service +import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent +import dev.fyloz.colorrecipesexplorer.dtos.CompanyDto +import dev.fyloz.colorrecipesexplorer.model.Company +import dev.fyloz.colorrecipesexplorer.service.CompanyService -interface CompanyLogic : - ExternalNamedModelService { - /** Checks if the given [company] is used by one or more recipes. */ - fun isLinkedToRecipes(company: Company): Boolean -} +interface CompanyLogic : Logic -@Service -@RequireDatabase -class DefaultCompanyLogic( - companyRepository: CompanyRepository, - @Lazy val recipeLogic: RecipeLogic -) : - AbstractExternalNamedModelService( - companyRepository - ), - CompanyLogic { - override fun idNotFoundException(id: Long) = companyIdNotFoundException(id) - override fun idAlreadyExistsException(id: Long) = companyIdAlreadyExistsException(id) - override fun nameNotFoundException(name: String) = companyNameNotFoundException(name) - override fun nameAlreadyExistsException(name: String) = companyNameAlreadyExistsException(name) +@LogicComponent +class DefaultCompanyLogic(service: CompanyService) : + BaseLogic(service, Company::class.simpleName!!), CompanyLogic { + override fun save(dto: CompanyDto): CompanyDto { + throwIfNameAlreadyExists(dto.name) - override fun Company.toOutput() = this - - override fun isLinkedToRecipes(company: Company): Boolean = recipeLogic.existsByCompany(company) - - override fun update(entity: CompanyUpdateDto): Company { - // Lazy loaded to prevent checking the database when not necessary - val persistedCompany by lazy { getById(entity.id) } - - return update(with(entity) { - company( - id = id, - name = if (name != null && name.isNotBlank()) name else persistedCompany.name - ) - }) + return super.save(dto) } - override fun delete(entity: Company) { - if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteCompany(entity) - super.delete(entity) + override fun update(dto: CompanyDto): CompanyDto { + throwIfNameAlreadyExists(dto.name, dto.id) + + return super.update(dto) } -} + + override fun deleteById(id: Long) { + if (service.recipesDependsOnCompanyById(id)) { + throw cannotDeleteException("Cannot delete the company with the id '$id' because one or more recipes depends on it") + } + + super.deleteById(id) + } + + 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/Logic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt new file mode 100644 index 0000000..647d6de --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt @@ -0,0 +1,93 @@ +package dev.fyloz.colorrecipesexplorer.logic + +import dev.fyloz.colorrecipesexplorer.dtos.EntityDto +import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException +import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException +import dev.fyloz.colorrecipesexplorer.exception.NotFoundException +import dev.fyloz.colorrecipesexplorer.service.Service + +/** + * Represents the logic for a DTO type. + * + * @param D The type of the DTO. + * @param S The service for the DTO. + */ +interface Logic> { + /** Checks if a DTO with the given [id] exists. */ + fun existsById(id: Long): Boolean + + /** Get all DTOs. */ + fun getAll(): Collection + + /** Get the DTO for the given [id]. */ + fun getById(id: Long): D + + /** Saves the given [dto]. */ + fun save(dto: D): D + + /** Updates the given [dto]. */ + fun update(dto: D): D + + /** Deletes the dto with the given [id]. */ + fun deleteById(id: Long) +} + +abstract class BaseLogic>( + protected val service: S, + protected val typeName: String +) : Logic { + protected val typeNameLowerCase = typeName.lowercase() + + override fun existsById(id: Long) = + service.existsById(id) + + override fun getAll() = + service.getAll() + + override fun getById(id: Long) = + service.getById(id) ?: throw notFoundException(value = id) + + override fun save(dto: D) = + service.save(dto) + + override fun update(dto: D): D { + if (!existsById(dto.id)) { + throw notFoundException(value = dto.id) + } + + return service.save(dto) + } + + override fun deleteById(id: Long) = + service.deleteById(id) + + protected fun notFoundException(identifierName: String = idIdentifierName, value: Any) = + NotFoundException( + typeNameLowerCase, + "$typeName not found", + "A $typeNameLowerCase with the $identifierName '$value' could not be found", + value, + identifierName + ) + + protected fun alreadyExistsException(identifierName: String = nameIdentifierName, value: Any) = + AlreadyExistsException( + typeNameLowerCase, + "$typeName already exists", + "A $typeNameLowerCase with the $identifierName '$value' already exists", + value, + identifierName + ) + + protected fun cannotDeleteException(details: String) = + CannotDeleteException( + typeNameLowerCase, + "Cannot delete $typeNameLowerCase", + details + ) + + companion object { + const val idIdentifierName = "id" + const val nameIdentifierName = "name" + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Service.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/OldService.kt similarity index 79% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Service.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/OldService.kt index 314d93b..25e08b4 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Service.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/OldService.kt @@ -3,8 +3,8 @@ package dev.fyloz.colorrecipesexplorer.logic import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.model.EntityDto -import dev.fyloz.colorrecipesexplorer.model.Model -import dev.fyloz.colorrecipesexplorer.model.NamedModel +import dev.fyloz.colorrecipesexplorer.model.ModelEntity +import dev.fyloz.colorrecipesexplorer.model.NamedModelEntity import dev.fyloz.colorrecipesexplorer.repository.NamedJpaRepository import io.jsonwebtoken.lang.Assert import org.springframework.data.jpa.repository.JpaRepository @@ -16,7 +16,7 @@ import org.springframework.data.repository.findByIdOrNull * @param E The entity type * @param R The entity repository type */ -interface Service> { +interface OldService> { val repository: R /** Gets all entities. */ @@ -32,8 +32,8 @@ interface Service> { fun delete(entity: E) } -/** A service for entities implementing the [Model] interface. This service add supports for numeric identifiers. */ -interface ModelService> : Service { +/** A service for entities implementing the [ModelEntity] interface. This service add supports for numeric identifiers. */ +interface ModelService> : OldService { /** Checks if an entity with the given [id] exists. */ fun existsById(id: Long): Boolean @@ -44,8 +44,8 @@ interface ModelService> : Service { fun deleteById(id: Long) } -/** A service for entities implementing the [NamedModel] interface. This service add supports for name identifiers. */ -interface NamedModelService> : ModelService { +/** A service for entities implementing the [NamedModelEntity] interface. This service add supports for name identifiers. */ +interface NamedModelService> : ModelService { /** Checks if an entity with the given [name] exists. */ fun existsByName(name: String): Boolean @@ -54,14 +54,14 @@ interface NamedModelService> : ModelServ } -abstract class AbstractService>(override val repository: R) : Service { +abstract class AbstractService>(override val repository: R) : OldService { override fun getAll(): Collection = repository.findAll() override fun save(entity: E): E = repository.save(entity) override fun update(entity: E): E = repository.save(entity) override fun delete(entity: E) = repository.delete(entity) } -abstract class AbstractModelService>(repository: R) : +abstract class AbstractModelService>(repository: R) : AbstractService(repository), ModelService { protected abstract fun idNotFoundException(id: Long): NotFoundException protected abstract fun idAlreadyExistsException(id: Long): AlreadyExistsException @@ -90,7 +90,7 @@ abstract class AbstractModelService>(repos } } -abstract class AbstractNamedModelService>(repository: R) : +abstract class AbstractNamedModelService>(repository: R) : AbstractModelService(repository), NamedModelService { protected abstract fun nameNotFoundException(name: String): NotFoundException protected abstract fun nameAlreadyExistsException(name: String): AlreadyExistsException @@ -126,7 +126,7 @@ abstract class AbstractNamedModelService, U : EntityDto, O, R : JpaRepository> : Service { +interface ExternalService, U : EntityDto, O, R : JpaRepository> : OldService { /** Gets all entities mapped to their output model. */ fun getAllForOutput(): Collection @@ -140,15 +140,15 @@ interface ExternalService, U : EntityDto, O, R : JpaRepos fun E.toOutput(): O } -/** An [ExternalService] for entities implementing the [Model] interface. */ -interface ExternalModelService, U : EntityDto, O, R : JpaRepository> : +/** An [ExternalService] for entities implementing the [ModelEntity] interface. */ +interface ExternalModelService, U : EntityDto, O, R : JpaRepository> : ModelService, ExternalService { /** Gets the entity with the given [id] mapped to its output model. */ fun getByIdForOutput(id: Long): O } -/** An [ExternalService] for entities implementing the [NamedModel] interface. */ -interface ExternalNamedModelService, U : EntityDto, O, R : JpaRepository> : +/** An [ExternalService] for entities implementing the [NamedModelEntity] interface. */ +interface ExternalNamedModelService, U : EntityDto, O, R : JpaRepository> : NamedModelService, ExternalModelService /** An [AbstractService] with the functionalities of a [ExternalService]. */ @@ -160,7 +160,7 @@ abstract class AbstractExternalService, U : EntityDto, O, } /** An [AbstractModelService] with the functionalities of a [ExternalService]. */ -abstract class AbstractExternalModelService, U : EntityDto, O, R : JpaRepository>( +abstract class AbstractExternalModelService, U : EntityDto, O, R : JpaRepository>( repository: R ) : AbstractModelService(repository), ExternalModelService { override fun getAllForOutput() = @@ -171,7 +171,7 @@ abstract class AbstractExternalModelService, U : Ent } /** An [AbstractNamedModelService] with the functionalities of a [ExternalService]. */ -abstract class AbstractExternalNamedModelService, U : EntityDto, O, R : NamedJpaRepository>( +abstract class AbstractExternalNamedModelService, U : EntityDto, O, R : NamedJpaRepository>( repository: R ) : AbstractNamedModelService(repository), ExternalNamedModelService { override fun getAllForOutput() = diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt index ddc8cb6..775b64f 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt @@ -96,7 +96,7 @@ class DefaultRecipeLogic( override fun getAllByCompany(company: Company) = repository.findAllByCompany(company) override fun save(entity: RecipeSaveDto): Recipe { - val company = companyLogic.getById(entity.companyId) + val company = company(companyLogic.getById(entity.companyId)) if (existsByNameAndCompany(entity.name, company)) { throw recipeNameAlreadyExistsForCompanyException(entity.name, company) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt index d2bcba7..b2e1158 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt @@ -1,12 +1,7 @@ package dev.fyloz.colorrecipesexplorer.model -import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException -import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException -import dev.fyloz.colorrecipesexplorer.exception.NotFoundException -import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank +import dev.fyloz.colorrecipesexplorer.dtos.CompanyDto import javax.persistence.* -import javax.validation.constraints.NotBlank -import javax.validation.constraints.NotNull @Entity @Table(name = "company") @@ -16,30 +11,8 @@ data class Company( override val id: Long?, @Column(unique = true) - override val name: String -) : NamedModel { - override fun toString(): String { - return name - } -} - - -open class CompanySaveDto( - @field:NotBlank val name: String -) : EntityDto { - override fun toEntity(): Company = Company(null, name) -} - - -open class CompanyUpdateDto( - val id: Long, - - @field:NotBlank - val name: String? -) : EntityDto { - override fun toEntity(): Company = Company(id, name ?: "") -} +) : ModelEntity // ==== DSL ==== fun company( @@ -48,60 +21,12 @@ fun company( op: Company.() -> Unit = {} ) = Company(id, name).apply(op) -fun companySaveDto( - name: String = "name", - op: CompanySaveDto.() -> Unit = {} -) = CompanySaveDto(name).apply(op) +@Deprecated("Temporary DSL for transition") +fun company( + dto: CompanyDto +) = Company(dto.id, dto.name) -fun companyUpdateDto( - id: Long = 0L, - name: String? = "name", - op: CompanyUpdateDto.() -> Unit = {} -) = CompanyUpdateDto(id, name).apply(op) - -// ==== Exceptions ==== -private const val COMPANY_NOT_FOUND_EXCEPTION_TITLE = "Company not found" -private const val COMPANY_ALREADY_EXISTS_EXCEPTION_TITLE = "Company already exists" -private const val COMPANY_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete company" -private const val COMPANY_EXCEPTION_ERROR_CODE = "company" - -fun companyIdNotFoundException(id: Long) = - NotFoundException( - COMPANY_EXCEPTION_ERROR_CODE, - COMPANY_NOT_FOUND_EXCEPTION_TITLE, - "A company with the id $id could not be found", - id - ) - -fun companyNameNotFoundException(name: String) = - NotFoundException( - COMPANY_EXCEPTION_ERROR_CODE, - COMPANY_NOT_FOUND_EXCEPTION_TITLE, - "A company with the name $name could not be found", - name, - "name" - ) - -fun companyIdAlreadyExistsException(id: Long) = - AlreadyExistsException( - COMPANY_EXCEPTION_ERROR_CODE, - COMPANY_ALREADY_EXISTS_EXCEPTION_TITLE, - "A company with the id $id already exists", - id - ) - -fun companyNameAlreadyExistsException(name: String) = - AlreadyExistsException( - COMPANY_EXCEPTION_ERROR_CODE, - COMPANY_ALREADY_EXISTS_EXCEPTION_TITLE, - "A company with the name $name already exists", - name, - "name" - ) - -fun cannotDeleteCompany(company: Company) = - CannotDeleteException( - COMPANY_EXCEPTION_ERROR_CODE, - COMPANY_CANNOT_DELETE_EXCEPTION_TITLE, - "Cannot delete the company ${company.name} because one or more recipes depends on it" - ) +@Deprecated("Temporary DSL for transition") +fun companyDto( + entity: Company +) = CompanyDto(entity.id!!, entity.name) \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt index 76f505a..3513570 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt @@ -8,7 +8,6 @@ import org.springframework.web.multipart.MultipartFile import javax.persistence.* import javax.validation.constraints.Min import javax.validation.constraints.NotBlank -import javax.validation.constraints.Size const val SIMDUT_FILES_PATH = "pdf/simdut" @@ -31,7 +30,7 @@ data class Material( @ManyToOne @JoinColumn(name = "material_type_id") var materialType: MaterialType? -) : NamedModel { +) : NamedModelEntity { val simdutFilePath @JsonIgnore @Transient @@ -71,7 +70,7 @@ data class MaterialOutputDto( val isMixType: Boolean, val materialType: MaterialType, val simdutUrl: String? -) : Model +) : ModelEntity data class MaterialQuantityDto( val material: Long, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt index 7abc3b7..a99033f 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt @@ -4,12 +4,9 @@ import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException import dev.fyloz.colorrecipesexplorer.exception.CannotUpdateException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException -import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank -import dev.fyloz.colorrecipesexplorer.model.validation.NullOrSize import org.hibernate.annotations.ColumnDefault import javax.persistence.* import javax.validation.constraints.NotBlank -import javax.validation.constraints.NotNull import javax.validation.constraints.Size private const val VALIDATION_PREFIX_SIZE = "Must contains exactly 3 characters" @@ -34,7 +31,7 @@ data class MaterialType( @Column(name = "system_type") @ColumnDefault("false") val systemType: Boolean = false -) : NamedModel +) : NamedModelEntity open class MaterialTypeSaveDto( @field:NotBlank diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt index 3622343..d7f0053 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt @@ -30,7 +30,7 @@ data class Mix( @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true) @JoinColumn(name = "mix_id") var mixMaterials: MutableSet, -) : Model +) : ModelEntity open class MixSaveDto( @field:NotBlank diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt index c48316b..afe56b2 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt @@ -4,7 +4,6 @@ import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import javax.persistence.* import javax.validation.constraints.Min -import javax.validation.constraints.NotNull @Entity @Table(name = "mix_material") @@ -20,7 +19,7 @@ data class MixMaterial( var quantity: Float, var position: Int -) : Model +) : ModelEntity data class MixMaterialDto( val materialId: Long, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixType.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixType.kt index 6281099..9953f80 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixType.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixType.kt @@ -20,7 +20,7 @@ data class MixType( @OneToOne(cascade = [CascadeType.ALL]) @JoinColumn(name = "material_id") var material: Material -) : NamedModel +) : NamedModelEntity // ==== DSL ==== fun mixType( diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Model.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/ModelEntity.kt similarity index 69% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Model.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/ModelEntity.kt index 147285f..6b790dc 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Model.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/ModelEntity.kt @@ -1,11 +1,11 @@ package dev.fyloz.colorrecipesexplorer.model -/** The model of a stored entity. Each model should implements its own equals and hashCode methods to keep compatibility with the legacy Java and Thymeleaf code. */ -interface Model { +/** Represents an entity, named differently to prevent conflicts with the JPA annotation. */ +interface ModelEntity { val id: Long? } -interface NamedModel : Model { +interface NamedModelEntity : ModelEntity { val name: String } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt index a78ba28..2e56962 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt @@ -52,7 +52,7 @@ data class Recipe( @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true) @JoinColumn(name = "recipe_id") val groupsInformation: Set -) : Model { +) : ModelEntity { /** The mix types contained in this recipe. */ val mixTypes: Collection @JsonIgnore @@ -150,7 +150,7 @@ data class RecipeOutputDto( val mixes: Set, val groupsInformation: Set, var imagesUrls: Set -) : Model +) : ModelEntity @Entity @Table(name = "recipe_group_information") @@ -168,7 +168,7 @@ data class RecipeGroupInformation( @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true) @JoinColumn(name = "recipe_group_information_id") var steps: MutableSet? -) : Model +) : ModelEntity data class RecipeStepsDto( val groupId: Long, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/RecipeStep.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/RecipeStep.kt index 1f5a3a7..51f9377 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/RecipeStep.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/RecipeStep.kt @@ -14,7 +14,7 @@ data class RecipeStep( val position: Int, val message: String -) : Model +) : ModelEntity // ==== DSL ==== fun recipeStep( diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/Group.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/Group.kt index 1169c5e..6f6b24c 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/Group.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/Group.kt @@ -4,14 +4,13 @@ import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.exception.RestException import dev.fyloz.colorrecipesexplorer.model.* +import dev.fyloz.colorrecipesexplorer.model.ModelEntity import org.hibernate.annotations.Fetch import org.hibernate.annotations.FetchMode import org.springframework.http.HttpStatus import javax.persistence.* import javax.validation.constraints.NotBlank import javax.validation.constraints.NotEmpty -import javax.validation.constraints.NotNull -import javax.validation.constraints.Size @Entity @Table(name = "user_group") @@ -29,7 +28,7 @@ data class Group( @Column(name = "permission") @Fetch(FetchMode.SUBSELECT) val permissions: MutableSet = mutableSetOf(), -) : NamedModel { +) : NamedModelEntity { val flatPermissions: Set get() = this.permissions .flatMap { it.flat() } @@ -66,7 +65,7 @@ data class GroupOutputDto( val name: String, val permissions: Set, val explicitPermissions: Set -): Model +): ModelEntity fun group( id: Long? = null, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/User.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/User.kt index ac6f5d6..633a1a4 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/User.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/User.kt @@ -4,7 +4,7 @@ import dev.fyloz.colorrecipesexplorer.SpringUserDetails import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.model.EntityDto -import dev.fyloz.colorrecipesexplorer.model.Model +import dev.fyloz.colorrecipesexplorer.model.ModelEntity import org.hibernate.annotations.Fetch import org.hibernate.annotations.FetchMode import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder @@ -50,7 +50,7 @@ data class User( @Column(name = "last_login_time") var lastLoginTime: LocalDateTime? = null -) : Model { +) : ModelEntity { val flatPermissions: Set get() = permissions .flatMap { it.flat() } @@ -103,7 +103,7 @@ data class UserOutputDto( val permissions: Set, val explicitPermissions: Set, val lastLoginTime: LocalDateTime? -) : Model +) : ModelEntity data class UserLoginRequest(val id: Long, val password: String) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt index 1a90530..b96738b 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt @@ -3,7 +3,7 @@ package dev.fyloz.colorrecipesexplorer.model.touchupkit import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.model.EntityDto -import dev.fyloz.colorrecipesexplorer.model.Model +import dev.fyloz.colorrecipesexplorer.model.ModelEntity import dev.fyloz.colorrecipesexplorer.model.VALIDATION_SIZE_GE_ONE import java.time.LocalDate import javax.persistence.* @@ -43,7 +43,7 @@ data class TouchUpKit( @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true) @JoinColumn(name = "touch_up_kit_id") val content: Set -) : Model { +) : ModelEntity { val finish get() = finishConcatenated.split(TOUCH_UP_KIT_DELIMITER) @@ -68,7 +68,7 @@ data class TouchUpKitProduct( val quantity: Float, val ready: Boolean -) : Model +) : ModelEntity data class TouchUpKitSaveDto( @field:NotBlank @@ -140,7 +140,7 @@ data class TouchUpKitOutputDto( val material: List, val content: Set, val pdfUrl: String -) : Model +) : ModelEntity data class TouchUpKitProductDto( val name: String, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/CompanyRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/CompanyRepository.kt index b0b4142..963e80d 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/CompanyRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/CompanyRepository.kt @@ -1,18 +1,21 @@ package dev.fyloz.colorrecipesexplorer.repository import dev.fyloz.colorrecipesexplorer.model.Company +import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository @Repository -interface CompanyRepository : NamedJpaRepository { +interface CompanyRepository : JpaRepository { + /** Checks if a company with the given [name] and an id different from the given [id] exists. */ + fun existsByNameAndIdNot(name: String, id: Long): Boolean + + /** Checks if a recipe depends on the company with the given [id]. */ @Query( - """ - select case when(count(r.id) > 0) then false else true end - from Company c - left join Recipe r on c.id = r.company.id - where c.id = :id + """ + select case when(count(r) > 0) then true else false end + from Recipe r where r.company.id = :id """ ) - fun canBeDeleted(id: Long): Boolean + fun recipesDependsOnCompanyById(id: Long): Boolean } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/Repository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/Repository.kt index 59ebe12..5e0843c 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/Repository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/Repository.kt @@ -1,12 +1,12 @@ package dev.fyloz.colorrecipesexplorer.repository -import dev.fyloz.colorrecipesexplorer.model.NamedModel +import dev.fyloz.colorrecipesexplorer.model.NamedModelEntity import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.repository.NoRepositoryBean /** Adds support for entities using a name identifier. */ @NoRepositoryBean -interface NamedJpaRepository : JpaRepository { +interface NamedJpaRepository : JpaRepository { /** Checks if an entity with the given [name]. */ fun existsByName(name: String): Boolean diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt index 3375cbd..e2a00f7 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt @@ -1,10 +1,9 @@ package dev.fyloz.colorrecipesexplorer.rest import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewCatalog -import dev.fyloz.colorrecipesexplorer.model.Company -import dev.fyloz.colorrecipesexplorer.model.CompanySaveDto -import dev.fyloz.colorrecipesexplorer.model.CompanyUpdateDto -import org.springframework.context.annotation.Profile +import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase +import dev.fyloz.colorrecipesexplorer.dtos.CompanyDto +import dev.fyloz.colorrecipesexplorer.logic.CompanyLogic import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.* import javax.validation.Valid @@ -13,35 +12,35 @@ private const val COMPANY_CONTROLLER_PATH = "api/company" @RestController @RequestMapping(COMPANY_CONTROLLER_PATH) -@Profile("!emergency") +@RequireDatabase @PreAuthorizeViewCatalog -class CompanyController(private val companyLogic: dev.fyloz.colorrecipesexplorer.logic.CompanyLogic) { +class CompanyController(private val companyLogic: CompanyLogic) { @GetMapping fun getAll() = - ok(companyLogic.getAllForOutput()) + ok(companyLogic.getAll()) @GetMapping("{id}") fun getById(@PathVariable id: Long) = - ok(companyLogic.getByIdForOutput(id)) + ok(companyLogic.getById(id)) @PostMapping @PreAuthorize("hasAuthority('EDIT_COMPANIES')") - fun save(@Valid @RequestBody company: CompanySaveDto) = - created(COMPANY_CONTROLLER_PATH) { - companyLogic.save(company) - } + fun save(@Valid @RequestBody company: CompanyDto) = + created(COMPANY_CONTROLLER_PATH) { + companyLogic.save(company) + } @PutMapping @PreAuthorize("hasAuthority('EDIT_COMPANIES')") - fun update(@Valid @RequestBody company: CompanyUpdateDto) = - noContent { - companyLogic.update(company) - } + fun update(@Valid @RequestBody company: CompanyDto) = + noContent { + companyLogic.update(company) + } @DeleteMapping("{id}") @PreAuthorize("hasAuthority('EDIT_COMPANIES')") fun deleteById(@PathVariable id: Long) = - noContent { - companyLogic.deleteById(id) - } + noContent { + companyLogic.deleteById(id) + } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt index 7147aa0..2c1e2f5 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt @@ -1,7 +1,8 @@ package dev.fyloz.colorrecipesexplorer.rest import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties -import dev.fyloz.colorrecipesexplorer.model.Model +import dev.fyloz.colorrecipesexplorer.dtos.EntityDto +import dev.fyloz.colorrecipesexplorer.model.ModelEntity import org.springframework.core.io.Resource import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus @@ -35,11 +36,21 @@ fun okFile(file: Resource, mediaType: String? = null): ResponseEntity .body(file) /** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */ -fun created(controllerPath: String, body: T): ResponseEntity = +fun created(controllerPath: String, body: T): ResponseEntity = created(controllerPath, body, body.id!!) +/** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */ +@JvmName("createdDto") +fun created(controllerPath: String, body: T): ResponseEntity = + created(controllerPath, body, body.id) + /** Creates a HTTP CREATED [ResponseEntity] with the result of the given [producer] as its body. */ -fun created(controllerPath: String, producer: () -> T): ResponseEntity = +fun created(controllerPath: String, producer: () -> T): ResponseEntity = + created(controllerPath, producer()) + +/** Creates a HTTP CREATED [ResponseEntity] with the result of the given [producer] as its body. */ +@JvmName("createdDto") +fun created(controllerPath: String, producer: () -> T): ResponseEntity = created(controllerPath, producer()) /** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */ diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt new file mode 100644 index 0000000..c65b91c --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt @@ -0,0 +1,27 @@ +package dev.fyloz.colorrecipesexplorer.service + +import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent +import dev.fyloz.colorrecipesexplorer.dtos.CompanyDto +import dev.fyloz.colorrecipesexplorer.model.Company +import dev.fyloz.colorrecipesexplorer.repository.CompanyRepository + +interface CompanyService : Service { + /** Checks if a company with the given [name] exists. */ + fun existsByName(name: String, id: Long?): Boolean + + /** Checks if a recipe depends on the company with the given [id]. */ + fun recipesDependsOnCompanyById(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 toDto(entity: Company) = + CompanyDto(entity.id!!, entity.name) + + override fun toEntity(dto: CompanyDto) = + Company(dto.id, dto.name) +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/Service.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/Service.kt new file mode 100644 index 0000000..bfcc7c6 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/Service.kt @@ -0,0 +1,64 @@ +package dev.fyloz.colorrecipesexplorer.service + +import dev.fyloz.colorrecipesexplorer.dtos.EntityDto +import dev.fyloz.colorrecipesexplorer.model.ModelEntity +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.repository.findByIdOrNull + +/** + * Represents a service between the logic and the repository. + * Gives access to the repository using a DTO. + * + * @param D The type of the entity DTO. + * @param E The type of the entity. + * @param R The repository of the entity. + */ +interface Service> { + /** Checks if an entity with the given [id] exists. */ + fun existsById(id: Long): Boolean + + /** Gets all entities as DTOs. */ + fun getAll(): Collection + + /** Gets the entity DTO with the given [id].*/ + fun getById(id: Long): D? + + /** Saves the given [dto]. */ + fun save(dto: D): D + + /** Deletes the given [dto]. */ + fun delete(dto: D) + + /** Deletes the entity with the given [id]. */ + fun deleteById(id: Long) +} + +abstract class BaseService>(protected val repository: R) : + Service { + override fun existsById(id: Long) = + repository.existsById(id) + + override fun getAll() = + repository.findAll().map(this::toDto) + + override fun getById(id: Long): D? { + val entity = repository.findByIdOrNull(id) ?: return null + return toDto(entity) + } + + override fun save(dto: D): D { + val entity = repository.save(toEntity(dto)) + return toDto(entity) + } + + override fun delete(dto: D) { + repository.delete(toEntity(dto)) + } + + override fun deleteById(id: Long) { + repository.deleteById(id) + } + + abstract fun toDto(entity: E): D + abstract fun toEntity(dto: D): E +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt index 00b853c..3aadbe8 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt @@ -1,6 +1,6 @@ package dev.fyloz.colorrecipesexplorer.utils -import dev.fyloz.colorrecipesexplorer.model.Model +import dev.fyloz.colorrecipesexplorer.model.ModelEntity /** 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 Iterable.mapMayThrow( @@ -46,8 +46,8 @@ inline fun MutableCollection.excludeAll(predicate: (T) -> Boolean): Itera return matching } -/** Merge to [Model] [Iterable]s and prevent id duplication. */ -fun Iterable.merge(other: Iterable) = +/** Merge to [ModelEntity] [Iterable]s and prevent id duplication. */ +fun Iterable.merge(other: Iterable) = this .filter { model -> other.all { it.id != model.id } } .plus(other) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/AbstractServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/AbstractServiceTest.kt index a37cf7b..968aa7b 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/AbstractServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/AbstractServiceTest.kt @@ -5,8 +5,8 @@ import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.exception.RestException import dev.fyloz.colorrecipesexplorer.model.EntityDto -import dev.fyloz.colorrecipesexplorer.model.Model -import dev.fyloz.colorrecipesexplorer.model.NamedModel +import dev.fyloz.colorrecipesexplorer.model.ModelEntity +import dev.fyloz.colorrecipesexplorer.model.NamedModelEntity import dev.fyloz.colorrecipesexplorer.repository.NamedJpaRepository import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test @@ -18,7 +18,7 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue import dev.fyloz.colorrecipesexplorer.logic.AbstractServiceTest as AbstractServiceTest1 -abstract class AbstractServiceTest, R : JpaRepository> { +abstract class AbstractServiceTest, R : JpaRepository> { protected abstract val repository: R protected abstract val logic: S @@ -90,7 +90,7 @@ abstract class AbstractServiceTest, R : JpaRepository } } -abstract class AbstractModelServiceTest, R : JpaRepository> : +abstract class AbstractModelServiceTest, R : JpaRepository> : AbstractServiceTest1() { // existsById() @@ -176,7 +176,7 @@ abstract class AbstractModelServiceTest, R : J } } -abstract class AbstractNamedModelServiceTest, R : NamedJpaRepository> : +abstract class AbstractNamedModelServiceTest, R : NamedJpaRepository> : AbstractModelServiceTest() { protected abstract val entityWithEntityName: E @@ -269,7 +269,7 @@ interface ExternalModelServiceTest { // ==== IMPLEMENTATIONS FOR EXTERNAL SERVICES ==== // Lots of code duplication but I don't have a better solution for now -abstract class AbstractExternalModelServiceTest, U : EntityDto, S : ExternalModelService, R : JpaRepository> : +abstract class AbstractExternalModelServiceTest, U : EntityDto, S : ExternalModelService, R : JpaRepository> : AbstractModelServiceTest(), ExternalModelServiceTest { protected abstract val entitySaveDto: N protected abstract val entityUpdateDto: U @@ -281,7 +281,7 @@ abstract class AbstractExternalModelServiceTest, U : } } -abstract class AbstractExternalNamedModelServiceTest, U : EntityDto, S : ExternalNamedModelService, R : NamedJpaRepository> : +abstract class AbstractExternalNamedModelServiceTest, U : EntityDto, S : ExternalNamedModelService, R : NamedJpaRepository> : AbstractNamedModelServiceTest(), ExternalModelServiceTest { protected abstract val entitySaveDto: N protected abstract val entityUpdateDto: U @@ -310,7 +310,7 @@ fun RestException.assertErrorCode(errorCode: String) { assertEquals(errorCode, this.errorCode) } -fun > withBaseSaveDtoTest( +fun > withBaseSaveDtoTest( entity: E, entitySaveDto: N, service: ExternalService, @@ -328,7 +328,7 @@ fun > withBaseSaveDtoTest( op() } -fun > withBaseUpdateDtoTest( +fun > withBaseUpdateDtoTest( entity: E, entityUpdateDto: U, service: ExternalModelService, diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/BaseLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/BaseLogicTest.kt new file mode 100644 index 0000000..bebab7d --- /dev/null +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/BaseLogicTest.kt @@ -0,0 +1,173 @@ +package dev.fyloz.colorrecipesexplorer.logic + +import dev.fyloz.colorrecipesexplorer.dtos.EntityDto +import dev.fyloz.colorrecipesexplorer.exception.NotFoundException +import dev.fyloz.colorrecipesexplorer.model.ModelEntity +import dev.fyloz.colorrecipesexplorer.service.Service +import io.mockk.* +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.data.jpa.repository.JpaRepository +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class BaseLogicTest { + private val serviceMock = mockk>>() + + private val baseLogic = spyk(TestBaseLogic(serviceMock)) + + private val dto = TestEntityDto(id = 1L) + + @AfterEach + internal fun afterEach() { + clearAllMocks() + } + + @Test + fun existsById_normalBehavior_returnsTrue() { + // Arrange + every { serviceMock.existsById(any()) } returns true + + // Act + val exists = baseLogic.existsById(dto.id) + + // Assert + assertTrue(exists) + } + + @Test + fun exists_notFound_returnsFalse() { + // Arrange + every { serviceMock.existsById(any()) } returns false + + // Act + val exists = baseLogic.existsById(dto.id) + + // Assert + assertFalse(exists) + } + + @Test + fun getAll_normalBehavior_returnsAllDtos() { + // Arrange + val expectedDtos = listOf(dto) + + every { serviceMock.getAll() } returns expectedDtos + + // Act + val actualDtos = baseLogic.getAll() + + // Assert + assertEquals(expectedDtos, actualDtos) + } + + @Test + fun getById_normalBehavior_returnsDtoWithGivenId() { + // Arrange + every { serviceMock.getById(any()) } returns dto + + // Act + val dtoById = baseLogic.getById(dto.id) + + // Assert + assertEquals(dto, dtoById) + } + + @Test + fun getById_notFound_throwsNotFoundException() { + // Arrange + every { serviceMock.getById(any()) } returns null + + // Act + // Assert + assertThrows { baseLogic.getById(dto.id) } + } + + @Test + fun save_normalBehavior_callsServiceSave() { + // Arrange + every { serviceMock.save(any()) } returns dto + + // Act + baseLogic.save(dto) + + // Assert + verify { + serviceMock.save(dto) + } + confirmVerified(serviceMock) + } + + @Test + fun save_normalBehavior_returnsSavedDto() { + // Arrange + every { serviceMock.save(any()) } returns dto + + // Act + val savedDto = baseLogic.save(dto) + + // Assert + assertEquals(dto, savedDto) + } + + @Test + fun update_normalBehavior_callsServiceSave() { + // Arrange + every { serviceMock.save(any()) } returns dto + every { baseLogic.existsById(any()) } returns true + + // Act + baseLogic.update(dto) + + // Assert + verify { + serviceMock.save(dto) + } + confirmVerified(serviceMock) + } + + @Test + fun update_normalBehavior_returnsUpdatedDto() { + // Arrange + every { serviceMock.save(any()) } returns dto + every { baseLogic.existsById(any()) } returns true + + // Act + val updatedDto = baseLogic.update(dto) + + // Assert + assertEquals(dto, updatedDto) + } + + @Test + fun update_notFound_throwsNotFoundException() { + // Arrange + every { serviceMock.save(any()) } returns dto + every { baseLogic.existsById(any()) } returns false + + // Act + // Assert + assertThrows { baseLogic.update(dto) } + } + + @Test + fun deleteById_normalBehavior_callsServiceDeleteById() { + // Arrange + every { serviceMock.deleteById(any()) } just runs + + // Act + baseLogic.deleteById(dto.id) + + // Assert + verify { + serviceMock.deleteById(dto.id) + } + confirmVerified(serviceMock) + } +} + +private data class TestEntityDto(override val id: Long) : EntityDto +private class TestBaseLogic>(service: S) : + BaseLogic(service, "UnitTestType") \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogicTest.kt deleted file mode 100644 index be6dba4..0000000 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogicTest.kt +++ /dev/null @@ -1,90 +0,0 @@ -package dev.fyloz.colorrecipesexplorer.logic - -import com.nhaarman.mockitokotlin2.* -import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.repository.CompanyRepository -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class CompanyLogicTest : - AbstractExternalNamedModelServiceTest() { - private val recipeLogic: RecipeLogic = mock() - override val repository: CompanyRepository = mock() - override val logic: CompanyLogic = spy( - DefaultCompanyLogic( - repository, - recipeLogic - ) - ) - - override val entity: Company = company(id = 0L, name = "company") - override val anotherEntity: Company = company(id = 1L, name = "another company") - override val entityWithEntityName: Company = company(id = 2L, name = entity.name) - override val entitySaveDto: CompanySaveDto = spy(companySaveDto()) - override val entityUpdateDto: CompanyUpdateDto = spy(companyUpdateDto(id = entity.id!!, name = null)) - - @AfterEach - override fun afterEach() { - reset(recipeLogic) - super.afterEach() - } - - // isLinkedToRecipes - - @Test - fun `isLinkedToRecipes() returns true when a given company is linked to one or more recipes`() { - whenever(recipeLogic.existsByCompany(entity)).doReturn(true) - - val found = logic.isLinkedToRecipes(entity) - - assertTrue(found) - } - - @Test - fun `isLinkedToRecipes() returns false when a given company is not linked to any recipe`() { - whenever(recipeLogic.existsByCompany(entity)).doReturn(false) - - val found = logic.isLinkedToRecipes(entity) - - assertFalse(found) - } - - // save() - - @Test - override fun `save(dto) calls and returns save() with the created entity`() { - withBaseSaveDtoTest(entity, entitySaveDto, logic) - } - - // update() - - @Test - override fun `update(dto) calls and returns update() with the created entity`() = - withBaseUpdateDtoTest(entity, entityUpdateDto, logic, { any() }) - - // 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`() - } - } - - private fun whenCanBeDeleted(id: Long = any(), test: () -> Unit) { - whenever(repository.canBeDeleted(id)).doReturn(true) - - test() - } -} diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultCompanyLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultCompanyLogicTest.kt new file mode 100644 index 0000000..6049ae9 --- /dev/null +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultCompanyLogicTest.kt @@ -0,0 +1,55 @@ +package dev.fyloz.colorrecipesexplorer.logic + +import dev.fyloz.colorrecipesexplorer.dtos.CompanyDto +import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException +import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException +import dev.fyloz.colorrecipesexplorer.service.CompanyService +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class DefaultCompanyLogicTest { + private val companyServiceMock = mockk() + + private val companyLogic = DefaultCompanyLogic(companyServiceMock) + + private val company = CompanyDto(id = 1L, name = "UnitTestCompany") + + @AfterEach + internal fun afterEach() { + clearAllMocks() + } + + @Test + fun save_nameExists_throwsAlreadyExistsException() { + // Arrange + every { companyServiceMock.existsByName(any(), any()) } returns true + + // Act + // Assert + assertThrows { companyLogic.save(company) } + } + + @Test + fun update_nameExists_throwsAlreadyExistsException() { + // Arrange + every { companyServiceMock.existsByName(any(), any()) } returns true + + // Act + // Assert + assertThrows { companyLogic.update(company) } + } + + @Test + fun deleteById_recipesDependsOnCompany_throwsCannotDeleteException() { + // Arrange + every { companyServiceMock.recipesDependsOnCompanyById(company.id) } returns true + + // Act + // Assert + assertThrows { companyLogic.deleteById(company.id) } + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogicTest.kt index 6c0d371..5be796c 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogicTest.kt @@ -157,14 +157,14 @@ class RecipeLogicTest : @Test override fun `save(dto) calls and returns save() with the created entity`() { - whenever(companyLogic.getById(company.id!!)).doReturn(company) + whenever(companyLogic.getById(company.id!!)).doReturn(companyDto(company)) doReturn(false).whenever(logic).existsByNameAndCompany(entity.name, company) withBaseSaveDtoTest(entity, entitySaveDto, logic, { argThat { this.id == null && this.color == color } }) } @Test fun `save(dto) throw AlreadyExistsException when a recipe with the given name and company exists in the repository`() { - whenever(companyLogic.getById(company.id!!)).doReturn(company) + whenever(companyLogic.getById(company.id!!)).doReturn(companyDto(company)) doReturn(true).whenever(logic).existsByNameAndCompany(entity.name, company) with(assertThrows { logic.save(entitySaveDto) }) {