Merge branch 'duplicated-products-in-mix' into 'master'

Resolve "Les ingrédients des mélanges sont enregistrés en plus de ceux qui étaient déjà enregistrés lors de la modification d'un mélange"

Closes #46

See merge request color-recipes-explorer/backend!7
This commit is contained in:
William Nolin 2021-03-09 19:35:10 +00:00
commit 7367816038
33 changed files with 382 additions and 168 deletions

View File

@ -81,7 +81,7 @@ tasks.test {
useJUnitPlatform()
testLogging {
events("passed", "skipped", "failed")
events("skipped", "failed")
}
}
@ -90,7 +90,11 @@ tasks.withType<JavaCompile> {
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.jvmTarget = "11"
kotlinOptions {
jvmTarget = "11"
useIR = true
freeCompilerArgs = freeCompilerArgs + "-Xopt-in=kotlin.contracts.ExperimentalContracts"
}
}
tasks.dokkaHtml {

View File

@ -13,12 +13,9 @@ import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.context.request.WebRequest
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
abstract class RestException(val httpStatus: HttpStatus) : RuntimeException() {
abstract val exceptionMessage: String
abstract class RestException(val exceptionMessage: String, val httpStatus: HttpStatus) : RuntimeException(exceptionMessage) {
abstract fun buildBody(): RestExceptionBody
override val message: String by lazy { exceptionMessage }
open inner class RestExceptionBody(val status: Int = httpStatus.value(), @JsonProperty("message") val message: String = exceptionMessage)
}

View File

@ -5,15 +5,32 @@ import dev.fyloz.trial.colorrecipesexplorer.model.Model
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.ResponseStatus
class EntityAlreadyExistsException(modelType: Class<out Model>, val identifierType: IdentifierType, val identifierName: String?, val requestedId: Any) : ModelException(modelType) {
constructor(modelType: Class<out Model>, identifierType: IdentifierType, requestedId: Any) : this(modelType, identifierType, identifierType.name, requestedId)
constructor(exception: EntityAlreadyExistsException) : this(exception.type, exception.identifierType, exception.identifierName, exception.requestedId)
class EntityAlreadyExistsException(
modelType: Class<out Model>,
val identifierType: IdentifierType,
val identifierName: String?,
val requestedId: Any
) : ModelException(modelType) {
constructor(modelType: Class<out Model>, identifierType: IdentifierType, requestedId: Any) : this(
modelType,
identifierType,
identifierType.name,
requestedId
)
constructor(exception: EntityAlreadyExistsException) : this(
exception.type,
exception.identifierType,
exception.identifierName,
exception.requestedId
)
}
@ResponseStatus(HttpStatus.CONFLICT)
class EntityAlreadyExistsRestException(val value: Any) : RestException(HttpStatus.CONFLICT) {
override val exceptionMessage: String = "An entity with the given identifier already exists"
class EntityAlreadyExistsRestException(val value: Any) :
RestException("An entity with the given identifier already exists", HttpStatus.CONFLICT) {
@Suppress("unused")
override fun buildBody(): RestExceptionBody = object : RestExceptionBody() {
val id = value
}

View File

@ -5,14 +5,17 @@ import dev.fyloz.trial.colorrecipesexplorer.model.Model
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.ResponseStatus
class EntityNotFoundException(modelType: Class<out Model>, val identifierType: IdentifierType, val identifierName: String, val requestedId: Any) : ModelException(modelType) {
constructor(modelType: Class<out Model>, identifierType: IdentifierType, requestedId: Any) : this(modelType, identifierType, identifierType.name, requestedId)
}
class EntityNotFoundException(
modelType: Class<out Model>,
val identifierType: IdentifierType,
val requestedId: Any
) : ModelException(modelType)
@ResponseStatus(HttpStatus.NOT_FOUND)
class EntityNotFoundRestException(val value: Any) : RestException(HttpStatus.NOT_FOUND) {
override val exceptionMessage: String = "An entity could not be found with the given identifier"
class EntityNotFoundRestException(val value: Any) :
RestException("An entity could not be found with the given identifier", HttpStatus.NOT_FOUND) {
@Suppress("unused")
override fun buildBody(): RestExceptionBody = object : RestExceptionBody() {
val id = value
}

View File

@ -66,7 +66,7 @@ public class MixTypeJavaService extends AbstractJavaService<MixType, MixTypeRepo
public MixType getByMaterial(Material material) {
Optional<MixType> found = findOptional(repository.findByMaterial(material));
if (found.isEmpty())
throw new EntityNotFoundException(type, ModelException.IdentifierType.OTHER, MixTypeKt.IDENTIFIER_MATERIAL_NAME, material);
throw new EntityNotFoundException(type, ModelException.IdentifierType.OTHER, material);
return found.get();
}

View File

@ -2,20 +2,15 @@ package dev.fyloz.trial.colorrecipesexplorer
import dev.fyloz.trial.colorrecipesexplorer.config.properties.CREProperties
import dev.fyloz.trial.colorrecipesexplorer.config.properties.MaterialTypeProperties
import org.slf4j.Logger
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.jdbc.DataSourceBuilder
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
import javax.sql.DataSource
@SpringBootApplication(exclude = [LiquibaseAutoConfiguration::class])
@EnableConfigurationProperties(MaterialTypeProperties::class, CREProperties::class, DatabaseUpdaterProperties::class)
class ColorRecipesExplorerApplication
fun main(args: Array<String>) {
fun main() {
runApplication<ColorRecipesExplorerApplication>()
}

View File

@ -189,9 +189,9 @@ const val defaultGroupCookieName = "Default-Group"
val blacklistedJwtTokens = mutableListOf<String>()
class JwtAuthenticationFilter(
val authManager: AuthenticationManager,
val employeeService: EmployeeService,
val securityConfigurationProperties: SecurityConfigurationProperties
private val authManager: AuthenticationManager,
private val employeeService: EmployeeService,
private val securityConfigurationProperties: SecurityConfigurationProperties
) : UsernamePasswordAuthenticationFilter() {
private var debugMode = false
@ -238,8 +238,8 @@ class JwtAuthenticationFilter(
}
class JwtAuthorizationFilter(
val userDetailsService: EmployeeUserDetailsServiceImpl,
val securityConfigurationProperties: SecurityConfigurationProperties,
private val userDetailsService: EmployeeUserDetailsServiceImpl,
private val securityConfigurationProperties: SecurityConfigurationProperties,
authenticationManager: AuthenticationManager
) : BasicAuthenticationFilter(authenticationManager) {
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {

View File

@ -2,6 +2,7 @@ package dev.fyloz.trial.colorrecipesexplorer.model
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty
import dev.fyloz.trial.colorrecipesexplorer.model.validation.NullOrNotBlank
import org.hibernate.annotations.Fetch
import org.hibernate.annotations.FetchMode
import org.springframework.security.core.GrantedAuthority
@ -109,18 +110,15 @@ open class EmployeeUpdateDto(
@field:NotNull(message = EMPLOYEE_ID_NULL_MESSAGE)
val id: Long,
@field:NotBlank(message = EMPLOYEE_FIRST_NAME_EMPTY_MESSAGE)
val firstName: String = "",
@field:NullOrNotBlank(message = EMPLOYEE_FIRST_NAME_EMPTY_MESSAGE)
val firstName: String?,
@field:NotBlank(message = EMPLOYEE_LAST_NAME_EMPTY_MESSAGE)
val lastName: String = "",
@field:NullOrNotBlank(message = EMPLOYEE_LAST_NAME_EMPTY_MESSAGE)
val lastName: String?,
@Enumerated(EnumType.STRING)
val permissions: Set<EmployeePermission> = mutableSetOf()
) : EntityDto<Employee> {
override fun toEntity(): Employee =
Employee(id, firstName, lastName, permissions = permissions.toMutableSet())
}
val permissions: Set<EmployeePermission>?
) : EntityDto<Employee>
private const val GROUP_ID_NULL_MESSAGE = "Un identifiant est requis"
@ -148,6 +146,7 @@ data class EmployeeGroup(
@JsonIgnore
val employees: MutableSet<Employee> = mutableSetOf()
) : NamedModel {
@JsonProperty("employeeCount")
fun getEmployeeCount() = employees.size
override fun equals(other: Any?): Boolean = other is EmployeeGroup && id == other.id && name == other.name
@ -313,16 +312,6 @@ fun employee(
lastLoginTime
).apply(op)
fun employee(
employee: Employee,
newId: Long? = null
) = with(employee) {
Employee(
newId
?: id, firstName, lastName, password, isDefaultGroupUser, isSystemUser, group, permissions, lastLoginTime
)
}
fun employeeSaveDto(
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
id: Long = 0L,
@ -350,12 +339,6 @@ fun employeeGroup(
op: EmployeeGroup.() -> Unit = {}
) = EmployeeGroup(id, name, permissions, employees).apply(op)
fun employeeGroup(
employeeGroup: EmployeeGroup,
newId: Long? = null,
newName: String? = null
) = with(employeeGroup) { EmployeeGroup(newId ?: id, newName ?: name, permissions, employees) }
fun employeeGroupSaveDto(
name: String = "name",
permissions: MutableSet<EmployeePermission> = mutableSetOf(),

View File

@ -9,7 +9,6 @@ import javax.persistence.*
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull
import javax.validation.constraints.Size
private const val MATERIAL_ID_NULL_MESSAGE = "Un identifiant est requis"
private const val MATERIAL_NAME_NULL_MESSAGE = "Un nom est requis"
@ -98,6 +97,7 @@ data class InventoryMaterial(
@NotNull val lowQuantity: Boolean = false
)
@Suppress("unused")
fun Material.toInventoryMaterial(minimumQuantity: Float): InventoryMaterial {
Assert.notNull(id, "Cannot convert a material without id to an inventory material")
Assert.notNull(name, "Cannot convert a material without name to an inventory material")

View File

@ -20,7 +20,7 @@ data class MixMaterial(
@JoinColumn(name = "material_id")
val material: Material,
val quantity: Float
var quantity: Float
) : Model {
constructor(mix: Mix, material: Material, quantity: Float) : this(null, mix, material, quantity)

View File

@ -11,7 +11,7 @@ interface NamedModel : Model {
interface EntityDto<out E> {
/** Converts the dto to an actual entity. */
fun toEntity(): E
fun toEntity(): E {
throw UnsupportedOperationException()
}
}
fun Collection<Model>.sortedById(): Collection<Model> = this.sortedBy { it.id }

View File

@ -50,6 +50,19 @@ data class Recipe(
@OneToMany(cascade = [CascadeType.ALL], mappedBy = "recipe")
var steps: MutableCollection<RecipeStep>
) : Model {
constructor() : this(
null,
"name",
"description",
0,
null,
"remark",
"note",
company(),
mutableListOf(),
mutableListOf()
)
constructor(
id: Long,
name: String,

View File

@ -1,10 +1,8 @@
package dev.fyloz.trial.colorrecipesexplorer.model
import com.fasterxml.jackson.annotation.JsonIgnore
import dev.fyloz.trial.colorrecipesexplorer.model.validation.NullOrNotBlank
import java.util.*
import javax.persistence.*
import javax.validation.constraints.NotNull
@Entity
@Table(name = "recipe_step")

View File

@ -3,7 +3,6 @@ package dev.fyloz.trial.colorrecipesexplorer.model.validation
import javax.validation.Constraint
import javax.validation.ConstraintValidator
import javax.validation.ConstraintValidatorContext
import javax.validation.Payload
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import kotlin.reflect.KClass
@ -15,8 +14,7 @@ private const val MESSAGE = "must be null or not blank"
@Constraint(validatedBy = [NullOrNotBlankValidator::class])
annotation class NullOrNotBlank(
val message: String = MESSAGE,
val groups: Array<KClass<*>> = [],
val payload: Array<KClass<out Payload>> = []
val groups: Array<KClass<*>> = []
)
class NullOrNotBlankValidator : ConstraintValidator<NullOrNotBlank, String> {
@ -42,5 +40,4 @@ fun isNotNullAndNotBlank(value: String?): Boolean {
return value != null && value.isNotBlank()
}
@ExperimentalContracts
fun String?.or(alternative: String): String = if (isNotNullAndNotBlank(this)) this else alternative
infix fun String?.or(alternative: String): String = if (isNotNullAndNotBlank(this)) this else alternative

View File

@ -3,7 +3,6 @@ package dev.fyloz.trial.colorrecipesexplorer.model.validation
import javax.validation.Constraint
import javax.validation.ConstraintValidator
import javax.validation.ConstraintValidatorContext
import javax.validation.Payload
import kotlin.reflect.KClass
private const val MIN_SIZE = Long.MIN_VALUE
@ -17,8 +16,7 @@ annotation class NullOrSize(
val min: Long = MIN_SIZE,
val max: Long = MAX_SIZE,
val message: String = MESSAGE,
val groups: Array<KClass<*>> = [],
val payload: Array<KClass<out Payload>> = []
val groups: Array<KClass<*>> = []
)
class NullOrSizeValidator : ConstraintValidator<NullOrSize, Any> {

View File

@ -1,5 +1,5 @@
//package dev.fyloz.trial.colorrecipesexplorer.rest
//
package dev.fyloz.trial.colorrecipesexplorer.rest
//import dev.fyloz.trial.colorrecipesexplorer.model.InventoryMaterial
//import dev.fyloz.trial.colorrecipesexplorer.service.InventoryService
//import org.springframework.http.ResponseEntity

View File

@ -34,7 +34,7 @@ interface RestModelApiController<E : Model, S : EntityDto<E>, U : EntityDto<E>>
abstract class AbstractRestApiController<E, N : EntityDto<E>, U : EntityDto<E>, S : ExternalService<E, N, U, *>>(
val service: S,
protected val controllerPath: String
private val controllerPath: String
) :
RestApiController<E, N, U> {
protected abstract fun getEntityId(entity: E): Any?

View File

@ -1,12 +1,12 @@
package dev.fyloz.trial.colorrecipesexplorer.service
import dev.fyloz.trial.colorrecipesexplorer.config.SecurityConfigurationProperties
import dev.fyloz.trial.colorrecipesexplorer.config.blacklistedJwtTokens
import dev.fyloz.trial.colorrecipesexplorer.config.defaultGroupCookieName
import dev.fyloz.trial.colorrecipesexplorer.exception.model.EntityAlreadyExistsRestException
import dev.fyloz.trial.colorrecipesexplorer.exception.model.EntityNotFoundException
import dev.fyloz.trial.colorrecipesexplorer.exception.model.EntityNotFoundRestException
import dev.fyloz.trial.colorrecipesexplorer.model.*
import dev.fyloz.trial.colorrecipesexplorer.model.validation.or
import dev.fyloz.trial.colorrecipesexplorer.repository.EmployeeGroupRepository
import dev.fyloz.trial.colorrecipesexplorer.repository.EmployeeRepository
import org.springframework.beans.factory.annotation.Autowired
@ -170,13 +170,13 @@ class EmployeeServiceImpl(employeeRepository: EmployeeRepository, val passwordEn
return update(with(entity) {
Employee(
id = id,
firstName = if (firstName.isNotBlank()) firstName else persistedEmployee.firstName,
lastName = if (lastName.isNotBlank()) lastName else persistedEmployee.lastName,
firstName = firstName or persistedEmployee.firstName,
lastName = lastName or persistedEmployee.lastName,
password = persistedEmployee.password,
isDefaultGroupUser = false,
isSystemUser = false,
group = persistedEmployee.group,
permissions = if (permissions.isNotEmpty()) permissions.toMutableSet() else persistedEmployee.permissions,
permissions = permissions?.toMutableSet() ?: persistedEmployee.permissions,
lastLoginTime = persistedEmployee.lastLoginTime
)
})

View File

@ -1,20 +1,12 @@
package dev.fyloz.trial.colorrecipesexplorer.service
import dev.fyloz.trial.colorrecipesexplorer.exception.TooLowQuantityException
import dev.fyloz.trial.colorrecipesexplorer.model.InventoryMaterial
import dev.fyloz.trial.colorrecipesexplorer.model.Material
import dev.fyloz.trial.colorrecipesexplorer.model.dto.InventoryDto
import dev.fyloz.trial.colorrecipesexplorer.model.toInventoryMaterial
import org.springframework.stereotype.Service
const val minimumQuantity = 100.0f // TODO quantity stored in database
@Service
class InventoryService(val materialService: MaterialService) {
fun getAllMaterials(): Collection<InventoryMaterial> {
return materialService.getAllNotMixType().map { it.toInventoryMaterial(minimumQuantity) }
}
/**
* Use all materials in the given [mixes].
* @throws TooLowQuantityException When there is not enough stock for a material

View File

@ -105,8 +105,7 @@ class MaterialTypeServiceImpl(repository: MaterialTypeRepository, private val ma
}
@ResponseStatus(HttpStatus.CONFLICT)
class CannotDeleteUsedMaterialTypeRestException : RestException(HttpStatus.CONFLICT) {
override val exceptionMessage: String = "Cannot delete a used material type"
class CannotDeleteUsedMaterialTypeRestException :
RestException("Cannot delete a used material type", HttpStatus.CONFLICT) {
override fun buildBody(): RestExceptionBody = object : RestExceptionBody() {}
}

View File

@ -15,8 +15,11 @@ interface MixMaterialService : ModelService<MixMaterial, MixMaterialRepository>
/** Creates [MixMaterial]s from the given [map]. The [map] must have the format <Material ID, Quantity>. */
fun createFromMap(mix: Mix, map: Map<Long, Float>): Collection<MixMaterial>
/** Creates a [MixMaterial] from the given [pair]. The [pair] must have the format <Material ID, Quantity>. */
fun createFromPair(mix: Mix, pair: Pair<Long, Float>): MixMaterial
/** Creates a [MixMaterial] with the material with the given [materialId] and the given [quantity]. */
fun create(mix: Mix, materialId: Long, quantity: Float): MixMaterial
/** Updates the [quantity] of the given [mixMaterial]. */
fun updateQuantity(mixMaterial: MixMaterial, quantity: Float): MixMaterial
}
@Service
@ -26,10 +29,13 @@ class MixMaterialServiceImpl(
) : AbstractModelService<MixMaterial, MixMaterialRepository>(mixMaterialRepository), MixMaterialService {
override fun existsByMaterial(material: Material): Boolean = repository.existsByMaterial(material)
override fun createFromMap(mix: Mix, map: Map<Long, Float>): Collection<MixMaterial> =
map.map { createFromPair(mix, it.toPair()) }
map.map { create(mix, it.key, it.value) }
override fun createFromPair(mix: Mix, pair: Pair<Long, Float>): MixMaterial {
val material = materialService.getById(pair.first)
return mixMaterial(mix = mix, material = material, quantity = pair.second)
}
override fun create(mix: Mix, materialId: Long, quantity: Float): MixMaterial =
mixMaterial(mix = mix, material = materialService.getById(materialId), quantity = quantity)
override fun updateQuantity(mixMaterial: MixMaterial, quantity: Float) =
update(mixMaterial.apply {
this.quantity = quantity
})
}

View File

@ -53,7 +53,33 @@ class MixServiceImpl(
return mix
}
@Transactional
override fun update(entity: MixUpdateDto): Mix {
fun updateMixMaterials(mix: Mix, mixMaterialsMap: Map<Long, Float>) {
val existingMixMaterialsMaterialIds = mix.mixMaterials.map { it.material.id }
val toDelete = mix.mixMaterials
mix.mixMaterials = mutableListOf(
// update existing mix materials
*mixMaterialsMap
.filter { it.key in existingMixMaterialsMaterialIds }
.map { (materialId, quantity) ->
val existingMixMaterial = mix.mixMaterials.first { it.material.id == materialId }
toDelete.remove(existingMixMaterial)
mixMaterialService.updateQuantity(existingMixMaterial, quantity)
}
.toTypedArray(),
// create new mix materials
*mixMaterialsMap
.filter { it.key !in existingMixMaterialsMaterialIds }
.map { (materialId, quantity) -> mixMaterialService.create(mix, materialId, quantity) }
.toTypedArray()
)
// delete unused mix materials
toDelete.forEach { mixMaterialService.delete(it) }
}
val mix = getById(entity.id)
if (entity.name != null || entity.materialTypeId != null) {
mix.mixType = if (mixTypeIsShared(mix.mixType)) {
@ -70,7 +96,7 @@ class MixServiceImpl(
}
}
if (entity.mixMaterials != null) {
mix.mixMaterials = mixMaterialService.createFromMap(mix, entity.mixMaterials).toMutableList()
updateMixMaterials(mix, entity.mixMaterials)
}
return update(mix)
}

View File

@ -19,7 +19,7 @@ interface RecipeService : ExternalModelService<Recipe, RecipeSaveDto, RecipeUpda
/** Gets all recipes with the given [company]. */
fun getAllByCompany(company: Company): Collection<Recipe>
/** Updates the public data of a recipe with the given [publicDateDto]. */
/** Updates the public data of a recipe with the given [publicDataDto]. */
fun updatePublicData(publicDataDto: RecipePublicDataDto)
/** Adds the given [mix] to the given [recipe]. */

View File

@ -11,7 +11,6 @@ import dev.fyloz.trial.colorrecipesexplorer.rest.RestApiController
import io.jsonwebtoken.lang.Assert
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.repository.findByIdOrNull
import kotlin.contracts.ExperimentalContracts
/**
* A service implementing the basics CRUD operations for the given entities.
@ -144,6 +143,7 @@ interface ExternalNamedModelService<E : NamedModel, S : EntityDto<E>, U : Entity
NamedModelService<E, R>, ExternalModelService<E, S, U, R>
/** An [AbstractService] with the functionalities of a [ExternalService]. */
@Suppress("unused")
abstract class AbstractExternalService<E, S : EntityDto<E>, U : EntityDto<E>, R : JpaRepository<E, *>>(repository: R) :
AbstractService<E, R>(repository), ExternalService<E, S, U, R>

View File

@ -5,6 +5,7 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager
@Suppress("UNUSED_PARAMETER")
@Disabled
@DataJpaTest
class RecipeStepRepositoryTest @Autowired constructor(

View File

@ -1,9 +1,6 @@
package dev.fyloz.trial.colorrecipesexplorer.service
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.reset
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import com.nhaarman.mockitokotlin2.*
import dev.fyloz.trial.colorrecipesexplorer.exception.model.EntityAlreadyExistsRestException
import dev.fyloz.trial.colorrecipesexplorer.exception.model.EntityNotFoundRestException
import dev.fyloz.trial.colorrecipesexplorer.model.EntityDto
@ -11,7 +8,6 @@ import dev.fyloz.trial.colorrecipesexplorer.model.Model
import dev.fyloz.trial.colorrecipesexplorer.model.NamedModel
import dev.fyloz.trial.colorrecipesexplorer.repository.NamedJpaRepository
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.springframework.data.jpa.repository.JpaRepository
@ -19,6 +15,7 @@ import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import dev.fyloz.trial.colorrecipesexplorer.service.AbstractServiceTest as AbstractServiceTest1
abstract class AbstractServiceTest<E, S : Service<E, *>, R : JpaRepository<E, *>> {
protected abstract val repository: R
@ -93,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>> :
AbstractServiceTest<E, S, R>() {
AbstractServiceTest1<E, S, R>() {
// existsById()
@ -279,10 +276,15 @@ abstract class AbstractNamedModelServiceTest<E : NamedModel, S : NamedModelServi
}
}
interface ExternalModelServiceTest {
fun `save(dto) calls and returns save() with the created entity`()
fun `update(dto) calls and returns update() with the created entity`()
}
// ==== 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>> :
AbstractModelServiceTest<E, S, R>() {
AbstractModelServiceTest<E, S, R>(), ExternalModelServiceTest {
protected abstract val entitySaveDto: N
protected abstract val entityUpdateDto: U
@ -292,21 +294,12 @@ abstract class AbstractExternalModelServiceTest<E : Model, N : EntityDto<E>, U :
super.afterEach()
}
// save()
@Test
open fun `save(dto) calls and returns save() with the created entity`() =
saveDtoTest(entity, entitySaveDto, service)
// update()
@Test
open fun `update(dto) calls and returns update() with the created entity`() =
updateDtoTest(entity, entityUpdateDto, service)
override fun `save(dto) calls and returns save() with the created entity`() =
withBaseSaveDtoTest(entity, entitySaveDto, service)
}
abstract class AbstractExternalNamedModelServiceTest<E : NamedModel, N : EntityDto<E>, U : EntityDto<E>, S : ExternalNamedModelService<E, N, U, *>, R : NamedJpaRepository<E>> :
AbstractNamedModelServiceTest<E, S, R>() {
AbstractNamedModelServiceTest<E, S, R>(), ExternalModelServiceTest {
protected abstract val entitySaveDto: N
protected abstract val entityUpdateDto: U
@ -316,20 +309,16 @@ abstract class AbstractExternalNamedModelServiceTest<E : NamedModel, N : EntityD
super.afterEach()
}
// save()
@Test
open fun `save(dto) calls and returns save() with the created entity`() =
saveDtoTest(entity, entitySaveDto, service)
// update()
@Test
open fun `update(dto) calls and returns update() with the created entity`() =
updateDtoTest(entity, entityUpdateDto, service)
override fun `save(dto) calls and returns save() with the created entity`() =
withBaseSaveDtoTest(entity, entitySaveDto, service)
}
fun <E, N : EntityDto<E>> saveDtoTest(entity: E, entitySaveDto: N, service: ExternalService<E, N, *, *>) {
fun <E, N : EntityDto<E>> withBaseSaveDtoTest(
entity: E,
entitySaveDto: N,
service: ExternalService<E, N, *, *>,
op: () -> Unit = {}
) {
doReturn(entity).whenever(service).save(entity)
doReturn(entity).whenever(entitySaveDto).toEntity()
@ -337,21 +326,26 @@ fun <E, N : EntityDto<E>> saveDtoTest(entity: E, entitySaveDto: N, service: Exte
verify(service).save(entity)
assertEquals(entity, found)
op()
}
fun <E : Model, U : EntityDto<E>> updateDtoTest(
fun <E : Model, U : EntityDto<E>> withBaseUpdateDtoTest(
entity: E,
entityUpdateDto: U,
service: ExternalModelService<E, *, U, *>
service: ExternalModelService<E, *, U, *>,
updateMockMatcher: E,
op: () -> Unit = {}
) {
// doReturn(entity).whenever(service).update(entity)
// doReturn(entity).whenever(entityUpdateDto).toEntity()
// doReturn(entity).whenever(service).getById(entity.id!!)
// doReturn(true).whenever(service).existsById(entity.id!!)
//
// val found = service.update(entityUpdateDto)
//
// verify(service).update(entity)
// assertEquals(entity, found)
assertTrue(true, "Disabled because the wrong methods are mocked for some reason")
doAnswer { it.arguments[0] }.whenever(service).update(updateMockMatcher)
doReturn(entity).whenever(entityUpdateDto).toEntity()
doReturn(entity).whenever(service).getById(entity.id!!)
doReturn(true).whenever(service).existsById(entity.id!!)
val found = service.update(entityUpdateDto)
verify(service).update(updateMockMatcher)
assertEquals(entity, found)
op()
}

View File

@ -1,7 +1,6 @@
package dev.fyloz.trial.colorrecipesexplorer.service
import com.nhaarman.mockitokotlin2.*
import dev.fyloz.trial.colorrecipesexplorer.config.SecurityConfigurationProperties
import dev.fyloz.trial.colorrecipesexplorer.config.defaultGroupCookieName
import dev.fyloz.trial.colorrecipesexplorer.exception.model.EntityAlreadyExistsRestException
import dev.fyloz.trial.colorrecipesexplorer.exception.model.EntityNotFoundRestException
@ -169,6 +168,9 @@ class EmployeeServiceTest :
// update()
override fun `update(dto) calls and returns update() with the created entity`() =
withBaseUpdateDtoTest(entity, entityUpdateDto, service, any())
@Test
fun `update() throws EntityAlreadyExistsRestException when a different employee with the given first name and last name exists`() {
whenever(repository.findByFirstNameAndLastName(entity.firstName, entity.lastName)).doReturn(
@ -392,6 +394,11 @@ class EmployeeGroupServiceTest :
verify(service, times(0)).update(group)
verify(employeeService, times(0)).update(employee)
}
// update()
override fun `update(dto) calls and returns update() with the created entity`() =
withBaseUpdateDtoTest(entity, entityUpdateDto, service, any())
}
class EmployeeUserDetailsServiceTest {

View File

@ -6,6 +6,7 @@ import dev.fyloz.trial.colorrecipesexplorer.repository.CompanyRepository
import dev.fyloz.trial.colorrecipesexplorer.service.model.RecipeJavaService
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
@ -29,6 +30,7 @@ class CompanyServiceTest :
// isLinkedToRecipes
@Test
fun `isLinkedToRecipes() returns true when a given company is linked to one or more recipes`() {
whenever(recipeService.existsByCompany(entity)).doReturn(true)
@ -37,6 +39,7 @@ class CompanyServiceTest :
assertTrue(found)
}
@Test
fun `isLinkedToRecipes() returns false when a given company is not linked to any recipe`() {
whenever(recipeService.existsByCompany(entity)).doReturn(false)
@ -44,4 +47,9 @@ class CompanyServiceTest :
assertFalse(found)
}
// update()
override fun `update(dto) calls and returns update() with the created entity`() =
withBaseUpdateDtoTest(entity, entityUpdateDto, service, any())
}

View File

@ -175,21 +175,21 @@ class MaterialServiceTest :
assertFalse(found contains anotherMixTypeMaterial)
}
// @Nested
// inner class UpdateDto {
// @Test
// fun `calls simdutService_update() with the updated entity`() {
// val mockSimdutFile = MockMultipartFile("simdut", byteArrayOf(0))
// val materialUpdateDto = spy(materialUpdateDto(id = 0L, simdutFile = mockSimdutFile))
//
// doReturn(entity).whenever(service).update(entity)
// doReturn(entity).whenever(materialUpdateDto).toEntity()
//
// service.update(materialUpdateDto)
//
// verify(simdutService).update(mockSimdutFile, entity)
// }
// }
// update()
@Test
override fun `update(dto) calls and returns update() with the created entity`() {
val mockSimdutFile = MockMultipartFile("simdut", byteArrayOf(1, 2, 3, 4, 5))
val materialUpdateDto = spy(materialUpdateDto(id = 0L, simdutFile = mockSimdutFile))
doReturn(entity).whenever(service).getById(materialUpdateDto.id)
doReturn(entity).whenever(service).update(any<Material>())
doReturn(entity).whenever(materialUpdateDto).toEntity()
service.update(materialUpdateDto)
verify(simdutService).update(eq(mockSimdutFile), any())
}
/** Helper function to replace collections.in because the id is not considered in the equals function of Material while Thymeleaf is supported. */
private infix fun Collection<Material>.contains(material: Material): Boolean =

View File

@ -109,6 +109,9 @@ class MaterialTypeServiceTest :
// update()
override fun `update(dto) calls and returns update() with the created entity`() =
withBaseUpdateDtoTest(entity, entityUpdateDto, service, any())
override fun `update() saves in the repository and returns the updated value`() {
whenever(repository.save(entity)).doReturn(entity)
whenever(repository.findByName(entity.name)).doReturn(null)

View File

@ -1,16 +1,12 @@
package dev.fyloz.trial.colorrecipesexplorer.service
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.spy
import com.nhaarman.mockitokotlin2.whenever
import dev.fyloz.trial.colorrecipesexplorer.model.Material
import dev.fyloz.trial.colorrecipesexplorer.model.MixMaterial
import dev.fyloz.trial.colorrecipesexplorer.model.material
import dev.fyloz.trial.colorrecipesexplorer.model.mixMaterial
import com.nhaarman.mockitokotlin2.*
import dev.fyloz.trial.colorrecipesexplorer.model.*
import dev.fyloz.trial.colorrecipesexplorer.repository.MixMaterialRepository
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotEquals
import kotlin.test.assertTrue
class MixMaterialServiceTest : AbstractModelServiceTest<MixMaterial, MixMaterialService, MixMaterialRepository>() {
@ -19,11 +15,12 @@ class MixMaterialServiceTest : AbstractModelServiceTest<MixMaterial, MixMaterial
override val service: MixMaterialService = spy(MixMaterialServiceImpl(repository, materialService))
private val material: Material = material(id = 0L)
override val entity: MixMaterial = mixMaterial(id = 0L, material = material)
override val entity: MixMaterial = mixMaterial(id = 0L, material = material, quantity = 1000f)
override val anotherEntity: MixMaterial = mixMaterial(id = 1L, material = material)
// existsByMaterial()
@Test
fun `existsByMaterial() returns true when a mix material with the given material exists`() {
whenever(repository.existsByMaterial(material)).doReturn(true)
@ -32,6 +29,7 @@ class MixMaterialServiceTest : AbstractModelServiceTest<MixMaterial, MixMaterial
assertTrue(found)
}
@Test
fun `existsByMaterial() returns false when no mix material with the given material exists`() {
whenever(repository.existsByMaterial(material)).doReturn(false)
@ -39,4 +37,54 @@ class MixMaterialServiceTest : AbstractModelServiceTest<MixMaterial, MixMaterial
assertFalse(found)
}
// createFromMap()
@Test
fun `createFromMap() calls create() for each map entry`() {
val mix = mix()
val map = mapOf(
1L to 1000f,
2L to 2000f,
5L to 5000f
)
val mixMaterials = map.map { mixMaterial(material = material(id = it.key), quantity = it.value) }
doAnswer { mixMaterials.first { mixMaterial -> mixMaterial.material.id == it.arguments[1] } }
.whenever(service).create(eq(mix), any(), any())
val found = service.createFromMap(mix, map)
assertEquals(mixMaterials, found)
}
// create()
@Test
fun `create() creates a mix material with the given mix, material and quantity`() {
val mix = mix()
val material = material(id = 0L)
val quantity = 1000f
val mixMaterial = mixMaterial(mix = mix, material = material, quantity = quantity)
whenever(materialService.getById(material.id!!)).doReturn(material)
val found = service.create(mix, material.id!!, quantity)
assertEquals(mixMaterial, found)
}
// updateQuantity()
@Test
fun `updateQuantity() updates the given mix material with the given quantity`() {
val quantity = 5000f
assertNotEquals(quantity, entity.quantity, message = "Quantities must not be equals for this test to works")
doAnswer { it.arguments[0] }.whenever(service).update(any())
val found = service.updateQuantity(entity, quantity)
assertEquals(found.quantity, quantity)
}
}

View File

@ -3,9 +3,9 @@ package dev.fyloz.trial.colorrecipesexplorer.service
import com.nhaarman.mockitokotlin2.*
import dev.fyloz.trial.colorrecipesexplorer.model.*
import dev.fyloz.trial.colorrecipesexplorer.repository.MixRepository
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class MixServiceTest : AbstractExternalModelServiceTest<Mix, MixSaveDto, MixUpdateDto, MixService, MixRepository>() {
override val repository: MixRepository = mock()
@ -73,6 +73,112 @@ class MixServiceTest : AbstractExternalModelServiceTest<Mix, MixSaveDto, MixUpda
assertEquals(mixWithMaterials, found)
}
// update()
private fun mixUpdateDtoTest(
scope: MixUpdateDtoTestScope = MixUpdateDtoTestScope(),
sharedMixType: Boolean = false,
op: MixUpdateDtoTestScope.() -> Unit
) {
with(scope) {
doReturn(true).whenever(service).existsById(mix.id!!)
doReturn(mix).whenever(service).getById(mix.id!!)
doReturn(sharedMixType).whenever(service).mixTypeIsShared(mix.mixType)
doAnswer { it.arguments[0] }.whenever(service).update(any<Mix>())
if (mixUpdateDto.materialTypeId != null) {
whenever(materialTypeService.getById(materialType.id!!)).doReturn(materialType)
}
op()
}
}
private fun mixUpdateDtoMixTypeTest(sharedMixType: Boolean = false, op: MixUpdateDtoTestScope.() -> Unit) {
with(MixUpdateDtoTestScope(mixUpdateDto = mixUpdateDto(id = 0L, name = "name", materialTypeId = 0L))) {
mixUpdateDtoTest(this, sharedMixType, op)
}
}
@Test
override fun `update(dto) calls and returns update() with the created entity`() {
val mixUpdateDto = spy(mixUpdateDto(id = 0L, name = null, materialTypeId = null))
doReturn(entity).whenever(service).getById(mixUpdateDto.id)
doReturn(entity).whenever(service).update(entity)
val found = service.update(mixUpdateDto)
verify(service).update(entity)
assertEquals(entity, found)
}
@Test
fun `update(dto) calls MixTypeService createForNameAndMaterialType() when mix type is shared`() {
mixUpdateDtoMixTypeTest(sharedMixType = true) {
whenever(mixTypeService.createForNameAndMaterialType(mixUpdateDto.name!!, materialType))
.doReturn(newMixType)
val found = service.update(mixUpdateDto)
verify(mixTypeService).createForNameAndMaterialType(mixUpdateDto.name!!, materialType)
assertEquals(newMixType, found.mixType)
}
}
@Test
fun `update(dto) calls MixTypeService updateForNameAndMaterialType() when mix type is not shared`() {
mixUpdateDtoMixTypeTest {
whenever(mixTypeService.updateForNameAndMaterialType(mixType, mixUpdateDto.name!!, materialType))
.doReturn(newMixType)
val found = service.update(mixUpdateDto)
verify(mixTypeService).updateForNameAndMaterialType(mixType, mixUpdateDto.name!!, materialType)
assertEquals(newMixType, found.mixType)
}
}
@Test
fun `update(dto) update, create and delete mix materials according to the given mix materials map`() {
mixUpdateDtoTest {
// Pairs exists, impairs don't
val materials = listOf(
material(id = 1L),
material(id = 2L),
material(id = 3L),
material(id = 4L),
material(id = 5L),
material(id = 6L),
)
val toDelete = mixMaterial(material = material(id = 7L), quantity = 7000f)
val mixMaterialsMap = mapOf(*materials.map { it.id!! to it.id!! * 1000f }.toTypedArray())
val allMixMaterials: Collection<MixMaterial> = materials
.map { mixMaterial(mix = mix, material = it, quantity = mixMaterialsMap[it.id]!!) }
val existingMixMaterials = allMixMaterials.filter { it.material.id!! % 2 == 0L }.toMutableList()
existingMixMaterials += toDelete
mix.mixMaterials = existingMixMaterials
(mixUpdateDto.mixMaterials as MutableMap<Long, Float>).putAll(mixMaterialsMap)
doAnswer { allMixMaterials.first { mixMaterial -> mixMaterial.material.id == (it.arguments[0] as MixMaterial).material.id } }
.whenever(mixMaterialService).updateQuantity(any(), any())
doAnswer { allMixMaterials.first { mixMaterial -> mixMaterial.material.id == it.arguments[1] } }
.whenever(mixMaterialService).create(eq(mix), any(), any())
val found: Mix = service.update(mixUpdateDto)
assertTrue { found.mixMaterials.containsAll(allMixMaterials) }
verify(mixMaterialService, times(3)).updateQuantity(argThat { material.id!! % 2 == 0L }, any())
verify(mixMaterialService, times(3)).create(eq(mix), any(), any())
verify(mixMaterialService).delete(toDelete)
}
}
// updateLocation()
@Test
@ -86,3 +192,18 @@ class MixServiceTest : AbstractExternalModelServiceTest<Mix, MixSaveDto, MixUpda
verify(service).update(expected)
}
}
data class MixUpdateDtoTestScope(
val mixType: MixType = mixType(name = "mix type"),
val newMixType: MixType = mixType(name = "another mix type"),
val materialType: MaterialType = materialType(id = 0L),
val mix: Mix = mix(id = 0L, mixType = mixType),
val mixUpdateDto: MixUpdateDto = spy(
mixUpdateDto(
id = 0L,
name = null,
materialTypeId = null,
mixMaterials = mutableMapOf()
)
)
)

View File

@ -6,7 +6,6 @@ import dev.fyloz.trial.colorrecipesexplorer.model.*
import dev.fyloz.trial.colorrecipesexplorer.repository.RecipeRepository
import dev.fyloz.trial.colorrecipesexplorer.service.files.FilesService
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.springframework.mock.web.MockMultipartFile
@ -66,9 +65,14 @@ class RecipeServiceTest :
@Test
override fun `save(dto) calls and returns save() with the created entity`() {
whenever(companyService.getById(company.id!!)).doReturn(company)
saveDtoTest(entity, entitySaveDto, service)
withBaseSaveDtoTest(entity, entitySaveDto, service)
}
// update()
override fun `update(dto) calls and returns update() with the created entity`() =
withBaseUpdateDtoTest(entity, entityUpdateDto, service, any())
// updatePublicData()
@Test