From d0965d75a007c034ce2f8d246fb290b878cbef6d Mon Sep 17 00:00:00 2001 From: FyloZ Date: Wed, 20 Apr 2022 22:17:38 -0400 Subject: [PATCH] #25 Migrate users and groups to new logic --- .../fyloz/colorrecipesexplorer/Constants.kt | 6 + .../config/security/JwtFilters.kt | 13 +- .../config/security/SecurityConfig.kt | 9 +- .../colorrecipesexplorer/dtos/GroupDto.kt | 25 ++ .../colorrecipesexplorer/dtos/RecipeDto.kt | 3 +- .../colorrecipesexplorer/dtos/UserDto.kt | 94 +++++ .../exception/NoDefaultGroupException.kt | 10 + .../fyloz/colorrecipesexplorer/logic/Logic.kt | 11 - .../colorrecipesexplorer/logic/OldService.kt | 182 --------- .../logic/RecipeStepLogic.kt | 5 +- .../logic/users/GroupLogic.kt | 115 +++--- .../logic/users/JwtLogic.kt | 18 +- .../logic/users/UserDetailsLogic.kt | 29 +- .../logic/users/UserLogic.kt | 249 ++++++------- .../colorrecipesexplorer/model/Company.kt | 2 +- .../colorrecipesexplorer/model/Material.kt | 2 +- .../model/MaterialType.kt | 2 +- .../fyloz/colorrecipesexplorer/model/Mix.kt | 4 +- .../colorrecipesexplorer/model/MixMaterial.kt | 6 +- .../colorrecipesexplorer/model/MixType.kt | 2 +- .../colorrecipesexplorer/model/ModelEntity.kt | 15 +- .../colorrecipesexplorer/model/Recipe.kt | 8 +- .../colorrecipesexplorer/model/RecipeStep.kt | 2 +- .../model/account/Group.kt | 118 +----- .../model/account/User.kt | 205 +--------- .../model/touchupkit/TouchUpKit.kt | 4 +- .../repository/AccountRepository.kt | 20 +- .../repository/Repository.kt | 18 - .../rest/AccountControllers.kt | 47 ++- .../rest/InventoryController.kt | 5 +- .../colorrecipesexplorer/rest/RestUtils.kt | 8 - .../service/CompanyService.kt | 2 +- .../service/GroupService.kt | 31 ++ .../service/MaterialService.kt | 2 +- .../service/MaterialTypeService.kt | 2 +- .../service/MixMaterialService.kt | 2 +- .../service/MixService.kt | 2 +- .../service/MixTypeService.kt | 2 +- .../service/RecipeService.kt | 14 +- .../service/RecipeStepService.kt | 2 +- .../service/TouchUpKitService.kt | 4 +- .../service/UserService.kt | 103 ++++++ .../logic/AbstractServiceTest.kt | 349 ------------------ .../logic/AccountsServiceTest.kt | 348 ----------------- ...JwtLogicTest.kt => DefaultJwtLogicTest.kt} | 36 +- .../logic/DefaultRecipeLogicTest.kt | 7 +- .../logic/DefaultRecipeStepLogicTest.kt | 6 +- .../logic/TouchUpKitLogicTest.kt | 138 ------- .../logic/account/DefaultGroupLogicTest.kt | 87 +++++ .../logic/account/DefaultUserLogicTest.kt | 306 +++++++++++++++ ...leLogicTest.kt => DefaultFileLogicTest.kt} | 2 +- 51 files changed, 975 insertions(+), 1707 deletions(-) create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/GroupDto.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/UserDto.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/exception/NoDefaultGroupException.kt delete mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/OldService.kt delete mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/Repository.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/GroupService.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/UserService.kt delete mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/AbstractServiceTest.kt delete mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/AccountsServiceTest.kt rename src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/{JwtLogicTest.kt => DefaultJwtLogicTest.kt} (74%) delete mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/TouchUpKitLogicTest.kt create mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/DefaultGroupLogicTest.kt create mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/DefaultUserLogicTest.kt rename src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/files/{FileLogicTest.kt => DefaultFileLogicTest.kt} (99%) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt index 4c754aa..d7853b7 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt @@ -4,11 +4,14 @@ object Constants { object ControllerPaths { const val COMPANY = "/api/company" const val FILE = "/api/file" + const val GROUP = "/api/user/group" + const val INVENTORY = "/api/inventory" const val MATERIAL = "/api/material" const val MATERIAL_TYPE = "/api/materialtype" const val MIX = "/api/recipe/mix" const val RECIPE = "/api/recipe" const val TOUCH_UP_KIT = "/api/touchupkit" + const val USER = "/api/user" } object FilePaths { @@ -22,6 +25,7 @@ object Constants { object ModelNames { const val COMPANY = "Company" + const val GROUP = "Group" const val MATERIAL = "Material" const val MATERIAL_TYPE = "MaterialType" const val MIX = "Mix" @@ -30,12 +34,14 @@ object Constants { const val RECIPE = "Recipe" const val RECIPE_STEP = "RecipeStep" const val TOUCH_UP_KIT = "TouchUpKit" + const val USER = "User" } object ValidationMessages { 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 RANGE_OUTSIDE_PERCENTS = "Must be between 0 and 100" + const val PASSWORD_TOO_SMALL = "Must contains at least 8 characters" } object ValidationRegexes { diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/JwtFilters.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/JwtFilters.kt index c013e2f..47f997f 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/JwtFilters.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/JwtFilters.kt @@ -2,13 +2,12 @@ package dev.fyloz.colorrecipesexplorer.config.security import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 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.logic.users.JwtLogic 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 io.jsonwebtoken.ExpiredJwtException import org.springframework.security.authentication.AuthenticationManager @@ -40,7 +39,7 @@ class JwtAuthenticationFilter( } 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}...") return authManager.authenticate(UsernamePasswordAuthenticationToken(loginRequest.id, loginRequest.password)) } @@ -116,8 +115,8 @@ class JwtAuthorizationFilter( } } - private fun getAuthenticationToken(user: UserOutputDto) = - UsernamePasswordAuthenticationToken(user.id, null, user.permissions.toAuthorities()) + private fun getAuthenticationToken(user: UserDto) = + UsernamePasswordAuthenticationToken(user.id, null, user.authorities) private fun getAuthenticationToken(userId: Long): UsernamePasswordAuthenticationToken? = try { val userDetails = userDetailsLogic.loadUserById(userId) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/SecurityConfig.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/SecurityConfig.kt index ad2c214..c17ee93 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/SecurityConfig.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/SecurityConfig.kt @@ -1,12 +1,12 @@ package dev.fyloz.colorrecipesexplorer.config.security import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties +import dev.fyloz.colorrecipesexplorer.dtos.UserDto import dev.fyloz.colorrecipesexplorer.emergencyMode import dev.fyloz.colorrecipesexplorer.logic.users.JwtLogic import dev.fyloz.colorrecipesexplorer.logic.users.UserDetailsLogic import dev.fyloz.colorrecipesexplorer.logic.users.UserLogic import dev.fyloz.colorrecipesexplorer.model.account.Permission -import dev.fyloz.colorrecipesexplorer.model.account.User import mu.KotlinLogging import org.slf4j.Logger import org.springframework.boot.context.properties.EnableConfigurationProperties @@ -147,13 +147,14 @@ class SecurityConfig( with(securityProperties.root!!) { if (!userLogic.existsById(this.id)) { userLogic.save( - User( + UserDto( id = this.id, firstName = rootUserFirstName, lastName = rootUserLastName, + group = null, password = passwordEncoder.encode(this.password), - isSystemUser = true, - permissions = mutableSetOf(Permission.ADMIN) + permissions = listOf(Permission.ADMIN), + isSystemUser = true ) ) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/GroupDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/GroupDto.kt new file mode 100644 index 0000000..78ab227 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/GroupDto.kt @@ -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, + + val explicitPermissions: List = listOf() +) : EntityDto { + @get:JsonIgnore + val defaultGroupUserId = getDefaultGroupUserId(id) + + companion object { + fun getDefaultGroupUserId(id: Long) = 1000000 + id + } +} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/RecipeDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/RecipeDto.kt index 80c22c4..0a76be4 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/RecipeDto.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/RecipeDto.kt @@ -2,7 +2,6 @@ package dev.fyloz.colorrecipesexplorer.dtos import com.fasterxml.jackson.annotation.JsonIgnore import dev.fyloz.colorrecipesexplorer.Constants -import dev.fyloz.colorrecipesexplorer.model.account.Group import java.time.LocalDate import javax.validation.constraints.Max import javax.validation.constraints.Min @@ -94,7 +93,7 @@ data class RecipeUpdateDto( data class RecipeGroupInformationDto( override val id: Long = 0L, - val group: Group, + val group: GroupDto, val note: String? = null, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/UserDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/UserDto.kt new file mode 100644 index 0000000..edfaef4 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/UserDto.kt @@ -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, + + val explicitPermissions: List = 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, + + // 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 +) + +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 +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/exception/NoDefaultGroupException.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/exception/NoDefaultGroupException.kt new file mode 100644 index 0000000..7a0025c --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/exception/NoDefaultGroupException.kt @@ -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" +) \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt index 997d089..8fef428 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt @@ -94,17 +94,6 @@ abstract class BaseLogic>( 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 { const val ID_IDENTIFIER_NAME = "id" const val NAME_IDENTIFIER_NAME = "name" diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/OldService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/OldService.kt deleted file mode 100644 index 25e08b4..0000000 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/OldService.kt +++ /dev/null @@ -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> { - val repository: R - - /** Gets all entities. */ - fun getAll(): Collection - - /** 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> : OldService { - /** 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> : ModelService { - /** 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>(override val repository: R) : OldService { - override fun getAll(): Collection = repository.findAll() - override fun save(entity: E): E = repository.save(entity) - override fun update(entity: E): E = repository.save(entity) - override fun delete(entity: E) = repository.delete(entity) -} - -abstract class AbstractModelService>(repository: R) : - AbstractService(repository), ModelService { - 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>(repository: R) : - AbstractModelService(repository), NamedModelService { - 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, U : EntityDto, O, R : JpaRepository> : OldService { - /** Gets all entities mapped to their output model. */ - fun getAllForOutput(): Collection - - /** 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, U : EntityDto, O, R : JpaRepository> : - ModelService, ExternalService { - /** Gets the entity with the given [id] mapped to its output model. */ - fun getByIdForOutput(id: Long): O -} - -/** An [ExternalService] for entities implementing the [NamedModelEntity] interface. */ -interface ExternalNamedModelService, U : EntityDto, O, R : JpaRepository> : - NamedModelService, ExternalModelService - -/** An [AbstractService] with the functionalities of a [ExternalService]. */ -@Suppress("unused") -abstract class AbstractExternalService, U : EntityDto, O, R : JpaRepository>(repository: R) : - AbstractService(repository), ExternalService { - override fun getAllForOutput() = - getAll().map { it.toOutput() } -} - -/** An [AbstractModelService] with the functionalities of a [ExternalService]. */ -abstract class AbstractExternalModelService, U : EntityDto, O, R : JpaRepository>( - repository: R -) : AbstractModelService(repository), ExternalModelService { - 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, U : EntityDto, O, R : NamedJpaRepository>( - repository: R -) : AbstractNamedModelService(repository), ExternalNamedModelService { - override fun getAllForOutput() = - getAll().map { it.toOutput() } - - override fun getByIdForOutput(id: Long) = - getById(id).toOutput() -} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt index 7bbf75c..4aaf79f 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt @@ -2,6 +2,7 @@ package dev.fyloz.colorrecipesexplorer.logic import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent +import dev.fyloz.colorrecipesexplorer.dtos.GroupDto import dev.fyloz.colorrecipesexplorer.dtos.RecipeGroupInformationDto import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError @@ -30,7 +31,7 @@ class DefaultRecipeStepLogic(recipeStepService: RecipeStepService) : } class InvalidGroupStepsPositionsException( - val group: Group, + val group: GroupDto, val exception: InvalidPositionsException ) : RestException( "invalid-groupinformation-recipestep-position", @@ -39,7 +40,7 @@ class InvalidGroupStepsPositionsException( "The position of steps for the group ${group.name} are invalid", mapOf( "group" to group.name, - "groupId" to group.id!!, + "groupId" to group.id, "invalidSteps" to exception.errors ) ) { diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/GroupLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/GroupLogic.kt index b22c006..b421223 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/GroupLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/GroupLogic.kt @@ -1,97 +1,80 @@ 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.logic.AbstractExternalNamedModelService -import dev.fyloz.colorrecipesexplorer.logic.ExternalNamedModelService -import dev.fyloz.colorrecipesexplorer.model.account.* -import dev.fyloz.colorrecipesexplorer.repository.GroupRepository -import org.springframework.context.annotation.Profile -import org.springframework.stereotype.Service +import dev.fyloz.colorrecipesexplorer.dtos.GroupDto +import dev.fyloz.colorrecipesexplorer.dtos.UserDto +import dev.fyloz.colorrecipesexplorer.exception.NoDefaultGroupException +import dev.fyloz.colorrecipesexplorer.logic.BaseLogic +import dev.fyloz.colorrecipesexplorer.logic.Logic +import dev.fyloz.colorrecipesexplorer.service.GroupService +import org.springframework.transaction.annotation.Transactional import org.springframework.web.util.WebUtils import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse -import javax.transaction.Transactional const val defaultGroupCookieMaxAge = 10 * 365 * 24 * 60 * 60 // 10 ans -interface GroupLogic : - ExternalNamedModelService { +interface GroupLogic : Logic { /** Gets all the users of the group with the given [id]. */ - fun getUsersForGroup(id: Long): Collection + fun getUsersForGroup(id: Long): Collection /** 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]. */ - fun setResponseDefaultGroup(groupId: Long, response: HttpServletResponse) + fun setResponseDefaultGroup(id: Long, response: HttpServletResponse) } -@Service -@Profile("!emergency") -class DefaultGroupLogic( - private val userLogic: UserLogic, - groupRepository: GroupRepository -) : AbstractExternalNamedModelService( - groupRepository -), +@LogicComponent +class DefaultGroupLogic(service: GroupService, private val userLogic: UserLogic) : + BaseLogic(service, Constants.ModelNames.GROUP), GroupLogic { - override fun idNotFoundException(id: Long) = groupIdNotFoundException(id) - override fun idAlreadyExistsException(id: Long) = groupIdAlreadyExistsException(id) - override fun nameNotFoundException(name: String) = groupNameNotFoundException(name) - override fun nameAlreadyExistsException(name: String) = groupNameAlreadyExistsException(name) + override fun getUsersForGroup(id: Long) = userLogic.getAllByGroup(getById(id)) - override fun Group.toOutput() = GroupOutputDto( - this.id!!, - this.name, - this.permissions, - this.flatPermissions - ) - - override fun existsByName(name: String): Boolean = repository.existsByName(name) - override fun getUsersForGroup(id: Long): Collection = - userLogic.getByGroup(getById(id)) - - @Transactional - override fun save(entity: Group): Group { - return super.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 { + override fun getRequestDefaultGroup(request: HttpServletRequest): GroupDto { val defaultGroupCookie = WebUtils.getCookie(request, defaultGroupCookieName) ?: throw NoDefaultGroupException() val defaultGroupUser = userLogic.getById( defaultGroupCookie.value.toLong(), - ignoreDefaultGroupUsers = false, - ignoreSystemUsers = true + isSystemUser = false, + isDefaultGroupUser = true ) return defaultGroupUser.group!! } - override fun setResponseDefaultGroup(groupId: Long, response: HttpServletResponse) { - val group = getById(groupId) - val defaultGroupUser = userLogic.getDefaultGroupUser(group) + override fun setResponseDefaultGroup(id: Long, response: HttpServletResponse) { + val defaultGroupUser = userLogic.getDefaultGroupUser(getById(id)) response.addHeader( "Set-Cookie", "$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) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/JwtLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/JwtLogic.kt index e7ff36e..47469bf 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/JwtLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/JwtLogic.kt @@ -3,10 +3,8 @@ package dev.fyloz.colorrecipesexplorer.logic.users import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties -import dev.fyloz.colorrecipesexplorer.model.account.User -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.dtos.UserDetails +import dev.fyloz.colorrecipesexplorer.dtos.UserDto import dev.fyloz.colorrecipesexplorer.utils.base64encode import dev.fyloz.colorrecipesexplorer.utils.toDate import io.jsonwebtoken.Jwts @@ -23,10 +21,10 @@ interface JwtLogic { fun buildJwt(userDetails: UserDetails): String /** 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. */ - fun parseJwt(jwt: String): UserOutputDto + fun parseJwt(jwt: String): UserDto } @Service @@ -54,14 +52,14 @@ class DefaultJwtLogic( override fun buildJwt(userDetails: UserDetails) = buildJwt(userDetails.user) - override fun buildJwt(user: User): String = + override fun buildJwt(user: UserDto): String = jwtBuilder .setSubject(user.id.toString()) .setExpiration(getCurrentExpirationDate()) .claim(jwtClaimUser, user.serialize()) .compact() - override fun parseJwt(jwt: String): UserOutputDto = + override fun parseJwt(jwt: String): UserDto = with( jwtParser.parseClaimsJws(jwt) .body.get(jwtClaimUser, String::class.java) @@ -74,6 +72,6 @@ class DefaultJwtLogic( .plusSeconds(securityProperties.jwtDuration) .toDate() - private fun User.serialize(): String = - objectMapper.writeValueAsString(this.toOutputDto()) + private fun UserDto.serialize(): String = + objectMapper.writeValueAsString(this) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/UserDetailsLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/UserDetailsLogic.kt index 4286ed9..25b8369 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/UserDetailsLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/UserDetailsLogic.kt @@ -4,18 +4,18 @@ import dev.fyloz.colorrecipesexplorer.SpringUserDetails import dev.fyloz.colorrecipesexplorer.SpringUserDetailsService import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase 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.model.account.Permission 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.security.core.userdetails.UsernameNotFoundException import org.springframework.stereotype.Service interface UserDetailsLogic : SpringUserDetailsService { /** Loads an [User] for the given [id]. */ - fun loadUserById(id: Long, ignoreDefaultGroupUsers: Boolean = false): UserDetails + fun loadUserById(id: Long, isDefaultGroupUser: Boolean = true): UserDetails } @Service @@ -25,17 +25,17 @@ class DefaultUserDetailsLogic( ) : UserDetailsLogic { override fun loadUserByUsername(username: String): UserDetails { try { - return loadUserById(username.toLong(), true) + return loadUserById(username.toLong(), false) } catch (ex: NotFoundException) { throw UsernameNotFoundException(username) } } - override fun loadUserById(id: Long, ignoreDefaultGroupUsers: Boolean): UserDetails { + override fun loadUserById(id: Long, isDefaultGroupUser: Boolean): UserDetails { val user = userLogic.getById( id, - ignoreDefaultGroupUsers = ignoreDefaultGroupUsers, - ignoreSystemUsers = false + isSystemUser = true, + isDefaultGroupUser = isDefaultGroupUser ) return UserDetails(user) } @@ -46,7 +46,7 @@ class DefaultUserDetailsLogic( class EmergencyUserDetailsLogic( securityProperties: CreSecurityProperties ) : UserDetailsLogic { - private val users: Set + private val users: Set init { if (securityProperties.root == null) { @@ -56,20 +56,23 @@ class EmergencyUserDetailsLogic( users = setOf( // Add root user with(securityProperties.root!!) { - user( + UserDto( id = this.id, - plainPassword = this.password, - permissions = mutableSetOf(Permission.ADMIN) + firstName = "Root", + lastName = "User", + group = null, + password = this.password, + permissions = listOf(Permission.ADMIN) ) } ) } 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 } ?: throw UsernameNotFoundException(id.toString()) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/UserLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/UserLogic.kt index ef92174..bd17f04 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/UserLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/UserLogic.kt @@ -1,189 +1,146 @@ 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.logic.AbstractExternalModelService -import dev.fyloz.colorrecipesexplorer.logic.ExternalModelService -import dev.fyloz.colorrecipesexplorer.model.account.* -import dev.fyloz.colorrecipesexplorer.repository.UserRepository +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.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.Profile -import org.springframework.stereotype.Service +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.web.util.WebUtils import java.time.LocalDateTime import javax.servlet.http.HttpServletRequest -interface UserLogic : - ExternalModelService { - /** Check if an [User] with the given [firstName] and [lastName] exists. */ - fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean +interface UserLogic : Logic { + /** Gets all users which have the given [group]. */ + fun getAllByGroup(group: GroupDto): Collection /** Gets the user with the given [id]. */ - fun getById(id: Long, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): User - - /** Gets all users which have the given [group]. */ - fun getByGroup(group: Group): Collection + fun getById(id: Long, isSystemUser: Boolean, isDefaultGroupUser: Boolean): UserDto /** 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]. */ - fun saveDefaultGroupUser(group: Group) + fun saveDefaultGroupUser(group: GroupDto) - /** Updates de given [entity]. **/ - fun update(entity: User, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): User + /** Saves the given [dto]. */ + fun save(dto: UserSaveDto): UserDto - /** Updates the last login time of the user with the given [userId]. */ - fun updateLastLoginTime(userId: Long, time: LocalDateTime = LocalDateTime.now()): User + /** Updates the given [dto]. */ + 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]. */ - 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]. */ - fun addPermission(userId: Long, permission: Permission): User + /** Adds the given [permission] to the user with the given [id]. */ + fun addPermission(id: Long, permission: Permission): UserDto - /** Removes the given [permission] from the user with the given [userId]. */ - fun removePermission(userId: Long, permission: Permission): User + /** Removes the given [permission] from the user with the given [id]. */ + 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) } -@Service -@Profile("!emergency") +@LogicComponent class DefaultUserLogic( - userRepository: UserRepository, - @Lazy val groupLogic: GroupLogic, -) : AbstractExternalModelService( - userRepository -), - UserLogic { - override fun idNotFoundException(id: Long) = userIdNotFoundException(id) - override fun idAlreadyExistsException(id: Long) = userIdAlreadyExistsException(id) + service: UserService, @Lazy private val groupLogic: GroupLogic, @Lazy private val passwordEncoder: PasswordEncoder +) : BaseLogic(service, Constants.ModelNames.USER), UserLogic { + override fun getAll() = service.getAll(isSystemUser = false, isDefaultGroupUser = false) - override fun User.toOutput() = this.toOutputDto() + override fun getAllByGroup(group: GroupDto) = service.getAllByGroup(group) - override fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean = - repository.existsByFirstNameAndLastName(firstName, lastName) + override fun getById(id: Long) = getById(id, isSystemUser = false, isDefaultGroupUser = false) + override fun getById(id: Long, isSystemUser: Boolean, isDefaultGroupUser: Boolean) = + service.getById(id, !isDefaultGroupUser, !isSystemUser) ?: throw notFoundException(value = id) - override fun getAll(): Collection = - super.getAll().filter { !it.isSystemUser && !it.isDefaultGroupUser } + override fun getDefaultGroupUser(group: GroupDto) = + service.getDefaultGroupUser(group) ?: throw notFoundException(identifierName = "groupId", value = group.id) - override fun getById(id: Long): User = - 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 = - 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.save(entity) - } - - override fun saveDefaultGroupUser(group: Group) { + override fun saveDefaultGroupUser(group: GroupDto) { save( - user( - id = 1000000L + group.id!!, + UserSaveDto( + id = group.defaultGroupUserId, firstName = group.name, lastName = "User", - plainPassword = group.name, - group = group, + password = group.name, + groupId = group.id, + permissions = listOf(), isDefaultGroupUser = true ) ) } - override fun updateLastLoginTime(userId: Long, time: LocalDateTime): User { - val user = getById(userId, ignoreDefaultGroupUsers = true, ignoreSystemUsers = false) - user.lastLoginTime = time + override fun save(dto: UserSaveDto) = save( + UserDto( + 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( - user, - ignoreDefaultGroupUsers = true, - ignoreSystemUsers = false + user.copy( + firstName = dto.firstName, + lastName = dto.lastName, + group = if (dto.groupId != null) groupLogic.getById(dto.groupId) else null, + permissions = dto.permissions + ) ) } - override fun update(entity: UserUpdateDto): User { - val persistedUser by lazy { getById(entity.id) } - return update(with(entity) { - User( - 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(dto: UserDto): UserDto { + throwIfFirstNameAndLastNameAlreadyExists(dto.firstName, dto.lastName, dto.id) + + return super.update(dto) } - override fun update(entity: User): User = - update(entity, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true) - - 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 updateLastLoginTime(id: Long, time: LocalDateTime) = with(getById(id)) { + update(this.copy(lastLoginTime = time)) } - override fun updatePassword(id: Long, password: String): User { - val persistedUser = getById(id, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true) - return super.update(with(persistedUser) { - user( - id, - firstName, - lastName, - plainPassword = password, - isDefaultGroupUser, - isSystemUser, - group, - permissions, - lastLoginTime - ) - }) + override fun updatePassword(id: Long, password: String) = with(getById(id)) { + update(this.copy(password = passwordEncoder.encode(password))) } - override fun addPermission(userId: Long, permission: Permission): User = - super.update(getById(userId).apply { permissions += permission }) + override fun addPermission(id: Long, permission: Permission) = with(getById(id)) { + update(this.copy(permissions = this.permissions + permission)) + } - override fun removePermission(userId: Long, permission: Permission): User = - super.update(getById(userId).apply { permissions -= permission }) + override fun removePermission(id: Long, permission: Permission) = with(getById(id)) { + update(this.copy(permissions = this.permissions - permission)) + } override fun logout(request: HttpServletRequest) { - val authorizationCookie = WebUtils.getCookie(request, "Authorization") + val authorizationCookie = WebUtils.getCookie(request, authorizationCookieName) if (authorizationCookie != null) { val authorizationToken = authorizationCookie.value 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" + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt index f0d37a4..e05a6ef 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt @@ -7,7 +7,7 @@ import javax.persistence.* data class Company( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - override val id: Long?, + override val id: Long, @Column(unique = true) val name: String diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt index ac2eb93..05e04a6 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt @@ -8,7 +8,7 @@ import javax.persistence.* data class Material( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - override val id: Long?, + override val id: Long, @Column(unique = true) val name: String, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt index 9082e82..597fc8b 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt @@ -8,7 +8,7 @@ import javax.persistence.* data class MaterialType( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - override val id: Long? = null, + override val id: Long, @Column(unique = true) val name: String = "", diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt index 8fa3a19..86ecad7 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt @@ -7,9 +7,9 @@ import javax.persistence.* data class Mix( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - override val id: Long?, + override val id: Long, - var location: String?, + val location: String?, @Column(name = "recipe_id") val recipeId: Long, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt index e243e14..f3f8f0f 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt @@ -7,13 +7,13 @@ import javax.persistence.* data class MixMaterial( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - override val id: Long?, + override val id: Long, @ManyToOne @JoinColumn(name = "material_id") val material: Material, - var quantity: Float, + val quantity: Float, - var position: Int + val position: Int ) : ModelEntity \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixType.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixType.kt index 13dd296..dec9c12 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixType.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixType.kt @@ -7,7 +7,7 @@ import javax.persistence.* data class MixType( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - override val id: Long?, + override val id: Long, val name: String, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/ModelEntity.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/ModelEntity.kt index 4185cac..00465ef 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/ModelEntity.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/ModelEntity.kt @@ -1,17 +1,6 @@ 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 { - val id: Long? -} - -interface NamedModelEntity : ModelEntity { - val name: String -} - -interface EntityDto { - /** Converts the dto to an actual entity. */ - fun toEntity(): E { - throw UnsupportedOperationException() - } + val id: Long } \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt index 9692a56..7392285 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt @@ -9,7 +9,7 @@ import javax.persistence.* data class Recipe( @Id @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]. */ val name: String, @@ -47,15 +47,15 @@ data class Recipe( data class RecipeGroupInformation( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - override val id: Long?, + override val id: Long, @ManyToOne @JoinColumn(name = "group_id") val group: Group, - var note: String?, + val note: String?, @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true) @JoinColumn(name = "recipe_group_information_id") - var steps: List? + val steps: List? ) : ModelEntity \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/RecipeStep.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/RecipeStep.kt index e71803b..14ea885 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/RecipeStep.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/RecipeStep.kt @@ -7,7 +7,7 @@ import javax.persistence.* data class RecipeStep( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - override val id: Long?, + override val id: Long, val position: Int, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/Group.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/Group.kt index 6f6b24c..ebf84b6 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/Group.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/Group.kt @@ -1,134 +1,24 @@ 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 org.hibernate.annotations.Fetch import org.hibernate.annotations.FetchMode -import org.springframework.http.HttpStatus import javax.persistence.* -import javax.validation.constraints.NotBlank -import javax.validation.constraints.NotEmpty @Entity @Table(name = "user_group") data class Group( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - override var id: Long? = null, + override val id: Long, @Column(unique = true) - override val name: String = "", + val name: String, @Enumerated(EnumType.STRING) @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "group_permission", joinColumns = [JoinColumn(name = "group_id")]) @Column(name = "permission") @Fetch(FetchMode.SUBSELECT) - val permissions: MutableSet = mutableSetOf(), -) : NamedModelEntity { - val flatPermissions: Set - get() = this.permissions - .flatMap { it.flat() } - .filter { !it.deprecated } - .toSet() -} - -open class GroupSaveDto( - @field:NotBlank - val name: String, - - @field:NotEmpty - val permissions: MutableSet -) : EntityDto { - override fun toEntity(): Group = - Group(null, name, permissions) -} - -open class GroupUpdateDto( - val id: Long, - - @field:NotBlank - val name: String, - - @field:NotEmpty - val permissions: MutableSet -) : EntityDto { - override fun toEntity(): Group = - Group(id, name, permissions) -} - -data class GroupOutputDto( - override val id: Long, - val name: String, - val permissions: Set, - val explicitPermissions: Set -): ModelEntity - -fun group( - id: Long? = null, - name: String = "name", - permissions: MutableSet = mutableSetOf(), - op: Group.() -> Unit = {} -) = Group(id, name, permissions).apply(op) - -fun groupSaveDto( - name: String = "name", - permissions: MutableSet = mutableSetOf(), - op: GroupSaveDto.() -> Unit = {} -) = GroupSaveDto(name, permissions).apply(op) - -fun groupUpdateDto( - id: Long = 0L, - name: String = "name", - permissions: MutableSet = 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" - ) + val permissions: List, +) : ModelEntity \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/User.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/User.kt index 633a1a4..7d8ce3a 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/User.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/User.kt @@ -1,20 +1,10 @@ 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 org.hibernate.annotations.Fetch import org.hibernate.annotations.FetchMode -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder -import org.springframework.security.crypto.password.PasswordEncoder import java.time.LocalDateTime 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 @Table(name = "user") @@ -23,210 +13,31 @@ data class User( override val id: Long, @Column(name = "first_name") - val firstName: String = "", + val firstName: String, @Column(name = "last_name") - val lastName: String = "", + val lastName: String, - val password: String = "", + val password: String, @Column(name = "default_group_user") - val isDefaultGroupUser: Boolean = false, + val isDefaultGroupUser: Boolean, @Column(name = "system_user") - val isSystemUser: Boolean = false, + val isSystemUser: Boolean, @ManyToOne @JoinColumn(name = "group_id") @Fetch(FetchMode.SELECT) - var group: Group? = null, + val group: Group?, @Enumerated(EnumType.STRING) @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "user_permission", joinColumns = [JoinColumn(name = "user_id")]) @Column(name = "permission") @Fetch(FetchMode.SUBSELECT) - val permissions: MutableSet = mutableSetOf(), + val permissions: List, @Column(name = "last_login_time") - var lastLoginTime: LocalDateTime? = null -) : ModelEntity { - val flatPermissions: Set - 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 = mutableSetOf() -) : EntityDto - -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? -) : EntityDto - -data class UserOutputDto( - override val id: Long, - val firstName: String, - val lastName: String, - val group: Group?, - val permissions: Set, - val explicitPermissions: Set, val lastLoginTime: LocalDateTime? -) : 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 = 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 = 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 = 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 = mutableSetOf(), - op: UserUpdateDto.() -> Unit = {} -) = UserUpdateDto(id, firstName, lastName, groupId, permissions).apply(op) - -// ==== Extensions ==== -fun Set.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" - ) +) : ModelEntity \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt index 1f7ada4..0539585 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt @@ -9,7 +9,7 @@ import javax.persistence.* data class TouchUpKit( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - override val id: Long?, + override val id: Long, val project: String, @@ -41,7 +41,7 @@ data class TouchUpKit( data class TouchUpKitProduct( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - override val id: Long?, + override val id: Long, val name: String, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/AccountRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/AccountRepository.kt index d2548ea..82575bd 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/AccountRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/AccountRepository.kt @@ -3,18 +3,28 @@ package dev.fyloz.colorrecipesexplorer.repository import dev.fyloz.colorrecipesexplorer.model.account.Group import dev.fyloz.colorrecipesexplorer.model.account.User import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository @Repository interface UserRepository : JpaRepository { - fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean - - fun findByFirstNameAndLastName(firstName: String, lastName: String): User? + /** Checks if a user with the given [firstName], [lastName] and a different [id] exists. */ + fun existsByFirstNameAndLastNameAndIdNot(firstName: String, lastName: String, id: Long): Boolean + /** 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 - 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 -interface GroupRepository : NamedJpaRepository +interface GroupRepository : JpaRepository { + /** Checks if a group with the given [name] and a different [id] exists. */ + fun existsByNameAndIdNot(name: String, id: Long): Boolean +} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/Repository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/Repository.kt deleted file mode 100644 index 5e0843c..0000000 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/Repository.kt +++ /dev/null @@ -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 : JpaRepository { - /** 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) -} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/AccountControllers.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/AccountControllers.kt index 87d42e4..dd423b4 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/AccountControllers.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/AccountControllers.kt @@ -1,10 +1,15 @@ package dev.fyloz.colorrecipesexplorer.rest +import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditUsers 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.UserLogic -import dev.fyloz.colorrecipesexplorer.model.account.* +import dev.fyloz.colorrecipesexplorer.model.account.Permission import org.springframework.context.annotation.Profile import org.springframework.http.MediaType import org.springframework.security.access.prepost.PreAuthorize @@ -13,30 +18,25 @@ import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse import javax.validation.Valid -private const val USER_CONTROLLER_PATH = "api/user" -private const val GROUP_CONTROLLER_PATH = "api/user/group" - @RestController -@RequestMapping(USER_CONTROLLER_PATH) +@RequestMapping(Constants.ControllerPaths.USER) @Profile("!emergency") class UserController(private val userLogic: UserLogic) { @GetMapping @PreAuthorizeViewUsers fun getAll() = - ok(userLogic.getAllForOutput()) + ok(userLogic.getAll()) @GetMapping("{id}") @PreAuthorizeViewUsers fun getById(@PathVariable id: Long) = - ok(userLogic.getByIdForOutput(id)) + ok(userLogic.getById(id)) @PostMapping @PreAuthorizeEditUsers fun save(@Valid @RequestBody user: UserSaveDto) = - created(USER_CONTROLLER_PATH) { - with(userLogic) { - save(user).toOutput() - } + created(Constants.ControllerPaths.USER) { + userLogic.save(user) } @PutMapping @@ -78,7 +78,7 @@ class UserController(private val userLogic: UserLogic) { } @RestController -@RequestMapping(GROUP_CONTROLLER_PATH) +@RequestMapping(Constants.ControllerPaths.GROUP) @Profile("!emergency") class GroupsController( private val groupLogic: GroupLogic, @@ -87,20 +87,17 @@ class GroupsController( @GetMapping @PreAuthorize("hasAnyAuthority('VIEW_RECIPES', 'VIEW_USERS')") fun getAll() = - ok(groupLogic.getAllForOutput()) + ok(groupLogic.getAll()) @GetMapping("{id}") @PreAuthorizeViewUsers fun getById(@PathVariable id: Long) = - ok(groupLogic.getByIdForOutput(id)) + ok(groupLogic.getById(id)) @GetMapping("{id}/users") @PreAuthorizeViewUsers fun getUsersForGroup(@PathVariable id: Long) = - ok(with(userLogic) { - groupLogic.getUsersForGroup(id) - .map { it.toOutput() } - }) + ok(groupLogic.getUsersForGroup(id)) @PostMapping("default/{groupId}") @PreAuthorizeViewUsers @@ -113,27 +110,25 @@ class GroupsController( @PreAuthorizeViewUsers fun getRequestDefaultGroup(request: HttpServletRequest) = ok(with(groupLogic) { - getRequestDefaultGroup(request).toOutput() + getRequestDefaultGroup(request) }) @GetMapping("currentuser") fun getCurrentGroupUser(request: HttpServletRequest) = ok(with(groupLogic.getRequestDefaultGroup(request)) { - userLogic.getDefaultGroupUser(this).toOutputDto() + userLogic.getDefaultGroupUser(this) }) @PostMapping @PreAuthorizeEditUsers - fun save(@Valid @RequestBody group: GroupSaveDto) = - created(GROUP_CONTROLLER_PATH) { - with(groupLogic) { - save(group).toOutput() - } + fun save(@Valid @RequestBody group: GroupDto) = + created(Constants.ControllerPaths.GROUP) { + groupLogic.save(group) } @PutMapping @PreAuthorizeEditUsers - fun update(@Valid @RequestBody group: GroupUpdateDto) = + fun update(@Valid @RequestBody group: GroupDto) = noContent { groupLogic.update(group) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/InventoryController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/InventoryController.kt index 5f5cbc5..2f5edb6 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/InventoryController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/InventoryController.kt @@ -1,5 +1,6 @@ package dev.fyloz.colorrecipesexplorer.rest +import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.dtos.MaterialQuantityDto import dev.fyloz.colorrecipesexplorer.dtos.MixDeductDto 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.RestController -private const val INVENTORY_CONTROLLER_PATH = "api/inventory" - @RestController -@RequestMapping(INVENTORY_CONTROLLER_PATH) +@RequestMapping(Constants.ControllerPaths.INVENTORY) @Profile("!emergency") class InventoryController( private val inventoryLogic: InventoryLogic diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt index d50ce29..0ffcfe8 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt @@ -44,19 +44,11 @@ fun fileCreated(basePath: String, producer: () -> String): ResponseEntity created(controllerPath: String, body: T): ResponseEntity = - created(controllerPath, body, body.id!!) - /** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */ @JvmName("createdDto") fun created(controllerPath: String, body: T): ResponseEntity = created(controllerPath, body, body.id) -/** Creates a HTTP CREATED [ResponseEntity] with the result of the given [producer] as its body. */ -fun created(controllerPath: String, producer: () -> T): ResponseEntity = - created(controllerPath, producer()) - /** Creates a HTTP CREATED [ResponseEntity] with the result of the given [producer] as its body. */ @JvmName("createdDto") fun created(controllerPath: String, producer: () -> T): ResponseEntity = diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt index 70057e4..5004973 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt @@ -20,7 +20,7 @@ class DefaultCompanyService(repository: CompanyRepository) : override fun isUsedByRecipe(id: Long) = repository.isUsedByRecipe(id) override fun toDto(entity: Company) = - CompanyDto(entity.id!!, entity.name) + CompanyDto(entity.id, entity.name) override fun toEntity(dto: CompanyDto) = Company(dto.id, dto.name) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/GroupService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/GroupService.kt new file mode 100644 index 0000000..1b13ced --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/GroupService.kt @@ -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 { + /** 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 +} + +@ServiceComponent +class DefaultGroupService(repository: GroupRepository) : BaseService(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 } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt index 1e2c6bc..1806da7 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt @@ -35,7 +35,7 @@ class DefaultMaterialService( override fun toDto(entity: Material) = MaterialDto( - entity.id!!, + entity.id, entity.name, entity.inventoryQuantity, entity.isMixType, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeService.kt index 74820c4..1acbcb8 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeService.kt @@ -37,7 +37,7 @@ class DefaultMaterialTypeService(repository: MaterialTypeRepository) : override fun isUsedByMaterial(id: Long) = repository.isUsedByMaterial(id) 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) = MaterialType(dto.id, dto.name, dto.prefix, dto.usePercentages, dto.systemType) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialService.kt index 491763b..50df79e 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialService.kt @@ -16,7 +16,7 @@ class DefaultMixMaterialService(repository: MixMaterialRepository, private val m override fun existsByMaterialId(materialId: Long) = repository.existsByMaterialId(materialId) 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) = MixMaterial(dto.id, materialService.toEntity(dto.material), dto.quantity, dto.position) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt index 96b914e..13f9e37 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt @@ -33,7 +33,7 @@ class DefaultMixService( override fun toDto(entity: Mix) = MixDto( - entity.id!!, + entity.id, entity.location, entity.recipeId, mixTypeService.toDto(entity.mixType), diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeService.kt index eaa7393..d739b0b 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeService.kt @@ -37,7 +37,7 @@ class DefaultMixTypeService( override fun toDto(entity: MixType) = MixTypeDto( - entity.id!!, + entity.id, entity.name, materialTypeService.toDto(entity.materialType), if (entity.material != null) materialService.toDto(entity.material) else null diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt index de556e9..8532a02 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt @@ -27,6 +27,7 @@ class DefaultRecipeService( private val companyService: CompanyService, private val mixService: MixService, private val recipeStepService: RecipeStepService, + private val groupService: GroupService, private val configLogic: ConfigurationLogic ) : BaseService(repository), RecipeService { @@ -39,7 +40,7 @@ class DefaultRecipeService( @Transactional override fun toDto(entity: Recipe) = RecipeDto( - entity.id!!, + entity.id, entity.name, entity.description, entity.color, @@ -55,8 +56,8 @@ class DefaultRecipeService( private fun groupInformationToDto(entity: RecipeGroupInformation) = RecipeGroupInformationDto( - entity.id!!, - entity.group, + entity.id, + groupService.toDto(entity.group), entity.note, entity.steps?.lazyMap(recipeStepService::toDto) ?: listOf() ) @@ -77,7 +78,12 @@ class DefaultRecipeService( ) 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? = with(Period.parse(configLogic.getContent(ConfigurationType.RECIPE_APPROBATION_EXPIRATION))) { diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt index 57a8777..556f40b 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt @@ -11,7 +11,7 @@ interface RecipeStepService : Service(repository), RecipeStepService { override fun toDto(entity: RecipeStep) = - RecipeStepDto(entity.id!!, entity.position, entity.message) + RecipeStepDto(entity.id, entity.position, entity.message) override fun toEntity(dto: RecipeStepDto) = RecipeStep(dto.id, dto.position, dto.message) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/TouchUpKitService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/TouchUpKitService.kt index b45d954..73dfa70 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/TouchUpKitService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/TouchUpKitService.kt @@ -24,7 +24,7 @@ class DefaultTouchUpKitService(repository: TouchUpKitRepository, private val con override fun toDto(entity: TouchUpKit) = TouchUpKitDto( - entity.id!!, + entity.id, entity.project, entity.buggy, entity.company, @@ -39,7 +39,7 @@ class DefaultTouchUpKitService(repository: TouchUpKitRepository, private val con ) 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) = TouchUpKit( diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/UserService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/UserService.kt new file mode 100644 index 0000000..4792397 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/UserService.kt @@ -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 { + /** 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 + + /** Gets all users for the given [group]. */ + fun getAllByGroup(group: GroupDto): Collection + + /** 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(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 { + val perms = user.permissions.flatMap { it.flat() }.filter { !it.deprecated } + + if (user.group != null) { + return perms + groupService.flattenPermissions(user.group) + } + + return perms + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/AbstractServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/AbstractServiceTest.kt deleted file mode 100644 index 968aa7b..0000000 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/AbstractServiceTest.kt +++ /dev/null @@ -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, R : JpaRepository> { - protected abstract val repository: R - protected abstract val logic: S - - protected abstract val entity: E - protected abstract val anotherEntity: E - - protected val entityList: List - 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, R : JpaRepository> : - AbstractServiceTest1() { - - // 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 { 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 { 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 { 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, R : NamedJpaRepository> : - AbstractModelServiceTest() { - 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 { 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 { 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 { 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 { 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, U : EntityDto, S : ExternalModelService, R : JpaRepository> : - AbstractModelServiceTest(), ExternalModelServiceTest { - protected abstract val entitySaveDto: N - protected abstract val entityUpdateDto: U - - @AfterEach - override fun afterEach() { - reset(entitySaveDto, entityUpdateDto) - super.afterEach() - } -} - -abstract class AbstractExternalNamedModelServiceTest, U : EntityDto, S : ExternalNamedModelService, R : NamedJpaRepository> : - AbstractNamedModelServiceTest(), 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 > withBaseSaveDtoTest( - entity: E, - entitySaveDto: N, - service: ExternalService, - 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 > withBaseUpdateDtoTest( - entity: E, - entityUpdateDto: U, - service: ExternalModelService, - 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() -} diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/AccountsServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/AccountsServiceTest.kt deleted file mode 100644 index bc31f97..0000000 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/AccountsServiceTest.kt +++ /dev/null @@ -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() { - 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 { - 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 { - 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 { 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()) - - val found = logic.save(entitySaveDto) - - verify(logic).save(argThat { 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 { - logic.update( - entity, - true, - ignoreSystemUsers = true - ) - }.assertErrorCode("fullName") - } -} - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class GroupLogicTest : - AbstractExternalNamedModelServiceTest() { - 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 = 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 { 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 { 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) - } -} diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/JwtLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultJwtLogicTest.kt similarity index 74% rename from src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/JwtLogicTest.kt rename to src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultJwtLogicTest.kt index 8d99969..e5ddab7 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/JwtLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultJwtLogicTest.kt @@ -3,23 +3,23 @@ package dev.fyloz.colorrecipesexplorer.logic import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue 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.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.isAround import io.jsonwebtoken.Jwts import io.jsonwebtoken.jackson.io.JacksonDeserializer +import io.mockk.clearAllMocks import io.mockk.spyk +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import java.time.Instant import kotlin.test.assertEquals import kotlin.test.assertTrue -class JwtLogicTest { +class DefaultJwtLogicTest { private val objectMapper = jacksonObjectMapper() private val securityProperties = CreSecurityProperties().apply { jwtSecret = "XRRm7OflmFuCrOB2Xvmfsercih9DCKom" @@ -34,12 +34,14 @@ class JwtLogicTest { private val jwtService = spyk(DefaultJwtLogic(objectMapper, securityProperties)) - private val user = user() - private val userOutputDto = user.toOutputDto() + private val user = UserDto(0L, "Unit test", "User", "", null, listOf()) - // 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) .body.get(jwtClaimUser, String::class.java) @@ -47,27 +49,27 @@ class JwtLogicTest { } @Test - fun `buildJwt(userDetails) returns jwt string with valid user`() { + fun buildJwt_userDetails_normalBehavior_returnsJwtStringWithValidUser() { val userDetails = UserDetails(user) val builtJwt = jwtService.buildJwt(userDetails) withParsedUserOutputDto(builtJwt) { parsedUser -> - assertEquals(user.toOutputDto(), parsedUser) + assertEquals(user, parsedUser) } } @Test - fun `buildJwt() returns jwt string with valid user`() { + fun buildJwt_user_normalBehavior_returnsJwtStringWithValidUser() { val builtJwt = jwtService.buildJwt(user) withParsedUserOutputDto(builtJwt) { parsedUser -> - assertEquals(user.toOutputDto(), parsedUser) + assertEquals(user, parsedUser) } } @Test - fun `buildJwt() returns jwt string with valid subject`() { + fun buildJwt_user_normalBehavior_returnsJwtStringWithValidSubject() { val builtJwt = jwtService.buildJwt(user) val jwtSubject = jwtParser.parseClaimsJws(builtJwt).body.subject @@ -75,7 +77,7 @@ class JwtLogicTest { } @Test - fun `buildJwt() returns jwt with valid expiration date`() { + fun buildJwt_user_returnsJwtWithValidExpirationDate() { val jwtExpectedExpirationDate = Instant.now().plusSeconds(securityProperties.jwtDuration) val builtJwt = jwtService.buildJwt(user) @@ -89,10 +91,10 @@ class JwtLogicTest { // parseJwt() @Test - fun `parseJwt() returns expected user`() { + fun parseJwt_normalBehavior_returnsExpectedUser() { val jwt = jwtService.buildJwt(user) val parsedUser = jwtService.parseJwt(jwt) - assertEquals(userOutputDto, parsedUser) + assertEquals(user, parsedUser) } } diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeLogicTest.kt index cdc0ae8..101da52 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeLogicTest.kt @@ -3,7 +3,6 @@ package dev.fyloz.colorrecipesexplorer.logic import dev.fyloz.colorrecipesexplorer.dtos.* import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic -import dev.fyloz.colorrecipesexplorer.model.account.Group import dev.fyloz.colorrecipesexplorer.service.RecipeService import io.mockk.* import org.junit.jupiter.api.AfterEach @@ -23,7 +22,7 @@ class DefaultRecipeLogicTest { spyk(DefaultRecipeLogic(recipeServiceMock, companyLogicMock, recipeStepLogicMock, mixLogicMock, groupLogicMock)) 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( 1L, "Unit test recipe", @@ -160,7 +159,7 @@ class DefaultRecipeLogicTest { 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()) // Act @@ -189,7 +188,7 @@ class DefaultRecipeLogicTest { // Arrange 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) // Act diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeStepLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeStepLogicTest.kt index d24ef2f..23aac43 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeStepLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeStepLogicTest.kt @@ -1,10 +1,10 @@ package dev.fyloz.colorrecipesexplorer.logic +import dev.fyloz.colorrecipesexplorer.dtos.GroupDto import dev.fyloz.colorrecipesexplorer.dtos.RecipeGroupInformationDto import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException -import dev.fyloz.colorrecipesexplorer.model.account.Group import dev.fyloz.colorrecipesexplorer.service.RecipeStepService import dev.fyloz.colorrecipesexplorer.utils.PositionUtils import io.mockk.* @@ -28,7 +28,7 @@ class DefaultRecipeStepLogicTest { mockkObject(PositionUtils) 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 groupInfo = RecipeGroupInformationDto(1L, group, "A note", steps) @@ -49,7 +49,7 @@ class DefaultRecipeStepLogicTest { mockkObject(PositionUtils) 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 groupInfo = RecipeGroupInformationDto(1L, group, "A note", steps) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/TouchUpKitLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/TouchUpKitLogicTest.kt deleted file mode 100644 index 0bc8ffe..0000000 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/TouchUpKitLogicTest.kt +++ /dev/null @@ -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() -// val fileService = mockk { -// every { write(any(), any(), any()) } just Runs -// } -// val configService = mockk(relaxed = true) -// val touchUpKitService = spyk(DefaultTouchUpKitLogic(fileService, configService, touchUpKitRepository)) -// val pdfDocumentData = mockk() -// val pdfDocument = mockk { -// 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(), 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() -// } -} diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/DefaultGroupLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/DefaultGroupLogicTest.kt new file mode 100644 index 0000000..1f208b1 --- /dev/null +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/DefaultGroupLogicTest.kt @@ -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 { + 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 { + 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 { groupLogic.save(group) } + } + + @Test + fun update_normalBehavior_throwsAlreadyExists() { + // Arrange + every { groupServiceMock.existsByName(any(), any()) } returns true + + // Act + // Assert + assertThrows { groupLogic.update(group) } + } + + @Test + fun deleteById_normalBehavior_callsDeleteByIdInUserLogicWithDefaultGroupUserId() { + // Arrange + // Act + groupLogic.deleteById(group.id) + + // Assert + verify { + userLogicMock.deleteById(group.defaultGroupUserId) + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/DefaultUserLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/DefaultUserLogicTest.kt new file mode 100644 index 0000000..d4f8b32 --- /dev/null +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/account/DefaultUserLogicTest.kt @@ -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 { + 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 { + every { getById(any()) } returns group + } + private val passwordEncoderMock = mockk { + every { encode(any()) } answers { "encoded ${this.firstArg()}" } + } + + 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 { 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 { userLogic.getDefaultGroupUser(group) } + } + + @Test + fun saveDefaultGroupUser_normalBehavior_callsSaveWithValidSaveDto() { + // Arrange + every { userLogic.save(any()) } 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()) } 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 { userLogic.save(user) } + } + + @Test + fun save_fullNameAlreadyExists_throwsAlreadyExistsException() { + // Arrange + every { userServiceMock.existsByFirstNameAndLastName(any(), any(), any()) } returns true + + // Act + // Assert + assertThrows { userLogic.save(userSaveDto) } + } + + @Test + fun update_dto_normalBehavior_callsUpdateWithValidUser() { + // Arrange + every { userLogic.getById(any(), any(), any()) } returns user + every { userLogic.update(any()) } 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 { userLogic.update(user) } + } + + @Test + fun updateLastLoginTime_normalBehavior_callsUpdateWithUpdatedTime() { + // Arrange + every { userLogic.getById(any()) } returns user + every { userLogic.update(any()) } 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()) } 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()) } 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()) } returns user + + // Act + userLogic.removePermission(user.id, removedPermission) + + // Assert + verify { + userLogic.update(user) + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/files/FileLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/files/DefaultFileLogicTest.kt similarity index 99% rename from src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/files/FileLogicTest.kt rename to src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/files/DefaultFileLogicTest.kt index ce8e5d1..7cb3fde 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/files/FileLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/files/DefaultFileLogicTest.kt @@ -21,7 +21,7 @@ private const val mockFilePath = "existingFile" private val mockFilePathPath = Path.of(mockFilePath) private val mockFileData = byteArrayOf(0x1, 0x8, 0xa, 0xf) -class FileLogicTest { +class DefaultFileLogicTest { private val fileCacheMock = mockk { every { setExists(any(), any()) } just runs }