#25 Migrate users and groups to new logic

This commit is contained in:
FyloZ 2022-04-20 22:17:38 -04:00
parent 129fc4dcb9
commit d0965d75a0
Signed by: william
GPG Key ID: 835378AE9AF4AE97
51 changed files with 975 additions and 1707 deletions

View File

@ -4,11 +4,14 @@ object Constants {
object ControllerPaths { object ControllerPaths {
const val COMPANY = "/api/company" const val COMPANY = "/api/company"
const val FILE = "/api/file" const val FILE = "/api/file"
const val GROUP = "/api/user/group"
const val INVENTORY = "/api/inventory"
const val MATERIAL = "/api/material" const val MATERIAL = "/api/material"
const val MATERIAL_TYPE = "/api/materialtype" const val MATERIAL_TYPE = "/api/materialtype"
const val MIX = "/api/recipe/mix" const val MIX = "/api/recipe/mix"
const val RECIPE = "/api/recipe" const val RECIPE = "/api/recipe"
const val TOUCH_UP_KIT = "/api/touchupkit" const val TOUCH_UP_KIT = "/api/touchupkit"
const val USER = "/api/user"
} }
object FilePaths { object FilePaths {
@ -22,6 +25,7 @@ object Constants {
object ModelNames { object ModelNames {
const val COMPANY = "Company" const val COMPANY = "Company"
const val GROUP = "Group"
const val MATERIAL = "Material" const val MATERIAL = "Material"
const val MATERIAL_TYPE = "MaterialType" const val MATERIAL_TYPE = "MaterialType"
const val MIX = "Mix" const val MIX = "Mix"
@ -30,12 +34,14 @@ object Constants {
const val RECIPE = "Recipe" const val RECIPE = "Recipe"
const val RECIPE_STEP = "RecipeStep" const val RECIPE_STEP = "RecipeStep"
const val TOUCH_UP_KIT = "TouchUpKit" const val TOUCH_UP_KIT = "TouchUpKit"
const val USER = "User"
} }
object ValidationMessages { object ValidationMessages {
const val SIZE_GREATER_OR_EQUALS_ZERO = "Must be greater or equals to 0" const val SIZE_GREATER_OR_EQUALS_ZERO = "Must be greater or equals to 0"
const val SIZE_GREATER_OR_EQUALS_ONE = "Must be greater or equals to 1" const val SIZE_GREATER_OR_EQUALS_ONE = "Must be greater or equals to 1"
const val RANGE_OUTSIDE_PERCENTS = "Must be between 0 and 100" const val RANGE_OUTSIDE_PERCENTS = "Must be between 0 and 100"
const val PASSWORD_TOO_SMALL = "Must contains at least 8 characters"
} }
object ValidationRegexes { object ValidationRegexes {

View File

@ -2,13 +2,12 @@ package dev.fyloz.colorrecipesexplorer.config.security
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
import dev.fyloz.colorrecipesexplorer.dtos.UserDetails
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
import dev.fyloz.colorrecipesexplorer.dtos.UserLoginRequestDto
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.logic.users.JwtLogic import dev.fyloz.colorrecipesexplorer.logic.users.JwtLogic
import dev.fyloz.colorrecipesexplorer.logic.users.UserDetailsLogic import dev.fyloz.colorrecipesexplorer.logic.users.UserDetailsLogic
import dev.fyloz.colorrecipesexplorer.model.account.UserDetails
import dev.fyloz.colorrecipesexplorer.model.account.UserLoginRequest
import dev.fyloz.colorrecipesexplorer.model.account.UserOutputDto
import dev.fyloz.colorrecipesexplorer.model.account.toAuthorities
import dev.fyloz.colorrecipesexplorer.utils.addCookie import dev.fyloz.colorrecipesexplorer.utils.addCookie
import io.jsonwebtoken.ExpiredJwtException import io.jsonwebtoken.ExpiredJwtException
import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.authentication.AuthenticationManager
@ -40,7 +39,7 @@ class JwtAuthenticationFilter(
} }
override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication { override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication {
val loginRequest = jacksonObjectMapper().readValue(request.inputStream, UserLoginRequest::class.java) val loginRequest = jacksonObjectMapper().readValue(request.inputStream, UserLoginRequestDto::class.java)
logger.debug("Login attempt for user ${loginRequest.id}...") logger.debug("Login attempt for user ${loginRequest.id}...")
return authManager.authenticate(UsernamePasswordAuthenticationToken(loginRequest.id, loginRequest.password)) return authManager.authenticate(UsernamePasswordAuthenticationToken(loginRequest.id, loginRequest.password))
} }
@ -116,8 +115,8 @@ class JwtAuthorizationFilter(
} }
} }
private fun getAuthenticationToken(user: UserOutputDto) = private fun getAuthenticationToken(user: UserDto) =
UsernamePasswordAuthenticationToken(user.id, null, user.permissions.toAuthorities()) UsernamePasswordAuthenticationToken(user.id, null, user.authorities)
private fun getAuthenticationToken(userId: Long): UsernamePasswordAuthenticationToken? = try { private fun getAuthenticationToken(userId: Long): UsernamePasswordAuthenticationToken? = try {
val userDetails = userDetailsLogic.loadUserById(userId) val userDetails = userDetailsLogic.loadUserById(userId)

View File

@ -1,12 +1,12 @@
package dev.fyloz.colorrecipesexplorer.config.security package dev.fyloz.colorrecipesexplorer.config.security
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
import dev.fyloz.colorrecipesexplorer.emergencyMode import dev.fyloz.colorrecipesexplorer.emergencyMode
import dev.fyloz.colorrecipesexplorer.logic.users.JwtLogic import dev.fyloz.colorrecipesexplorer.logic.users.JwtLogic
import dev.fyloz.colorrecipesexplorer.logic.users.UserDetailsLogic import dev.fyloz.colorrecipesexplorer.logic.users.UserDetailsLogic
import dev.fyloz.colorrecipesexplorer.logic.users.UserLogic import dev.fyloz.colorrecipesexplorer.logic.users.UserLogic
import dev.fyloz.colorrecipesexplorer.model.account.Permission import dev.fyloz.colorrecipesexplorer.model.account.Permission
import dev.fyloz.colorrecipesexplorer.model.account.User
import mu.KotlinLogging import mu.KotlinLogging
import org.slf4j.Logger import org.slf4j.Logger
import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.context.properties.EnableConfigurationProperties
@ -147,13 +147,14 @@ class SecurityConfig(
with(securityProperties.root!!) { with(securityProperties.root!!) {
if (!userLogic.existsById(this.id)) { if (!userLogic.existsById(this.id)) {
userLogic.save( userLogic.save(
User( UserDto(
id = this.id, id = this.id,
firstName = rootUserFirstName, firstName = rootUserFirstName,
lastName = rootUserLastName, lastName = rootUserLastName,
group = null,
password = passwordEncoder.encode(this.password), password = passwordEncoder.encode(this.password),
isSystemUser = true, permissions = listOf(Permission.ADMIN),
permissions = mutableSetOf(Permission.ADMIN) isSystemUser = true
) )
) )
} }

View File

@ -0,0 +1,25 @@
package dev.fyloz.colorrecipesexplorer.dtos
import com.fasterxml.jackson.annotation.JsonIgnore
import dev.fyloz.colorrecipesexplorer.model.account.Permission
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotEmpty
data class GroupDto(
override val id: Long = 0L,
@field:NotBlank
val name: String,
@field:NotEmpty
val permissions: List<Permission>,
val explicitPermissions: List<Permission> = listOf()
) : EntityDto {
@get:JsonIgnore
val defaultGroupUserId = getDefaultGroupUserId(id)
companion object {
fun getDefaultGroupUserId(id: Long) = 1000000 + id
}
}

View File

@ -2,7 +2,6 @@ package dev.fyloz.colorrecipesexplorer.dtos
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.model.account.Group
import java.time.LocalDate import java.time.LocalDate
import javax.validation.constraints.Max import javax.validation.constraints.Max
import javax.validation.constraints.Min import javax.validation.constraints.Min
@ -94,7 +93,7 @@ data class RecipeUpdateDto(
data class RecipeGroupInformationDto( data class RecipeGroupInformationDto(
override val id: Long = 0L, override val id: Long = 0L,
val group: Group, val group: GroupDto,
val note: String? = null, val note: String? = null,

View File

@ -0,0 +1,94 @@
package dev.fyloz.colorrecipesexplorer.dtos
import com.fasterxml.jackson.annotation.JsonIgnore
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.SpringUserDetails
import dev.fyloz.colorrecipesexplorer.model.account.Permission
import dev.fyloz.colorrecipesexplorer.model.account.toAuthority
import java.time.LocalDateTime
import javax.validation.constraints.NotBlank
import javax.validation.constraints.Size
data class UserDto(
override val id: Long = 0L,
val firstName: String,
val lastName: String,
@field:JsonIgnore
val password: String = "",
val group: GroupDto?,
val permissions: List<Permission>,
val explicitPermissions: List<Permission> = listOf(),
val lastLoginTime: LocalDateTime? = null,
@field:JsonIgnore
val isDefaultGroupUser: Boolean = false,
@field:JsonIgnore
val isSystemUser: Boolean = false
) : EntityDto {
@get:JsonIgnore
val authorities
get() = permissions
.map { it.toAuthority() }
.toMutableSet()
}
data class UserSaveDto(
val id: Long = 0L,
@field:NotBlank
val firstName: String,
@field:NotBlank
val lastName: String,
@field:NotBlank
@field:Size(min = 8, message = Constants.ValidationMessages.PASSWORD_TOO_SMALL)
val password: String,
val groupId: Long?,
val permissions: List<Permission>,
// TODO WN: Test if working
// @JsonProperty(access = JsonProperty.Access.READ_ONLY)
@field:JsonIgnore
val isSystemUser: Boolean = false,
@field:JsonIgnore
val isDefaultGroupUser: Boolean = false
)
data class UserUpdateDto(
val id: Long = 0L,
@field:NotBlank
val firstName: String,
@field:NotBlank
val lastName: String,
val groupId: Long?,
val permissions: List<Permission>
)
data class UserLoginRequestDto(val id: Long, val password: String)
class UserDetails(val user: UserDto) : SpringUserDetails {
override fun getPassword() = user.password
override fun getUsername() = user.id.toString()
override fun getAuthorities() = user.authorities
override fun isAccountNonExpired() = true
override fun isAccountNonLocked() = true
override fun isCredentialsNonExpired() = true
override fun isEnabled() = true
}

View File

@ -0,0 +1,10 @@
package dev.fyloz.colorrecipesexplorer.exception
import org.springframework.http.HttpStatus
class NoDefaultGroupException : RestException(
"nodefaultgroup",
"No default group",
HttpStatus.NOT_FOUND,
"No default group cookie is defined in the current request"
)

View File

@ -94,17 +94,6 @@ abstract class BaseLogic<D : EntityDto, S : Service<D, *, *>>(
details details
) )
private fun loadRelations(dto: D, relationSelectors: Collection<(D) -> Iterable<*>>) {
relationSelectors.map { it(dto) }
.forEach {
if (it is LazyMapList<*, *>) {
it.initialize()
} else {
println("Can't load :(")
}
}
}
companion object { companion object {
const val ID_IDENTIFIER_NAME = "id" const val ID_IDENTIFIER_NAME = "id"
const val NAME_IDENTIFIER_NAME = "name" const val NAME_IDENTIFIER_NAME = "name"

View File

@ -1,182 +0,0 @@
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.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
import org.springframework.data.repository.findByIdOrNull
/**
* A service implementing the basics CRUD operations for the given entities.
*
* @param E The entity type
* @param R The entity repository type
*/
interface OldService<E, R : JpaRepository<E, *>> {
val repository: R
/** Gets all entities. */
fun getAll(): Collection<E>
/** Saves a given [entity]. */
fun save(entity: E): E
/** Updates a given [entity]. */
fun update(entity: E): E
/** Deletes a given [entity]. */
fun delete(entity: E)
}
/** 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
/** Gets the entity with the given [id]. */
fun getById(id: Long): E
/** Deletes the entity with the given [id]. */
fun deleteById(id: Long)
}
/** 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
/** Gets the entity with the given [name]. */
fun getByName(name: String): E
}
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 : 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
override fun existsById(id: Long): Boolean = repository.existsById(id)
override fun getById(id: Long): E = repository.findByIdOrNull(id) ?: throw idNotFoundException(id)
override fun save(entity: E): E {
if (entity.id != null && existsById(entity.id!!))
throw idAlreadyExistsException(entity.id!!)
return super.save(entity)
}
override fun update(entity: E): E {
assertId(entity.id)
if (!existsById(entity.id!!))
throw idNotFoundException(entity.id!!)
return super.update(entity)
}
override fun deleteById(id: Long) =
delete(getById(id)) // Use delete(entity) to prevent code duplication and to ease testing
protected fun assertId(id: Long?) {
Assert.notNull(id, "${javaClass.simpleName}.update() was called with a null identifier")
}
}
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
override fun existsByName(name: String): Boolean = repository.existsByName(name)
override fun getByName(name: String): E = repository.findByName(name) ?: throw nameNotFoundException(name)
override fun save(entity: E): E {
if (existsByName(entity.name))
throw nameAlreadyExistsException(entity.name)
return super.save(entity)
}
override fun update(entity: E): E {
assertId(entity.id)
assertName(entity.name)
with(repository.findByName(entity.name)) {
if (this != null && id != entity.id)
throw nameAlreadyExistsException(entity.name)
}
return super.update(entity)
}
private fun assertName(name: String) {
Assert.notNull(name, "${javaClass.simpleName}.update() was called with a null name")
}
}
/**
* A service that will receive *external* interactions, from the REST API, for example.
*
* @param E The entity type
* @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, *>> : OldService<E, R> {
/** Gets all entities mapped to their output model. */
fun getAllForOutput(): Collection<O>
/** Saves a given [entity]. */
fun save(entity: S): E = save(entity.toEntity())
/** Updates a given [entity]. */
fun update(entity: U): E
/** Convert the given entity to its output model. */
fun E.toOutput(): O
}
/** 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 [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]. */
@Suppress("unused")
abstract class AbstractExternalService<E, S : EntityDto<E>, U : EntityDto<E>, O, R : JpaRepository<E, *>>(repository: R) :
AbstractService<E, R>(repository), ExternalService<E, S, U, O, R> {
override fun getAllForOutput() =
getAll().map { it.toOutput() }
}
/** An [AbstractModelService] with the functionalities of a [ExternalService]. */
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() =
getAll().map { it.toOutput() }
override fun getByIdForOutput(id: Long) =
getById(id).toOutput()
}
/** An [AbstractNamedModelService] with the functionalities of a [ExternalService]. */
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() =
getAll().map { it.toOutput() }
override fun getByIdForOutput(id: Long) =
getById(id).toOutput()
}

View File

@ -2,6 +2,7 @@ package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
import dev.fyloz.colorrecipesexplorer.dtos.RecipeGroupInformationDto import dev.fyloz.colorrecipesexplorer.dtos.RecipeGroupInformationDto
import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto
import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError
@ -30,7 +31,7 @@ class DefaultRecipeStepLogic(recipeStepService: RecipeStepService) :
} }
class InvalidGroupStepsPositionsException( class InvalidGroupStepsPositionsException(
val group: Group, val group: GroupDto,
val exception: InvalidPositionsException val exception: InvalidPositionsException
) : RestException( ) : RestException(
"invalid-groupinformation-recipestep-position", "invalid-groupinformation-recipestep-position",
@ -39,7 +40,7 @@ class InvalidGroupStepsPositionsException(
"The position of steps for the group ${group.name} are invalid", "The position of steps for the group ${group.name} are invalid",
mapOf( mapOf(
"group" to group.name, "group" to group.name,
"groupId" to group.id!!, "groupId" to group.id,
"invalidSteps" to exception.errors "invalidSteps" to exception.errors
) )
) { ) {

View File

@ -1,97 +1,80 @@
package dev.fyloz.colorrecipesexplorer.logic.users package dev.fyloz.colorrecipesexplorer.logic.users
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
import dev.fyloz.colorrecipesexplorer.config.security.defaultGroupCookieName import dev.fyloz.colorrecipesexplorer.config.security.defaultGroupCookieName
import dev.fyloz.colorrecipesexplorer.logic.AbstractExternalNamedModelService import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
import dev.fyloz.colorrecipesexplorer.logic.ExternalNamedModelService import dev.fyloz.colorrecipesexplorer.dtos.UserDto
import dev.fyloz.colorrecipesexplorer.model.account.* import dev.fyloz.colorrecipesexplorer.exception.NoDefaultGroupException
import dev.fyloz.colorrecipesexplorer.repository.GroupRepository import dev.fyloz.colorrecipesexplorer.logic.BaseLogic
import org.springframework.context.annotation.Profile import dev.fyloz.colorrecipesexplorer.logic.Logic
import org.springframework.stereotype.Service import dev.fyloz.colorrecipesexplorer.service.GroupService
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.util.WebUtils import org.springframework.web.util.WebUtils
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse import javax.servlet.http.HttpServletResponse
import javax.transaction.Transactional
const val defaultGroupCookieMaxAge = 10 * 365 * 24 * 60 * 60 // 10 ans const val defaultGroupCookieMaxAge = 10 * 365 * 24 * 60 * 60 // 10 ans
interface GroupLogic : interface GroupLogic : Logic<GroupDto, GroupService> {
ExternalNamedModelService<Group, GroupSaveDto, GroupUpdateDto, GroupOutputDto, GroupRepository> {
/** Gets all the users of the group with the given [id]. */ /** Gets all the users of the group with the given [id]. */
fun getUsersForGroup(id: Long): Collection<User> fun getUsersForGroup(id: Long): Collection<UserDto>
/** Gets the default group from a cookie in the given HTTP [request]. */ /** Gets the default group from a cookie in the given HTTP [request]. */
fun getRequestDefaultGroup(request: HttpServletRequest): Group fun getRequestDefaultGroup(request: HttpServletRequest): GroupDto
/** Sets the default group cookie for the given HTTP [response]. */ /** Sets the default group cookie for the given HTTP [response]. */
fun setResponseDefaultGroup(groupId: Long, response: HttpServletResponse) fun setResponseDefaultGroup(id: Long, response: HttpServletResponse)
} }
@Service @LogicComponent
@Profile("!emergency") class DefaultGroupLogic(service: GroupService, private val userLogic: UserLogic) :
class DefaultGroupLogic( BaseLogic<GroupDto, GroupService>(service, Constants.ModelNames.GROUP),
private val userLogic: UserLogic,
groupRepository: GroupRepository
) : AbstractExternalNamedModelService<Group, GroupSaveDto, GroupUpdateDto, GroupOutputDto, GroupRepository>(
groupRepository
),
GroupLogic { GroupLogic {
override fun idNotFoundException(id: Long) = groupIdNotFoundException(id) override fun getUsersForGroup(id: Long) = userLogic.getAllByGroup(getById(id))
override fun idAlreadyExistsException(id: Long) = groupIdAlreadyExistsException(id)
override fun nameNotFoundException(name: String) = groupNameNotFoundException(name)
override fun nameAlreadyExistsException(name: String) = groupNameAlreadyExistsException(name)
override fun Group.toOutput() = GroupOutputDto( override fun getRequestDefaultGroup(request: HttpServletRequest): GroupDto {
this.id!!,
this.name,
this.permissions,
this.flatPermissions
)
override fun existsByName(name: String): Boolean = repository.existsByName(name)
override fun getUsersForGroup(id: Long): Collection<User> =
userLogic.getByGroup(getById(id))
@Transactional
override fun save(entity: Group): Group {
return super<AbstractExternalNamedModelService>.save(entity).apply {
userLogic.saveDefaultGroupUser(this)
}
}
override fun update(entity: GroupUpdateDto): Group {
val persistedGroup by lazy { getById(entity.id) }
return update(with(entity) {
Group(
entity.id,
if (name.isNotBlank()) entity.name else persistedGroup.name,
if (permissions.isNotEmpty()) entity.permissions else persistedGroup.permissions
)
})
}
@Transactional
override fun delete(entity: Group) {
userLogic.delete(userLogic.getDefaultGroupUser(entity))
super.delete(entity)
}
override fun getRequestDefaultGroup(request: HttpServletRequest): Group {
val defaultGroupCookie = WebUtils.getCookie(request, defaultGroupCookieName) val defaultGroupCookie = WebUtils.getCookie(request, defaultGroupCookieName)
?: throw NoDefaultGroupException() ?: throw NoDefaultGroupException()
val defaultGroupUser = userLogic.getById( val defaultGroupUser = userLogic.getById(
defaultGroupCookie.value.toLong(), defaultGroupCookie.value.toLong(),
ignoreDefaultGroupUsers = false, isSystemUser = false,
ignoreSystemUsers = true isDefaultGroupUser = true
) )
return defaultGroupUser.group!! return defaultGroupUser.group!!
} }
override fun setResponseDefaultGroup(groupId: Long, response: HttpServletResponse) { override fun setResponseDefaultGroup(id: Long, response: HttpServletResponse) {
val group = getById(groupId) val defaultGroupUser = userLogic.getDefaultGroupUser(getById(id))
val defaultGroupUser = userLogic.getDefaultGroupUser(group)
response.addHeader( response.addHeader(
"Set-Cookie", "Set-Cookie",
"$defaultGroupCookieName=${defaultGroupUser.id}; Max-Age=$defaultGroupCookieMaxAge; Path=/api; HttpOnly; Secure; SameSite=strict" "$defaultGroupCookieName=${defaultGroupUser.id}; Max-Age=$defaultGroupCookieMaxAge; Path=/api; HttpOnly; Secure; SameSite=strict"
) )
} }
}
@Transactional
override fun save(dto: GroupDto): GroupDto {
throwIfNameAlreadyExists(dto.name)
return super.save(dto).also {
userLogic.saveDefaultGroupUser(it)
}
}
override fun update(dto: GroupDto): GroupDto {
throwIfNameAlreadyExists(dto.name, dto.id)
return super.update(dto)
}
override fun deleteById(id: Long) {
userLogic.deleteById(GroupDto.getDefaultGroupUserId(id))
super.deleteById(id)
}
private fun throwIfNameAlreadyExists(name: String, id: Long? = null) {
if (service.existsByName(name, id)) {
throw alreadyExistsException(value = name)
}
}
}

View File

@ -3,10 +3,8 @@ package dev.fyloz.colorrecipesexplorer.logic.users
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
import dev.fyloz.colorrecipesexplorer.model.account.User import dev.fyloz.colorrecipesexplorer.dtos.UserDetails
import dev.fyloz.colorrecipesexplorer.model.account.UserDetails import dev.fyloz.colorrecipesexplorer.dtos.UserDto
import dev.fyloz.colorrecipesexplorer.model.account.UserOutputDto
import dev.fyloz.colorrecipesexplorer.model.account.toOutputDto
import dev.fyloz.colorrecipesexplorer.utils.base64encode import dev.fyloz.colorrecipesexplorer.utils.base64encode
import dev.fyloz.colorrecipesexplorer.utils.toDate import dev.fyloz.colorrecipesexplorer.utils.toDate
import io.jsonwebtoken.Jwts import io.jsonwebtoken.Jwts
@ -23,10 +21,10 @@ interface JwtLogic {
fun buildJwt(userDetails: UserDetails): String fun buildJwt(userDetails: UserDetails): String
/** Build a JWT token for the given [user]. */ /** Build a JWT token for the given [user]. */
fun buildJwt(user: User): String fun buildJwt(user: UserDto): String
/** Parses a user from the given [jwt] token. */ /** Parses a user from the given [jwt] token. */
fun parseJwt(jwt: String): UserOutputDto fun parseJwt(jwt: String): UserDto
} }
@Service @Service
@ -54,14 +52,14 @@ class DefaultJwtLogic(
override fun buildJwt(userDetails: UserDetails) = override fun buildJwt(userDetails: UserDetails) =
buildJwt(userDetails.user) buildJwt(userDetails.user)
override fun buildJwt(user: User): String = override fun buildJwt(user: UserDto): String =
jwtBuilder jwtBuilder
.setSubject(user.id.toString()) .setSubject(user.id.toString())
.setExpiration(getCurrentExpirationDate()) .setExpiration(getCurrentExpirationDate())
.claim(jwtClaimUser, user.serialize()) .claim(jwtClaimUser, user.serialize())
.compact() .compact()
override fun parseJwt(jwt: String): UserOutputDto = override fun parseJwt(jwt: String): UserDto =
with( with(
jwtParser.parseClaimsJws(jwt) jwtParser.parseClaimsJws(jwt)
.body.get(jwtClaimUser, String::class.java) .body.get(jwtClaimUser, String::class.java)
@ -74,6 +72,6 @@ class DefaultJwtLogic(
.plusSeconds(securityProperties.jwtDuration) .plusSeconds(securityProperties.jwtDuration)
.toDate() .toDate()
private fun User.serialize(): String = private fun UserDto.serialize(): String =
objectMapper.writeValueAsString(this.toOutputDto()) objectMapper.writeValueAsString(this)
} }

View File

@ -4,18 +4,18 @@ import dev.fyloz.colorrecipesexplorer.SpringUserDetails
import dev.fyloz.colorrecipesexplorer.SpringUserDetailsService import dev.fyloz.colorrecipesexplorer.SpringUserDetailsService
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
import dev.fyloz.colorrecipesexplorer.dtos.UserDetails
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.account.Permission import dev.fyloz.colorrecipesexplorer.model.account.Permission
import dev.fyloz.colorrecipesexplorer.model.account.User import dev.fyloz.colorrecipesexplorer.model.account.User
import dev.fyloz.colorrecipesexplorer.model.account.UserDetails
import dev.fyloz.colorrecipesexplorer.model.account.user
import org.springframework.context.annotation.Profile import org.springframework.context.annotation.Profile
import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
interface UserDetailsLogic : SpringUserDetailsService { interface UserDetailsLogic : SpringUserDetailsService {
/** Loads an [User] for the given [id]. */ /** Loads an [User] for the given [id]. */
fun loadUserById(id: Long, ignoreDefaultGroupUsers: Boolean = false): UserDetails fun loadUserById(id: Long, isDefaultGroupUser: Boolean = true): UserDetails
} }
@Service @Service
@ -25,17 +25,17 @@ class DefaultUserDetailsLogic(
) : UserDetailsLogic { ) : UserDetailsLogic {
override fun loadUserByUsername(username: String): UserDetails { override fun loadUserByUsername(username: String): UserDetails {
try { try {
return loadUserById(username.toLong(), true) return loadUserById(username.toLong(), false)
} catch (ex: NotFoundException) { } catch (ex: NotFoundException) {
throw UsernameNotFoundException(username) throw UsernameNotFoundException(username)
} }
} }
override fun loadUserById(id: Long, ignoreDefaultGroupUsers: Boolean): UserDetails { override fun loadUserById(id: Long, isDefaultGroupUser: Boolean): UserDetails {
val user = userLogic.getById( val user = userLogic.getById(
id, id,
ignoreDefaultGroupUsers = ignoreDefaultGroupUsers, isSystemUser = true,
ignoreSystemUsers = false isDefaultGroupUser = isDefaultGroupUser
) )
return UserDetails(user) return UserDetails(user)
} }
@ -46,7 +46,7 @@ class DefaultUserDetailsLogic(
class EmergencyUserDetailsLogic( class EmergencyUserDetailsLogic(
securityProperties: CreSecurityProperties securityProperties: CreSecurityProperties
) : UserDetailsLogic { ) : UserDetailsLogic {
private val users: Set<User> private val users: Set<UserDto>
init { init {
if (securityProperties.root == null) { if (securityProperties.root == null) {
@ -56,20 +56,23 @@ class EmergencyUserDetailsLogic(
users = setOf( users = setOf(
// Add root user // Add root user
with(securityProperties.root!!) { with(securityProperties.root!!) {
user( UserDto(
id = this.id, id = this.id,
plainPassword = this.password, firstName = "Root",
permissions = mutableSetOf(Permission.ADMIN) lastName = "User",
group = null,
password = this.password,
permissions = listOf(Permission.ADMIN)
) )
} }
) )
} }
override fun loadUserByUsername(username: String): SpringUserDetails { override fun loadUserByUsername(username: String): SpringUserDetails {
return loadUserById(username.toLong(), true) return loadUserById(username.toLong(), false)
} }
override fun loadUserById(id: Long, ignoreDefaultGroupUsers: Boolean): UserDetails { override fun loadUserById(id: Long, isDefaultGroupUser: Boolean): UserDetails {
val user = users.firstOrNull { it.id == id } val user = users.firstOrNull { it.id == id }
?: throw UsernameNotFoundException(id.toString()) ?: throw UsernameNotFoundException(id.toString())

View File

@ -1,189 +1,146 @@
package dev.fyloz.colorrecipesexplorer.logic.users package dev.fyloz.colorrecipesexplorer.logic.users
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
import dev.fyloz.colorrecipesexplorer.config.security.authorizationCookieName
import dev.fyloz.colorrecipesexplorer.config.security.blacklistedJwtTokens import dev.fyloz.colorrecipesexplorer.config.security.blacklistedJwtTokens
import dev.fyloz.colorrecipesexplorer.logic.AbstractExternalModelService import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
import dev.fyloz.colorrecipesexplorer.logic.ExternalModelService import dev.fyloz.colorrecipesexplorer.dtos.UserDto
import dev.fyloz.colorrecipesexplorer.model.account.* import dev.fyloz.colorrecipesexplorer.dtos.UserSaveDto
import dev.fyloz.colorrecipesexplorer.repository.UserRepository import dev.fyloz.colorrecipesexplorer.dtos.UserUpdateDto
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.logic.BaseLogic
import dev.fyloz.colorrecipesexplorer.logic.Logic
import dev.fyloz.colorrecipesexplorer.model.account.Permission
import dev.fyloz.colorrecipesexplorer.service.UserService
import org.springframework.context.annotation.Lazy import org.springframework.context.annotation.Lazy
import org.springframework.context.annotation.Profile import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.stereotype.Service import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.web.util.WebUtils import org.springframework.web.util.WebUtils
import java.time.LocalDateTime import java.time.LocalDateTime
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
interface UserLogic : interface UserLogic : Logic<UserDto, UserService> {
ExternalModelService<User, UserSaveDto, UserUpdateDto, UserOutputDto, UserRepository> { /** Gets all users which have the given [group]. */
/** Check if an [User] with the given [firstName] and [lastName] exists. */ fun getAllByGroup(group: GroupDto): Collection<UserDto>
fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean
/** Gets the user with the given [id]. */ /** Gets the user with the given [id]. */
fun getById(id: Long, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): User fun getById(id: Long, isSystemUser: Boolean, isDefaultGroupUser: Boolean): UserDto
/** Gets all users which have the given [group]. */
fun getByGroup(group: Group): Collection<User>
/** Gets the default user of the given [group]. */ /** Gets the default user of the given [group]. */
fun getDefaultGroupUser(group: Group): User fun getDefaultGroupUser(group: GroupDto): UserDto
/** Save a default group user for the given [group]. */ /** Save a default group user for the given [group]. */
fun saveDefaultGroupUser(group: Group) fun saveDefaultGroupUser(group: GroupDto)
/** Updates de given [entity]. **/ /** Saves the given [dto]. */
fun update(entity: User, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): User fun save(dto: UserSaveDto): UserDto
/** Updates the last login time of the user with the given [userId]. */ /** Updates the given [dto]. */
fun updateLastLoginTime(userId: Long, time: LocalDateTime = LocalDateTime.now()): User fun update(dto: UserUpdateDto): UserDto
/** Updates the last login time of the user with the given [id]. */
fun updateLastLoginTime(id: Long, time: LocalDateTime = LocalDateTime.now()): UserDto
/** Updates the password of the user with the given [id]. */ /** Updates the password of the user with the given [id]. */
fun updatePassword(id: Long, password: String): User fun updatePassword(id: Long, password: String): UserDto
/** Adds the given [permission] to the user with the given [userId]. */ /** Adds the given [permission] to the user with the given [id]. */
fun addPermission(userId: Long, permission: Permission): User fun addPermission(id: Long, permission: Permission): UserDto
/** Removes the given [permission] from the user with the given [userId]. */ /** Removes the given [permission] from the user with the given [id]. */
fun removePermission(userId: Long, permission: Permission): User fun removePermission(id: Long, permission: Permission): UserDto
/** Logout an user. Add the authorization token of the given [request] to the blacklisted tokens. */ /** Logout a user. Add the authorization token of the given [request] to the blacklisted tokens. */
fun logout(request: HttpServletRequest) fun logout(request: HttpServletRequest)
} }
@Service @LogicComponent
@Profile("!emergency")
class DefaultUserLogic( class DefaultUserLogic(
userRepository: UserRepository, service: UserService, @Lazy private val groupLogic: GroupLogic, @Lazy private val passwordEncoder: PasswordEncoder
@Lazy val groupLogic: GroupLogic, ) : BaseLogic<UserDto, UserService>(service, Constants.ModelNames.USER), UserLogic {
) : AbstractExternalModelService<User, UserSaveDto, UserUpdateDto, UserOutputDto, UserRepository>( override fun getAll() = service.getAll(isSystemUser = false, isDefaultGroupUser = false)
userRepository
),
UserLogic {
override fun idNotFoundException(id: Long) = userIdNotFoundException(id)
override fun idAlreadyExistsException(id: Long) = userIdAlreadyExistsException(id)
override fun User.toOutput() = this.toOutputDto() override fun getAllByGroup(group: GroupDto) = service.getAllByGroup(group)
override fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean = override fun getById(id: Long) = getById(id, isSystemUser = false, isDefaultGroupUser = false)
repository.existsByFirstNameAndLastName(firstName, lastName) override fun getById(id: Long, isSystemUser: Boolean, isDefaultGroupUser: Boolean) =
service.getById(id, !isDefaultGroupUser, !isSystemUser) ?: throw notFoundException(value = id)
override fun getAll(): Collection<User> = override fun getDefaultGroupUser(group: GroupDto) =
super.getAll().filter { !it.isSystemUser && !it.isDefaultGroupUser } service.getDefaultGroupUser(group) ?: throw notFoundException(identifierName = "groupId", value = group.id)
override fun getById(id: Long): User = override fun saveDefaultGroupUser(group: GroupDto) {
getById(id, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)
override fun getById(id: Long, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): User =
super.getById(id).apply {
if (ignoreSystemUsers && isSystemUser || ignoreDefaultGroupUsers && isDefaultGroupUser)
throw idNotFoundException(id)
}
override fun getByGroup(group: Group): Collection<User> =
repository.findAllByGroup(group).filter {
!it.isSystemUser && !it.isDefaultGroupUser
}
override fun getDefaultGroupUser(group: Group): User =
repository.findByIsDefaultGroupUserIsTrueAndGroupIs(group)
override fun save(entity: UserSaveDto): User =
save(with(entity) {
user(
id = id,
firstName = firstName,
lastName = lastName,
plainPassword = password,
isDefaultGroupUser = false,
isSystemUser = false,
group = if (groupId != null) groupLogic.getById(groupId) else null,
permissions = permissions
)
})
override fun save(entity: User): User {
if (existsById(entity.id))
throw userIdAlreadyExistsException(entity.id)
if (existsByFirstNameAndLastName(entity.firstName, entity.lastName))
throw userFullNameAlreadyExistsException(entity.firstName, entity.lastName)
return super<AbstractExternalModelService>.save(entity)
}
override fun saveDefaultGroupUser(group: Group) {
save( save(
user( UserSaveDto(
id = 1000000L + group.id!!, id = group.defaultGroupUserId,
firstName = group.name, firstName = group.name,
lastName = "User", lastName = "User",
plainPassword = group.name, password = group.name,
group = group, groupId = group.id,
permissions = listOf(),
isDefaultGroupUser = true isDefaultGroupUser = true
) )
) )
} }
override fun updateLastLoginTime(userId: Long, time: LocalDateTime): User { override fun save(dto: UserSaveDto) = save(
val user = getById(userId, ignoreDefaultGroupUsers = true, ignoreSystemUsers = false) UserDto(
user.lastLoginTime = time id = dto.id,
firstName = dto.firstName,
lastName = dto.lastName,
password = passwordEncoder.encode(dto.password),
group = if (dto.groupId != null) groupLogic.getById(dto.groupId) else null,
permissions = dto.permissions,
isSystemUser = dto.isSystemUser,
isDefaultGroupUser = dto.isDefaultGroupUser
)
)
override fun save(dto: UserDto): UserDto {
throwIfIdAlreadyExists(dto.id)
throwIfFirstNameAndLastNameAlreadyExists(dto.firstName, dto.lastName)
return super.save(dto)
}
override fun update(dto: UserUpdateDto): UserDto {
val user = getById(dto.id, isSystemUser = false, isDefaultGroupUser = false)
return update( return update(
user, user.copy(
ignoreDefaultGroupUsers = true, firstName = dto.firstName,
ignoreSystemUsers = false lastName = dto.lastName,
group = if (dto.groupId != null) groupLogic.getById(dto.groupId) else null,
permissions = dto.permissions
)
) )
} }
override fun update(entity: UserUpdateDto): User { override fun update(dto: UserDto): UserDto {
val persistedUser by lazy { getById(entity.id) } throwIfFirstNameAndLastNameAlreadyExists(dto.firstName, dto.lastName, dto.id)
return update(with(entity) {
User( return super.update(dto)
id = id,
firstName = firstName ?: persistedUser.firstName,
lastName = lastName ?: persistedUser.lastName,
password = persistedUser.password,
isDefaultGroupUser = false,
isSystemUser = false,
group = if (entity.groupId != null) groupLogic.getById(entity.groupId) else persistedUser.group,
permissions = permissions?.toMutableSet() ?: persistedUser.permissions,
lastLoginTime = persistedUser.lastLoginTime
)
})
} }
override fun update(entity: User): User = override fun updateLastLoginTime(id: Long, time: LocalDateTime) = with(getById(id)) {
update(entity, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true) update(this.copy(lastLoginTime = time))
override fun update(entity: User, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): User {
with(repository.findByFirstNameAndLastName(entity.firstName, entity.lastName)) {
if (this != null && id != entity.id)
throw userFullNameAlreadyExistsException(entity.firstName, entity.lastName)
}
return super.update(entity)
} }
override fun updatePassword(id: Long, password: String): User { override fun updatePassword(id: Long, password: String) = with(getById(id)) {
val persistedUser = getById(id, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true) update(this.copy(password = passwordEncoder.encode(password)))
return super.update(with(persistedUser) {
user(
id,
firstName,
lastName,
plainPassword = password,
isDefaultGroupUser,
isSystemUser,
group,
permissions,
lastLoginTime
)
})
} }
override fun addPermission(userId: Long, permission: Permission): User = override fun addPermission(id: Long, permission: Permission) = with(getById(id)) {
super.update(getById(userId).apply { permissions += permission }) update(this.copy(permissions = this.permissions + permission))
}
override fun removePermission(userId: Long, permission: Permission): User = override fun removePermission(id: Long, permission: Permission) = with(getById(id)) {
super.update(getById(userId).apply { permissions -= permission }) update(this.copy(permissions = this.permissions - permission))
}
override fun logout(request: HttpServletRequest) { override fun logout(request: HttpServletRequest) {
val authorizationCookie = WebUtils.getCookie(request, "Authorization") val authorizationCookie = WebUtils.getCookie(request, authorizationCookieName)
if (authorizationCookie != null) { if (authorizationCookie != null) {
val authorizationToken = authorizationCookie.value val authorizationToken = authorizationCookie.value
if (authorizationToken != null && authorizationToken.startsWith("Bearer")) { if (authorizationToken != null && authorizationToken.startsWith("Bearer")) {
@ -191,4 +148,22 @@ class DefaultUserLogic(
} }
} }
} }
}
private fun throwIfIdAlreadyExists(id: Long) {
if (service.existsById(id)) {
throw alreadyExistsException(identifierName = ID_IDENTIFIER_NAME, value = id)
}
}
private fun throwIfFirstNameAndLastNameAlreadyExists(firstName: String, lastName: String, id: Long? = null) {
if (service.existsByFirstNameAndLastName(firstName, lastName, id)) {
throw AlreadyExistsException(
typeNameLowerCase,
"$typeName already exists",
"A $typeNameLowerCase with the name '$firstName $lastName' already exists",
"$firstName $lastName",
"fullName"
)
}
}
}

View File

@ -7,7 +7,7 @@ import javax.persistence.*
data class Company( data class Company(
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?, override val id: Long,
@Column(unique = true) @Column(unique = true)
val name: String val name: String

View File

@ -8,7 +8,7 @@ import javax.persistence.*
data class Material( data class Material(
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?, override val id: Long,
@Column(unique = true) @Column(unique = true)
val name: String, val name: String,

View File

@ -8,7 +8,7 @@ import javax.persistence.*
data class MaterialType( data class MaterialType(
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long? = null, override val id: Long,
@Column(unique = true) @Column(unique = true)
val name: String = "", val name: String = "",

View File

@ -7,9 +7,9 @@ import javax.persistence.*
data class Mix( data class Mix(
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?, override val id: Long,
var location: String?, val location: String?,
@Column(name = "recipe_id") @Column(name = "recipe_id")
val recipeId: Long, val recipeId: Long,

View File

@ -7,13 +7,13 @@ import javax.persistence.*
data class MixMaterial( data class MixMaterial(
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?, override val id: Long,
@ManyToOne @ManyToOne
@JoinColumn(name = "material_id") @JoinColumn(name = "material_id")
val material: Material, val material: Material,
var quantity: Float, val quantity: Float,
var position: Int val position: Int
) : ModelEntity ) : ModelEntity

View File

@ -7,7 +7,7 @@ import javax.persistence.*
data class MixType( data class MixType(
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?, override val id: Long,
val name: String, val name: String,

View File

@ -1,17 +1,6 @@
package dev.fyloz.colorrecipesexplorer.model package dev.fyloz.colorrecipesexplorer.model
/** Represents an entity, named differently to prevent conflicts with the JPA annotation. */ /** Represents an entity with an id, named differently to prevent conflicts with the JPA annotation. */
interface ModelEntity { interface ModelEntity {
val id: Long? val id: Long
}
interface NamedModelEntity : ModelEntity {
val name: String
}
interface EntityDto<out E> {
/** Converts the dto to an actual entity. */
fun toEntity(): E {
throw UnsupportedOperationException()
}
} }

View File

@ -9,7 +9,7 @@ import javax.persistence.*
data class Recipe( data class Recipe(
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?, override val id: Long,
/** The name of the recipe. It is not unique in the entire system, but is unique in the scope of a [Company]. */ /** The name of the recipe. It is not unique in the entire system, but is unique in the scope of a [Company]. */
val name: String, val name: String,
@ -47,15 +47,15 @@ data class Recipe(
data class RecipeGroupInformation( data class RecipeGroupInformation(
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?, override val id: Long,
@ManyToOne @ManyToOne
@JoinColumn(name = "group_id") @JoinColumn(name = "group_id")
val group: Group, val group: Group,
var note: String?, val note: String?,
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true) @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
@JoinColumn(name = "recipe_group_information_id") @JoinColumn(name = "recipe_group_information_id")
var steps: List<RecipeStep>? val steps: List<RecipeStep>?
) : ModelEntity ) : ModelEntity

View File

@ -7,7 +7,7 @@ import javax.persistence.*
data class RecipeStep( data class RecipeStep(
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?, override val id: Long,
val position: Int, val position: Int,

View File

@ -1,134 +1,24 @@
package dev.fyloz.colorrecipesexplorer.model.account package dev.fyloz.colorrecipesexplorer.model.account
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 dev.fyloz.colorrecipesexplorer.model.ModelEntity
import org.hibernate.annotations.Fetch import org.hibernate.annotations.Fetch
import org.hibernate.annotations.FetchMode import org.hibernate.annotations.FetchMode
import org.springframework.http.HttpStatus
import javax.persistence.* import javax.persistence.*
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotEmpty
@Entity @Entity
@Table(name = "user_group") @Table(name = "user_group")
data class Group( data class Group(
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
override var id: Long? = null, override val id: Long,
@Column(unique = true) @Column(unique = true)
override val name: String = "", val name: String,
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
@ElementCollection(fetch = FetchType.EAGER) @ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "group_permission", joinColumns = [JoinColumn(name = "group_id")]) @CollectionTable(name = "group_permission", joinColumns = [JoinColumn(name = "group_id")])
@Column(name = "permission") @Column(name = "permission")
@Fetch(FetchMode.SUBSELECT) @Fetch(FetchMode.SUBSELECT)
val permissions: MutableSet<Permission> = mutableSetOf(), val permissions: List<Permission>,
) : NamedModelEntity { ) : ModelEntity
val flatPermissions: Set<Permission>
get() = this.permissions
.flatMap { it.flat() }
.filter { !it.deprecated }
.toSet()
}
open class GroupSaveDto(
@field:NotBlank
val name: String,
@field:NotEmpty
val permissions: MutableSet<Permission>
) : EntityDto<Group> {
override fun toEntity(): Group =
Group(null, name, permissions)
}
open class GroupUpdateDto(
val id: Long,
@field:NotBlank
val name: String,
@field:NotEmpty
val permissions: MutableSet<Permission>
) : EntityDto<Group> {
override fun toEntity(): Group =
Group(id, name, permissions)
}
data class GroupOutputDto(
override val id: Long,
val name: String,
val permissions: Set<Permission>,
val explicitPermissions: Set<Permission>
): ModelEntity
fun group(
id: Long? = null,
name: String = "name",
permissions: MutableSet<Permission> = mutableSetOf(),
op: Group.() -> Unit = {}
) = Group(id, name, permissions).apply(op)
fun groupSaveDto(
name: String = "name",
permissions: MutableSet<Permission> = mutableSetOf(),
op: GroupSaveDto.() -> Unit = {}
) = GroupSaveDto(name, permissions).apply(op)
fun groupUpdateDto(
id: Long = 0L,
name: String = "name",
permissions: MutableSet<Permission> = mutableSetOf(),
op: GroupUpdateDto.() -> Unit = {}
) = GroupUpdateDto(id, name, permissions).apply(op)
// ==== Exceptions ====
private const val GROUP_NOT_FOUND_EXCEPTION_TITLE = "Group not found"
private const val GROUP_ALREADY_EXISTS_EXCEPTION_TITLE = "Group already exists"
private const val GROUP_EXCEPTION_ERROR_CODE = "group"
class NoDefaultGroupException : RestException(
"nodefaultgroup",
"No default group",
HttpStatus.NOT_FOUND,
"No default group cookie is defined in the current request"
)
fun groupIdNotFoundException(id: Long) =
NotFoundException(
GROUP_EXCEPTION_ERROR_CODE,
GROUP_NOT_FOUND_EXCEPTION_TITLE,
"A group with the id $id could not be found",
id
)
fun groupNameNotFoundException(name: String) =
NotFoundException(
GROUP_EXCEPTION_ERROR_CODE,
GROUP_NOT_FOUND_EXCEPTION_TITLE,
"A group with the name $name could not be found",
name,
"name"
)
fun groupIdAlreadyExistsException(id: Long) =
AlreadyExistsException(
GROUP_EXCEPTION_ERROR_CODE,
GROUP_ALREADY_EXISTS_EXCEPTION_TITLE,
"A group with the id $id already exists",
id,
)
fun groupNameAlreadyExistsException(name: String) =
AlreadyExistsException(
GROUP_EXCEPTION_ERROR_CODE,
GROUP_ALREADY_EXISTS_EXCEPTION_TITLE,
"A group with the name $name already exists",
name,
"name"
)

View File

@ -1,20 +1,10 @@
package dev.fyloz.colorrecipesexplorer.model.account package dev.fyloz.colorrecipesexplorer.model.account
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.ModelEntity import dev.fyloz.colorrecipesexplorer.model.ModelEntity
import org.hibernate.annotations.Fetch import org.hibernate.annotations.Fetch
import org.hibernate.annotations.FetchMode import org.hibernate.annotations.FetchMode
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import java.time.LocalDateTime import java.time.LocalDateTime
import javax.persistence.* import javax.persistence.*
import javax.validation.constraints.NotBlank
import javax.validation.constraints.Size
private const val VALIDATION_PASSWORD_LENGTH = "Must contains at least 8 characters"
@Entity @Entity
@Table(name = "user") @Table(name = "user")
@ -23,210 +13,31 @@ data class User(
override val id: Long, override val id: Long,
@Column(name = "first_name") @Column(name = "first_name")
val firstName: String = "", val firstName: String,
@Column(name = "last_name") @Column(name = "last_name")
val lastName: String = "", val lastName: String,
val password: String = "", val password: String,
@Column(name = "default_group_user") @Column(name = "default_group_user")
val isDefaultGroupUser: Boolean = false, val isDefaultGroupUser: Boolean,
@Column(name = "system_user") @Column(name = "system_user")
val isSystemUser: Boolean = false, val isSystemUser: Boolean,
@ManyToOne @ManyToOne
@JoinColumn(name = "group_id") @JoinColumn(name = "group_id")
@Fetch(FetchMode.SELECT) @Fetch(FetchMode.SELECT)
var group: Group? = null, val group: Group?,
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
@ElementCollection(fetch = FetchType.EAGER) @ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "user_permission", joinColumns = [JoinColumn(name = "user_id")]) @CollectionTable(name = "user_permission", joinColumns = [JoinColumn(name = "user_id")])
@Column(name = "permission") @Column(name = "permission")
@Fetch(FetchMode.SUBSELECT) @Fetch(FetchMode.SUBSELECT)
val permissions: MutableSet<Permission> = mutableSetOf(), val permissions: List<Permission>,
@Column(name = "last_login_time") @Column(name = "last_login_time")
var lastLoginTime: LocalDateTime? = null
) : ModelEntity {
val flatPermissions: Set<Permission>
get() = permissions
.flatMap { it.flat() }
.filter { !it.deprecated }
.toMutableSet()
.apply {
if (group != null) this.addAll(group!!.flatPermissions)
}
}
open class UserSaveDto(
val id: Long,
@field:NotBlank
val firstName: String,
@field:NotBlank
val lastName: String,
@field:NotBlank
@field:Size(min = 8, message = VALIDATION_PASSWORD_LENGTH)
val password: String,
val groupId: Long?,
@Enumerated(EnumType.STRING)
val permissions: MutableSet<Permission> = mutableSetOf()
) : EntityDto<User>
open class UserUpdateDto(
val id: Long,
@field:NotBlank
val firstName: String?,
@field:NotBlank
val lastName: String?,
val groupId: Long?,
@Enumerated(EnumType.STRING)
val permissions: Set<Permission>?
) : EntityDto<User>
data class UserOutputDto(
override val id: Long,
val firstName: String,
val lastName: String,
val group: Group?,
val permissions: Set<Permission>,
val explicitPermissions: Set<Permission>,
val lastLoginTime: LocalDateTime? val lastLoginTime: LocalDateTime?
) : ModelEntity ) : ModelEntity
data class UserLoginRequest(val id: Long, val password: String)
data class UserDetails(val user: User) : SpringUserDetails {
override fun getPassword() = user.password
override fun getUsername() = user.id.toString()
override fun getAuthorities() = user.flatPermissions.toAuthorities()
override fun isAccountNonExpired() = true
override fun isAccountNonLocked() = true
override fun isCredentialsNonExpired() = true
override fun isEnabled() = true
}
// ==== DSL ====
fun user(
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
password: String = "password",
isDefaultGroupUser: Boolean = false,
isSystemUser: Boolean = false,
group: Group? = null,
permissions: MutableSet<Permission> = mutableSetOf(),
lastLoginTime: LocalDateTime? = null,
op: User.() -> Unit = {}
) = User(
id,
firstName,
lastName,
password,
isDefaultGroupUser,
isSystemUser,
group,
permissions,
lastLoginTime
).apply(op)
fun user(
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
plainPassword: String = "password",
isDefaultGroupUser: Boolean = false,
isSystemUser: Boolean = false,
group: Group? = null,
permissions: MutableSet<Permission> = mutableSetOf(),
lastLoginTime: LocalDateTime? = null,
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
op: User.() -> Unit = {}
) = User(
id,
firstName,
lastName,
passwordEncoder.encode(plainPassword),
isDefaultGroupUser,
isSystemUser,
group,
permissions,
lastLoginTime
).apply(op)
fun userSaveDto(
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
password: String = passwordEncoder.encode("password"),
groupId: Long? = null,
permissions: MutableSet<Permission> = mutableSetOf(),
op: UserSaveDto.() -> Unit = {}
) = UserSaveDto(id, firstName, lastName, password, groupId, permissions).apply(op)
fun userUpdateDto(
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
groupId: Long? = null,
permissions: MutableSet<Permission> = mutableSetOf(),
op: UserUpdateDto.() -> Unit = {}
) = UserUpdateDto(id, firstName, lastName, groupId, permissions).apply(op)
// ==== Extensions ====
fun Set<Permission>.toAuthorities() =
this.map { it.toAuthority() }.toMutableSet()
fun User.toOutputDto() =
UserOutputDto(
this.id,
this.firstName,
this.lastName,
this.group,
this.flatPermissions,
this.permissions,
this.lastLoginTime
)
// ==== Exceptions ====
private const val USER_NOT_FOUND_EXCEPTION_TITLE = "User not found"
private const val USER_ALREADY_EXISTS_EXCEPTION_TITLE = "User already exists"
private const val USER_EXCEPTION_ERROR_CODE = "user"
fun userIdNotFoundException(id: Long) =
NotFoundException(
USER_EXCEPTION_ERROR_CODE,
USER_NOT_FOUND_EXCEPTION_TITLE,
"An user with the id $id could not be found",
id
)
fun userIdAlreadyExistsException(id: Long) =
AlreadyExistsException(
USER_EXCEPTION_ERROR_CODE,
USER_ALREADY_EXISTS_EXCEPTION_TITLE,
"An user with the id $id already exists",
id
)
fun userFullNameAlreadyExistsException(firstName: String, lastName: String) =
AlreadyExistsException(
USER_EXCEPTION_ERROR_CODE,
USER_ALREADY_EXISTS_EXCEPTION_TITLE,
"An user with the name '$firstName $lastName' already exists",
"$firstName $lastName",
"fullName"
)

View File

@ -9,7 +9,7 @@ import javax.persistence.*
data class TouchUpKit( data class TouchUpKit(
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?, override val id: Long,
val project: String, val project: String,
@ -41,7 +41,7 @@ data class TouchUpKit(
data class TouchUpKitProduct( data class TouchUpKitProduct(
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?, override val id: Long,
val name: String, val name: String,

View File

@ -3,18 +3,28 @@ package dev.fyloz.colorrecipesexplorer.repository
import dev.fyloz.colorrecipesexplorer.model.account.Group import dev.fyloz.colorrecipesexplorer.model.account.Group
import dev.fyloz.colorrecipesexplorer.model.account.User import dev.fyloz.colorrecipesexplorer.model.account.User
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
@Repository @Repository
interface UserRepository : JpaRepository<User, Long> { interface UserRepository : JpaRepository<User, Long> {
fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean /** Checks if a user with the given [firstName], [lastName] and a different [id] exists. */
fun existsByFirstNameAndLastNameAndIdNot(firstName: String, lastName: String, id: Long): Boolean
fun findByFirstNameAndLastName(firstName: String, lastName: String): User?
/** Finds all users for the given [group]. */
@Query("SELECT u FROM User u WHERE u.group = :group AND u.isSystemUser IS FALSE AND u.isDefaultGroupUser IS FALSE")
fun findAllByGroup(group: Group): Collection<User> fun findAllByGroup(group: Group): Collection<User>
fun findByIsDefaultGroupUserIsTrueAndGroupIs(group: Group): User /** Finds the user with the given [firstName] and [lastName]. */
fun findByFirstNameAndLastName(firstName: String, lastName: String): User?
/** Finds the default user for the given [group]. */
@Query("SELECT u FROM User u WHERE u.group = :group AND u.isDefaultGroupUser IS TRUE")
fun findDefaultGroupUser(group: Group): User?
} }
@Repository @Repository
interface GroupRepository : NamedJpaRepository<Group> interface GroupRepository : JpaRepository<Group, Long> {
/** Checks if a group with the given [name] and a different [id] exists. */
fun existsByNameAndIdNot(name: String, id: Long): Boolean
}

View File

@ -1,18 +0,0 @@
package dev.fyloz.colorrecipesexplorer.repository
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 : NamedModelEntity> : JpaRepository<E, Long> {
/** Checks if an entity with the given [name]. */
fun existsByName(name: String): Boolean
/** Gets the entity with the given [name]. */
fun findByName(name: String): E?
/** Removes the entity with the given [name]. */
fun deleteByName(name: String)
}

View File

@ -1,10 +1,15 @@
package dev.fyloz.colorrecipesexplorer.rest package dev.fyloz.colorrecipesexplorer.rest
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditUsers import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditUsers
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewUsers import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewUsers
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
import dev.fyloz.colorrecipesexplorer.dtos.UserSaveDto
import dev.fyloz.colorrecipesexplorer.dtos.UserUpdateDto
import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic
import dev.fyloz.colorrecipesexplorer.logic.users.UserLogic import dev.fyloz.colorrecipesexplorer.logic.users.UserLogic
import dev.fyloz.colorrecipesexplorer.model.account.* import dev.fyloz.colorrecipesexplorer.model.account.Permission
import org.springframework.context.annotation.Profile import org.springframework.context.annotation.Profile
import org.springframework.http.MediaType import org.springframework.http.MediaType
import org.springframework.security.access.prepost.PreAuthorize import org.springframework.security.access.prepost.PreAuthorize
@ -13,30 +18,25 @@ import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse import javax.servlet.http.HttpServletResponse
import javax.validation.Valid import javax.validation.Valid
private const val USER_CONTROLLER_PATH = "api/user"
private const val GROUP_CONTROLLER_PATH = "api/user/group"
@RestController @RestController
@RequestMapping(USER_CONTROLLER_PATH) @RequestMapping(Constants.ControllerPaths.USER)
@Profile("!emergency") @Profile("!emergency")
class UserController(private val userLogic: UserLogic) { class UserController(private val userLogic: UserLogic) {
@GetMapping @GetMapping
@PreAuthorizeViewUsers @PreAuthorizeViewUsers
fun getAll() = fun getAll() =
ok(userLogic.getAllForOutput()) ok(userLogic.getAll())
@GetMapping("{id}") @GetMapping("{id}")
@PreAuthorizeViewUsers @PreAuthorizeViewUsers
fun getById(@PathVariable id: Long) = fun getById(@PathVariable id: Long) =
ok(userLogic.getByIdForOutput(id)) ok(userLogic.getById(id))
@PostMapping @PostMapping
@PreAuthorizeEditUsers @PreAuthorizeEditUsers
fun save(@Valid @RequestBody user: UserSaveDto) = fun save(@Valid @RequestBody user: UserSaveDto) =
created<UserOutputDto>(USER_CONTROLLER_PATH) { created<UserDto>(Constants.ControllerPaths.USER) {
with(userLogic) { userLogic.save(user)
save(user).toOutput()
}
} }
@PutMapping @PutMapping
@ -78,7 +78,7 @@ class UserController(private val userLogic: UserLogic) {
} }
@RestController @RestController
@RequestMapping(GROUP_CONTROLLER_PATH) @RequestMapping(Constants.ControllerPaths.GROUP)
@Profile("!emergency") @Profile("!emergency")
class GroupsController( class GroupsController(
private val groupLogic: GroupLogic, private val groupLogic: GroupLogic,
@ -87,20 +87,17 @@ class GroupsController(
@GetMapping @GetMapping
@PreAuthorize("hasAnyAuthority('VIEW_RECIPES', 'VIEW_USERS')") @PreAuthorize("hasAnyAuthority('VIEW_RECIPES', 'VIEW_USERS')")
fun getAll() = fun getAll() =
ok(groupLogic.getAllForOutput()) ok(groupLogic.getAll())
@GetMapping("{id}") @GetMapping("{id}")
@PreAuthorizeViewUsers @PreAuthorizeViewUsers
fun getById(@PathVariable id: Long) = fun getById(@PathVariable id: Long) =
ok(groupLogic.getByIdForOutput(id)) ok(groupLogic.getById(id))
@GetMapping("{id}/users") @GetMapping("{id}/users")
@PreAuthorizeViewUsers @PreAuthorizeViewUsers
fun getUsersForGroup(@PathVariable id: Long) = fun getUsersForGroup(@PathVariable id: Long) =
ok(with(userLogic) { ok(groupLogic.getUsersForGroup(id))
groupLogic.getUsersForGroup(id)
.map { it.toOutput() }
})
@PostMapping("default/{groupId}") @PostMapping("default/{groupId}")
@PreAuthorizeViewUsers @PreAuthorizeViewUsers
@ -113,27 +110,25 @@ class GroupsController(
@PreAuthorizeViewUsers @PreAuthorizeViewUsers
fun getRequestDefaultGroup(request: HttpServletRequest) = fun getRequestDefaultGroup(request: HttpServletRequest) =
ok(with(groupLogic) { ok(with(groupLogic) {
getRequestDefaultGroup(request).toOutput() getRequestDefaultGroup(request)
}) })
@GetMapping("currentuser") @GetMapping("currentuser")
fun getCurrentGroupUser(request: HttpServletRequest) = fun getCurrentGroupUser(request: HttpServletRequest) =
ok(with(groupLogic.getRequestDefaultGroup(request)) { ok(with(groupLogic.getRequestDefaultGroup(request)) {
userLogic.getDefaultGroupUser(this).toOutputDto() userLogic.getDefaultGroupUser(this)
}) })
@PostMapping @PostMapping
@PreAuthorizeEditUsers @PreAuthorizeEditUsers
fun save(@Valid @RequestBody group: GroupSaveDto) = fun save(@Valid @RequestBody group: GroupDto) =
created<GroupOutputDto>(GROUP_CONTROLLER_PATH) { created<GroupDto>(Constants.ControllerPaths.GROUP) {
with(groupLogic) { groupLogic.save(group)
save(group).toOutput()
}
} }
@PutMapping @PutMapping
@PreAuthorizeEditUsers @PreAuthorizeEditUsers
fun update(@Valid @RequestBody group: GroupUpdateDto) = fun update(@Valid @RequestBody group: GroupDto) =
noContent { noContent {
groupLogic.update(group) groupLogic.update(group)
} }

View File

@ -1,5 +1,6 @@
package dev.fyloz.colorrecipesexplorer.rest package dev.fyloz.colorrecipesexplorer.rest
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.dtos.MaterialQuantityDto import dev.fyloz.colorrecipesexplorer.dtos.MaterialQuantityDto
import dev.fyloz.colorrecipesexplorer.dtos.MixDeductDto import dev.fyloz.colorrecipesexplorer.dtos.MixDeductDto
import dev.fyloz.colorrecipesexplorer.logic.InventoryLogic import dev.fyloz.colorrecipesexplorer.logic.InventoryLogic
@ -10,10 +11,8 @@ import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
private const val INVENTORY_CONTROLLER_PATH = "api/inventory"
@RestController @RestController
@RequestMapping(INVENTORY_CONTROLLER_PATH) @RequestMapping(Constants.ControllerPaths.INVENTORY)
@Profile("!emergency") @Profile("!emergency")
class InventoryController( class InventoryController(
private val inventoryLogic: InventoryLogic private val inventoryLogic: InventoryLogic

View File

@ -44,19 +44,11 @@ fun fileCreated(basePath: String, producer: () -> String): ResponseEntity<String
return ResponseEntity.created(URI.create(path)).body(fileName) return ResponseEntity.created(URI.create(path)).body(fileName)
} }
/** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */
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. */ /** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */
@JvmName("createdDto") @JvmName("createdDto")
fun <T : EntityDto> created(controllerPath: String, body: T): ResponseEntity<T> = fun <T : EntityDto> created(controllerPath: String, body: T): ResponseEntity<T> =
created(controllerPath, body, body.id) created(controllerPath, body, body.id)
/** Creates a HTTP CREATED [ResponseEntity] with the result of the given [producer] as its body. */
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. */ /** Creates a HTTP CREATED [ResponseEntity] with the result of the given [producer] as its body. */
@JvmName("createdDto") @JvmName("createdDto")
fun <T : EntityDto> created(controllerPath: String, producer: () -> T): ResponseEntity<T> = fun <T : EntityDto> created(controllerPath: String, producer: () -> T): ResponseEntity<T> =

View File

@ -20,7 +20,7 @@ class DefaultCompanyService(repository: CompanyRepository) :
override fun isUsedByRecipe(id: Long) = repository.isUsedByRecipe(id) override fun isUsedByRecipe(id: Long) = repository.isUsedByRecipe(id)
override fun toDto(entity: Company) = override fun toDto(entity: Company) =
CompanyDto(entity.id!!, entity.name) CompanyDto(entity.id, entity.name)
override fun toEntity(dto: CompanyDto) = override fun toEntity(dto: CompanyDto) =
Company(dto.id, dto.name) Company(dto.id, dto.name)

View File

@ -0,0 +1,31 @@
package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
import dev.fyloz.colorrecipesexplorer.model.account.Group
import dev.fyloz.colorrecipesexplorer.model.account.Permission
import dev.fyloz.colorrecipesexplorer.model.account.flat
import dev.fyloz.colorrecipesexplorer.repository.GroupRepository
interface GroupService : Service<GroupDto, Group, GroupRepository> {
/** Checks if a group with the given [name] and a different [id] exists. */
fun existsByName(name: String, id: Long? = null): Boolean
/** Flatten the given the permissions of the given [group]. */
fun flattenPermissions(group: Group): List<Permission>
}
@ServiceComponent
class DefaultGroupService(repository: GroupRepository) : BaseService<GroupDto, Group, GroupRepository>(repository),
GroupService {
override fun existsByName(name: String, id: Long?) = repository.existsByNameAndIdNot(name, id ?: 0L)
override fun toDto(entity: Group) =
GroupDto(entity.id, entity.name, flattenPermissions(entity), entity.permissions)
override fun toEntity(dto: GroupDto) =
Group(dto.id, dto.name, dto.permissions)
override fun flattenPermissions(group: Group) =
group.permissions.flatMap { it.flat() }.filter { !it.deprecated }
}

View File

@ -35,7 +35,7 @@ class DefaultMaterialService(
override fun toDto(entity: Material) = override fun toDto(entity: Material) =
MaterialDto( MaterialDto(
entity.id!!, entity.id,
entity.name, entity.name,
entity.inventoryQuantity, entity.inventoryQuantity,
entity.isMixType, entity.isMixType,

View File

@ -37,7 +37,7 @@ class DefaultMaterialTypeService(repository: MaterialTypeRepository) :
override fun isUsedByMaterial(id: Long) = repository.isUsedByMaterial(id) override fun isUsedByMaterial(id: Long) = repository.isUsedByMaterial(id)
override fun toDto(entity: MaterialType) = override fun toDto(entity: MaterialType) =
MaterialTypeDto(entity.id!!, entity.name, entity.prefix, entity.usePercentages, entity.systemType) MaterialTypeDto(entity.id, entity.name, entity.prefix, entity.usePercentages, entity.systemType)
override fun toEntity(dto: MaterialTypeDto) = override fun toEntity(dto: MaterialTypeDto) =
MaterialType(dto.id, dto.name, dto.prefix, dto.usePercentages, dto.systemType) MaterialType(dto.id, dto.name, dto.prefix, dto.usePercentages, dto.systemType)

View File

@ -16,7 +16,7 @@ class DefaultMixMaterialService(repository: MixMaterialRepository, private val m
override fun existsByMaterialId(materialId: Long) = repository.existsByMaterialId(materialId) override fun existsByMaterialId(materialId: Long) = repository.existsByMaterialId(materialId)
override fun toDto(entity: MixMaterial) = override fun toDto(entity: MixMaterial) =
MixMaterialDto(entity.id!!, materialService.toDto(entity.material), entity.quantity, entity.position) MixMaterialDto(entity.id, materialService.toDto(entity.material), entity.quantity, entity.position)
override fun toEntity(dto: MixMaterialDto) = override fun toEntity(dto: MixMaterialDto) =
MixMaterial(dto.id, materialService.toEntity(dto.material), dto.quantity, dto.position) MixMaterial(dto.id, materialService.toEntity(dto.material), dto.quantity, dto.position)

View File

@ -33,7 +33,7 @@ class DefaultMixService(
override fun toDto(entity: Mix) = override fun toDto(entity: Mix) =
MixDto( MixDto(
entity.id!!, entity.id,
entity.location, entity.location,
entity.recipeId, entity.recipeId,
mixTypeService.toDto(entity.mixType), mixTypeService.toDto(entity.mixType),

View File

@ -37,7 +37,7 @@ class DefaultMixTypeService(
override fun toDto(entity: MixType) = override fun toDto(entity: MixType) =
MixTypeDto( MixTypeDto(
entity.id!!, entity.id,
entity.name, entity.name,
materialTypeService.toDto(entity.materialType), materialTypeService.toDto(entity.materialType),
if (entity.material != null) materialService.toDto(entity.material) else null if (entity.material != null) materialService.toDto(entity.material) else null

View File

@ -27,6 +27,7 @@ class DefaultRecipeService(
private val companyService: CompanyService, private val companyService: CompanyService,
private val mixService: MixService, private val mixService: MixService,
private val recipeStepService: RecipeStepService, private val recipeStepService: RecipeStepService,
private val groupService: GroupService,
private val configLogic: ConfigurationLogic private val configLogic: ConfigurationLogic
) : ) :
BaseService<RecipeDto, Recipe, RecipeRepository>(repository), RecipeService { BaseService<RecipeDto, Recipe, RecipeRepository>(repository), RecipeService {
@ -39,7 +40,7 @@ class DefaultRecipeService(
@Transactional @Transactional
override fun toDto(entity: Recipe) = override fun toDto(entity: Recipe) =
RecipeDto( RecipeDto(
entity.id!!, entity.id,
entity.name, entity.name,
entity.description, entity.description,
entity.color, entity.color,
@ -55,8 +56,8 @@ class DefaultRecipeService(
private fun groupInformationToDto(entity: RecipeGroupInformation) = private fun groupInformationToDto(entity: RecipeGroupInformation) =
RecipeGroupInformationDto( RecipeGroupInformationDto(
entity.id!!, entity.id,
entity.group, groupService.toDto(entity.group),
entity.note, entity.note,
entity.steps?.lazyMap(recipeStepService::toDto) ?: listOf() entity.steps?.lazyMap(recipeStepService::toDto) ?: listOf()
) )
@ -77,7 +78,12 @@ class DefaultRecipeService(
) )
private fun groupInformationToEntity(dto: RecipeGroupInformationDto) = private fun groupInformationToEntity(dto: RecipeGroupInformationDto) =
RecipeGroupInformation(dto.id, dto.group, dto.note, dto.steps.map(recipeStepService::toEntity)) RecipeGroupInformation(
dto.id,
groupService.toEntity(dto.group),
dto.note,
dto.steps.map(recipeStepService::toEntity)
)
private fun isApprobationExpired(recipe: Recipe): Boolean? = private fun isApprobationExpired(recipe: Recipe): Boolean? =
with(Period.parse(configLogic.getContent(ConfigurationType.RECIPE_APPROBATION_EXPIRATION))) { with(Period.parse(configLogic.getContent(ConfigurationType.RECIPE_APPROBATION_EXPIRATION))) {

View File

@ -11,7 +11,7 @@ interface RecipeStepService : Service<RecipeStepDto, RecipeStep, RecipeStepRepos
class DefaultRecipeStepService(repository: RecipeStepRepository) : class DefaultRecipeStepService(repository: RecipeStepRepository) :
BaseService<RecipeStepDto, RecipeStep, RecipeStepRepository>(repository), RecipeStepService { BaseService<RecipeStepDto, RecipeStep, RecipeStepRepository>(repository), RecipeStepService {
override fun toDto(entity: RecipeStep) = override fun toDto(entity: RecipeStep) =
RecipeStepDto(entity.id!!, entity.position, entity.message) RecipeStepDto(entity.id, entity.position, entity.message)
override fun toEntity(dto: RecipeStepDto) = override fun toEntity(dto: RecipeStepDto) =
RecipeStep(dto.id, dto.position, dto.message) RecipeStep(dto.id, dto.position, dto.message)

View File

@ -24,7 +24,7 @@ class DefaultTouchUpKitService(repository: TouchUpKitRepository, private val con
override fun toDto(entity: TouchUpKit) = override fun toDto(entity: TouchUpKit) =
TouchUpKitDto( TouchUpKitDto(
entity.id!!, entity.id,
entity.project, entity.project,
entity.buggy, entity.buggy,
entity.company, entity.company,
@ -39,7 +39,7 @@ class DefaultTouchUpKitService(repository: TouchUpKitRepository, private val con
) )
private fun touchUpKitProductToDto(entity: TouchUpKitProduct) = private fun touchUpKitProductToDto(entity: TouchUpKitProduct) =
TouchUpKitProductDto(entity.id!!, entity.name, entity.description, entity.quantity, entity.ready) TouchUpKitProductDto(entity.id, entity.name, entity.description, entity.quantity, entity.ready)
override fun toEntity(dto: TouchUpKitDto) = override fun toEntity(dto: TouchUpKitDto) =
TouchUpKit( TouchUpKit(

View File

@ -0,0 +1,103 @@
package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
import dev.fyloz.colorrecipesexplorer.model.account.Permission
import dev.fyloz.colorrecipesexplorer.model.account.User
import dev.fyloz.colorrecipesexplorer.model.account.flat
import dev.fyloz.colorrecipesexplorer.repository.UserRepository
import org.springframework.data.repository.findByIdOrNull
interface UserService : Service<UserDto, User, UserRepository> {
/** Checks if a user with the given [firstName] and [lastName] exists. */
fun existsByFirstNameAndLastName(firstName: String, lastName: String, id: Long? = null): Boolean
/** Gets all users, depending on [isSystemUser] and [isDefaultGroupUser]. */
fun getAll(isSystemUser: Boolean, isDefaultGroupUser: Boolean): Collection<UserDto>
/** Gets all users for the given [group]. */
fun getAllByGroup(group: GroupDto): Collection<UserDto>
/** Finds the user with the given [id], depending on [isSystemUser] and [isDefaultGroupUser]. */
fun getById(id: Long, isSystemUser: Boolean, isDefaultGroupUser: Boolean): UserDto?
/** Finds the user with the given [firstName] and [lastName]. */
fun getByFirstNameAndLastName(firstName: String, lastName: String): UserDto?
/** Find the default user for the given [group]. */
fun getDefaultGroupUser(group: GroupDto): UserDto?
}
@ServiceComponent
class DefaultUserService(repository: UserRepository, private val groupService: GroupService) :
BaseService<UserDto, User, UserRepository>(repository), UserService {
override fun existsByFirstNameAndLastName(firstName: String, lastName: String, id: Long?) =
repository.existsByFirstNameAndLastNameAndIdNot(firstName, lastName, id ?: 0L)
override fun getAll(isSystemUser: Boolean, isDefaultGroupUser: Boolean) =
repository.findAll()
.filter { isSystemUser || !it.isSystemUser }
.filter { isDefaultGroupUser || !it.isDefaultGroupUser }
.map(::toDto)
override fun getAllByGroup(group: GroupDto) =
repository.findAllByGroup(groupService.toEntity(group))
.map(::toDto)
override fun getById(id: Long, isSystemUser: Boolean, isDefaultGroupUser: Boolean): UserDto? {
val user = repository.findByIdOrNull(id) ?: return null
if ((!isSystemUser && user.isSystemUser) ||
!isDefaultGroupUser && user.isDefaultGroupUser
) {
return null
}
return toDto(user)
}
override fun getByFirstNameAndLastName(firstName: String, lastName: String): UserDto? {
val user = repository.findByFirstNameAndLastName(firstName, lastName)
return if (user != null) toDto(user) else null
}
override fun getDefaultGroupUser(group: GroupDto): UserDto? {
val user = repository.findDefaultGroupUser(groupService.toEntity(group))
return if (user != null) toDto(user) else null
}
override fun toDto(entity: User) = UserDto(
entity.id,
entity.firstName,
entity.lastName,
entity.password,
if (entity.group != null) groupService.toDto(entity.group) else null,
getFlattenPermissions(entity),
entity.permissions,
entity.lastLoginTime,
entity.isDefaultGroupUser,
entity.isSystemUser
)
override fun toEntity(dto: UserDto) = User(
dto.id,
dto.firstName,
dto.lastName,
dto.password,
dto.isDefaultGroupUser,
dto.isSystemUser,
if (dto.group != null) groupService.toEntity(dto.group) else null,
dto.explicitPermissions,
dto.lastLoginTime
)
private fun getFlattenPermissions(user: User): List<Permission> {
val perms = user.permissions.flatMap { it.flat() }.filter { !it.deprecated }
if (user.group != null) {
return perms + groupService.flattenPermissions(user.group)
}
return perms
}
}

View File

@ -1,349 +0,0 @@
package dev.fyloz.colorrecipesexplorer.logic
import com.nhaarman.mockitokotlin2.*
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.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
import org.junit.jupiter.api.assertThrows
import org.springframework.data.jpa.repository.JpaRepository
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import dev.fyloz.colorrecipesexplorer.logic.AbstractServiceTest as AbstractServiceTest1
abstract class AbstractServiceTest<E, S : OldService<E, *>, R : JpaRepository<E, *>> {
protected abstract val repository: R
protected abstract val logic: S
protected abstract val entity: E
protected abstract val anotherEntity: E
protected val entityList: List<E>
get() = listOf(
entity,
anotherEntity
)
@AfterEach
open fun afterEach() {
reset(repository, logic)
}
// getAll()
@Test
open fun `getAll() returns all available entities`() {
whenever(repository.findAll()).doReturn(entityList)
val found = logic.getAll()
assertEquals(entityList, found)
}
@Test
open fun `getAll() returns empty list when there is no entities`() {
whenever(repository.findAll()).doReturn(listOf())
val found = logic.getAll()
assertTrue { found.isEmpty() }
}
// save()
@Test
open fun `save() saves in the repository and returns the saved value`() {
whenever(repository.save(entity)).doReturn(entity)
val found = logic.save(entity)
verify(repository).save(entity)
assertEquals(entity, found)
}
// update()
@Test
open fun `update() saves in the repository and returns the updated value`() {
whenever(repository.save(entity)).doReturn(entity)
val found = logic.update(entity)
verify(repository).save(entity)
assertEquals(entity, found)
}
// delete()
@Test
open fun `delete() deletes in the repository`() {
logic.delete(entity)
verify(repository).delete(entity)
}
}
abstract class AbstractModelServiceTest<E : ModelEntity, S : ModelService<E, *>, R : JpaRepository<E, Long>> :
AbstractServiceTest1<E, S, R>() {
// existsById()
@Test
open fun `existsById() returns true when an entity with the given id exists in the repository`() {
whenever(repository.existsById(entity.id!!)).doReturn(true)
val found = logic.existsById(entity.id!!)
assertTrue(found)
}
@Test
open fun `existsById() returns false when no entity with the given id exists in the repository`() {
whenever(repository.existsById(entity.id!!)).doReturn(false)
val found = logic.existsById(entity.id!!)
assertFalse(found)
}
// getById()
@Test
open fun `getById() returns the entity with the given id from the repository`() {
whenever(repository.findById(entity.id!!)).doReturn(Optional.of(entity))
val found = logic.getById(entity.id!!)
assertEquals(entity, found)
}
@Test
open fun `getById() throws NotFoundException when no entity with the given id exists in the repository`() {
whenever(repository.findById(entity.id!!)).doReturn(Optional.empty())
assertThrows<NotFoundException> { logic.getById(entity.id!!) }
.assertErrorCode()
}
// save()
@Test
open fun `save() throws AlreadyExistsException when an entity with the given id exists in the repository`() {
doReturn(true).whenever(logic).existsById(entity.id!!)
assertThrows<AlreadyExistsException> { logic.save(entity) }
.assertErrorCode()
}
// update()
@Test
override fun `update() saves in the repository and returns the updated value`() {
whenever(repository.save(entity)).doReturn(entity)
doReturn(true).whenever(logic).existsById(entity.id!!)
doReturn(entity).whenever(logic).getById(entity.id!!)
val found = logic.update(entity)
verify(repository).save(entity)
assertEquals(entity, found)
}
@Test
open fun `update() throws NotFoundException when no entity with the given id exists in the repository`() {
doReturn(false).whenever(logic).existsById(entity.id!!)
assertThrows<NotFoundException> { logic.update(entity) }
.assertErrorCode()
}
// deleteById()
@Test
open fun `deleteById() deletes the entity with the given id in the repository`() {
doReturn(entity).whenever(logic).getById(entity.id!!)
logic.deleteById(entity.id!!)
verify(repository).delete(entity)
}
}
abstract class AbstractNamedModelServiceTest<E : NamedModelEntity, S : NamedModelService<E, *>, R : NamedJpaRepository<E>> :
AbstractModelServiceTest<E, S, R>() {
protected abstract val entityWithEntityName: E
// existsByName()
@Test
open fun `existsByName() returns true when an entity with the given name exists`() {
whenever(repository.existsByName(entity.name)).doReturn(true)
val found = logic.existsByName(entity.name)
assertTrue(found)
}
@Test
open fun `existsByName() returns false when no entity with the given name exists`() {
whenever(repository.existsByName(entity.name)).doReturn(false)
val found = logic.existsByName(entity.name)
assertFalse(found)
}
// getByName()
@Test
open fun `getByName() returns the entity with the given name`() {
whenever(repository.findByName(entity.name)).doReturn(entity)
val found = logic.getByName(entity.name)
assertEquals(entity, found)
}
@Test
open fun `getByName() throws NotFoundException when no entity with the given name exists`() {
whenever(repository.findByName(entity.name)).doReturn(null)
assertThrows<NotFoundException> { logic.getByName(entity.name) }
.assertErrorCode("name")
}
// save()
@Test
open fun `save() throws AlreadyExistsException when an entity with the given name exists`() {
doReturn(true).whenever(logic).existsByName(entity.name)
assertThrows<AlreadyExistsException> { logic.save(entity) }
.assertErrorCode("name")
}
// update()
@Test
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)
doReturn(true).whenever(logic).existsById(entity.id!!)
doReturn(entity).whenever(logic).getById(entity.id!!)
val found = logic.update(entity)
verify(repository).save(entity)
assertEquals(entity, found)
}
@Test
override fun `update() throws NotFoundException when no entity with the given id exists in the repository`() {
whenever(repository.findByName(entity.name)).doReturn(null)
doReturn(false).whenever(logic).existsById(entity.id!!)
assertThrows<NotFoundException> { logic.update(entity) }
}
@Test
open fun `update() throws AlreadyExistsException when an entity with the updated name exists`() {
whenever(repository.findByName(entity.name)).doReturn(entityWithEntityName)
doReturn(entity).whenever(logic).getById(entity.id!!)
assertThrows<AlreadyExistsException> { logic.update(entity) }
.assertErrorCode("name")
}
}
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 : 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
@AfterEach
override fun afterEach() {
reset(entitySaveDto, entityUpdateDto)
super.afterEach()
}
}
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
@AfterEach
override fun afterEach() {
reset(entitySaveDto, entityUpdateDto)
super.afterEach()
}
}
fun NotFoundException.assertErrorCode(identifierName: String = "id") =
this.assertErrorCode("notfound", identifierName)
fun AlreadyExistsException.assertErrorCode(identifierName: String = "id") =
this.assertErrorCode("exists", identifierName)
fun RestException.assertErrorCode(type: String, identifierName: String) {
assertTrue {
this.errorCode.startsWith(type) &&
this.errorCode.endsWith(identifierName)
}
}
fun RestException.assertErrorCode(errorCode: String) {
assertEquals(errorCode, this.errorCode)
}
fun <E : ModelEntity, N : EntityDto<E>> withBaseSaveDtoTest(
entity: E,
entitySaveDto: N,
service: ExternalService<E, N, *, *, *>,
saveMockMatcher: () -> E = { entity },
op: () -> Unit = {}
) {
doReturn(entity).whenever(service).save(saveMockMatcher())
doReturn(entity).whenever(entitySaveDto).toEntity()
val found = service.save(entitySaveDto)
verify(service).save(saveMockMatcher())
assertEquals(entity, found)
op()
}
fun <E : ModelEntity, U : EntityDto<E>> withBaseUpdateDtoTest(
entity: E,
entityUpdateDto: U,
service: ExternalModelService<E, *, U, *, *>,
updateMockMatcher: () -> E,
op: E.() -> Unit = {}
) {
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)
found.op()
}

View File

@ -1,348 +0,0 @@
package dev.fyloz.colorrecipesexplorer.logic
import com.nhaarman.mockitokotlin2.*
import dev.fyloz.colorrecipesexplorer.config.security.defaultGroupCookieName
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.account.*
import dev.fyloz.colorrecipesexplorer.repository.GroupRepository
import dev.fyloz.colorrecipesexplorer.repository.UserRepository
import dev.fyloz.colorrecipesexplorer.logic.users.*
import org.junit.jupiter.api.*
import org.springframework.mock.web.MockHttpServletResponse
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import java.util.*
import javax.servlet.http.Cookie
import javax.servlet.http.HttpServletRequest
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class UserLogicTest :
AbstractExternalModelServiceTest<User, UserSaveDto, UserUpdateDto, UserLogic, UserRepository>() {
private val passwordEncoder = BCryptPasswordEncoder()
override val entity: User = user(id = 0L, passwordEncoder = passwordEncoder)
override val anotherEntity: User = user(id = 1L, passwordEncoder = passwordEncoder)
private val entityDefaultGroupUser = user(id = 2L, isDefaultGroupUser = true, passwordEncoder = passwordEncoder)
private val entitySystemUser = user(id = 3L, isSystemUser = true, passwordEncoder = passwordEncoder)
private val group = group(id = 0L)
override val entitySaveDto: UserSaveDto = spy(userSaveDto(passwordEncoder, id = 0L))
override val entityUpdateDto: UserUpdateDto = spy(userUpdateDto(id = 0L))
override val repository: UserRepository = mock()
private val groupService: GroupLogic = mock()
override val logic: UserLogic = spy(DefaultUserLogic(repository, groupService))
private val entitySaveDtoUser = User(
entitySaveDto.id,
entitySaveDto.firstName,
entitySaveDto.lastName,
passwordEncoder.encode(entitySaveDto.password),
isDefaultGroupUser = false,
isSystemUser = false,
group = null,
permissions = entitySaveDto.permissions
)
@AfterEach
override fun afterEach() {
reset(groupService)
super.afterEach()
}
// existsByFirstNameAndLastName()
@Test
fun `existsByFirstNameAndLastName() returns true when an user with the given first name and last name exists`() {
whenever(repository.existsByFirstNameAndLastName(entity.firstName, entity.lastName)).doReturn(true)
val found = logic.existsByFirstNameAndLastName(entity.firstName, entity.lastName)
assertTrue(found)
}
@Test
fun `existsByFirstNameAndLastName() returns false when no user with the given first name and last name exists`() {
whenever(repository.existsByFirstNameAndLastName(entity.firstName, entity.lastName)).doReturn(false)
val found = logic.existsByFirstNameAndLastName(entity.firstName, entity.lastName)
assertFalse(found)
}
// getById()
@Test
fun `getById() throws NotFoundException when the corresponding user is a default group user`() {
whenever(repository.findById(entityDefaultGroupUser.id)).doReturn(Optional.of(entityDefaultGroupUser))
assertThrows<NotFoundException> {
logic.getById(
entityDefaultGroupUser.id,
ignoreDefaultGroupUsers = true,
ignoreSystemUsers = false
)
}.assertErrorCode()
}
@Test
fun `getById() throws NotFoundException when the corresponding user is a system user`() {
whenever(repository.findById(entitySystemUser.id)).doReturn(Optional.of(entitySystemUser))
assertThrows<NotFoundException> {
logic.getById(
entitySystemUser.id,
ignoreDefaultGroupUsers = false,
ignoreSystemUsers = true
)
}.assertErrorCode()
}
// getByGroup()
@Test
fun `getByGroup() returns all the users with the given group from the repository`() {
whenever(repository.findAllByGroup(group)).doReturn(entityList)
val found = logic.getByGroup(group)
assertTrue(found.containsAll(entityList))
assertTrue(entityList.containsAll(found))
}
@Test
fun `getByGroup() returns an empty list when there is no user with the given group in the repository`() {
whenever(repository.findAllByGroup(group)).doReturn(listOf())
val found = logic.getByGroup(group)
assertTrue(found.isEmpty())
}
// getDefaultGroupUser()
@Test
fun `getDefaultGroupUser() returns the default user of the given group from the repository`() {
whenever(repository.findByIsDefaultGroupUserIsTrueAndGroupIs(group)).doReturn(entityDefaultGroupUser)
val found = logic.getDefaultGroupUser(group)
assertEquals(entityDefaultGroupUser, found)
}
// save()
override fun `save() saves in the repository and returns the saved value`() {
whenever(repository.save(entity)).doReturn(entity)
doReturn(false).whenever(repository).existsByFirstNameAndLastName(entity.firstName, entity.lastName)
val found = logic.save(entity)
verify(repository).save(entity)
assertEquals(entity, found)
}
@Test
fun `save() throws AlreadyExistsException when firstName and lastName exists`() {
doReturn(true).whenever(repository).existsByFirstNameAndLastName(entity.firstName, entity.lastName)
assertThrows<AlreadyExistsException> { logic.save(entity) }
.assertErrorCode("fullName")
}
@Test
override fun `save(dto) calls and returns save() with the created entity`() {
withBaseSaveDtoTest(entity, entitySaveDto, logic, {
argThat {
this.id == entity.id && this.firstName == entity.firstName && this.lastName == entity.lastName
}
})
}
@Test
fun `save(dto) calls and returns save() with the created user`() {
doReturn(entitySaveDtoUser).whenever(logic).save(any<User>())
val found = logic.save(entitySaveDto)
verify(logic).save(argThat<User> { this.id == entity.id && this.firstName == entity.firstName && this.lastName == entity.lastName })
assertEquals(entitySaveDtoUser, found)
}
// update()
@Test
override fun `update(dto) calls and returns update() with the created entity`() =
withBaseUpdateDtoTest(entity, entityUpdateDto, logic, { any() })
@Test
fun `update() throws AlreadyExistsException when a different user with the given first name and last name exists`() {
whenever(repository.findByFirstNameAndLastName(entity.firstName, entity.lastName)).doReturn(
entityDefaultGroupUser
)
doReturn(entity).whenever(logic).getById(eq(entity.id), any(), any())
assertThrows<AlreadyExistsException> {
logic.update(
entity,
true,
ignoreSystemUsers = true
)
}.assertErrorCode("fullName")
}
}
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class GroupLogicTest :
AbstractExternalNamedModelServiceTest<Group, GroupSaveDto, GroupUpdateDto, GroupLogic, GroupRepository>() {
private val userService: UserLogic = mock()
override val repository: GroupRepository = mock()
override val logic: DefaultGroupLogic = spy(DefaultGroupLogic(userService, repository))
override val entity: Group = group(id = 0L, name = "group")
override val anotherEntity: Group = group(id = 1L, name = "another group")
override val entitySaveDto: GroupSaveDto = spy(groupSaveDto(name = "group"))
override val entityUpdateDto: GroupUpdateDto = spy(groupUpdateDto(id = 0L, name = "group"))
override val entityWithEntityName: Group = group(id = 2L, name = entity.name)
private val groupUserId = 1000000L + entity.id!!
private val groupUser = user(passwordEncoder = BCryptPasswordEncoder(), id = groupUserId, group = entity)
@BeforeEach
override fun afterEach() {
reset(userService)
super.afterEach()
}
// getUsersForGroup()
@Test
fun `getUsersForGroup() returns all users in the given group`() {
val group = group(id = 1L)
doReturn(group).whenever(logic).getById(group.id!!)
whenever(userService.getByGroup(group)).doReturn(listOf(groupUser))
val found = logic.getUsersForGroup(group.id!!)
assertTrue(found.contains(groupUser))
assertTrue(found.size == 1)
}
@Test
fun `getUsersForGroup() returns empty collection when the given group contains any user`() {
doReturn(entity).whenever(logic).getById(entity.id!!)
val found = logic.getUsersForGroup(entity.id!!)
assertTrue(found.isEmpty())
}
// getRequestDefaultGroup()
@Test
fun `getRequestDefaultGroup() returns the group contained in the cookie of the HTTP request`() {
val cookies: Array<Cookie> = arrayOf(Cookie(defaultGroupCookieName, groupUserId.toString()))
val request: HttpServletRequest = mock()
whenever(request.cookies).doReturn(cookies)
whenever(userService.getById(eq(groupUserId), any(), any())).doReturn(groupUser)
val found = logic.getRequestDefaultGroup(request)
assertEquals(entity, found)
}
@Test
fun `getRequestDefaultGroup() throws NoDefaultGroupException when the HTTP request does not contains a cookie for the default group`() {
val request: HttpServletRequest = mock()
whenever(request.cookies).doReturn(arrayOf())
assertThrows<NoDefaultGroupException> { logic.getRequestDefaultGroup(request) }
}
// setResponseDefaultGroup()
@Test
fun `setResponseDefaultGroup() the default group cookie has been added to the given HTTP response with the given group id`() {
val response = MockHttpServletResponse()
whenever(userService.getDefaultGroupUser(entity)).doReturn(groupUser)
doReturn(entity).whenever(logic).getById(entity.id!!)
logic.setResponseDefaultGroup(entity.id!!, response)
val found = response.getCookie(defaultGroupCookieName)
assertNotNull(found)
assertEquals(defaultGroupCookieName, found.name)
assertEquals(groupUserId.toString(), found.value)
assertEquals(defaultGroupCookieMaxAge, found.maxAge)
assertTrue(found.isHttpOnly)
assertTrue(found.secure)
}
// 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() })
}
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class UserUserDetailsLogicTest {
private val userLogic: UserLogic = mock()
private val logic = spy(DefaultUserDetailsLogic(userLogic))
private val user = user(id = 0L)
@BeforeEach
fun beforeEach() {
reset(userLogic, logic)
}
// loadUserByUsername()
@Test
fun `loadUserByUsername() calls loadUserByUserId() with the given username as an id`() {
whenever(userLogic.getById(eq(user.id), any(), any())).doReturn(user)
doReturn(UserDetails(user(id = user.id, plainPassword = user.password)))
.whenever(logic).loadUserById(user.id)
logic.loadUserByUsername(user.id.toString())
verify(logic).loadUserById(eq(user.id), any())
}
@Test
fun `loadUserByUsername() throws UsernameNotFoundException when no user with the given id exists`() {
whenever(userLogic.getById(eq(user.id), any(), any())).doThrow(
userIdNotFoundException(user.id)
)
assertThrows<UsernameNotFoundException> { logic.loadUserByUsername(user.id.toString()) }
}
// loadUserByUserId
@Test
fun `loadUserByUserId() returns an User corresponding to the user with the given id`() {
whenever(userLogic.getById(eq(user.id), any(), any())).doReturn(user)
val found = logic.loadUserById(user.id)
assertEquals(user.id, found.username.toLong())
assertEquals(user.password, found.password)
}
}

View File

@ -3,23 +3,23 @@ package dev.fyloz.colorrecipesexplorer.logic
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
import dev.fyloz.colorrecipesexplorer.dtos.UserDetails
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
import dev.fyloz.colorrecipesexplorer.logic.users.DefaultJwtLogic import dev.fyloz.colorrecipesexplorer.logic.users.DefaultJwtLogic
import dev.fyloz.colorrecipesexplorer.logic.users.jwtClaimUser import dev.fyloz.colorrecipesexplorer.logic.users.jwtClaimUser
import dev.fyloz.colorrecipesexplorer.model.account.UserDetails
import dev.fyloz.colorrecipesexplorer.model.account.UserOutputDto
import dev.fyloz.colorrecipesexplorer.model.account.toOutputDto
import dev.fyloz.colorrecipesexplorer.model.account.user
import dev.fyloz.colorrecipesexplorer.utils.base64encode import dev.fyloz.colorrecipesexplorer.utils.base64encode
import dev.fyloz.colorrecipesexplorer.utils.isAround import dev.fyloz.colorrecipesexplorer.utils.isAround
import io.jsonwebtoken.Jwts import io.jsonwebtoken.Jwts
import io.jsonwebtoken.jackson.io.JacksonDeserializer import io.jsonwebtoken.jackson.io.JacksonDeserializer
import io.mockk.clearAllMocks
import io.mockk.spyk import io.mockk.spyk
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.time.Instant import java.time.Instant
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
class JwtLogicTest { class DefaultJwtLogicTest {
private val objectMapper = jacksonObjectMapper() private val objectMapper = jacksonObjectMapper()
private val securityProperties = CreSecurityProperties().apply { private val securityProperties = CreSecurityProperties().apply {
jwtSecret = "XRRm7OflmFuCrOB2Xvmfsercih9DCKom" jwtSecret = "XRRm7OflmFuCrOB2Xvmfsercih9DCKom"
@ -34,12 +34,14 @@ class JwtLogicTest {
private val jwtService = spyk(DefaultJwtLogic(objectMapper, securityProperties)) private val jwtService = spyk(DefaultJwtLogic(objectMapper, securityProperties))
private val user = user() private val user = UserDto(0L, "Unit test", "User", "", null, listOf())
private val userOutputDto = user.toOutputDto()
// buildJwt() @AfterEach
internal fun afterEach() {
clearAllMocks()
}
private fun withParsedUserOutputDto(jwt: String, test: (UserOutputDto) -> Unit) { private fun withParsedUserOutputDto(jwt: String, test: (UserDto) -> Unit) {
val serializedUser = jwtParser.parseClaimsJws(jwt) val serializedUser = jwtParser.parseClaimsJws(jwt)
.body.get(jwtClaimUser, String::class.java) .body.get(jwtClaimUser, String::class.java)
@ -47,27 +49,27 @@ class JwtLogicTest {
} }
@Test @Test
fun `buildJwt(userDetails) returns jwt string with valid user`() { fun buildJwt_userDetails_normalBehavior_returnsJwtStringWithValidUser() {
val userDetails = UserDetails(user) val userDetails = UserDetails(user)
val builtJwt = jwtService.buildJwt(userDetails) val builtJwt = jwtService.buildJwt(userDetails)
withParsedUserOutputDto(builtJwt) { parsedUser -> withParsedUserOutputDto(builtJwt) { parsedUser ->
assertEquals(user.toOutputDto(), parsedUser) assertEquals(user, parsedUser)
} }
} }
@Test @Test
fun `buildJwt() returns jwt string with valid user`() { fun buildJwt_user_normalBehavior_returnsJwtStringWithValidUser() {
val builtJwt = jwtService.buildJwt(user) val builtJwt = jwtService.buildJwt(user)
withParsedUserOutputDto(builtJwt) { parsedUser -> withParsedUserOutputDto(builtJwt) { parsedUser ->
assertEquals(user.toOutputDto(), parsedUser) assertEquals(user, parsedUser)
} }
} }
@Test @Test
fun `buildJwt() returns jwt string with valid subject`() { fun buildJwt_user_normalBehavior_returnsJwtStringWithValidSubject() {
val builtJwt = jwtService.buildJwt(user) val builtJwt = jwtService.buildJwt(user)
val jwtSubject = jwtParser.parseClaimsJws(builtJwt).body.subject val jwtSubject = jwtParser.parseClaimsJws(builtJwt).body.subject
@ -75,7 +77,7 @@ class JwtLogicTest {
} }
@Test @Test
fun `buildJwt() returns jwt with valid expiration date`() { fun buildJwt_user_returnsJwtWithValidExpirationDate() {
val jwtExpectedExpirationDate = Instant.now().plusSeconds(securityProperties.jwtDuration) val jwtExpectedExpirationDate = Instant.now().plusSeconds(securityProperties.jwtDuration)
val builtJwt = jwtService.buildJwt(user) val builtJwt = jwtService.buildJwt(user)
@ -89,10 +91,10 @@ class JwtLogicTest {
// parseJwt() // parseJwt()
@Test @Test
fun `parseJwt() returns expected user`() { fun parseJwt_normalBehavior_returnsExpectedUser() {
val jwt = jwtService.buildJwt(user) val jwt = jwtService.buildJwt(user)
val parsedUser = jwtService.parseJwt(jwt) val parsedUser = jwtService.parseJwt(jwt)
assertEquals(userOutputDto, parsedUser) assertEquals(user, parsedUser)
} }
} }

View File

@ -3,7 +3,6 @@ package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.dtos.* import dev.fyloz.colorrecipesexplorer.dtos.*
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic
import dev.fyloz.colorrecipesexplorer.model.account.Group
import dev.fyloz.colorrecipesexplorer.service.RecipeService import dev.fyloz.colorrecipesexplorer.service.RecipeService
import io.mockk.* import io.mockk.*
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
@ -23,7 +22,7 @@ class DefaultRecipeLogicTest {
spyk(DefaultRecipeLogic(recipeServiceMock, companyLogicMock, recipeStepLogicMock, mixLogicMock, groupLogicMock)) spyk(DefaultRecipeLogic(recipeServiceMock, companyLogicMock, recipeStepLogicMock, mixLogicMock, groupLogicMock))
private val company = CompanyDto(1L, "Unit test company") private val company = CompanyDto(1L, "Unit test company")
private val group = Group(1L, "Unit test group") private val group = GroupDto(1L, "Unit test group", listOf())
private val recipe = RecipeDto( private val recipe = RecipeDto(
1L, 1L,
"Unit test recipe", "Unit test recipe",
@ -160,7 +159,7 @@ class DefaultRecipeLogicTest {
val expectedGroupInformation = RecipeGroupInformationDto(0L, group, "Unit test note", listOf()) val expectedGroupInformation = RecipeGroupInformationDto(0L, group, "Unit test note", listOf())
val groupNote = RecipeGroupNoteDto(group.id!!, expectedGroupInformation.note) val groupNote = RecipeGroupNoteDto(group.id, expectedGroupInformation.note)
val dto = RecipePublicDataDto(recipe.id, listOf(groupNote), listOf()) val dto = RecipePublicDataDto(recipe.id, listOf(groupNote), listOf())
// Act // Act
@ -189,7 +188,7 @@ class DefaultRecipeLogicTest {
// Arrange // Arrange
every { mixLogicMock.updateLocations(any()) } just runs every { mixLogicMock.updateLocations(any()) } just runs
val mixesLocation = listOf(MixLocationDto(group.id!!, "location")) val mixesLocation = listOf(MixLocationDto(group.id, "location"))
val dto = RecipePublicDataDto(recipe.id, listOf(), mixesLocation) val dto = RecipePublicDataDto(recipe.id, listOf(), mixesLocation)
// Act // Act

View File

@ -1,10 +1,10 @@
package dev.fyloz.colorrecipesexplorer.logic package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
import dev.fyloz.colorrecipesexplorer.dtos.RecipeGroupInformationDto import dev.fyloz.colorrecipesexplorer.dtos.RecipeGroupInformationDto
import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto
import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError
import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException
import dev.fyloz.colorrecipesexplorer.model.account.Group
import dev.fyloz.colorrecipesexplorer.service.RecipeStepService import dev.fyloz.colorrecipesexplorer.service.RecipeStepService
import dev.fyloz.colorrecipesexplorer.utils.PositionUtils import dev.fyloz.colorrecipesexplorer.utils.PositionUtils
import io.mockk.* import io.mockk.*
@ -28,7 +28,7 @@ class DefaultRecipeStepLogicTest {
mockkObject(PositionUtils) mockkObject(PositionUtils)
every { PositionUtils.validate(any()) } just runs every { PositionUtils.validate(any()) } just runs
val group = Group(1L, "Unit test group") val group = GroupDto(1L, "Unit test group", listOf())
val steps = listOf(RecipeStepDto(1L, 1, "A message")) val steps = listOf(RecipeStepDto(1L, 1, "A message"))
val groupInfo = RecipeGroupInformationDto(1L, group, "A note", steps) val groupInfo = RecipeGroupInformationDto(1L, group, "A note", steps)
@ -49,7 +49,7 @@ class DefaultRecipeStepLogicTest {
mockkObject(PositionUtils) mockkObject(PositionUtils)
every { PositionUtils.validate(any()) } throws InvalidPositionsException(errors) every { PositionUtils.validate(any()) } throws InvalidPositionsException(errors)
val group = Group(1L, "Unit test group") val group = GroupDto(1L, "Unit test group", listOf())
val steps = listOf(RecipeStepDto(1L, 1, "A message")) val steps = listOf(RecipeStepDto(1L, 1, "A message"))
val groupInfo = RecipeGroupInformationDto(1L, group, "A note", steps) val groupInfo = RecipeGroupInformationDto(1L, group, "A note", steps)

View File

@ -1,138 +0,0 @@
package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic
import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic
import dev.fyloz.colorrecipesexplorer.model.ConfigurationType
import dev.fyloz.colorrecipesexplorer.model.configuration
import dev.fyloz.colorrecipesexplorer.repository.TouchUpKitRepository
import dev.fyloz.colorrecipesexplorer.utils.PdfDocument
import dev.fyloz.colorrecipesexplorer.utils.toByteArrayResource
import io.mockk.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.springframework.core.io.ByteArrayResource
import kotlin.test.assertEquals
//private class TouchUpKitServiceTestContext {
// val touchUpKitRepository = mockk<TouchUpKitRepository>()
// val fileService = mockk<WriteableFileLogic> {
// every { write(any<ByteArrayResource>(), any(), any()) } just Runs
// }
// val configService = mockk<ConfigurationLogic>(relaxed = true)
// val touchUpKitService = spyk(DefaultTouchUpKitLogic(fileService, configService, touchUpKitRepository))
// val pdfDocumentData = mockk<ByteArrayResource>()
// val pdfDocument = mockk<PdfDocument> {
// mockkStatic(PdfDocument::toByteArrayResource)
// mockkStatic(PdfDocument::toByteArrayResource)
// every { toByteArrayResource() } returns pdfDocumentData
// }
//}
class TouchUpKitLogicTest {
// private val job = "job"
//
// @AfterEach
// internal fun afterEach() {
// clearAllMocks()
// }
//
// // generateJobPdf()
//
// @Test
// fun `generateJobPdf() generates a valid PdfDocument for the given job`() {
// test {
// val generatedPdfDocument = touchUpKitService.generateJobPdf(job)
//
// setOf(0, 1).forEach {
// assertEquals(TOUCH_UP_TEXT_FR, generatedPdfDocument.containers[it].texts[0].text)
// assertEquals(TOUCH_UP_TEXT_EN, generatedPdfDocument.containers[it].texts[1].text)
// assertEquals(job, generatedPdfDocument.containers[it].texts[2].text)
// }
// }
// }
//
// // generateJobPdfResource()
//
// @Test
// fun `generateJobPdfResource() generates and returns a ByteArrayResource for the given job then cache it`() {
// test {
// every { touchUpKitService.generateJobPdf(any()) } returns pdfDocument
// with(touchUpKitService) {
// every { job.cachePdfDocument(pdfDocument) } just Runs
// }
//
// val generatedResource = touchUpKitService.generateJobPdfResource(job)
//
// assertEquals(pdfDocumentData, generatedResource)
//
// verify {
// with(touchUpKitService) {
// job.cachePdfDocument(pdfDocument)
// }
// }
// }
// }
//
// @Test
// fun `generateJobPdfResource() returns a cached ByteArrayResource from the FileService when caching is enabled and a cached file eixsts for the given job`() {
// test {
// enableCachePdf()
// every { fileService.exists(any()) } returns true
// every { fileService.read(any()) } returns pdfDocumentData
// every { configService.get(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns configuration(
// ConfigurationType.TOUCH_UP_KIT_CACHE_PDF,
// "true"
// )
//
// val redResource = touchUpKitService.generateJobPdfResource(job)
//
// assertEquals(pdfDocumentData, redResource)
// }
// }
//
// // String.cachePdfDocument()
//
// @Test
// fun `cachePdfDocument() does nothing when caching is disabled`() {
// test {
// disableCachePdf()
//
// with(touchUpKitService) {
// job.cachePdfDocument(pdfDocument)
// }
//
// verify(exactly = 0) {
// fileService.write(any<ByteArrayResource>(), any(), any())
// }
// }
// }
//
// @Test
// fun `cachePdfDocument() writes the given document to the FileService when cache is enabled`() {
// test {
// enableCachePdf()
//
// with(touchUpKitService) {
// job.cachePdfDocument(pdfDocument)
// }
//
// verify {
// fileService.write(pdfDocumentData, any(), true)
// }
// }
// }
//
// private fun TouchUpKitServiceTestContext.enableCachePdf() =
// this.setCachePdf(true)
//
// private fun TouchUpKitServiceTestContext.disableCachePdf() =
// this.setCachePdf(false)
//
// private fun TouchUpKitServiceTestContext.setCachePdf(enabled: Boolean) {
// every { configService.getContent(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns enabled.toString()
// }
//
// private fun test(test: TouchUpKitServiceTestContext.() -> Unit) {
// TouchUpKitServiceTestContext().test()
// }
}

View File

@ -0,0 +1,87 @@
package dev.fyloz.colorrecipesexplorer.logic.account
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.logic.users.DefaultGroupLogic
import dev.fyloz.colorrecipesexplorer.logic.users.UserLogic
import dev.fyloz.colorrecipesexplorer.service.GroupService
import io.mockk.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
class DefaultGroupLogicTest {
private val group = GroupDto(1L, "Unit test group", listOf())
private val user = UserDto(1L, "Unit test", "User", "asecurepassword", null, listOf())
private val groupServiceMock = mockk<GroupService> {
every { existsById(any()) } returns false
every { existsByName(any(), any()) } returns false
every { getAll() } returns listOf()
every { getById(any()) } returns group
every { save(any()) } returns group
every { deleteById(any()) } just runs
}
private val userLogicMock = mockk<UserLogic> {
every { getAllByGroup(any()) } returns listOf()
every { getById(any(), any(), any()) } returns user
every { getDefaultGroupUser(any()) } returns user
every { saveDefaultGroupUser(any()) } just runs
every { deleteById(any()) } just runs
}
private val groupLogic = spyk(DefaultGroupLogic(groupServiceMock, userLogicMock))
@AfterEach
internal fun afterEach() {
clearAllMocks()
}
@Test
fun getUsersForGroup_normalBehavior_callsGetAllByGroupInUserLogic() {
// Arrange
every { groupLogic.getById(any()) } returns group
// Act
groupLogic.getUsersForGroup(group.id)
// Assert
verify {
userLogicMock.getAllByGroup(group)
}
confirmVerified(userLogicMock)
}
@Test
fun save_nameAlreadyExists_throwsAlreadyExists() {
// Arrange
every { groupServiceMock.existsByName(any(), any()) } returns true
// Act
// Assert
assertThrows<AlreadyExistsException> { groupLogic.save(group) }
}
@Test
fun update_normalBehavior_throwsAlreadyExists() {
// Arrange
every { groupServiceMock.existsByName(any(), any()) } returns true
// Act
// Assert
assertThrows<AlreadyExistsException> { groupLogic.update(group) }
}
@Test
fun deleteById_normalBehavior_callsDeleteByIdInUserLogicWithDefaultGroupUserId() {
// Arrange
// Act
groupLogic.deleteById(group.id)
// Assert
verify {
userLogicMock.deleteById(group.defaultGroupUserId)
}
}
}

View File

@ -0,0 +1,306 @@
package dev.fyloz.colorrecipesexplorer.logic.account
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
import dev.fyloz.colorrecipesexplorer.dtos.UserSaveDto
import dev.fyloz.colorrecipesexplorer.dtos.UserUpdateDto
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.logic.users.DefaultUserLogic
import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic
import dev.fyloz.colorrecipesexplorer.model.account.Permission
import dev.fyloz.colorrecipesexplorer.service.UserService
import io.mockk.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.springframework.security.crypto.password.PasswordEncoder
import java.time.LocalDateTime
class DefaultUserLogicTest {
private val user = UserDto(1L, "Unit test", "User", "asecurepassword", null, listOf())
private val group = GroupDto(1L, "Unit test group", listOf())
private val userServiceMock = mockk<UserService> {
every { existsById(any()) } returns false
every { existsByFirstNameAndLastName(any(), any(), any()) } returns false
every { getAll(any(), any()) } returns listOf()
every { getAllByGroup(any()) } returns listOf()
every { getById(any(), any(), any()) } returns user
every { getByFirstNameAndLastName(any(), any()) } returns user
every { getDefaultGroupUser(any()) } returns user
}
private val groupLogicMock = mockk<GroupLogic> {
every { getById(any()) } returns group
}
private val passwordEncoderMock = mockk<PasswordEncoder> {
every { encode(any()) } answers { "encoded ${this.firstArg<String>()}" }
}
private val userLogic = spyk(DefaultUserLogic(userServiceMock, groupLogicMock, passwordEncoderMock))
private val userSaveDto = UserSaveDto(
user.id,
user.firstName,
user.lastName,
user.password,
null,
user.permissions,
user.isSystemUser,
user.isDefaultGroupUser
)
private val userUpdateDto = UserUpdateDto(user.id, user.firstName, user.lastName, null, listOf())
@AfterEach
internal fun afterEach() {
clearAllMocks()
}
@Test
fun getAll_normalBehavior_callsGetAllInServiceWithSpecialUsersDisabled() {
// Arrange
// Act
userLogic.getAll()
// Assert
verify {
userServiceMock.getAll(isSystemUser = false, isDefaultGroupUser = false)
}
confirmVerified(userServiceMock)
}
@Test
fun getAllByGroup_normalBehavior_callsGetAllByGroupInService() {
// Arrange
// Act
userLogic.getAllByGroup(group)
// Assert
verify {
userServiceMock.getAllByGroup(group)
}
confirmVerified(userServiceMock)
}
@Test
fun getById_default_normalBehavior_callsGetByIdWithSpecialUsersDisabled() {
// Arrange
// Act
userLogic.getById(user.id)
// Assert
verify {
userLogic.getById(user.id, isSystemUser = false, isDefaultGroupUser = false)
}
}
@Test
fun getById_normalBehavior_callsGetByIdInService() {
// Arrange
// Act
userLogic.getById(user.id, isSystemUser = false, isDefaultGroupUser = true)
// Assert
verify {
userServiceMock.getById(user.id, isSystemUser = false, isDefaultGroupUser = true)
}
confirmVerified(userServiceMock)
}
@Test
fun getById_notFound_throwsNotFoundException() {
// Arrange
every { userServiceMock.getById(any(), any(), any()) } returns null
// Act
// Assert
assertThrows<NotFoundException> { userLogic.getById(user.id) }
}
@Test
fun getDefaultGroupUser_normalBehavior_callsGetDefaultGroupUserInService() {
// Arrange
// Act
userLogic.getDefaultGroupUser(group)
// Assert
verify {
userServiceMock.getDefaultGroupUser(group)
}
confirmVerified(userServiceMock)
}
@Test
fun getDefaultGroupUser_notFound_throwsNotFoundException() {
// Arrange
every { userServiceMock.getDefaultGroupUser(any()) } returns null
// Act
// Assert
assertThrows<NotFoundException> { userLogic.getDefaultGroupUser(group) }
}
@Test
fun saveDefaultGroupUser_normalBehavior_callsSaveWithValidSaveDto() {
// Arrange
every { userLogic.save(any<UserSaveDto>()) } returns user
val expectedSaveDto = UserSaveDto(
group.defaultGroupUserId, group.name, "User", group.name, group.id, listOf(), isDefaultGroupUser = true
)
// Act
userLogic.saveDefaultGroupUser(group)
// Assert
verify {
userLogic.save(expectedSaveDto)
}
}
@Test
fun save_dto_normalBehavior_callsSaveWithValidUser() {
// Arrange
every { userLogic.save(any<UserDto>()) } returns user
val expectedUser = user.copy(password = "encoded ${user.password}")
// Act
userLogic.save(userSaveDto)
// Assert
verify {
userLogic.save(expectedUser)
}
}
// TODO Causes a stackoverflow because of a bug in mockk
// @Test
// fun save_normalBehavior_callsSaveInService() {
// // Arrange
// // Act
// userLogic.save(user)
//
// // Assert
// verify {
// userServiceMock.save(user)
// }
// }
@Test
fun save_idAlreadyExists_throwsAlreadyExistsException() {
// Arrange
every { userServiceMock.existsById(any()) } returns true
// Act
// Assert
assertThrows<AlreadyExistsException> { userLogic.save(user) }
}
@Test
fun save_fullNameAlreadyExists_throwsAlreadyExistsException() {
// Arrange
every { userServiceMock.existsByFirstNameAndLastName(any(), any(), any()) } returns true
// Act
// Assert
assertThrows<AlreadyExistsException> { userLogic.save(userSaveDto) }
}
@Test
fun update_dto_normalBehavior_callsUpdateWithValidUser() {
// Arrange
every { userLogic.getById(any(), any(), any()) } returns user
every { userLogic.update(any<UserDto>()) } returns user
// Act
userLogic.update(userUpdateDto)
// Assert
verify {
userLogic.update(user)
}
}
@Test
fun update_fullNameAlreadyExists_ThrowAlreadyExistsException() {
// Arrange
every { userServiceMock.existsByFirstNameAndLastName(any(), any(), any()) } returns true
// Act
// Assert
assertThrows<AlreadyExistsException> { userLogic.update(user) }
}
@Test
fun updateLastLoginTime_normalBehavior_callsUpdateWithUpdatedTime() {
// Arrange
every { userLogic.getById(any()) } returns user
every { userLogic.update(any<UserDto>()) } returns user
val time = LocalDateTime.now()
val expectedUser = user.copy(lastLoginTime = time)
// Act
userLogic.updateLastLoginTime(user.id, time)
// Assert
verify {
userLogic.update(expectedUser)
}
}
@Test
fun updatePassword_normalBehavior_callsUpdateWithUpdatedTime() {
// Arrange
every { userLogic.getById(any()) } returns user
every { userLogic.update(any<UserDto>()) } returns user
val updatedPassword = "updatedpassword"
val expectedUser = user.copy(password = "encoded $updatedPassword")
// Act
userLogic.updatePassword(user.id, updatedPassword)
// Assert
verify {
userLogic.update(expectedUser)
}
}
@Test
fun addPermission_normalBehavior_callsUpdateWithAddedPermission() {
// Arrange
every { userLogic.getById(any()) } returns user
every { userLogic.update(any<UserDto>()) } returns user
val addedPermission = Permission.VIEW_COMPANY
val expectedUser = user.copy(permissions = user.permissions + addedPermission)
// Act
userLogic.addPermission(user.id, addedPermission)
// Assert
verify {
userLogic.update(expectedUser)
}
}
@Test
fun removePermission_normalBehavior_callsUpdateWithAddedPermission() {
// Arrange
val removedPermission = Permission.VIEW_COMPANY
val baseUser = user.copy(permissions = user.permissions + removedPermission)
every { userLogic.getById(any()) } returns baseUser
every { userLogic.update(any<UserDto>()) } returns user
// Act
userLogic.removePermission(user.id, removedPermission)
// Assert
verify {
userLogic.update(user)
}
}
}

View File

@ -21,7 +21,7 @@ private const val mockFilePath = "existingFile"
private val mockFilePathPath = Path.of(mockFilePath) private val mockFilePathPath = Path.of(mockFilePath)
private val mockFileData = byteArrayOf(0x1, 0x8, 0xa, 0xf) private val mockFileData = byteArrayOf(0x1, 0x8, 0xa, 0xf)
class FileLogicTest { class DefaultFileLogicTest {
private val fileCacheMock = mockk<FileCache> { private val fileCacheMock = mockk<FileCache> {
every { setExists(any(), any()) } just runs every { setExists(any(), any()) } just runs
} }