feature/#25-dtos #28

Merged
william merged 11 commits from feature/#25-dtos into develop 2022-04-20 22:42:41 -04:00
31 changed files with 581 additions and 309 deletions
Showing only changes of commit cb355c9e0d - Show all commits

View File

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

View File

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

View File

@ -0,0 +1,5 @@
package dev.fyloz.colorrecipesexplorer.dtos
interface EntityDto {
val id: Long
}

View File

@ -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<Company, CompanySaveDto, CompanyUpdateDto, Company, CompanyRepository> {
/** Checks if the given [company] is used by one or more recipes. */
fun isLinkedToRecipes(company: Company): Boolean
}
interface CompanyLogic : Logic<CompanyDto, CompanyService>
@Service
@RequireDatabase
class DefaultCompanyLogic(
companyRepository: CompanyRepository,
@Lazy val recipeLogic: RecipeLogic
) :
AbstractExternalNamedModelService<Company, CompanySaveDto, CompanyUpdateDto, Company, CompanyRepository>(
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<CompanyDto, CompanyService>(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)
}
}
}

View File

@ -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<D : EntityDto, S : Service<D, *, *>> {
/** Checks if a DTO with the given [id] exists. */
fun existsById(id: Long): Boolean
/** Get all DTOs. */
fun getAll(): Collection<D>
/** 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<D : EntityDto, S : Service<D, *, *>>(
protected val service: S,
protected val typeName: String
) : Logic<D, S> {
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"
}
}

View File

@ -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<E, R : JpaRepository<E, *>> {
interface OldService<E, R : JpaRepository<E, *>> {
val repository: R
/** Gets all entities. */
@ -32,8 +32,8 @@ interface Service<E, R : JpaRepository<E, *>> {
fun delete(entity: E)
}
/** A service for entities implementing the [Model] interface. This service add supports for numeric identifiers. */
interface ModelService<E : Model, R : JpaRepository<E, *>> : Service<E, R> {
/** A service for entities implementing the [ModelEntity] interface. This service add supports for numeric identifiers. */
interface ModelService<E : ModelEntity, R : JpaRepository<E, *>> : OldService<E, R> {
/** Checks if an entity with the given [id] exists. */
fun existsById(id: Long): Boolean
@ -44,8 +44,8 @@ interface ModelService<E : Model, R : JpaRepository<E, *>> : Service<E, R> {
fun deleteById(id: Long)
}
/** A service for entities implementing the [NamedModel] interface. This service add supports for name identifiers. */
interface NamedModelService<E : NamedModel, R : JpaRepository<E, *>> : ModelService<E, R> {
/** A service for entities implementing the [NamedModelEntity] interface. This service add supports for name identifiers. */
interface NamedModelService<E : NamedModelEntity, R : JpaRepository<E, *>> : ModelService<E, R> {
/** Checks if an entity with the given [name] exists. */
fun existsByName(name: String): Boolean
@ -54,14 +54,14 @@ interface NamedModelService<E : NamedModel, R : JpaRepository<E, *>> : ModelServ
}
abstract class AbstractService<E, R : JpaRepository<E, *>>(override val repository: R) : Service<E, R> {
abstract class AbstractService<E, R : JpaRepository<E, *>>(override val repository: R) : OldService<E, R> {
override fun getAll(): Collection<E> = 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<E : Model, R : JpaRepository<E, Long>>(repository: R) :
abstract class AbstractModelService<E : ModelEntity, R : JpaRepository<E, Long>>(repository: R) :
AbstractService<E, R>(repository), ModelService<E, R> {
protected abstract fun idNotFoundException(id: Long): NotFoundException
protected abstract fun idAlreadyExistsException(id: Long): AlreadyExistsException
@ -90,7 +90,7 @@ abstract class AbstractModelService<E : Model, R : JpaRepository<E, Long>>(repos
}
}
abstract class AbstractNamedModelService<E : NamedModel, R : NamedJpaRepository<E>>(repository: R) :
abstract class AbstractNamedModelService<E : NamedModelEntity, R : NamedJpaRepository<E>>(repository: R) :
AbstractModelService<E, R>(repository), NamedModelService<E, R> {
protected abstract fun nameNotFoundException(name: String): NotFoundException
protected abstract fun nameAlreadyExistsException(name: String): AlreadyExistsException
@ -126,7 +126,7 @@ abstract class AbstractNamedModelService<E : NamedModel, R : NamedJpaRepository<
* @param S The entity save DTO type
* @param U The entity update DTO type
*/
interface ExternalService<E, S : EntityDto<E>, U : EntityDto<E>, O, R : JpaRepository<E, *>> : Service<E, R> {
interface ExternalService<E, S : EntityDto<E>, U : EntityDto<E>, O, R : JpaRepository<E, *>> : OldService<E, R> {
/** Gets all entities mapped to their output model. */
fun getAllForOutput(): Collection<O>
@ -140,15 +140,15 @@ interface ExternalService<E, S : EntityDto<E>, U : EntityDto<E>, O, R : JpaRepos
fun E.toOutput(): O
}
/** An [ExternalService] for entities implementing the [Model] interface. */
interface ExternalModelService<E : Model, S : EntityDto<E>, U : EntityDto<E>, O, R : JpaRepository<E, *>> :
/** An [ExternalService] for entities implementing the [ModelEntity] interface. */
interface ExternalModelService<E : ModelEntity, S : EntityDto<E>, U : EntityDto<E>, O, R : JpaRepository<E, *>> :
ModelService<E, R>, ExternalService<E, S, U, O, R> {
/** 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<E : NamedModel, S : EntityDto<E>, U : EntityDto<E>, O, R : JpaRepository<E, *>> :
/** An [ExternalService] for entities implementing the [NamedModelEntity] interface. */
interface ExternalNamedModelService<E : NamedModelEntity, S : EntityDto<E>, U : EntityDto<E>, O, R : JpaRepository<E, *>> :
NamedModelService<E, R>, ExternalModelService<E, S, U, O, R>
/** An [AbstractService] with the functionalities of a [ExternalService]. */
@ -160,7 +160,7 @@ abstract class AbstractExternalService<E, S : EntityDto<E>, U : EntityDto<E>, O,
}
/** An [AbstractModelService] with the functionalities of a [ExternalService]. */
abstract class AbstractExternalModelService<E : Model, S : EntityDto<E>, U : EntityDto<E>, O, R : JpaRepository<E, Long>>(
abstract class AbstractExternalModelService<E : ModelEntity, S : EntityDto<E>, U : EntityDto<E>, O, R : JpaRepository<E, Long>>(
repository: R
) : AbstractModelService<E, R>(repository), ExternalModelService<E, S, U, O, R> {
override fun getAllForOutput() =
@ -171,7 +171,7 @@ abstract class AbstractExternalModelService<E : Model, S : EntityDto<E>, U : Ent
}
/** An [AbstractNamedModelService] with the functionalities of a [ExternalService]. */
abstract class AbstractExternalNamedModelService<E : NamedModel, S : EntityDto<E>, U : EntityDto<E>, O, R : NamedJpaRepository<E>>(
abstract class AbstractExternalNamedModelService<E : NamedModelEntity, S : EntityDto<E>, U : EntityDto<E>, O, R : NamedJpaRepository<E>>(
repository: R
) : AbstractNamedModelService<E, R>(repository), ExternalNamedModelService<E, S, U, O, R> {
override fun getAllForOutput() =

View File

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

View File

@ -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<Company> {
override fun toEntity(): Company = Company(null, name)
}
open class CompanyUpdateDto(
val id: Long,
@field:NotBlank
val name: String?
) : EntityDto<Company> {
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)

View File

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

View File

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

View File

@ -30,7 +30,7 @@ data class Mix(
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
@JoinColumn(name = "mix_id")
var mixMaterials: MutableSet<MixMaterial>,
) : Model
) : ModelEntity
open class MixSaveDto(
@field:NotBlank

View File

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

View File

@ -20,7 +20,7 @@ data class MixType(
@OneToOne(cascade = [CascadeType.ALL])
@JoinColumn(name = "material_id")
var material: Material
) : NamedModel
) : NamedModelEntity
// ==== DSL ====
fun mixType(

View File

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

View File

@ -52,7 +52,7 @@ data class Recipe(
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
@JoinColumn(name = "recipe_id")
val groupsInformation: Set<RecipeGroupInformation>
) : Model {
) : ModelEntity {
/** The mix types contained in this recipe. */
val mixTypes: Collection<MixType>
@JsonIgnore
@ -150,7 +150,7 @@ data class RecipeOutputDto(
val mixes: Set<MixOutputDto>,
val groupsInformation: Set<RecipeGroupInformation>,
var imagesUrls: Set<String>
) : 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<RecipeStep>?
) : Model
) : ModelEntity
data class RecipeStepsDto(
val groupId: Long,

View File

@ -14,7 +14,7 @@ data class RecipeStep(
val position: Int,
val message: String
) : Model
) : ModelEntity
// ==== DSL ====
fun recipeStep(

View File

@ -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<Permission> = mutableSetOf(),
) : NamedModel {
) : NamedModelEntity {
val flatPermissions: Set<Permission>
get() = this.permissions
.flatMap { it.flat() }
@ -66,7 +65,7 @@ data class GroupOutputDto(
val name: String,
val permissions: Set<Permission>,
val explicitPermissions: Set<Permission>
): Model
): ModelEntity
fun group(
id: Long? = null,

View File

@ -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<Permission>
get() = permissions
.flatMap { it.flat() }
@ -103,7 +103,7 @@ data class UserOutputDto(
val permissions: Set<Permission>,
val explicitPermissions: Set<Permission>,
val lastLoginTime: LocalDateTime?
) : Model
) : ModelEntity
data class UserLoginRequest(val id: Long, val password: String)

View File

@ -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<TouchUpKitProduct>
) : 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<String>,
val content: Set<TouchUpKitProduct>,
val pdfUrl: String
) : Model
) : ModelEntity
data class TouchUpKitProductDto(
val name: String,

View File

@ -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<Company> {
interface CompanyRepository : JpaRepository<Company, Long> {
/** 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
}

View File

@ -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<E : NamedModel> : JpaRepository<E, Long> {
interface NamedJpaRepository<E : NamedModelEntity> : JpaRepository<E, Long> {
/** Checks if an entity with the given [name]. */
fun existsByName(name: String): Boolean

View File

@ -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>(COMPANY_CONTROLLER_PATH) {
companyLogic.save(company)
}
fun save(@Valid @RequestBody company: CompanyDto) =
created<CompanyDto>(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)
}
}

View File

@ -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<Resource>
.body(file)
/** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */
fun <T : Model> created(controllerPath: String, body: T): ResponseEntity<T> =
fun <T : ModelEntity> created(controllerPath: String, body: T): ResponseEntity<T> =
created(controllerPath, body, body.id!!)
/** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */
@JvmName("createdDto")
fun <T : EntityDto> created(controllerPath: String, body: T): ResponseEntity<T> =
created(controllerPath, body, body.id)
/** Creates a HTTP CREATED [ResponseEntity] with the result of the given [producer] as its body. */
fun <T : Model> created(controllerPath: String, producer: () -> T): ResponseEntity<T> =
fun <T : ModelEntity> created(controllerPath: String, producer: () -> T): ResponseEntity<T> =
created(controllerPath, producer())
/** Creates a HTTP CREATED [ResponseEntity] with the result of the given [producer] as its body. */
@JvmName("createdDto")
fun <T : EntityDto> created(controllerPath: String, producer: () -> T): ResponseEntity<T> =
created(controllerPath, producer())
/** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */

View File

@ -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<CompanyDto, Company, CompanyRepository> {
/** 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<CompanyDto, Company, CompanyRepository>(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)
}

View File

@ -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<D : EntityDto, E : ModelEntity, R : JpaRepository<E, Long>> {
/** Checks if an entity with the given [id] exists. */
fun existsById(id: Long): Boolean
/** Gets all entities as DTOs. */
fun getAll(): Collection<D>
/** 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<D : EntityDto, E : ModelEntity, R : JpaRepository<E, Long>>(protected val repository: R) :
Service<D, E, R> {
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
}

View File

@ -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 <T, R, reified E : Throwable> Iterable<T>.mapMayThrow(
@ -46,8 +46,8 @@ inline fun <T> MutableCollection<T>.excludeAll(predicate: (T) -> Boolean): Itera
return matching
}
/** Merge to [Model] [Iterable]s and prevent id duplication. */
fun <T : Model> Iterable<T>.merge(other: Iterable<T>) =
/** Merge to [ModelEntity] [Iterable]s and prevent id duplication. */
fun <T : ModelEntity> Iterable<T>.merge(other: Iterable<T>) =
this
.filter { model -> other.all { it.id != model.id } }
.plus(other)

View File

@ -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<E, S : Service<E, *>, R : JpaRepository<E, *>> {
abstract class AbstractServiceTest<E, S : OldService<E, *>, R : JpaRepository<E, *>> {
protected abstract val repository: R
protected abstract val logic: S
@ -90,7 +90,7 @@ abstract class AbstractServiceTest<E, S : Service<E, *>, R : JpaRepository<E, *>
}
}
abstract class AbstractModelServiceTest<E : Model, S : ModelService<E, *>, R : JpaRepository<E, Long>> :
abstract class AbstractModelServiceTest<E : ModelEntity, S : ModelService<E, *>, R : JpaRepository<E, Long>> :
AbstractServiceTest1<E, S, R>() {
// existsById()
@ -176,7 +176,7 @@ abstract class AbstractModelServiceTest<E : Model, S : ModelService<E, *>, R : J
}
}
abstract class AbstractNamedModelServiceTest<E : NamedModel, S : NamedModelService<E, *>, R : NamedJpaRepository<E>> :
abstract class AbstractNamedModelServiceTest<E : NamedModelEntity, S : NamedModelService<E, *>, R : NamedJpaRepository<E>> :
AbstractModelServiceTest<E, S, R>() {
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<E : Model, N : EntityDto<E>, U : EntityDto<E>, S : ExternalModelService<E, N, U, *, *>, R : JpaRepository<E, Long>> :
abstract class AbstractExternalModelServiceTest<E : ModelEntity, N : EntityDto<E>, U : EntityDto<E>, S : ExternalModelService<E, N, U, *, *>, R : JpaRepository<E, Long>> :
AbstractModelServiceTest<E, S, R>(), ExternalModelServiceTest {
protected abstract val entitySaveDto: N
protected abstract val entityUpdateDto: U
@ -281,7 +281,7 @@ abstract class AbstractExternalModelServiceTest<E : Model, N : EntityDto<E>, U :
}
}
abstract class AbstractExternalNamedModelServiceTest<E : NamedModel, N : EntityDto<E>, U : EntityDto<E>, S : ExternalNamedModelService<E, N, U, *, *>, R : NamedJpaRepository<E>> :
abstract class AbstractExternalNamedModelServiceTest<E : NamedModelEntity, N : EntityDto<E>, U : EntityDto<E>, S : ExternalNamedModelService<E, N, U, *, *>, R : NamedJpaRepository<E>> :
AbstractNamedModelServiceTest<E, S, R>(), 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 <E : Model, N : EntityDto<E>> withBaseSaveDtoTest(
fun <E : ModelEntity, N : EntityDto<E>> withBaseSaveDtoTest(
entity: E,
entitySaveDto: N,
service: ExternalService<E, N, *, *, *>,
@ -328,7 +328,7 @@ fun <E : Model, N : EntityDto<E>> withBaseSaveDtoTest(
op()
}
fun <E : Model, U : EntityDto<E>> withBaseUpdateDtoTest(
fun <E : ModelEntity, U : EntityDto<E>> withBaseUpdateDtoTest(
entity: E,
entityUpdateDto: U,
service: ExternalModelService<E, *, U, *, *>,

View File

@ -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<Service<TestEntityDto, ModelEntity, JpaRepository<ModelEntity, Long>>>()
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<NotFoundException> { 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<NotFoundException> { 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<D : EntityDto, S : Service<D, *, *>>(service: S) :
BaseLogic<D, S>(service, "UnitTestType")

View File

@ -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<Company, CompanySaveDto, CompanyUpdateDto, CompanyLogic, CompanyRepository>() {
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()
}
}

View File

@ -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<CompanyService>()
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<AlreadyExistsException> { companyLogic.save(company) }
}
@Test
fun update_nameExists_throwsAlreadyExistsException() {
// Arrange
every { companyServiceMock.existsByName(any(), any()) } returns true
// Act
// Assert
assertThrows<AlreadyExistsException> { companyLogic.update(company) }
}
@Test
fun deleteById_recipesDependsOnCompany_throwsCannotDeleteException() {
// Arrange
every { companyServiceMock.recipesDependsOnCompanyById(company.id) } returns true
// Act
// Assert
assertThrows<CannotDeleteException> { companyLogic.deleteById(company.id) }
}
}

View File

@ -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<AlreadyExistsException> { logic.save(entitySaveDto) }) {