From d8395437046b24f8d4e7b531c0f0bd0cc994f7ae Mon Sep 17 00:00:00 2001 From: FyloZ Date: Sun, 13 Feb 2022 22:05:05 -0500 Subject: [PATCH 01/11] #25 Rename service to logic --- .../{service => logic}/files/XlsService.java | 15 +- .../DatabaseVersioning.kt | 9 +- .../config/ApplicationListeners.kt | 17 +-- .../config/CreConfiguration.kt | 4 +- .../initializers/MaterialTypeInitializer.kt | 17 ++- .../config/initializers/MixInitializer.kt | 8 +- .../config/initializers/RecipeInitializer.kt | 8 +- .../config/security/JwtFilters.kt | 16 +-- .../config/security/SecurityConfig.kt | 37 +++-- .../CompanyLogic.kt} | 16 +-- .../InventoryLogic.kt} | 26 ++-- .../MaterialLogic.kt} | 34 ++--- .../MaterialTypeLogic.kt} | 14 +- .../MixService.kt => logic/MixLogic.kt} | 44 +++--- .../MixMaterialLogic.kt} | 14 +- .../MixTypeLogic.kt} | 19 ++- .../RecipeService.kt => logic/RecipeLogic.kt} | 46 +++---- .../RecipeStepLogic.kt} | 17 ++- .../{service => logic}/Service.kt | 2 +- .../TouchUpKitLogic.kt} | 21 ++- .../config/ConfigurationLogic.kt} | 22 +-- .../config/ConfigurationSource.kt | 8 +- .../{service => logic}/files/FileCache.kt | 2 +- .../files/FileLogic.kt} | 10 +- .../files/ResourceFileLogic.kt} | 6 +- .../jobs/TouchUpKitRemover.kt | 10 +- .../users/GroupLogic.kt} | 24 ++-- .../JwtService.kt => logic/users/JwtLogic.kt} | 8 +- .../users/UserDetailsLogic.kt} | 19 +-- .../users/UserLogic.kt} | 19 ++- .../rest/AccountControllers.kt | 53 ++++--- .../rest/CompanyController.kt | 13 +- .../rest/ConfigurationController.kt | 20 ++- .../rest/FileController.kt | 16 +-- .../rest/InventoryController.kt | 11 +- .../rest/MaterialController.kt | 23 ++-- .../rest/MaterialTypeController.kt | 14 +- .../rest/RecipeController.kt | 38 +++--- .../rest/TouchUpKitController.kt | 18 +-- .../{service => logic}/AbstractServiceTest.kt | 70 +++++----- .../{service => logic}/AccountsServiceTest.kt | 96 ++++++------- .../CompanyLogicTest.kt} | 29 ++-- .../ConfigurationLogicTest.kt} | 129 +++++++++--------- .../InventoryLogicTest.kt} | 50 +++---- .../JwtLogicTest.kt} | 11 +- .../MaterialLogicTest.kt} | 66 ++++----- .../MaterialTypeLogicTest.kt} | 56 ++++---- .../MixLogicTest.kt} | 54 ++++---- .../MixMaterialLogicTest.kt} | 28 ++-- .../MixTypeLogicTest.kt} | 47 ++++--- .../RecipeLogicTest.kt} | 102 +++++++------- .../RecipeStepLogicTest.kt} | 18 +-- .../TouchUpKitLogicTest.kt} | 14 +- .../files/DefaultFileCacheTest.kt | 2 +- .../files/FileLogicTest.kt} | 6 +- .../files/ResourceFileLogicTest.kt} | 22 +-- 56 files changed, 756 insertions(+), 762 deletions(-) rename src/main/java/dev/fyloz/colorrecipesexplorer/{service => logic}/files/XlsService.java (86%) rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/{service/CompanyService.kt => logic/CompanyLogic.kt} (85%) rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/{service/InventoryService.kt => logic/InventoryLogic.kt} (87%) rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/{service/MaterialService.kt => logic/MaterialLogic.kt} (87%) rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/{service/MaterialTypeService.kt => logic/MaterialTypeLogic.kt} (91%) rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/{service/MixService.kt => logic/MixLogic.kt} (69%) rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/{service/MixMaterialService.kt => logic/MixMaterialLogic.kt} (93%) rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/{service/MixTypeService.kt => logic/MixTypeLogic.kt} (89%) rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/{service/RecipeService.kt => logic/RecipeLogic.kt} (89%) rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/{service/RecipeStepService.kt => logic/RecipeStepLogic.kt} (87%) rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/{service => logic}/Service.kt (99%) rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/{service/TouchUpKitService.kt => logic/TouchUpKitLogic.kt} (90%) rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/{service/config/ConfigurationService.kt => logic/config/ConfigurationLogic.kt} (94%) rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/{service => logic}/config/ConfigurationSource.kt (97%) rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/{service => logic}/files/FileCache.kt (99%) rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/{service/files/FileService.kt => logic/files/FileLogic.kt} (97%) rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/{service/files/ResourceFileService.kt => logic/files/ResourceFileLogic.kt} (91%) rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/{service => logic}/jobs/TouchUpKitRemover.kt (71%) rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/{service/users/GroupService.kt => logic/users/GroupLogic.kt} (84%) rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/{service/users/JwtService.kt => logic/users/JwtLogic.kt} (95%) rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/{service/users/UserDetailsService.kt => logic/users/UserDetailsLogic.kt} (85%) rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/{service/users/UserService.kt => logic/users/UserLogic.kt} (92%) rename src/test/kotlin/dev/fyloz/colorrecipesexplorer/{service => logic}/AbstractServiceTest.kt (81%) rename src/test/kotlin/dev/fyloz/colorrecipesexplorer/{service => logic}/AccountsServiceTest.kt (76%) rename src/test/kotlin/dev/fyloz/colorrecipesexplorer/{service/CompanyServiceTest.kt => logic/CompanyLogicTest.kt} (74%) rename src/test/kotlin/dev/fyloz/colorrecipesexplorer/{service/ConfigurationServiceTest.kt => logic/ConfigurationLogicTest.kt} (72%) rename src/test/kotlin/dev/fyloz/colorrecipesexplorer/{service/InventoryServiceTest.kt => logic/InventoryLogicTest.kt} (77%) rename src/test/kotlin/dev/fyloz/colorrecipesexplorer/{service/JwtServiceTest.kt => logic/JwtLogicTest.kt} (90%) rename src/test/kotlin/dev/fyloz/colorrecipesexplorer/{service/MaterialServiceTest.kt => logic/MaterialLogicTest.kt} (79%) rename src/test/kotlin/dev/fyloz/colorrecipesexplorer/{service/MaterialTypeServiceTest.kt => logic/MaterialTypeLogicTest.kt} (77%) rename src/test/kotlin/dev/fyloz/colorrecipesexplorer/{service/MixServiceTest.kt => logic/MixLogicTest.kt} (82%) rename src/test/kotlin/dev/fyloz/colorrecipesexplorer/{service/MixMaterialServiceTest.kt => logic/MixMaterialLogicTest.kt} (86%) rename src/test/kotlin/dev/fyloz/colorrecipesexplorer/{service/MixTypeServiceTest.kt => logic/MixTypeLogicTest.kt} (72%) rename src/test/kotlin/dev/fyloz/colorrecipesexplorer/{service/RecipeServiceTest.kt => logic/RecipeLogicTest.kt} (76%) rename src/test/kotlin/dev/fyloz/colorrecipesexplorer/{service/RecipeStepServiceTest.kt => logic/RecipeStepLogicTest.kt} (87%) rename src/test/kotlin/dev/fyloz/colorrecipesexplorer/{service/TouchUpKitServiceTest.kt => logic/TouchUpKitLogicTest.kt} (90%) rename src/test/kotlin/dev/fyloz/colorrecipesexplorer/{service => logic}/files/DefaultFileCacheTest.kt (99%) rename src/test/kotlin/dev/fyloz/colorrecipesexplorer/{service/files/FileServiceTest.kt => logic/files/FileLogicTest.kt} (98%) rename src/test/kotlin/dev/fyloz/colorrecipesexplorer/{service/files/ResourceFileServiceTest.kt => logic/files/ResourceFileLogicTest.kt} (85%) diff --git a/src/main/java/dev/fyloz/colorrecipesexplorer/service/files/XlsService.java b/src/main/java/dev/fyloz/colorrecipesexplorer/logic/files/XlsService.java similarity index 86% rename from src/main/java/dev/fyloz/colorrecipesexplorer/service/files/XlsService.java rename to src/main/java/dev/fyloz/colorrecipesexplorer/logic/files/XlsService.java index 812bd0a..f9a09e6 100644 --- a/src/main/java/dev/fyloz/colorrecipesexplorer/service/files/XlsService.java +++ b/src/main/java/dev/fyloz/colorrecipesexplorer/logic/files/XlsService.java @@ -1,8 +1,9 @@ -package dev.fyloz.colorrecipesexplorer.service.files; +package dev.fyloz.colorrecipesexplorer.logic.files; +import dev.fyloz.colorrecipesexplorer.logic.RecipeLogic; import dev.fyloz.colorrecipesexplorer.model.Recipe; -import dev.fyloz.colorrecipesexplorer.service.RecipeService; import dev.fyloz.colorrecipesexplorer.xlsx.XlsxExporter; +import mu.KotlinLogging; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Profile; @@ -17,14 +18,12 @@ import java.util.zip.ZipOutputStream; @Service @Profile("!emergency") public class XlsService { - - private final RecipeService recipeService; - private final Logger logger; + private final RecipeLogic recipeService; + private final Logger logger = KotlinLogging.INSTANCE.logger("XlsService"); @Autowired - public XlsService(RecipeService recipeService, Logger logger) { - this.recipeService = recipeService; - this.logger = logger; + public XlsService(RecipeLogic recipeLogic) { + this.recipeService = recipeLogic; } /** diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/DatabaseVersioning.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/DatabaseVersioning.kt index 5f020fb..aacabee 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/DatabaseVersioning.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/DatabaseVersioning.kt @@ -3,9 +3,9 @@ package dev.fyloz.colorrecipesexplorer import dev.fyloz.colorrecipesexplorer.databasemanager.CreDatabase import dev.fyloz.colorrecipesexplorer.databasemanager.databaseContext import dev.fyloz.colorrecipesexplorer.databasemanager.databaseUpdaterProperties -import dev.fyloz.colorrecipesexplorer.model.Configuration +import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic import dev.fyloz.colorrecipesexplorer.model.ConfigurationType -import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationService +import mu.KotlinLogging import org.slf4j.Logger import org.springframework.boot.jdbc.DataSourceBuilder import org.springframework.context.annotation.Bean @@ -23,11 +23,12 @@ val DATABASE_NAME_REGEX = Regex("(\\w+)$") @SpringConfiguration @DependsOn("configurationsInitializer", "configurationService") class DataSourceConfiguration { + private val logger = KotlinLogging.logger {} + @Bean(name = ["dataSource"]) fun customDataSource( - logger: Logger, environment: ConfigurableEnvironment, - configurationService: ConfigurationService + configurationService: ConfigurationLogic ): DataSource { fun getConfiguration(type: ConfigurationType) = if (type.secure) configurationService.getSecure(type) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/ApplicationListeners.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/ApplicationListeners.kt index 2f92bf5..f4bff95 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/ApplicationListeners.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/ApplicationListeners.kt @@ -4,10 +4,10 @@ import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase import dev.fyloz.colorrecipesexplorer.config.initializers.AbstractInitializer import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties import dev.fyloz.colorrecipesexplorer.emergencyMode +import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic import dev.fyloz.colorrecipesexplorer.rest.CRE_PROPERTIES import dev.fyloz.colorrecipesexplorer.restartApplication -import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationService -import org.slf4j.Logger +import mu.KotlinLogging import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent import org.springframework.context.ApplicationListener import org.springframework.context.annotation.Configuration @@ -20,10 +20,11 @@ import kotlin.concurrent.thread @Order(Ordered.HIGHEST_PRECEDENCE) @RequireDatabase class ApplicationReadyListener( - private val configurationService: ConfigurationService, - private val creProperties: CreProperties, - private val logger: Logger + private val configurationLogic: ConfigurationLogic, + private val creProperties: CreProperties ) : AbstractInitializer() { + private val logger = KotlinLogging.logger {} + override fun initialize() { if (emergencyMode) { logger.error("Emergency mode is enabled, default material types will not be created") @@ -40,17 +41,17 @@ class ApplicationReadyListener( } private fun initDatabaseConfigurations() { - configurationService.initializeProperties { !it.file } + configurationLogic.initializeProperties { !it.file } } } @Configuration("configurationsInitializer") class ConfigurationsInitializer( - private val configurationService: ConfigurationService + private val configurationLogic: ConfigurationLogic ) { @PostConstruct fun initializeFileConfigurations() { - configurationService.initializeProperties { it.file } + configurationLogic.initializeProperties { it.file } } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/CreConfiguration.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/CreConfiguration.kt index 13f6ffb..20a08d9 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/CreConfiguration.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/CreConfiguration.kt @@ -2,7 +2,7 @@ package dev.fyloz.colorrecipesexplorer.config import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties import dev.fyloz.colorrecipesexplorer.config.properties.MaterialTypeProperties -import dev.fyloz.colorrecipesexplorer.service.files.CachedFileSystemItem +import dev.fyloz.colorrecipesexplorer.logic.files.CachedFileSystemItem import dev.fyloz.memorycache.ExpiringMemoryCache import dev.fyloz.memorycache.MemoryCache import org.springframework.boot.context.properties.EnableConfigurationProperties @@ -13,6 +13,6 @@ import org.springframework.context.annotation.Configuration @EnableConfigurationProperties(MaterialTypeProperties::class, CreProperties::class) class CreConfiguration(private val creProperties: CreProperties) { @Bean - fun fileCache(): MemoryCache = + fun fileMemoryCache(): MemoryCache = ExpiringMemoryCache(maxAccessCount = creProperties.fileCacheMaxAccessCount) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MaterialTypeInitializer.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MaterialTypeInitializer.kt index d9ea14f..99f0a3f 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MaterialTypeInitializer.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MaterialTypeInitializer.kt @@ -2,16 +2,15 @@ package dev.fyloz.colorrecipesexplorer.config.initializers import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase import dev.fyloz.colorrecipesexplorer.config.properties.MaterialTypeProperties +import dev.fyloz.colorrecipesexplorer.logic.MaterialTypeLogic import dev.fyloz.colorrecipesexplorer.model.MaterialType -import dev.fyloz.colorrecipesexplorer.model.materialType -import dev.fyloz.colorrecipesexplorer.service.MaterialTypeService import mu.KotlinLogging import org.springframework.context.annotation.Configuration @Configuration @RequireDatabase class MaterialTypeInitializer( - private val materialTypeService: MaterialTypeService, + private val materialTypeLogic: MaterialTypeLogic, private val materialTypeProperties: MaterialTypeProperties ) : AbstractInitializer() { private val logger = KotlinLogging.logger {} @@ -24,21 +23,21 @@ class MaterialTypeInitializer( private fun ensureSystemMaterialTypesExists() { val systemTypes = materialTypeProperties.systemTypes.map { it.toMaterialType() } - val oldSystemTypes = materialTypeService.getAllSystemTypes().toMutableSet() + val oldSystemTypes = materialTypeLogic.getAllSystemTypes().toMutableSet() fun saveOrUpdateSystemType(type: MaterialType) { - if (materialTypeService.existsByName(type.name)) { - with(materialTypeService.getByName(type.name)) { + if (materialTypeLogic.existsByName(type.name)) { + with(materialTypeLogic.getByName(type.name)) { if (!this.systemType) { logger.info("Material type '${type.name}' already exists and will be flagged as a system type") - materialTypeService.update(this.copy(systemType = true)) + materialTypeLogic.update(this.copy(systemType = true)) } else { logger.debug("System material type '${type.name}' already exists") } } } else { logger.info("System material type '${type.name}' will be created") - materialTypeService.save(type) + materialTypeLogic.save(type) } } @@ -51,7 +50,7 @@ class MaterialTypeInitializer( // Remove old system types oldSystemTypes.forEach { logger.info("Material type '${it.name}' is not a system type anymore") - materialTypeService.updateSystemType(it.copy(systemType = false)) + materialTypeLogic.updateSystemType(it.copy(systemType = false)) } } } \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MixInitializer.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MixInitializer.kt index 0f0921f..1781762 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MixInitializer.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MixInitializer.kt @@ -1,9 +1,9 @@ package dev.fyloz.colorrecipesexplorer.config.initializers import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase +import dev.fyloz.colorrecipesexplorer.logic.MixLogic import dev.fyloz.colorrecipesexplorer.model.Mix import dev.fyloz.colorrecipesexplorer.model.MixMaterial -import dev.fyloz.colorrecipesexplorer.service.MixService import dev.fyloz.colorrecipesexplorer.utils.merge import mu.KotlinLogging import org.springframework.context.annotation.Configuration @@ -12,7 +12,7 @@ import java.util.* @Configuration @RequireDatabase class MixInitializer( - private val mixService: MixService + private val mixLogic: MixLogic ) : AbstractInitializer() { private val logger = KotlinLogging.logger {} @@ -24,7 +24,7 @@ class MixInitializer( private fun fixAllPositions() { logger.debug("Validating mix materials positions...") - mixService.getAll() + mixLogic.getAll() .filter { mix -> mix.mixMaterials.any { it.position == 0 } } .forEach(this::fixMixPositions) @@ -48,7 +48,7 @@ class MixInitializer( val updatedMixMaterials = mix.mixMaterials.merge(fixedMixMaterials) with(mix.copy(mixMaterials = updatedMixMaterials.toMutableSet())) { - mixService.update(this) + mixLogic.update(this) } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/RecipeInitializer.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/RecipeInitializer.kt index 4d49ab5..1491d76 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/RecipeInitializer.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/RecipeInitializer.kt @@ -1,10 +1,10 @@ package dev.fyloz.colorrecipesexplorer.config.initializers import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase +import dev.fyloz.colorrecipesexplorer.logic.RecipeLogic import dev.fyloz.colorrecipesexplorer.model.Recipe import dev.fyloz.colorrecipesexplorer.model.RecipeGroupInformation import dev.fyloz.colorrecipesexplorer.model.RecipeStep -import dev.fyloz.colorrecipesexplorer.service.RecipeService import dev.fyloz.colorrecipesexplorer.utils.merge import mu.KotlinLogging import org.springframework.context.annotation.Configuration @@ -12,7 +12,7 @@ import org.springframework.context.annotation.Configuration @Configuration @RequireDatabase class RecipeInitializer( - private val recipeService: RecipeService + private val recipeLogic: RecipeLogic ) : AbstractInitializer() { private val logger = KotlinLogging.logger {} @@ -24,7 +24,7 @@ class RecipeInitializer( private fun fixAllPositions() { logger.debug("Validating recipes steps positions...") - recipeService.getAll() + recipeLogic.getAll() .forEach(this::fixRecipePositions) logger.debug("Recipes steps positions are valid!") @@ -39,7 +39,7 @@ class RecipeInitializer( val updatedGroupInformation = recipe.groupsInformation.merge(fixedGroupInformation) with(recipe.copy(groupsInformation = updatedGroupInformation.toMutableSet())) { - recipeService.update(this) + recipeLogic.update(this) } } 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 b7c27a8..c013e2f 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/JwtFilters.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/JwtFilters.kt @@ -3,12 +3,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.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.service.users.JwtService -import dev.fyloz.colorrecipesexplorer.service.users.UserDetailsService import dev.fyloz.colorrecipesexplorer.utils.addCookie import io.jsonwebtoken.ExpiredJwtException import org.springframework.security.authentication.AuthenticationManager @@ -28,7 +28,7 @@ val blacklistedJwtTokens = mutableListOf() // Not working, move to a ca class JwtAuthenticationFilter( private val authManager: AuthenticationManager, - private val jwtService: JwtService, + private val jwtLogic: JwtLogic, private val securityProperties: CreSecurityProperties, private val updateUserLoginTime: (Long) -> Unit ) : UsernamePasswordAuthenticationFilter() { @@ -52,7 +52,7 @@ class JwtAuthenticationFilter( auth: Authentication ) { val userDetails = auth.principal as UserDetails - val token = jwtService.buildJwt(userDetails) + val token = jwtLogic.buildJwt(userDetails) with(userDetails.user) { logger.info("User ${this.id} (${this.firstName} ${this.lastName}) has logged in successfully") @@ -72,9 +72,9 @@ class JwtAuthenticationFilter( } class JwtAuthorizationFilter( - private val jwtService: JwtService, + private val jwtLogic: JwtLogic, authenticationManager: AuthenticationManager, - private val userDetailsService: UserDetailsService + private val userDetailsLogic: UserDetailsLogic ) : BasicAuthenticationFilter(authenticationManager) { override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) { fun tryLoginFromBearer(): Boolean { @@ -109,7 +109,7 @@ class JwtAuthorizationFilter( private fun getAuthentication(token: String): UsernamePasswordAuthenticationToken? { return try { - val user = jwtService.parseJwt(token.replace("Bearer", "")) + val user = jwtLogic.parseJwt(token.replace("Bearer", "")) getAuthenticationToken(user) } catch (_: ExpiredJwtException) { null @@ -120,7 +120,7 @@ class JwtAuthorizationFilter( UsernamePasswordAuthenticationToken(user.id, null, user.permissions.toAuthorities()) private fun getAuthenticationToken(userId: Long): UsernamePasswordAuthenticationToken? = try { - val userDetails = userDetailsService.loadUserById(userId) + val userDetails = userDetailsLogic.loadUserById(userId) UsernamePasswordAuthenticationToken(userDetails.username, null, userDetails.authorities) } catch (_: NotFoundException) { null 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 f2a70e7..ad2c214 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/SecurityConfig.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/security/SecurityConfig.kt @@ -2,12 +2,11 @@ package dev.fyloz.colorrecipesexplorer.config.security import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties 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 dev.fyloz.colorrecipesexplorer.service.users.JwtService -import dev.fyloz.colorrecipesexplorer.service.users.UserDetailsService -import dev.fyloz.colorrecipesexplorer.service.users.UserService -import mu.KLogger import mu.KotlinLogging import org.slf4j.Logger import org.springframework.boot.context.properties.EnableConfigurationProperties @@ -38,8 +37,8 @@ private const val rootUserFirstName = "Root" private const val rootUserLastName = "User" abstract class BaseSecurityConfig( - private val userDetailsService: UserDetailsService, - private val jwtService: JwtService, + private val userDetailsLogic: UserDetailsLogic, + private val jwtLogic: JwtLogic, private val environment: Environment, protected val securityProperties: CreSecurityProperties ) : WebSecurityConfigurerAdapter() { @@ -70,7 +69,7 @@ abstract class BaseSecurityConfig( } override fun configure(authBuilder: AuthenticationManagerBuilder) { - authBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder) + authBuilder.userDetailsService(userDetailsLogic).passwordEncoder(passwordEncoder) } override fun configure(http: HttpSecurity) { @@ -81,13 +80,13 @@ abstract class BaseSecurityConfig( .addFilter( JwtAuthenticationFilter( authenticationManager(), - jwtService, + jwtLogic, securityProperties, this::updateUserLoginTime ) ) .addFilter( - JwtAuthorizationFilter(jwtService, authenticationManager(), userDetailsService) + JwtAuthorizationFilter(jwtLogic, authenticationManager(), userDetailsLogic) ) .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() @@ -118,12 +117,12 @@ abstract class BaseSecurityConfig( @EnableGlobalMethodSecurity(prePostEnabled = true) @EnableConfigurationProperties(CreSecurityProperties::class) class SecurityConfig( - @Lazy userDetailsService: UserDetailsService, - @Lazy private val userService: UserService, - jwtService: JwtService, + @Lazy userDetailsLogic: UserDetailsLogic, + @Lazy private val userLogic: UserLogic, + jwtLogic: JwtLogic, environment: Environment, securityProperties: CreSecurityProperties -) : BaseSecurityConfig(userDetailsService, jwtService, environment, securityProperties) { +) : BaseSecurityConfig(userDetailsLogic, jwtLogic, environment, securityProperties) { override val logger = KotlinLogging.logger {} @PostConstruct @@ -137,7 +136,7 @@ class SecurityConfig( } override fun updateUserLoginTime(userId: Long) { - userService.updateLastLoginTime(userId) + userLogic.updateLastLoginTime(userId) } private fun createRootUser() { @@ -146,8 +145,8 @@ class SecurityConfig( } with(securityProperties.root!!) { - if (!userService.existsById(this.id)) { - userService.save( + if (!userLogic.existsById(this.id)) { + userLogic.save( User( id = this.id, firstName = rootUserFirstName, @@ -166,11 +165,11 @@ class SecurityConfig( @Profile("emergency") @EnableConfigurationProperties(CreSecurityProperties::class) class EmergencySecurityConfig( - userDetailsService: UserDetailsService, - jwtService: JwtService, + userDetailsLogic: UserDetailsLogic, + jwtLogic: JwtLogic, environment: Environment, securityProperties: CreSecurityProperties -) : BaseSecurityConfig(userDetailsService, jwtService, environment, securityProperties) { +) : BaseSecurityConfig(userDetailsLogic, jwtLogic, environment, securityProperties) { override val logger = KotlinLogging.logger {} init { diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogic.kt similarity index 85% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogic.kt index 3e8e0a9..0bb2e2b 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogic.kt @@ -1,27 +1,27 @@ -package dev.fyloz.colorrecipesexplorer.service +package dev.fyloz.colorrecipesexplorer.logic +import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.repository.CompanyRepository import org.springframework.context.annotation.Lazy -import org.springframework.context.annotation.Profile import org.springframework.stereotype.Service -interface CompanyService : +interface CompanyLogic : ExternalNamedModelService { /** Checks if the given [company] is used by one or more recipes. */ fun isLinkedToRecipes(company: Company): Boolean } @Service -@Profile("!emergency") -class CompanyServiceImpl( +@RequireDatabase +class DefaultCompanyLogic( companyRepository: CompanyRepository, - @Lazy val recipeService: RecipeService + @Lazy val recipeLogic: RecipeLogic ) : AbstractExternalNamedModelService( companyRepository ), - CompanyService { + CompanyLogic { override fun idNotFoundException(id: Long) = companyIdNotFoundException(id) override fun idAlreadyExistsException(id: Long) = companyIdAlreadyExistsException(id) override fun nameNotFoundException(name: String) = companyNameNotFoundException(name) @@ -29,7 +29,7 @@ class CompanyServiceImpl( override fun Company.toOutput() = this - override fun isLinkedToRecipes(company: Company): Boolean = recipeService.existsByCompany(company) + override fun isLinkedToRecipes(company: Company): Boolean = recipeLogic.existsByCompany(company) override fun update(entity: CompanyUpdateDto): Company { // Lazy loaded to prevent checking the database when not necessary diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/InventoryService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogic.kt similarity index 87% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/InventoryService.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogic.kt index 77f10cb..ea7bafe 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/InventoryService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogic.kt @@ -1,14 +1,14 @@ -package dev.fyloz.colorrecipesexplorer.service +package dev.fyloz.colorrecipesexplorer.logic +import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase import dev.fyloz.colorrecipesexplorer.exception.RestException import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.utils.mapMayThrow -import org.springframework.context.annotation.Profile import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import javax.transaction.Transactional -interface InventoryService { +interface InventoryLogic { /** Adds each given [MaterialQuantityDto] to the inventory and returns the updated quantities. */ fun add(materialQuantities: Collection): Collection @@ -26,11 +26,11 @@ interface InventoryService { } @Service -@Profile("!emergency") -class InventoryServiceImpl( - private val materialService: MaterialService, - private val mixService: MixService -) : InventoryService { +@RequireDatabase +class DefaultInventoryLogic( + private val materialLogic: MaterialLogic, + private val mixLogic: MixLogic +) : InventoryLogic { @Transactional override fun add(materialQuantities: Collection) = materialQuantities.map { @@ -38,14 +38,14 @@ class InventoryServiceImpl( } override fun add(materialQuantity: MaterialQuantityDto) = - materialService.updateQuantity( - materialService.getById(materialQuantity.material), + materialLogic.updateQuantity( + materialLogic.getById(materialQuantity.material), materialQuantity.quantity ) @Transactional override fun deductMix(mixRatio: MixDeductDto): Collection { - val mix = mixService.getById(mixRatio.id) + val mix = mixLogic.getById(mixRatio.id) val firstMixMaterial = mix.mixMaterials.first() val adjustedFirstMaterialQuantity = firstMixMaterial.quantity * mixRatio.ratio @@ -80,9 +80,9 @@ class InventoryServiceImpl( } override fun deduct(materialQuantity: MaterialQuantityDto): Float = - with(materialService.getById(materialQuantity.material)) { + with(materialLogic.getById(materialQuantity.material)) { if (this.inventoryQuantity >= materialQuantity.quantity) { - materialService.updateQuantity(this, -materialQuantity.quantity) + materialLogic.updateQuantity(this, -materialQuantity.quantity) } else { throw NotEnoughInventoryException(materialQuantity.quantity, this) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogic.kt similarity index 87% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogic.kt index 327d6e2..5f877cb 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogic.kt @@ -1,18 +1,18 @@ -package dev.fyloz.colorrecipesexplorer.service +package dev.fyloz.colorrecipesexplorer.logic +import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase +import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic +import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.repository.MaterialRepository import dev.fyloz.colorrecipesexplorer.rest.FILE_CONTROLLER_PATH -import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationService -import dev.fyloz.colorrecipesexplorer.service.files.WriteableFileService import io.jsonwebtoken.lang.Assert import org.springframework.context.annotation.Lazy -import org.springframework.context.annotation.Profile import org.springframework.stereotype.Service import java.net.URLEncoder import java.nio.charset.StandardCharsets -interface MaterialService : +interface MaterialLogic : ExternalNamedModelService { /** Checks if a material with the given [materialType] exists. */ fun existsByMaterialType(materialType: MaterialType): Boolean @@ -34,19 +34,19 @@ interface MaterialService : } @Service -@Profile("!emergency") -class MaterialServiceImpl( +@RequireDatabase +class DefaultMaterialLogic( materialRepository: MaterialRepository, - val recipeService: RecipeService, - val mixService: MixService, - @Lazy val materialTypeService: MaterialTypeService, - val fileService: WriteableFileService, - val configService: ConfigurationService + val recipeLogic: RecipeLogic, + val mixLogic: MixLogic, + @Lazy val materialTypeLogic: MaterialTypeLogic, + val fileService: WriteableFileLogic, + val configService: ConfigurationLogic ) : AbstractExternalNamedModelService( materialRepository ), - MaterialService { + MaterialLogic { override fun idNotFoundException(id: Long) = materialIdNotFoundException(id) override fun idAlreadyExistsException(id: Long) = materialIdAlreadyExistsException(id) override fun nameNotFoundException(name: String) = materialNameNotFoundException(name) @@ -80,7 +80,7 @@ class MaterialServiceImpl( material( name = entity.name, inventoryQuantity = entity.inventoryQuantity, - materialType = materialTypeService.getById(materialTypeId), + materialType = materialTypeLogic.getById(materialTypeId), isMixType = false ) }).apply { @@ -102,7 +102,7 @@ class MaterialServiceImpl( name = if (name != null && name.isNotBlank()) name else persistedMaterial.name, inventoryQuantity = if (inventoryQuantity != null && inventoryQuantity != Float.MIN_VALUE) inventoryQuantity else persistedMaterial.inventoryQuantity, isMixType = persistedMaterial.isMixType, - materialType = if (materialTypeId != null) materialTypeService.getById(materialTypeId) else persistedMaterial.materialType + materialType = if (materialTypeId != null) materialTypeLogic.getById(materialTypeId) else persistedMaterial.materialType ) }).apply { if (entity.simdutFile != null && !entity.simdutFile.isEmpty) fileService.write( @@ -120,13 +120,13 @@ class MaterialServiceImpl( } override fun getAllForMixCreation(recipeId: Long): Collection { - val recipesMixTypes = recipeService.getById(recipeId).mixTypes + val recipesMixTypes = recipeLogic.getById(recipeId).mixTypes return getAllForOutput() .filter { !it.isMixType || recipesMixTypes.any { mixType -> mixType.material.id == it.id } } } override fun getAllForMixUpdate(mixId: Long): Collection { - val mix = mixService.getById(mixId) + val mix = mixLogic.getById(mixId) val recipesMixTypes = mix.recipe.mixTypes return getAllForOutput() .filter { !it.isMixType || recipesMixTypes.any { mixType -> mixType.material.id == it.id } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogic.kt similarity index 91% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeService.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogic.kt index 9890fa8..b3cb0eb 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogic.kt @@ -1,12 +1,12 @@ -package dev.fyloz.colorrecipesexplorer.service +package dev.fyloz.colorrecipesexplorer.logic +import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.model.validation.isNotNullAndNotBlank import dev.fyloz.colorrecipesexplorer.repository.MaterialTypeRepository -import org.springframework.context.annotation.Profile import org.springframework.stereotype.Service -interface MaterialTypeService : +interface MaterialTypeLogic : ExternalNamedModelService { /** Checks if a material type with the given [prefix] exists. */ fun existsByPrefix(prefix: String): Boolean @@ -25,11 +25,11 @@ interface MaterialTypeService : } @Service -@Profile("!emergency") -class MaterialTypeServiceImpl(repository: MaterialTypeRepository, private val materialService: MaterialService) : +@RequireDatabase +class DefaultMaterialTypeLogic(repository: MaterialTypeRepository, private val materialLogic: MaterialLogic) : AbstractExternalNamedModelService( repository - ), MaterialTypeService { + ), MaterialTypeLogic { override fun idNotFoundException(id: Long) = materialTypeIdNotFoundException(id) override fun idAlreadyExistsException(id: Long) = materialIdAlreadyExistsException(id) override fun nameNotFoundException(name: String) = materialTypeNameNotFoundException(name) @@ -39,7 +39,7 @@ class MaterialTypeServiceImpl(repository: MaterialTypeRepository, private val ma override fun existsByPrefix(prefix: String): Boolean = repository.existsByPrefix(prefix) override fun isUsedByMaterial(materialType: MaterialType): Boolean = - materialService.existsByMaterialType(materialType) + materialLogic.existsByMaterialType(materialType) override fun getAllSystemTypes(): Collection = repository.findAllBySystemTypeIs(true) override fun getAllNonSystemType(): Collection = repository.findAllBySystemTypeIs(false) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt similarity index 69% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt index 72c0009..8de0da0 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt @@ -1,14 +1,14 @@ -package dev.fyloz.colorrecipesexplorer.service +package dev.fyloz.colorrecipesexplorer.logic +import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.repository.MixRepository import dev.fyloz.colorrecipesexplorer.utils.setAll import org.springframework.context.annotation.Lazy -import org.springframework.context.annotation.Profile import org.springframework.stereotype.Service import javax.transaction.Transactional -interface MixService : ExternalModelService { +interface MixLogic : ExternalModelService { /** Gets all mixes with the given [mixType]. */ fun getAllByMixType(mixType: MixType): Collection @@ -23,15 +23,15 @@ interface MixService : ExternalModelService(mixRepository), - MixService { + MixLogic { override fun idNotFoundException(id: Long) = mixIdNotFoundException(id) override fun idAlreadyExistsException(id: Long) = mixIdAlreadyExistsException(id) @@ -43,7 +43,7 @@ class MixServiceImpl( this.location, this.mixType, this.mixMaterials.map { - with(mixMaterialService) { + with(mixMaterialLogic) { return@with it.toOutput() } }.toSet() @@ -51,17 +51,17 @@ class MixServiceImpl( @Transactional override fun save(entity: MixSaveDto): Mix { - val recipe = recipeService.getById(entity.recipeId) - val materialType = materialTypeService.getById(entity.materialTypeId) - val mixType = mixTypeService.getOrCreateForNameAndMaterialType(entity.name, materialType) + val recipe = recipeLogic.getById(entity.recipeId) + val materialType = materialTypeLogic.getById(entity.materialTypeId) + val mixType = mixTypeLogic.getOrCreateForNameAndMaterialType(entity.name, materialType) - val mixMaterials = if (entity.mixMaterials != null) mixMaterialService.create(entity.mixMaterials) else setOf() - mixMaterialService.validateMixMaterials(mixMaterials) + val mixMaterials = if (entity.mixMaterials != null) mixMaterialLogic.create(entity.mixMaterials) else setOf() + mixMaterialLogic.validateMixMaterials(mixMaterials) var mix = mix(recipe = recipe, mixType = mixType, mixMaterials = mixMaterials.toMutableSet()) mix = save(mix) - recipeService.addMix(recipe, mix) + recipeLogic.addMix(recipe, mix) return mix } @@ -72,18 +72,18 @@ class MixServiceImpl( if (entity.name != null || entity.materialTypeId != null) { val name = entity.name ?: mix.mixType.name val materialType = if (entity.materialTypeId != null) - materialTypeService.getById(entity.materialTypeId) + materialTypeLogic.getById(entity.materialTypeId) else mix.mixType.material.materialType!! mix.mixType = if (mixTypeIsShared(mix.mixType)) { - mixTypeService.saveForNameAndMaterialType(name, materialType) + mixTypeLogic.saveForNameAndMaterialType(name, materialType) } else { - mixTypeService.updateForNameAndMaterialType(mix.mixType, name, materialType) + mixTypeLogic.updateForNameAndMaterialType(mix.mixType, name, materialType) } } if (entity.mixMaterials != null) { - mix.mixMaterials.setAll(mixMaterialService.create(entity.mixMaterials!!).toMutableSet()) + mix.mixMaterials.setAll(mixMaterialLogic.create(entity.mixMaterials!!).toMutableSet()) } return update(mix) } @@ -99,7 +99,7 @@ class MixServiceImpl( @Transactional override fun delete(entity: Mix) { if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteMixException(entity) - recipeService.removeMix(entity) + recipeLogic.removeMix(entity) super.delete(entity) } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt similarity index 93% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialService.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt index e977852..3aea9df 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt @@ -1,4 +1,4 @@ -package dev.fyloz.colorrecipesexplorer.service +package dev.fyloz.colorrecipesexplorer.logic import dev.fyloz.colorrecipesexplorer.exception.RestException import dev.fyloz.colorrecipesexplorer.model.* @@ -10,7 +10,7 @@ import org.springframework.context.annotation.Profile import org.springframework.http.HttpStatus import org.springframework.stereotype.Service -interface MixMaterialService : ModelService { +interface MixMaterialLogic : ModelService { /** Checks if one or more mix materials have the given [material]. */ fun existsByMaterial(material: Material): Boolean @@ -35,16 +35,16 @@ interface MixMaterialService : ModelService @Service @Profile("!emergency") -class MixMaterialServiceImpl( +class DefaultMixMaterialLogic( mixMaterialRepository: MixMaterialRepository, - @Lazy val materialService: MaterialService -) : AbstractModelService(mixMaterialRepository), MixMaterialService { + @Lazy val materialLogic: MaterialLogic +) : AbstractModelService(mixMaterialRepository), MixMaterialLogic { override fun idNotFoundException(id: Long) = mixMaterialIdNotFoundException(id) override fun idAlreadyExistsException(id: Long) = mixMaterialIdAlreadyExistsException(id) override fun MixMaterial.toOutput() = MixMaterialOutputDto( this.id!!, - with(materialService) { this@toOutput.material.toOutput() }, + with(materialLogic) { this@toOutput.material.toOutput() }, this.quantity, this.position ) @@ -55,7 +55,7 @@ class MixMaterialServiceImpl( override fun create(mixMaterial: MixMaterialDto): MixMaterial = mixMaterial( - material = materialService.getById(mixMaterial.materialId), + material = materialLogic.getById(mixMaterial.materialId), quantity = mixMaterial.quantity, position = mixMaterial.position ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt similarity index 89% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeService.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt index 92327d8..bbc241a 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt @@ -1,12 +1,12 @@ -package dev.fyloz.colorrecipesexplorer.service +package dev.fyloz.colorrecipesexplorer.logic +import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.repository.MixTypeRepository import org.springframework.context.annotation.Lazy -import org.springframework.context.annotation.Profile import org.springframework.stereotype.Service -interface MixTypeService : NamedModelService { +interface MixTypeLogic : NamedModelService { /** Checks if a [MixType] with the given [name] and [materialType] exists. */ fun existsByNameAndMaterialType(name: String, materialType: MaterialType): Boolean @@ -27,13 +27,12 @@ interface MixTypeService : NamedModelService { } @Service -@Profile("!emergency") -class MixTypeServiceImpl( - mixTypeRepository: MixTypeRepository, - @Lazy val materialService: MaterialService, - @Lazy val mixService: MixService +@RequireDatabase +class DefaultMixTypeLogic( + mixTypeRepository: MixTypeRepository, + @Lazy val materialLogic: MaterialLogic ) : - AbstractNamedModelService(mixTypeRepository), MixTypeService { + AbstractNamedModelService(mixTypeRepository), MixTypeLogic { override fun idNotFoundException(id: Long) = mixTypeIdNotFoundException(id) override fun idAlreadyExistsException(id: Long) = mixTypeIdAlreadyExistsException(id) override fun nameNotFoundException(name: String) = mixTypeNameNotFoundException(name) @@ -56,7 +55,7 @@ class MixTypeServiceImpl( saveForNameAndMaterialType(name, materialType) override fun save(entity: MixType): MixType { - if (materialService.existsByName(entity.name)) + if (materialLogic.existsByName(entity.name)) throw materialNameAlreadyExistsException(entity.name) return super.save(entity) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt similarity index 89% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt index d900aef..ddc8cb6 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt @@ -1,13 +1,13 @@ -package dev.fyloz.colorrecipesexplorer.service +package dev.fyloz.colorrecipesexplorer.logic import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase +import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic +import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic +import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.model.account.Group import dev.fyloz.colorrecipesexplorer.model.validation.or import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository -import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationService -import dev.fyloz.colorrecipesexplorer.service.files.WriteableFileService -import dev.fyloz.colorrecipesexplorer.service.users.GroupService import dev.fyloz.colorrecipesexplorer.utils.setAll import org.springframework.context.annotation.Lazy import org.springframework.stereotype.Service @@ -16,7 +16,7 @@ import java.time.LocalDate import java.time.Period import javax.transaction.Transactional -interface RecipeService : +interface RecipeLogic : ExternalModelService { /** Checks if one or more recipes have the given [company]. */ fun existsByCompany(company: Company): Boolean @@ -45,19 +45,19 @@ interface RecipeService : @Service @RequireDatabase -class RecipeServiceImpl( +class DefaultRecipeLogic( recipeRepository: RecipeRepository, - val companyService: CompanyService, - val mixService: MixService, - val recipeStepService: RecipeStepService, - @Lazy val groupService: GroupService, - val recipeImageService: RecipeImageService, - val configService: ConfigurationService + val companyLogic: CompanyLogic, + val mixLogic: MixLogic, + val recipeStepLogic: RecipeStepLogic, + @Lazy val groupLogic: GroupLogic, + val recipeImageLogic: RecipeImageLogic, + val configService: ConfigurationLogic ) : AbstractExternalModelService( recipeRepository ), - RecipeService { + RecipeLogic { override fun idNotFoundException(id: Long) = recipeIdNotFoundException(id) override fun idAlreadyExistsException(id: Long) = recipeIdAlreadyExistsException(id) @@ -73,12 +73,12 @@ class RecipeServiceImpl( this.remark, this.company, this.mixes.map { - with(mixService) { + with(mixLogic) { it.toOutput() } }.toSet(), this.groupsInformation, - recipeImageService.getAllImages(this) + recipeImageLogic.getAllImages(this) .map { this.imageUrl(configService.getContent(ConfigurationType.INSTANCE_URL), it) } .toSet() ) @@ -96,7 +96,7 @@ class RecipeServiceImpl( override fun getAllByCompany(company: Company) = repository.findAllByCompany(company) override fun save(entity: RecipeSaveDto): Recipe { - val company = companyService.getById(entity.companyId) + val company = companyLogic.getById(entity.companyId) if (existsByNameAndCompany(entity.name, company)) { throw recipeNameAlreadyExistsForCompanyException(entity.name, company) @@ -160,12 +160,12 @@ class RecipeServiceImpl( this.steps = it.steps.toMutableSet() } } ?: recipeGroupInformation( - group = groupService.getById(it.groupId), + group = groupLogic.getById(it.groupId), steps = it.steps.toMutableSet() ) updatedGroupsInformation.add(updatedGroupInformation) - recipeStepService.validateGroupInformationSteps(updatedGroupInformation) + recipeStepLogic.validateGroupInformationSteps(updatedGroupInformation) } } @@ -192,7 +192,7 @@ class RecipeServiceImpl( } if (publicDataDto.mixesLocation != null) { - mixService.updateLocations(publicDataDto.mixesLocation) + mixLogic.updateLocations(publicDataDto.mixesLocation) } } @@ -203,7 +203,7 @@ class RecipeServiceImpl( update(mix.recipe.apply { mixes.remove(mix) }) } -interface RecipeImageService { +interface RecipeImageLogic { /** Gets the name of every images associated to the recipe with the given [recipe]. */ fun getAllImages(recipe: Recipe): Set @@ -219,9 +219,9 @@ const val RECIPE_IMAGE_EXTENSION = ".jpg" @Service @RequireDatabase -class RecipeImageServiceImpl( - val fileService: WriteableFileService -) : RecipeImageService { +class DefaultRecipeImageLogic( + val fileService: WriteableFileLogic +) : RecipeImageLogic { override fun getAllImages(recipe: Recipe) = fileService.listDirectoryFiles(recipe.imagesDirectoryPath) .map { it.name } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt similarity index 87% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt index 0bde45d..3bc0d2c 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt @@ -1,16 +1,19 @@ -package dev.fyloz.colorrecipesexplorer.service +package dev.fyloz.colorrecipesexplorer.logic +import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase import dev.fyloz.colorrecipesexplorer.exception.RestException -import dev.fyloz.colorrecipesexplorer.model.* +import dev.fyloz.colorrecipesexplorer.model.RecipeGroupInformation +import dev.fyloz.colorrecipesexplorer.model.RecipeStep import dev.fyloz.colorrecipesexplorer.model.account.Group +import dev.fyloz.colorrecipesexplorer.model.recipeStepIdAlreadyExistsException +import dev.fyloz.colorrecipesexplorer.model.recipeStepIdNotFoundException import dev.fyloz.colorrecipesexplorer.repository.RecipeStepRepository import dev.fyloz.colorrecipesexplorer.utils.findDuplicated import dev.fyloz.colorrecipesexplorer.utils.hasGaps -import org.springframework.context.annotation.Profile import org.springframework.http.HttpStatus import org.springframework.stereotype.Service -interface RecipeStepService : ModelService { +interface RecipeStepLogic : ModelService { /** Validates the steps of the given [groupInformation], according to the criteria of [validateSteps]. */ fun validateGroupInformationSteps(groupInformation: RecipeGroupInformation) @@ -23,10 +26,10 @@ interface RecipeStepService : ModelService { } @Service -@Profile("!emergency") -class RecipeStepServiceImpl(recipeStepRepository: RecipeStepRepository) : +@RequireDatabase +class DefaultRecipeStepLogic(recipeStepRepository: RecipeStepRepository) : AbstractModelService(recipeStepRepository), - RecipeStepService { + RecipeStepLogic { override fun idNotFoundException(id: Long) = recipeStepIdNotFoundException(id) override fun idAlreadyExistsException(id: Long) = recipeStepIdAlreadyExistsException(id) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/Service.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Service.kt similarity index 99% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/Service.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Service.kt index 902a32c..314d93b 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/Service.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Service.kt @@ -1,4 +1,4 @@ -package dev.fyloz.colorrecipesexplorer.service +package dev.fyloz.colorrecipesexplorer.logic import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/TouchUpKitService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/TouchUpKitLogic.kt similarity index 90% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/TouchUpKitService.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/TouchUpKitLogic.kt index a6bbc1e..7ae0f8f 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/TouchUpKitService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/TouchUpKitLogic.kt @@ -1,14 +1,13 @@ -package dev.fyloz.colorrecipesexplorer.service +package dev.fyloz.colorrecipesexplorer.logic +import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase +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.touchupkit.* import dev.fyloz.colorrecipesexplorer.repository.TouchUpKitRepository import dev.fyloz.colorrecipesexplorer.rest.TOUCH_UP_KIT_CONTROLLER_PATH -import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationService -import dev.fyloz.colorrecipesexplorer.service.files.FileService -import dev.fyloz.colorrecipesexplorer.service.files.WriteableFileService import dev.fyloz.colorrecipesexplorer.utils.* -import org.springframework.context.annotation.Profile import org.springframework.core.io.ByteArrayResource import org.springframework.core.io.Resource import org.springframework.stereotype.Service @@ -20,7 +19,7 @@ private const val TOUCH_UP_KIT_FILES_PATH = "pdf/touchupkits" const val TOUCH_UP_TEXT_FR = "KIT DE RETOUCHE" const val TOUCH_UP_TEXT_EN = "TOUCH UP KIT" -interface TouchUpKitService : +interface TouchUpKitLogic : ExternalModelService { fun isExpired(touchUpKit: TouchUpKit): Boolean @@ -42,14 +41,14 @@ interface TouchUpKitService : } @Service -@Profile("!emergency") -class TouchUpKitServiceImpl( - private val fileService: WriteableFileService, - private val configService: ConfigurationService, +@RequireDatabase +class DefaultTouchUpKitLogic( + private val fileService: WriteableFileLogic, + private val configService: ConfigurationLogic, touchUpKitRepository: TouchUpKitRepository ) : AbstractExternalModelService( touchUpKitRepository -), TouchUpKitService { +), TouchUpKitLogic { private val cacheGeneratedFiles by lazy { configService.getContent(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) == true.toString() } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/config/ConfigurationService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/config/ConfigurationLogic.kt similarity index 94% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/config/ConfigurationService.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/config/ConfigurationLogic.kt index f6db17e..14d3de2 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/config/ConfigurationService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/config/ConfigurationLogic.kt @@ -1,19 +1,19 @@ -package dev.fyloz.colorrecipesexplorer.service.config +package dev.fyloz.colorrecipesexplorer.logic.config import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties +import dev.fyloz.colorrecipesexplorer.logic.files.ResourceFileLogic +import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.service.files.ResourceFileService -import dev.fyloz.colorrecipesexplorer.service.files.WriteableFileService import dev.fyloz.colorrecipesexplorer.utils.decrypt import dev.fyloz.colorrecipesexplorer.utils.encrypt -import org.slf4j.Logger +import mu.KotlinLogging import org.springframework.context.annotation.Lazy import org.springframework.core.io.Resource import org.springframework.security.crypto.keygen.KeyGenerators import org.springframework.stereotype.Service import org.springframework.web.multipart.MultipartFile -interface ConfigurationService { +interface ConfigurationLogic { /** Gets all set configurations. */ fun getAll(): List @@ -73,13 +73,13 @@ const val CONFIGURATION_ICON_FILE_PATH = "images/icon" const val CONFIGURATION_FORMATTED_LIST_DELIMITER = ';' @Service("configurationService") -class ConfigurationServiceImpl( - @Lazy private val fileService: WriteableFileService, - private val resourceFileService: ResourceFileService, +class DefaultConfigurationLogic( + @Lazy private val fileService: WriteableFileLogic, + private val resourceFileService: ResourceFileLogic, private val configurationSource: ConfigurationSource, - private val securityProperties: CreSecurityProperties, - private val logger: Logger -) : ConfigurationService { + private val securityProperties: CreSecurityProperties +) : ConfigurationLogic { + private val logger = KotlinLogging.logger { } private val saltConfigurationType = ConfigurationType.GENERATED_ENCRYPTION_SALT private val encryptionSalt by lazy { securityProperties.configSalt ?: getGeneratedSalt() diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/config/ConfigurationSource.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/config/ConfigurationSource.kt similarity index 97% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/config/ConfigurationSource.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/config/ConfigurationSource.kt index f7db611..0fbe70a 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/config/ConfigurationSource.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/config/ConfigurationSource.kt @@ -1,4 +1,4 @@ -package dev.fyloz.colorrecipesexplorer.service.config +package dev.fyloz.colorrecipesexplorer.logic.config import dev.fyloz.colorrecipesexplorer.JavaFile import dev.fyloz.colorrecipesexplorer.SUPPORTED_DATABASE_VERSION @@ -11,7 +11,7 @@ import dev.fyloz.colorrecipesexplorer.model.configuration import dev.fyloz.colorrecipesexplorer.repository.ConfigurationRepository import dev.fyloz.colorrecipesexplorer.utils.create import dev.fyloz.colorrecipesexplorer.utils.excludeAll -import org.slf4j.Logger +import mu.KotlinLogging import org.springframework.boot.info.BuildProperties import org.springframework.context.annotation.Lazy import org.springframework.data.repository.findByIdOrNull @@ -36,9 +36,9 @@ interface ConfigurationSource { class CompositeConfigurationSource( @Lazy private val configurationRepository: ConfigurationRepository, private val properties: CreProperties, - private val buildInfo: BuildProperties, - private val logger: Logger + private val buildInfo: BuildProperties ) : ConfigurationSource { + private val logger = KotlinLogging.logger {} private val repository by lazy { RepositoryConfigurationSource(configurationRepository) } private val file by lazy { FileConfigurationSource("${properties.configDirectory}/$CONFIGURATION_FILE_PATH") diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileCache.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/files/FileCache.kt similarity index 99% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileCache.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/files/FileCache.kt index 1961138..c666abe 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileCache.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/files/FileCache.kt @@ -1,4 +1,4 @@ -package dev.fyloz.colorrecipesexplorer.service.files +package dev.fyloz.colorrecipesexplorer.logic.files import dev.fyloz.colorrecipesexplorer.JavaFile import dev.fyloz.colorrecipesexplorer.utils.File diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/files/FileLogic.kt similarity index 97% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileService.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/files/FileLogic.kt index b2615b2..060d651 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/files/FileLogic.kt @@ -1,4 +1,4 @@ -package dev.fyloz.colorrecipesexplorer.service.files +package dev.fyloz.colorrecipesexplorer.logic.files import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties import dev.fyloz.colorrecipesexplorer.exception.RestException @@ -21,7 +21,7 @@ val BANNED_FILE_PATH_SHARDS = setOf( "//" ) -interface FileService { +interface FileLogic { /** Checks if the file at the given [path] exists. */ fun exists(path: String): Boolean @@ -35,7 +35,7 @@ interface FileService { fun fullPath(path: String): FilePath } -interface WriteableFileService : FileService { +interface WriteableFileLogic : FileLogic { /** Creates a file at the given [path]. */ fun create(path: String) @@ -56,10 +56,10 @@ interface WriteableFileService : FileService { } @Service -class FileServiceImpl( +class DefaultFileLogic( private val fileCache: FileCache, private val creProperties: CreProperties -) : WriteableFileService { +) : WriteableFileLogic { private val logger = KotlinLogging.logger {} override fun exists(path: String): Boolean { diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/ResourceFileService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/files/ResourceFileLogic.kt similarity index 91% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/ResourceFileService.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/files/ResourceFileLogic.kt index 1502233..765070b 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/ResourceFileService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/files/ResourceFileLogic.kt @@ -1,4 +1,4 @@ -package dev.fyloz.colorrecipesexplorer.service.files +package dev.fyloz.colorrecipesexplorer.logic.files import dev.fyloz.colorrecipesexplorer.utils.FilePath import org.springframework.core.io.Resource @@ -6,9 +6,9 @@ import org.springframework.core.io.ResourceLoader import org.springframework.stereotype.Service @Service -class ResourceFileService( +class ResourceFileLogic( private val resourceLoader: ResourceLoader -) : FileService { +) : FileLogic { override fun exists(path: String) = fullPath(path).resource.exists() diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/jobs/TouchUpKitRemover.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/jobs/TouchUpKitRemover.kt similarity index 71% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/jobs/TouchUpKitRemover.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/jobs/TouchUpKitRemover.kt index 5cde5bf..d14be17 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/jobs/TouchUpKitRemover.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/jobs/TouchUpKitRemover.kt @@ -1,6 +1,6 @@ -package dev.fyloz.colorrecipesexplorer.service.jobs +package dev.fyloz.colorrecipesexplorer.logic.jobs -import dev.fyloz.colorrecipesexplorer.service.TouchUpKitService +import dev.fyloz.colorrecipesexplorer.logic.TouchUpKitLogic import mu.KotlinLogging import org.springframework.context.annotation.Profile import org.springframework.scheduling.annotation.Scheduled @@ -9,7 +9,7 @@ import org.springframework.stereotype.Component @Component @Profile("!emergency") class TouchUpKitRemover( - private val touchUpKitService: TouchUpKitService + private val touchUpKitLogic: TouchUpKitLogic ) { private val logger = KotlinLogging.logger {} @@ -20,10 +20,10 @@ class TouchUpKitRemover( } private fun removeExpiredKits() { - with(touchUpKitService.getAll().filter(touchUpKitService::isExpired)) { + with(touchUpKitLogic.getAll().filter(touchUpKitLogic::isExpired)) { this.forEach { logger.debug("Removed expired touch up kit ${it.id} (${it.project} ${it.buggy})") - touchUpKitService.delete(it) + touchUpKitLogic.delete(it) } logger.info("Removed ${this.size} expired touch up kits") } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/users/GroupService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/GroupLogic.kt similarity index 84% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/users/GroupService.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/GroupLogic.kt index 62ac1f0..b22c006 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/users/GroupService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/GroupLogic.kt @@ -1,10 +1,10 @@ -package dev.fyloz.colorrecipesexplorer.service.users +package dev.fyloz.colorrecipesexplorer.logic.users 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 dev.fyloz.colorrecipesexplorer.service.AbstractExternalNamedModelService -import dev.fyloz.colorrecipesexplorer.service.ExternalNamedModelService import org.springframework.context.annotation.Profile import org.springframework.stereotype.Service import org.springframework.web.util.WebUtils @@ -14,7 +14,7 @@ import javax.transaction.Transactional const val defaultGroupCookieMaxAge = 10 * 365 * 24 * 60 * 60 // 10 ans -interface GroupService : +interface GroupLogic : ExternalNamedModelService { /** Gets all the users of the group with the given [id]. */ fun getUsersForGroup(id: Long): Collection @@ -28,13 +28,13 @@ interface GroupService : @Service @Profile("!emergency") -class GroupServiceImpl( - private val userService: UserService, +class DefaultGroupLogic( + private val userLogic: UserLogic, groupRepository: GroupRepository ) : AbstractExternalNamedModelService( groupRepository ), - GroupService { + GroupLogic { override fun idNotFoundException(id: Long) = groupIdNotFoundException(id) override fun idAlreadyExistsException(id: Long) = groupIdAlreadyExistsException(id) override fun nameNotFoundException(name: String) = groupNameNotFoundException(name) @@ -49,12 +49,12 @@ class GroupServiceImpl( override fun existsByName(name: String): Boolean = repository.existsByName(name) override fun getUsersForGroup(id: Long): Collection = - userService.getByGroup(getById(id)) + userLogic.getByGroup(getById(id)) @Transactional override fun save(entity: Group): Group { return super.save(entity).apply { - userService.saveDefaultGroupUser(this) + userLogic.saveDefaultGroupUser(this) } } @@ -71,14 +71,14 @@ class GroupServiceImpl( @Transactional override fun delete(entity: Group) { - userService.delete(userService.getDefaultGroupUser(entity)) + userLogic.delete(userLogic.getDefaultGroupUser(entity)) super.delete(entity) } override fun getRequestDefaultGroup(request: HttpServletRequest): Group { val defaultGroupCookie = WebUtils.getCookie(request, defaultGroupCookieName) ?: throw NoDefaultGroupException() - val defaultGroupUser = userService.getById( + val defaultGroupUser = userLogic.getById( defaultGroupCookie.value.toLong(), ignoreDefaultGroupUsers = false, ignoreSystemUsers = true @@ -88,7 +88,7 @@ class GroupServiceImpl( override fun setResponseDefaultGroup(groupId: Long, response: HttpServletResponse) { val group = getById(groupId) - val defaultGroupUser = userService.getDefaultGroupUser(group) + val defaultGroupUser = userLogic.getDefaultGroupUser(group) response.addHeader( "Set-Cookie", "$defaultGroupCookieName=${defaultGroupUser.id}; Max-Age=$defaultGroupCookieMaxAge; Path=/api; HttpOnly; Secure; SameSite=strict" diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/users/JwtService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/JwtLogic.kt similarity index 95% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/users/JwtService.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/JwtLogic.kt index 282d6ba..e7ff36e 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/users/JwtService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/JwtLogic.kt @@ -1,4 +1,4 @@ -package dev.fyloz.colorrecipesexplorer.service.users +package dev.fyloz.colorrecipesexplorer.logic.users import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.readValue @@ -18,7 +18,7 @@ import java.util.* const val jwtClaimUser = "user" -interface JwtService { +interface JwtLogic { /** Build a JWT token for the given [userDetails]. */ fun buildJwt(userDetails: UserDetails): String @@ -30,10 +30,10 @@ interface JwtService { } @Service -class JwtServiceImpl( +class DefaultJwtLogic( val objectMapper: ObjectMapper, val securityProperties: CreSecurityProperties -) : JwtService { +) : JwtLogic { private val secretKey by lazy { securityProperties.jwtSecret.base64encode() } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/users/UserDetailsService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/UserDetailsLogic.kt similarity index 85% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/users/UserDetailsService.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/UserDetailsLogic.kt index 923abed..4286ed9 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/users/UserDetailsService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/UserDetailsLogic.kt @@ -1,7 +1,8 @@ -package dev.fyloz.colorrecipesexplorer.service.users +package dev.fyloz.colorrecipesexplorer.logic.users 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.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.model.account.Permission @@ -12,16 +13,16 @@ import org.springframework.context.annotation.Profile import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.stereotype.Service -interface UserDetailsService : SpringUserDetailsService { +interface UserDetailsLogic : SpringUserDetailsService { /** Loads an [User] for the given [id]. */ fun loadUserById(id: Long, ignoreDefaultGroupUsers: Boolean = false): UserDetails } @Service -@Profile("!emergency") -class UserDetailsServiceImpl( - private val userService: UserService -) : UserDetailsService { +@RequireDatabase +class DefaultUserDetailsLogic( + private val userLogic: UserLogic +) : UserDetailsLogic { override fun loadUserByUsername(username: String): UserDetails { try { return loadUserById(username.toLong(), true) @@ -31,7 +32,7 @@ class UserDetailsServiceImpl( } override fun loadUserById(id: Long, ignoreDefaultGroupUsers: Boolean): UserDetails { - val user = userService.getById( + val user = userLogic.getById( id, ignoreDefaultGroupUsers = ignoreDefaultGroupUsers, ignoreSystemUsers = false @@ -42,9 +43,9 @@ class UserDetailsServiceImpl( @Service @Profile("emergency") -class EmergencyUserDetailsServiceImpl( +class EmergencyUserDetailsLogic( securityProperties: CreSecurityProperties -) : UserDetailsService { +) : UserDetailsLogic { private val users: Set init { diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/users/UserService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/UserLogic.kt similarity index 92% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/users/UserService.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/UserLogic.kt index 04add2b..1de1f80 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/users/UserService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/UserLogic.kt @@ -1,20 +1,19 @@ -package dev.fyloz.colorrecipesexplorer.service.users +package dev.fyloz.colorrecipesexplorer.logic.users 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.model.validation.or import dev.fyloz.colorrecipesexplorer.repository.UserRepository -import dev.fyloz.colorrecipesexplorer.service.AbstractExternalModelService -import dev.fyloz.colorrecipesexplorer.service.ExternalModelService import org.springframework.context.annotation.Lazy import org.springframework.context.annotation.Profile -import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.stereotype.Service import org.springframework.web.util.WebUtils import java.time.LocalDateTime import javax.servlet.http.HttpServletRequest -interface UserService : +interface UserLogic : ExternalModelService { /** Check if an [User] with the given [firstName] and [lastName] exists. */ fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean @@ -52,13 +51,13 @@ interface UserService : @Service @Profile("!emergency") -class UserServiceImpl( +class DefaultUserLogic( userRepository: UserRepository, - @Lazy val groupService: GroupService, + @Lazy val groupLogic: GroupLogic, ) : AbstractExternalModelService( userRepository ), - UserService { + UserLogic { override fun idNotFoundException(id: Long) = userIdNotFoundException(id) override fun idAlreadyExistsException(id: Long) = userIdAlreadyExistsException(id) @@ -96,7 +95,7 @@ class UserServiceImpl( plainPassword = password, isDefaultGroupUser = false, isSystemUser = false, - group = if (groupId != null) groupService.getById(groupId) else null, + group = if (groupId != null) groupLogic.getById(groupId) else null, permissions = permissions ) }) @@ -142,7 +141,7 @@ class UserServiceImpl( password = persistedUser.password, isDefaultGroupUser = false, isSystemUser = false, - group = if (entity.groupId != null) groupService.getById(entity.groupId) else persistedUser.group, + group = if (entity.groupId != null) groupLogic.getById(entity.groupId) else persistedUser.group, permissions = permissions?.toMutableSet() ?: persistedUser.permissions, lastLoginTime = persistedUser.lastLoginTime ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/AccountControllers.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/AccountControllers.kt index e316ddf..87d42e4 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/AccountControllers.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/AccountControllers.kt @@ -2,10 +2,9 @@ package dev.fyloz.colorrecipesexplorer.rest import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditUsers import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewUsers +import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic +import dev.fyloz.colorrecipesexplorer.logic.users.UserLogic import dev.fyloz.colorrecipesexplorer.model.account.* -import dev.fyloz.colorrecipesexplorer.service.users.GroupService -import dev.fyloz.colorrecipesexplorer.service.users.UserService -import mu.KotlinLogging import org.springframework.context.annotation.Profile import org.springframework.http.MediaType import org.springframework.security.access.prepost.PreAuthorize @@ -20,22 +19,22 @@ private const val GROUP_CONTROLLER_PATH = "api/user/group" @RestController @RequestMapping(USER_CONTROLLER_PATH) @Profile("!emergency") -class UserController(private val userService: UserService) { +class UserController(private val userLogic: UserLogic) { @GetMapping @PreAuthorizeViewUsers fun getAll() = - ok(userService.getAllForOutput()) + ok(userLogic.getAllForOutput()) @GetMapping("{id}") @PreAuthorizeViewUsers fun getById(@PathVariable id: Long) = - ok(userService.getByIdForOutput(id)) + ok(userLogic.getByIdForOutput(id)) @PostMapping @PreAuthorizeEditUsers fun save(@Valid @RequestBody user: UserSaveDto) = created(USER_CONTROLLER_PATH) { - with(userService) { + with(userLogic) { save(user).toOutput() } } @@ -44,14 +43,14 @@ class UserController(private val userService: UserService) { @PreAuthorizeEditUsers fun update(@Valid @RequestBody user: UserUpdateDto) = noContent { - userService.update(user) + userLogic.update(user) } @PutMapping("{id}/password", consumes = [MediaType.TEXT_PLAIN_VALUE]) @PreAuthorizeEditUsers fun updatePassword(@PathVariable id: Long, @RequestBody password: String) = noContent { - userService.updatePassword(id, password) + userLogic.updatePassword(id, password) } @PutMapping("{userId}/permissions/{permission}") @@ -60,7 +59,7 @@ class UserController(private val userService: UserService) { @PathVariable userId: Long, @PathVariable permission: Permission ) = noContent { - userService.addPermission(userId, permission) + userLogic.addPermission(userId, permission) } @DeleteMapping("{userId}/permissions/{permission}") @@ -69,37 +68,37 @@ class UserController(private val userService: UserService) { @PathVariable userId: Long, @PathVariable permission: Permission ) = noContent { - userService.removePermission(userId, permission) + userLogic.removePermission(userId, permission) } @DeleteMapping("{id}") @PreAuthorizeEditUsers fun deleteById(@PathVariable id: Long) = - userService.deleteById(id) + userLogic.deleteById(id) } @RestController @RequestMapping(GROUP_CONTROLLER_PATH) @Profile("!emergency") class GroupsController( - private val groupService: GroupService, - private val userService: UserService + private val groupLogic: GroupLogic, + private val userLogic: UserLogic ) { @GetMapping @PreAuthorize("hasAnyAuthority('VIEW_RECIPES', 'VIEW_USERS')") fun getAll() = - ok(groupService.getAllForOutput()) + ok(groupLogic.getAllForOutput()) @GetMapping("{id}") @PreAuthorizeViewUsers fun getById(@PathVariable id: Long) = - ok(groupService.getByIdForOutput(id)) + ok(groupLogic.getByIdForOutput(id)) @GetMapping("{id}/users") @PreAuthorizeViewUsers fun getUsersForGroup(@PathVariable id: Long) = - ok(with(userService) { - groupService.getUsersForGroup(id) + ok(with(userLogic) { + groupLogic.getUsersForGroup(id) .map { it.toOutput() } }) @@ -107,27 +106,27 @@ class GroupsController( @PreAuthorizeViewUsers fun setDefaultGroup(@PathVariable groupId: Long, response: HttpServletResponse) = noContent { - groupService.setResponseDefaultGroup(groupId, response) + groupLogic.setResponseDefaultGroup(groupId, response) } @GetMapping("default") @PreAuthorizeViewUsers fun getRequestDefaultGroup(request: HttpServletRequest) = - ok(with(groupService) { + ok(with(groupLogic) { getRequestDefaultGroup(request).toOutput() }) @GetMapping("currentuser") fun getCurrentGroupUser(request: HttpServletRequest) = - ok(with(groupService.getRequestDefaultGroup(request)) { - userService.getDefaultGroupUser(this).toOutputDto() + ok(with(groupLogic.getRequestDefaultGroup(request)) { + userLogic.getDefaultGroupUser(this).toOutputDto() }) @PostMapping @PreAuthorizeEditUsers fun save(@Valid @RequestBody group: GroupSaveDto) = created(GROUP_CONTROLLER_PATH) { - with(groupService) { + with(groupLogic) { save(group).toOutput() } } @@ -136,25 +135,25 @@ class GroupsController( @PreAuthorizeEditUsers fun update(@Valid @RequestBody group: GroupUpdateDto) = noContent { - groupService.update(group) + groupLogic.update(group) } @DeleteMapping("{id}") @PreAuthorizeEditUsers fun deleteById(@PathVariable id: Long) = noContent { - groupService.deleteById(id) + groupLogic.deleteById(id) } } @RestController @RequestMapping("api") @Profile("!emergency") -class LogoutController(private val userService: UserService) { +class LogoutController(private val userLogic: UserLogic) { @GetMapping("logout") @PreAuthorize("isFullyAuthenticated()") fun logout(request: HttpServletRequest) = ok { - userService.logout(request) + userLogic.logout(request) } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt index 77704da..3375cbd 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt @@ -4,7 +4,6 @@ import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewCatalog import dev.fyloz.colorrecipesexplorer.model.Company import dev.fyloz.colorrecipesexplorer.model.CompanySaveDto import dev.fyloz.colorrecipesexplorer.model.CompanyUpdateDto -import dev.fyloz.colorrecipesexplorer.service.CompanyService import org.springframework.context.annotation.Profile import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.* @@ -16,33 +15,33 @@ private const val COMPANY_CONTROLLER_PATH = "api/company" @RequestMapping(COMPANY_CONTROLLER_PATH) @Profile("!emergency") @PreAuthorizeViewCatalog -class CompanyController(private val companyService: CompanyService) { +class CompanyController(private val companyLogic: dev.fyloz.colorrecipesexplorer.logic.CompanyLogic) { @GetMapping fun getAll() = - ok(companyService.getAllForOutput()) + ok(companyLogic.getAllForOutput()) @GetMapping("{id}") fun getById(@PathVariable id: Long) = - ok(companyService.getByIdForOutput(id)) + ok(companyLogic.getByIdForOutput(id)) @PostMapping @PreAuthorize("hasAuthority('EDIT_COMPANIES')") fun save(@Valid @RequestBody company: CompanySaveDto) = created(COMPANY_CONTROLLER_PATH) { - companyService.save(company) + companyLogic.save(company) } @PutMapping @PreAuthorize("hasAuthority('EDIT_COMPANIES')") fun update(@Valid @RequestBody company: CompanyUpdateDto) = noContent { - companyService.update(company) + companyLogic.update(company) } @DeleteMapping("{id}") @PreAuthorize("hasAuthority('EDIT_COMPANIES')") fun deleteById(@PathVariable id: Long) = noContent { - companyService.deleteById(id) + companyLogic.deleteById(id) } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/ConfigurationController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/ConfigurationController.kt index db64365..d66b67b 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/ConfigurationController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/ConfigurationController.kt @@ -1,37 +1,35 @@ package dev.fyloz.colorrecipesexplorer.rest +import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic import dev.fyloz.colorrecipesexplorer.model.ConfigurationBase import dev.fyloz.colorrecipesexplorer.model.ConfigurationDto -import dev.fyloz.colorrecipesexplorer.model.ConfigurationImageDto import dev.fyloz.colorrecipesexplorer.model.account.Permission import dev.fyloz.colorrecipesexplorer.model.account.toAuthority import dev.fyloz.colorrecipesexplorer.restartApplication -import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationService import org.springframework.http.MediaType import org.springframework.security.access.prepost.PreAuthorize import org.springframework.security.core.Authentication import org.springframework.web.bind.annotation.* import org.springframework.web.multipart.MultipartFile -import javax.validation.constraints.NotBlank @RestController @RequestMapping("api/config") -class ConfigurationController(val configurationService: ConfigurationService) { +class ConfigurationController(val configurationLogic: ConfigurationLogic) { @GetMapping fun getAll(@RequestParam(required = false) keys: String?, authentication: Authentication?) = - ok(with(configurationService) { + ok(with(configurationLogic) { if (keys != null) getAll(keys) else getAll() }.filter { authentication.hasAuthority(it) }) @GetMapping("{key}") - fun get(@PathVariable key: String, authentication: Authentication?) = with(configurationService.get(key)) { + fun get(@PathVariable key: String, authentication: Authentication?) = with(configurationLogic.get(key)) { if (authentication.hasAuthority(this)) ok(this) else forbidden() } @PutMapping @PreAuthorize("hasAuthority('ADMIN')") fun set(@RequestBody configurations: List) = noContent { - configurationService.set(configurations) + configurationLogic.set(configurations) } @PostMapping("restart") @@ -44,24 +42,24 @@ class ConfigurationController(val configurationService: ConfigurationService) { @GetMapping("icon") fun getIcon() = - okFile(configurationService.getConfiguredIcon(), MediaType.IMAGE_PNG_VALUE) + okFile(configurationLogic.getConfiguredIcon(), MediaType.IMAGE_PNG_VALUE) @PutMapping("icon") @PreAuthorize("hasAuthority('ADMIN')") fun setIcon(@RequestParam icon: MultipartFile) = noContent { - configurationService.setConfiguredIcon(icon) + configurationLogic.setConfiguredIcon(icon) } // Logo @GetMapping("logo") fun getLogo() = - okFile(configurationService.getConfiguredLogo(), MediaType.IMAGE_PNG_VALUE) + okFile(configurationLogic.getConfiguredLogo(), MediaType.IMAGE_PNG_VALUE) @PutMapping("logo") @PreAuthorize("hasAuthority('ADMIN')") fun setLogo(@RequestParam logo: MultipartFile) = noContent { - configurationService.setConfiguredLogo(logo) + configurationLogic.setConfiguredLogo(logo) } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/FileController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/FileController.kt index 92b078a..5d6aa84 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/FileController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/FileController.kt @@ -1,8 +1,8 @@ package dev.fyloz.colorrecipesexplorer.rest +import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic +import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic import dev.fyloz.colorrecipesexplorer.model.ConfigurationType -import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationService -import dev.fyloz.colorrecipesexplorer.service.files.WriteableFileService import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.security.access.prepost.PreAuthorize @@ -15,14 +15,14 @@ const val FILE_CONTROLLER_PATH = "/api/file" @RestController @RequestMapping(FILE_CONTROLLER_PATH) class FileController( - private val fileService: WriteableFileService, - private val configService: ConfigurationService + private val fileLogic: WriteableFileLogic, + private val configurationLogic: ConfigurationLogic ) { @GetMapping(produces = [MediaType.APPLICATION_OCTET_STREAM_VALUE]) fun upload( @RequestParam path: String, @RequestParam(required = false) mediaType: String? - ) = okFile(fileService.read(path), mediaType) + ) = okFile(fileLogic.read(path), mediaType) @PutMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) @PreAuthorize("hasAnyAuthority('WRITE_FILE')") @@ -31,7 +31,7 @@ class FileController( @RequestParam path: String, @RequestParam(required = false) overwrite: Boolean = false ): ResponseEntity { - fileService.write(file, path, overwrite) + fileLogic.write(file, path, overwrite) return created(path) } @@ -39,11 +39,11 @@ class FileController( @PreAuthorize("hasAnyAuthority('WRITE_FILE')") fun delete(@RequestParam path: String): ResponseEntity = noContent { - fileService.delete(path) + fileLogic.delete(path) } private fun created(path: String): ResponseEntity = ResponseEntity - .created(URI.create("${configService.get(ConfigurationType.INSTANCE_URL)}$FILE_CONTROLLER_PATH?path=$path")) + .created(URI.create("${configurationLogic.get(ConfigurationType.INSTANCE_URL)}$FILE_CONTROLLER_PATH?path=$path")) .build() } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/InventoryController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/InventoryController.kt index abf6d49..47d0b96 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/InventoryController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/InventoryController.kt @@ -1,10 +1,9 @@ package dev.fyloz.colorrecipesexplorer.rest +import dev.fyloz.colorrecipesexplorer.logic.InventoryLogic import dev.fyloz.colorrecipesexplorer.model.MaterialQuantityDto import dev.fyloz.colorrecipesexplorer.model.MixDeductDto -import dev.fyloz.colorrecipesexplorer.service.InventoryService import org.springframework.context.annotation.Profile -import org.springframework.http.ResponseEntity import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestBody @@ -17,20 +16,20 @@ private const val INVENTORY_CONTROLLER_PATH = "api/inventory" @RequestMapping(INVENTORY_CONTROLLER_PATH) @Profile("!emergency") class InventoryController( - private val inventoryService: InventoryService + private val inventoryLogic: InventoryLogic ) { @PutMapping("add") @PreAuthorize("hasAuthority('ADD_TO_INVENTORY')") fun add(@RequestBody quantities: Collection) = - ok(inventoryService.add(quantities)) + ok(inventoryLogic.add(quantities)) @PutMapping("deduct") @PreAuthorize("hasAuthority('DEDUCT_FROM_INVENTORY')") fun deduct(@RequestBody quantities: Collection) = - ok(inventoryService.deduct(quantities)) + ok(inventoryLogic.deduct(quantities)) @PutMapping("deduct/mix") @PreAuthorize("hasAuthority('DEDUCT_FROM_INVENTORY')") fun deduct(@RequestBody mixRatio: MixDeductDto) = - ok(inventoryService.deductMix(mixRatio)) + ok(inventoryLogic.deductMix(mixRatio)) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt index e5d13f9..9401f28 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt @@ -1,16 +1,13 @@ package dev.fyloz.colorrecipesexplorer.rest import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewCatalog -import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties +import dev.fyloz.colorrecipesexplorer.logic.MaterialLogic import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.service.MaterialService import org.springframework.context.annotation.Profile import org.springframework.http.MediaType -import org.springframework.http.ResponseEntity import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.* import org.springframework.web.multipart.MultipartFile -import java.net.URI import javax.validation.Valid private const val MATERIAL_CONTROLLER_PATH = "api/material" @@ -20,25 +17,25 @@ private const val MATERIAL_CONTROLLER_PATH = "api/material" @Profile("!emergency") @PreAuthorizeViewCatalog class MaterialController( - private val materialService: MaterialService + private val materialLogic: MaterialLogic ) { @GetMapping fun getAll() = - ok(materialService.getAllForOutput()) + ok(materialLogic.getAllForOutput()) @GetMapping("notmixtype") fun getAllNotMixType() = - ok(materialService.getAllNotMixType()) + ok(materialLogic.getAllNotMixType()) @GetMapping("{id}") fun getById(@PathVariable id: Long) = - ok(materialService.getByIdForOutput(id)) + ok(materialLogic.getByIdForOutput(id)) @PostMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) @PreAuthorize("hasAuthority('EDIT_MATERIALS')") fun save(@Valid material: MaterialSaveDto, simdutFile: MultipartFile?) = created(MATERIAL_CONTROLLER_PATH) { - with(materialService) { + with(materialLogic) { save( materialSaveDto( name = material.name, @@ -54,7 +51,7 @@ class MaterialController( @PreAuthorize("hasAuthority('EDIT_MATERIALS')") fun update(@Valid material: MaterialUpdateDto, simdutFile: MultipartFile?) = noContent { - materialService.update( + materialLogic.update( materialUpdateDto( id = material.id, name = material.name, @@ -69,15 +66,15 @@ class MaterialController( @PreAuthorize("hasAuthority('EDIT_MATERIALS')") fun deleteById(@PathVariable id: Long) = noContent { - materialService.deleteById(id) + materialLogic.deleteById(id) } @GetMapping("mix/create/{recipeId}") fun getAllForMixCreation(@PathVariable recipeId: Long) = - ok(materialService.getAllForMixCreation(recipeId)) + ok(materialLogic.getAllForMixCreation(recipeId)) @GetMapping("mix/update/{mixId}") fun getAllForMixUpdate(@PathVariable mixId: Long) = - ok(materialService.getAllForMixUpdate(mixId)) + ok(materialLogic.getAllForMixUpdate(mixId)) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialTypeController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialTypeController.kt index a8ff9bd..f0d5e90 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialTypeController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialTypeController.kt @@ -1,10 +1,10 @@ package dev.fyloz.colorrecipesexplorer.rest import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewCatalog +import dev.fyloz.colorrecipesexplorer.logic.MaterialTypeLogic import dev.fyloz.colorrecipesexplorer.model.MaterialType import dev.fyloz.colorrecipesexplorer.model.MaterialTypeSaveDto import dev.fyloz.colorrecipesexplorer.model.MaterialTypeUpdateDto -import dev.fyloz.colorrecipesexplorer.service.MaterialTypeService import org.springframework.context.annotation.Profile import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.* @@ -16,34 +16,34 @@ private const val MATERIAL_TYPE_CONTROLLER_PATH = "api/materialtype" @RequestMapping(MATERIAL_TYPE_CONTROLLER_PATH) @Profile("!emergency") @PreAuthorizeViewCatalog -class MaterialTypeController(private val materialTypeService: MaterialTypeService) { +class MaterialTypeController(private val materialTypeLogic: MaterialTypeLogic) { @GetMapping fun getAll() = - ok(materialTypeService.getAllForOutput()) + ok(materialTypeLogic.getAllForOutput()) @GetMapping("{id}") fun getById(@PathVariable id: Long) = - ok(materialTypeService.getByIdForOutput(id)) + ok(materialTypeLogic.getByIdForOutput(id)) @PostMapping @PreAuthorize("hasAuthority('EDIT_MATERIAL_TYPES')") fun save(@Valid @RequestBody materialType: MaterialTypeSaveDto) = created(MATERIAL_TYPE_CONTROLLER_PATH) { - materialTypeService.save(materialType) + materialTypeLogic.save(materialType) } @PutMapping @PreAuthorize("hasAuthority('EDIT_MATERIAL_TYPES')") fun update(@Valid @RequestBody materialType: MaterialTypeUpdateDto) = noContent { - materialTypeService.update(materialType) + materialTypeLogic.update(materialType) } @DeleteMapping("{id}") @PreAuthorize("hasAuthority('EDIT_MATERIAL_TYPES')") fun deleteById(@PathVariable id: Long) = noContent { - materialTypeService.deleteById(id) + materialTypeLogic.deleteById(id) } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt index 9d601ba..90abb8a 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt @@ -2,10 +2,10 @@ package dev.fyloz.colorrecipesexplorer.rest import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditRecipes import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewRecipes +import dev.fyloz.colorrecipesexplorer.logic.MixLogic +import dev.fyloz.colorrecipesexplorer.logic.RecipeImageLogic +import dev.fyloz.colorrecipesexplorer.logic.RecipeLogic import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.service.MixService -import dev.fyloz.colorrecipesexplorer.service.RecipeImageService -import dev.fyloz.colorrecipesexplorer.service.RecipeService import org.springframework.context.annotation.Profile import org.springframework.http.MediaType import org.springframework.http.ResponseEntity @@ -23,27 +23,27 @@ private const val MIX_CONTROLLER_PATH = "api/recipe/mix" @Profile("!emergency") @PreAuthorizeViewRecipes class RecipeController( - private val recipeService: RecipeService, - private val recipeImageService: RecipeImageService + private val recipeLogic: RecipeLogic, + private val recipeImageLogic: RecipeImageLogic ) { @GetMapping fun getAll(@RequestParam(required = false) name: String?) = if (name == null) - ok(recipeService.getAllForOutput()) + ok(recipeLogic.getAllForOutput()) else - ok(with(recipeService) { + ok(with(recipeLogic) { getAllByName(name).map { it.toOutput() } }) @GetMapping("{id}") fun getById(@PathVariable id: Long) = - ok(recipeService.getByIdForOutput(id)) + ok(recipeLogic.getByIdForOutput(id)) @PostMapping @PreAuthorizeEditRecipes fun save(@Valid @RequestBody recipe: RecipeSaveDto) = created(RECIPE_CONTROLLER_PATH) { - with(recipeService) { + with(recipeLogic) { save(recipe).toOutput() } } @@ -52,27 +52,27 @@ class RecipeController( @PreAuthorizeEditRecipes fun update(@Valid @RequestBody recipe: RecipeUpdateDto) = noContent { - recipeService.update(recipe) + recipeLogic.update(recipe) } @PutMapping("public") @PreAuthorize("hasAuthority('EDIT_RECIPES_PUBLIC_DATA')") fun updatePublicData(@Valid @RequestBody publicDataDto: RecipePublicDataDto) = noContent { - recipeService.updatePublicData(publicDataDto) + recipeLogic.updatePublicData(publicDataDto) } @DeleteMapping("{id}") @PreAuthorizeEditRecipes fun deleteById(@PathVariable id: Long) = noContent { - recipeService.deleteById(id) + recipeLogic.deleteById(id) } @PutMapping("{recipeId}/image", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) @PreAuthorizeEditRecipes fun downloadImage(@PathVariable recipeId: Long, image: MultipartFile): ResponseEntity { - recipeImageService.download(image, recipeService.getById(recipeId)) + recipeImageLogic.download(image, recipeLogic.getById(recipeId)) return getById(recipeId) } @@ -80,7 +80,7 @@ class RecipeController( @PreAuthorizeEditRecipes fun deleteImage(@PathVariable recipeId: Long, @PathVariable name: String) = noContent { - recipeImageService.delete(recipeService.getById(recipeId), name) + recipeImageLogic.delete(recipeLogic.getById(recipeId), name) } } @@ -88,29 +88,29 @@ class RecipeController( @RequestMapping(MIX_CONTROLLER_PATH) @Profile("!emergency") @PreAuthorizeViewRecipes -class MixController(private val mixService: MixService) { +class MixController(private val mixLogic: MixLogic) { @GetMapping("{id}") fun getById(@PathVariable id: Long) = - ok(mixService.getByIdForOutput(id)) + ok(mixLogic.getByIdForOutput(id)) @PostMapping @PreAuthorizeEditRecipes fun save(@Valid @RequestBody mix: MixSaveDto) = created(MIX_CONTROLLER_PATH) { - mixService.save(mix) + mixLogic.save(mix) } @PutMapping @PreAuthorizeEditRecipes fun update(@Valid @RequestBody mix: MixUpdateDto) = noContent { - mixService.update(mix) + mixLogic.update(mix) } @DeleteMapping("{id}") @PreAuthorizeEditRecipes fun deleteById(@PathVariable id: Long) = noContent { - mixService.deleteById(id) + mixLogic.deleteById(id) } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/TouchUpKitController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/TouchUpKitController.kt index 027d71d..4ef42ef 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/TouchUpKitController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/TouchUpKitController.kt @@ -1,9 +1,9 @@ package dev.fyloz.colorrecipesexplorer.rest +import dev.fyloz.colorrecipesexplorer.logic.TouchUpKitLogic import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKitOutputDto import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKitSaveDto import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKitUpdateDto -import dev.fyloz.colorrecipesexplorer.service.TouchUpKitService import org.springframework.context.annotation.Profile import org.springframework.core.io.Resource import org.springframework.http.MediaType @@ -19,21 +19,21 @@ const val TOUCH_UP_KIT_CONTROLLER_PATH = "/api/touchupkit" @Profile("!emergency") @PreAuthorize("hasAuthority('VIEW_TOUCH_UP_KITS')") class TouchUpKitController( - private val touchUpKitService: TouchUpKitService + private val touchUpKitLogic: TouchUpKitLogic ) { @GetMapping fun getAll() = - ok(touchUpKitService.getAllForOutput()) + ok(touchUpKitLogic.getAllForOutput()) @GetMapping("{id}") fun getById(@PathVariable id: Long) = - ok(touchUpKitService.getByIdForOutput(id)) + ok(touchUpKitLogic.getByIdForOutput(id)) @PostMapping @PreAuthorize("hasAuthority('EDIT_TOUCH_UP_KITS')") fun save(@Valid @RequestBody touchUpKit: TouchUpKitSaveDto) = created(TOUCH_UP_KIT_CONTROLLER_PATH) { - with(touchUpKitService) { + with(touchUpKitLogic) { save(touchUpKit).toOutput() } } @@ -41,24 +41,24 @@ class TouchUpKitController( @PutMapping @PreAuthorize("hasAuthority('EDIT_TOUCH_UP_KITS')") fun update(@Valid @RequestBody touchUpKit: TouchUpKitUpdateDto) = noContent { - touchUpKitService.update(touchUpKit) + touchUpKitLogic.update(touchUpKit) } @PutMapping("{id}/complete") @PreAuthorize("hasAuthority('EDIT_TOUCH_UP_KITS')") fun complete(@PathVariable id: Long) = noContent { - touchUpKitService.complete(id) + touchUpKitLogic.complete(id) } @DeleteMapping("{id}") @PreAuthorize("hasAuthority('EDIT_TOUCH_UP_KITS')") fun deleteById(@PathVariable id: Long) = noContent { - touchUpKitService.deleteById(id) + touchUpKitLogic.deleteById(id) } @GetMapping("pdf") fun getJobPdf(@RequestParam project: String): ResponseEntity { - with(touchUpKitService.generateJobPdfResource(project)) { + with(touchUpKitLogic.generateJobPdfResource(project)) { return ResponseEntity.ok() .header("Content-Disposition", "filename=TouchUpKit_$project.pdf") .contentLength(this.contentLength()) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AbstractServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/AbstractServiceTest.kt similarity index 81% rename from src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AbstractServiceTest.kt rename to src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/AbstractServiceTest.kt index 8d7f95c..a37cf7b 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AbstractServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/AbstractServiceTest.kt @@ -1,4 +1,4 @@ -package dev.fyloz.colorrecipesexplorer.service +package dev.fyloz.colorrecipesexplorer.logic import com.nhaarman.mockitokotlin2.* import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException @@ -16,11 +16,11 @@ import java.util.* import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue -import dev.fyloz.colorrecipesexplorer.service.AbstractServiceTest as AbstractServiceTest1 +import dev.fyloz.colorrecipesexplorer.logic.AbstractServiceTest as AbstractServiceTest1 abstract class AbstractServiceTest, R : JpaRepository> { protected abstract val repository: R - protected abstract val service: S + protected abstract val logic: S protected abstract val entity: E protected abstract val anotherEntity: E @@ -33,7 +33,7 @@ abstract class AbstractServiceTest, R : JpaRepository @AfterEach open fun afterEach() { - reset(repository, service) + reset(repository, logic) } // getAll() @@ -42,7 +42,7 @@ abstract class AbstractServiceTest, R : JpaRepository open fun `getAll() returns all available entities`() { whenever(repository.findAll()).doReturn(entityList) - val found = service.getAll() + val found = logic.getAll() assertEquals(entityList, found) } @@ -51,7 +51,7 @@ abstract class AbstractServiceTest, R : JpaRepository open fun `getAll() returns empty list when there is no entities`() { whenever(repository.findAll()).doReturn(listOf()) - val found = service.getAll() + val found = logic.getAll() assertTrue { found.isEmpty() } } @@ -62,7 +62,7 @@ abstract class AbstractServiceTest, R : JpaRepository open fun `save() saves in the repository and returns the saved value`() { whenever(repository.save(entity)).doReturn(entity) - val found = service.save(entity) + val found = logic.save(entity) verify(repository).save(entity) assertEquals(entity, found) @@ -74,7 +74,7 @@ abstract class AbstractServiceTest, R : JpaRepository open fun `update() saves in the repository and returns the updated value`() { whenever(repository.save(entity)).doReturn(entity) - val found = service.update(entity) + val found = logic.update(entity) verify(repository).save(entity) assertEquals(entity, found) @@ -84,7 +84,7 @@ abstract class AbstractServiceTest, R : JpaRepository @Test open fun `delete() deletes in the repository`() { - service.delete(entity) + logic.delete(entity) verify(repository).delete(entity) } @@ -99,7 +99,7 @@ abstract class AbstractModelServiceTest, R : J 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 = service.existsById(entity.id!!) + val found = logic.existsById(entity.id!!) assertTrue(found) } @@ -108,7 +108,7 @@ abstract class AbstractModelServiceTest, R : J 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 = service.existsById(entity.id!!) + val found = logic.existsById(entity.id!!) assertFalse(found) } @@ -119,7 +119,7 @@ abstract class AbstractModelServiceTest, R : J open fun `getById() returns the entity with the given id from the repository`() { whenever(repository.findById(entity.id!!)).doReturn(Optional.of(entity)) - val found = service.getById(entity.id!!) + val found = logic.getById(entity.id!!) assertEquals(entity, found) } @@ -128,7 +128,7 @@ abstract class AbstractModelServiceTest, R : J 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 { service.getById(entity.id!!) } + assertThrows { logic.getById(entity.id!!) } .assertErrorCode() } @@ -136,9 +136,9 @@ abstract class AbstractModelServiceTest, R : J @Test open fun `save() throws AlreadyExistsException when an entity with the given id exists in the repository`() { - doReturn(true).whenever(service).existsById(entity.id!!) + doReturn(true).whenever(logic).existsById(entity.id!!) - assertThrows { service.save(entity) } + assertThrows { logic.save(entity) } .assertErrorCode() } @@ -147,10 +147,10 @@ abstract class AbstractModelServiceTest, R : J @Test override fun `update() saves in the repository and returns the updated value`() { whenever(repository.save(entity)).doReturn(entity) - doReturn(true).whenever(service).existsById(entity.id!!) - doReturn(entity).whenever(service).getById(entity.id!!) + doReturn(true).whenever(logic).existsById(entity.id!!) + doReturn(entity).whenever(logic).getById(entity.id!!) - val found = service.update(entity) + val found = logic.update(entity) verify(repository).save(entity) assertEquals(entity, found) @@ -158,9 +158,9 @@ abstract class AbstractModelServiceTest, R : J @Test open fun `update() throws NotFoundException when no entity with the given id exists in the repository`() { - doReturn(false).whenever(service).existsById(entity.id!!) + doReturn(false).whenever(logic).existsById(entity.id!!) - assertThrows { service.update(entity) } + assertThrows { logic.update(entity) } .assertErrorCode() } @@ -168,9 +168,9 @@ abstract class AbstractModelServiceTest, R : J @Test open fun `deleteById() deletes the entity with the given id in the repository`() { - doReturn(entity).whenever(service).getById(entity.id!!) + doReturn(entity).whenever(logic).getById(entity.id!!) - service.deleteById(entity.id!!) + logic.deleteById(entity.id!!) verify(repository).delete(entity) } @@ -186,7 +186,7 @@ abstract class AbstractNamedModelServiceTest { service.getByName(entity.name) } + assertThrows { logic.getByName(entity.name) } .assertErrorCode("name") } @@ -223,9 +223,9 @@ abstract class AbstractNamedModelServiceTest { service.save(entity) } + assertThrows { logic.save(entity) } .assertErrorCode("name") } @@ -235,10 +235,10 @@ abstract class AbstractNamedModelServiceTest { service.update(entity) } + 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(service).getById(entity.id!!) + doReturn(entity).whenever(logic).getById(entity.id!!) - assertThrows { service.update(entity) } + assertThrows { logic.update(entity) } .assertErrorCode("name") } } diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountsServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/AccountsServiceTest.kt similarity index 76% rename from src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountsServiceTest.kt rename to src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/AccountsServiceTest.kt index 7fd9b53..bc31f97 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountsServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/AccountsServiceTest.kt @@ -1,4 +1,4 @@ -package dev.fyloz.colorrecipesexplorer.service +package dev.fyloz.colorrecipesexplorer.logic import com.nhaarman.mockitokotlin2.* import dev.fyloz.colorrecipesexplorer.config.security.defaultGroupCookieName @@ -7,7 +7,7 @@ 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.service.users.* +import dev.fyloz.colorrecipesexplorer.logic.users.* import org.junit.jupiter.api.* import org.springframework.mock.web.MockHttpServletResponse import org.springframework.security.core.userdetails.UsernameNotFoundException @@ -21,8 +21,8 @@ import kotlin.test.assertNotNull import kotlin.test.assertTrue @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class UserServiceTest : - AbstractExternalModelServiceTest() { +class UserLogicTest : + AbstractExternalModelServiceTest() { private val passwordEncoder = BCryptPasswordEncoder() override val entity: User = user(id = 0L, passwordEncoder = passwordEncoder) @@ -34,8 +34,8 @@ class UserServiceTest : override val entityUpdateDto: UserUpdateDto = spy(userUpdateDto(id = 0L)) override val repository: UserRepository = mock() - private val groupService: GroupService = mock() - override val service: UserService = spy(UserServiceImpl(repository, groupService)) + private val groupService: GroupLogic = mock() + override val logic: UserLogic = spy(DefaultUserLogic(repository, groupService)) private val entitySaveDtoUser = User( entitySaveDto.id, @@ -60,7 +60,7 @@ class UserServiceTest : 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 = service.existsByFirstNameAndLastName(entity.firstName, entity.lastName) + val found = logic.existsByFirstNameAndLastName(entity.firstName, entity.lastName) assertTrue(found) } @@ -69,7 +69,7 @@ class UserServiceTest : 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 = service.existsByFirstNameAndLastName(entity.firstName, entity.lastName) + val found = logic.existsByFirstNameAndLastName(entity.firstName, entity.lastName) assertFalse(found) } @@ -81,7 +81,7 @@ class UserServiceTest : whenever(repository.findById(entityDefaultGroupUser.id)).doReturn(Optional.of(entityDefaultGroupUser)) assertThrows { - service.getById( + logic.getById( entityDefaultGroupUser.id, ignoreDefaultGroupUsers = true, ignoreSystemUsers = false @@ -94,7 +94,7 @@ class UserServiceTest : whenever(repository.findById(entitySystemUser.id)).doReturn(Optional.of(entitySystemUser)) assertThrows { - service.getById( + logic.getById( entitySystemUser.id, ignoreDefaultGroupUsers = false, ignoreSystemUsers = true @@ -108,7 +108,7 @@ class UserServiceTest : fun `getByGroup() returns all the users with the given group from the repository`() { whenever(repository.findAllByGroup(group)).doReturn(entityList) - val found = service.getByGroup(group) + val found = logic.getByGroup(group) assertTrue(found.containsAll(entityList)) assertTrue(entityList.containsAll(found)) @@ -118,7 +118,7 @@ class UserServiceTest : 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 = service.getByGroup(group) + val found = logic.getByGroup(group) assertTrue(found.isEmpty()) } @@ -129,7 +129,7 @@ class UserServiceTest : fun `getDefaultGroupUser() returns the default user of the given group from the repository`() { whenever(repository.findByIsDefaultGroupUserIsTrueAndGroupIs(group)).doReturn(entityDefaultGroupUser) - val found = service.getDefaultGroupUser(group) + val found = logic.getDefaultGroupUser(group) assertEquals(entityDefaultGroupUser, found) } @@ -140,7 +140,7 @@ class UserServiceTest : whenever(repository.save(entity)).doReturn(entity) doReturn(false).whenever(repository).existsByFirstNameAndLastName(entity.firstName, entity.lastName) - val found = service.save(entity) + val found = logic.save(entity) verify(repository).save(entity) assertEquals(entity, found) @@ -150,13 +150,13 @@ class UserServiceTest : fun `save() throws AlreadyExistsException when firstName and lastName exists`() { doReturn(true).whenever(repository).existsByFirstNameAndLastName(entity.firstName, entity.lastName) - assertThrows { service.save(entity) } + assertThrows { logic.save(entity) } .assertErrorCode("fullName") } @Test override fun `save(dto) calls and returns save() with the created entity`() { - withBaseSaveDtoTest(entity, entitySaveDto, service, { + withBaseSaveDtoTest(entity, entitySaveDto, logic, { argThat { this.id == entity.id && this.firstName == entity.firstName && this.lastName == entity.lastName } @@ -165,11 +165,11 @@ class UserServiceTest : @Test fun `save(dto) calls and returns save() with the created user`() { - doReturn(entitySaveDtoUser).whenever(service).save(any()) + doReturn(entitySaveDtoUser).whenever(logic).save(any()) - val found = service.save(entitySaveDto) + val found = logic.save(entitySaveDto) - verify(service).save(argThat { this.id == entity.id && this.firstName == entity.firstName && this.lastName == entity.lastName }) + verify(logic).save(argThat { this.id == entity.id && this.firstName == entity.firstName && this.lastName == entity.lastName }) assertEquals(entitySaveDtoUser, found) } @@ -177,17 +177,17 @@ class UserServiceTest : @Test override fun `update(dto) calls and returns update() with the created entity`() = - withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() }) + 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(service).getById(eq(entity.id), any(), any()) + doReturn(entity).whenever(logic).getById(eq(entity.id), any(), any()) assertThrows { - service.update( + logic.update( entity, true, ignoreSystemUsers = true @@ -197,11 +197,11 @@ class UserServiceTest : } @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class GroupServiceTest : - AbstractExternalNamedModelServiceTest() { - private val userService: UserService = mock() +class GroupLogicTest : + AbstractExternalNamedModelServiceTest() { + private val userService: UserLogic = mock() override val repository: GroupRepository = mock() - override val service: GroupServiceImpl = spy(GroupServiceImpl(userService, repository)) + 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") @@ -224,10 +224,10 @@ class GroupServiceTest : fun `getUsersForGroup() returns all users in the given group`() { val group = group(id = 1L) - doReturn(group).whenever(service).getById(group.id!!) + doReturn(group).whenever(logic).getById(group.id!!) whenever(userService.getByGroup(group)).doReturn(listOf(groupUser)) - val found = service.getUsersForGroup(group.id!!) + val found = logic.getUsersForGroup(group.id!!) assertTrue(found.contains(groupUser)) assertTrue(found.size == 1) @@ -235,9 +235,9 @@ class GroupServiceTest : @Test fun `getUsersForGroup() returns empty collection when the given group contains any user`() { - doReturn(entity).whenever(service).getById(entity.id!!) + doReturn(entity).whenever(logic).getById(entity.id!!) - val found = service.getUsersForGroup(entity.id!!) + val found = logic.getUsersForGroup(entity.id!!) assertTrue(found.isEmpty()) } @@ -252,7 +252,7 @@ class GroupServiceTest : whenever(request.cookies).doReturn(cookies) whenever(userService.getById(eq(groupUserId), any(), any())).doReturn(groupUser) - val found = service.getRequestDefaultGroup(request) + val found = logic.getRequestDefaultGroup(request) assertEquals(entity, found) } @@ -263,7 +263,7 @@ class GroupServiceTest : whenever(request.cookies).doReturn(arrayOf()) - assertThrows { service.getRequestDefaultGroup(request) } + assertThrows { logic.getRequestDefaultGroup(request) } } // setResponseDefaultGroup() @@ -273,9 +273,9 @@ class GroupServiceTest : val response = MockHttpServletResponse() whenever(userService.getDefaultGroupUser(entity)).doReturn(groupUser) - doReturn(entity).whenever(service).getById(entity.id!!) + doReturn(entity).whenever(logic).getById(entity.id!!) - service.setResponseDefaultGroup(entity.id!!, response) + logic.setResponseDefaultGroup(entity.id!!, response) val found = response.getCookie(defaultGroupCookieName) assertNotNull(found) @@ -290,57 +290,57 @@ class GroupServiceTest : @Test override fun `save(dto) calls and returns save() with the created entity`() { - withBaseSaveDtoTest(entity, entitySaveDto, service) + withBaseSaveDtoTest(entity, entitySaveDto, logic) } // update() @Test override fun `update(dto) calls and returns update() with the created entity`() = - withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() }) + withBaseUpdateDtoTest(entity, entityUpdateDto, logic, { any() }) } @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class UserUserDetailsServiceTest { - private val userService: UserService = mock() - private val service = spy(UserDetailsServiceImpl(userService)) +class UserUserDetailsLogicTest { + private val userLogic: UserLogic = mock() + private val logic = spy(DefaultUserDetailsLogic(userLogic)) private val user = user(id = 0L) @BeforeEach fun beforeEach() { - reset(userService, service) + reset(userLogic, logic) } // loadUserByUsername() @Test fun `loadUserByUsername() calls loadUserByUserId() with the given username as an id`() { - whenever(userService.getById(eq(user.id), any(), any())).doReturn(user) + whenever(userLogic.getById(eq(user.id), any(), any())).doReturn(user) doReturn(UserDetails(user(id = user.id, plainPassword = user.password))) - .whenever(service).loadUserById(user.id) + .whenever(logic).loadUserById(user.id) - service.loadUserByUsername(user.id.toString()) + logic.loadUserByUsername(user.id.toString()) - verify(service).loadUserById(eq(user.id), any()) + verify(logic).loadUserById(eq(user.id), any()) } @Test fun `loadUserByUsername() throws UsernameNotFoundException when no user with the given id exists`() { - whenever(userService.getById(eq(user.id), any(), any())).doThrow( + whenever(userLogic.getById(eq(user.id), any(), any())).doThrow( userIdNotFoundException(user.id) ) - assertThrows { service.loadUserByUsername(user.id.toString()) } + assertThrows { logic.loadUserByUsername(user.id.toString()) } } // loadUserByUserId @Test fun `loadUserByUserId() returns an User corresponding to the user with the given id`() { - whenever(userService.getById(eq(user.id), any(), any())).doReturn(user) + whenever(userLogic.getById(eq(user.id), any(), any())).doReturn(user) - val found = service.loadUserById(user.id) + 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/service/CompanyServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogicTest.kt similarity index 74% rename from src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyServiceTest.kt rename to src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogicTest.kt index 7843cae..be6dba4 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogicTest.kt @@ -1,4 +1,4 @@ -package dev.fyloz.colorrecipesexplorer.service +package dev.fyloz.colorrecipesexplorer.logic import com.nhaarman.mockitokotlin2.* import dev.fyloz.colorrecipesexplorer.model.* @@ -10,11 +10,16 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class CompanyServiceTest : - AbstractExternalNamedModelServiceTest() { - private val recipeService: RecipeService = mock() +class CompanyLogicTest : + AbstractExternalNamedModelServiceTest() { + private val recipeLogic: RecipeLogic = mock() override val repository: CompanyRepository = mock() - override val service: CompanyService = spy(CompanyServiceImpl(repository, recipeService)) + override val logic: CompanyLogic = spy( + DefaultCompanyLogic( + repository, + recipeLogic + ) + ) override val entity: Company = company(id = 0L, name = "company") override val anotherEntity: Company = company(id = 1L, name = "another company") @@ -24,7 +29,7 @@ class CompanyServiceTest : @AfterEach override fun afterEach() { - reset(recipeService) + reset(recipeLogic) super.afterEach() } @@ -32,18 +37,18 @@ class CompanyServiceTest : @Test fun `isLinkedToRecipes() returns true when a given company is linked to one or more recipes`() { - whenever(recipeService.existsByCompany(entity)).doReturn(true) + whenever(recipeLogic.existsByCompany(entity)).doReturn(true) - val found = service.isLinkedToRecipes(entity) + val found = logic.isLinkedToRecipes(entity) assertTrue(found) } @Test fun `isLinkedToRecipes() returns false when a given company is not linked to any recipe`() { - whenever(recipeService.existsByCompany(entity)).doReturn(false) + whenever(recipeLogic.existsByCompany(entity)).doReturn(false) - val found = service.isLinkedToRecipes(entity) + val found = logic.isLinkedToRecipes(entity) assertFalse(found) } @@ -52,14 +57,14 @@ class CompanyServiceTest : @Test override fun `save(dto) calls and returns save() with the created entity`() { - withBaseSaveDtoTest(entity, entitySaveDto, service) + withBaseSaveDtoTest(entity, entitySaveDto, logic) } // update() @Test override fun `update(dto) calls and returns update() with the created entity`() = - withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() }) + withBaseUpdateDtoTest(entity, entityUpdateDto, logic, { any() }) // delete() diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/ConfigurationServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/ConfigurationLogicTest.kt similarity index 72% rename from src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/ConfigurationServiceTest.kt rename to src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/ConfigurationLogicTest.kt index ea4a73f..82dc865 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/ConfigurationServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/ConfigurationLogicTest.kt @@ -1,12 +1,12 @@ -package dev.fyloz.colorrecipesexplorer.service +package dev.fyloz.colorrecipesexplorer.logic import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties +import dev.fyloz.colorrecipesexplorer.logic.config.CONFIGURATION_FORMATTED_LIST_DELIMITER +import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationSource +import dev.fyloz.colorrecipesexplorer.logic.config.DefaultConfigurationLogic +import dev.fyloz.colorrecipesexplorer.logic.files.ResourceFileLogic +import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.service.config.CONFIGURATION_FORMATTED_LIST_DELIMITER -import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationServiceImpl -import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationSource -import dev.fyloz.colorrecipesexplorer.service.files.ResourceFileService -import dev.fyloz.colorrecipesexplorer.service.files.WriteableFileService import dev.fyloz.colorrecipesexplorer.utils.encrypt import io.mockk.* import org.junit.jupiter.api.AfterEach @@ -18,20 +18,19 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue -class ConfigurationServiceTest { - private val fileService = mockk() - private val resourceFileService = mockk() +class ConfigurationLogicTest { + private val fileLogic = mockk() + private val resourceFileLogic = mockk() private val configurationSource = mockk() private val securityProperties = mockk { every { configSalt } returns "d32270943af7e1cc" } - private val service = spyk( - ConfigurationServiceImpl( - fileService, - resourceFileService, + private val logic = spyk( + DefaultConfigurationLogic( + fileLogic, + resourceFileLogic, configurationSource, - securityProperties, - mockk() + securityProperties ) ) @@ -44,17 +43,17 @@ class ConfigurationServiceTest { @Test fun `getAll() gets the Configuration of each ConfigurationType`() { - every { service.get(any()) } answers { throw ConfigurationNotSetException(this.args[0] as ConfigurationType) } + every { logic.get(any()) } answers { throw ConfigurationNotSetException(this.args[0] as ConfigurationType) } - service.getAll() + logic.getAll() verify { - service.getAll() + logic.getAll() ConfigurationType.values().forEach { - service.get(it) + logic.get(it) } } - confirmVerified(service) + confirmVerified(logic) } @Test @@ -65,15 +64,15 @@ class ConfigurationServiceTest { ConfigurationType.INSTANCE_ICON_SET ) - every { service.get(match { it in unsetConfigurationTypes }) } answers { + every { logic.get(match { it in unsetConfigurationTypes }) } answers { throw ConfigurationNotSetException(this.firstArg() as ConfigurationType) } - every { service.get(match { it !in unsetConfigurationTypes }) } answers { + every { logic.get(match { it !in unsetConfigurationTypes }) } answers { val type = firstArg() configuration(type, type.key) } - val found = service.getAll() + val found = logic.getAll() assertFalse { found.any { @@ -82,12 +81,12 @@ class ConfigurationServiceTest { } verify { - service.getAll() + logic.getAll() ConfigurationType.values().forEach { - service.get(it) + logic.get(it) } } - confirmVerified(service) + confirmVerified(logic) } @Test @@ -101,24 +100,24 @@ class ConfigurationServiceTest { .map { it.key } .reduce { acc, s -> "$acc$CONFIGURATION_FORMATTED_LIST_DELIMITER$s" } - every { service.get(any()) } answers { + every { logic.get(any()) } answers { val key = firstArg() configuration(key.toConfigurationType(), key) } - val found = service.getAll(formattedKeyList) + val found = logic.getAll(formattedKeyList) assertTrue { found.all { it.type in configurationTypes } } verify { - service.getAll(formattedKeyList) + logic.getAll(formattedKeyList) configurationTypes.forEach { - service.get(it.key) + logic.get(it.key) } } - confirmVerified(service) + confirmVerified(logic) } // get() @@ -128,18 +127,18 @@ class ConfigurationServiceTest { val type = ConfigurationType.INSTANCE_ICON_SET val key = type.key - every { service.get(type) } answers { + every { logic.get(type) } answers { val type = firstArg() configuration(type, type.key) } - service.get(key) + logic.get(key) verify { - service.get(key) - service.get(type) + logic.get(key) + logic.get(type) } - confirmVerified(service) + confirmVerified(logic) } @Test @@ -149,7 +148,7 @@ class ConfigurationServiceTest { every { configurationSource.get(type) } returns configuration - val found = service.get(type) + val found = logic.get(type) assertEquals(configuration, found) } @@ -160,12 +159,12 @@ class ConfigurationServiceTest { every { configurationSource.get(type) } returns null - with(assertThrows { service.get(type) }) { + with(assertThrows { logic.get(type) }) { assertEquals(type, this.type) } verify { - service.get(type) + logic.get(type) configurationSource.get(type) } } @@ -174,7 +173,7 @@ class ConfigurationServiceTest { fun `get(type) throws InvalidConfigurationKeyException when the given ConfigurationType is encryption salt`() { val type = ConfigurationType.GENERATED_ENCRYPTION_SALT - assertThrows { service.get(type) } + assertThrows { logic.get(type) } } @Test @@ -187,7 +186,7 @@ class ConfigurationServiceTest { every { configurationSource.get(type) } returns configuration - val found = service.get(type) + val found = logic.get(type) assertTrue { found is SecureConfiguration } } @@ -200,9 +199,9 @@ class ConfigurationServiceTest { content = "content" ) - every { service.get(type) } returns configuration + every { logic.get(type) } returns configuration - val found = service.getContent(type) + val found = logic.getContent(type) assertEquals(configuration.content, found) } @@ -212,9 +211,9 @@ class ConfigurationServiceTest { val type = ConfigurationType.DATABASE_PASSWORD val configuration = secureConfiguration(type) - every { service.get(type) } returns configuration + every { logic.get(type) } returns configuration - assertThrows { service.getContent(type) } + assertThrows { logic.getContent(type) } } @Test @@ -228,7 +227,7 @@ class ConfigurationServiceTest { every { configurationSource.get(type) } returns configuration - val found = service.getSecure(type) + val found = logic.getSecure(type) assertEquals(content, found) } @@ -237,7 +236,7 @@ class ConfigurationServiceTest { fun `getSecure(type) throws UnsupportedOperationException when configuration is not secure`() { val type = ConfigurationType.INSTANCE_NAME - assertThrows { service.getSecure(type) } + assertThrows { logic.getSecure(type) } } private fun getConfiguredImageTest( @@ -247,9 +246,9 @@ class ConfigurationServiceTest { ) { val resource = mockk() val configuration = configuration(configurationType, imageSet.toString()) - val imageService = if (imageSet) fileService else resourceFileService + val imageService = if (imageSet) fileLogic else resourceFileLogic - every { service.get(configurationType) } returns configuration + every { logic.get(configurationType) } returns configuration every { imageService.read(any()) } returns resource test(resource) @@ -258,7 +257,7 @@ class ConfigurationServiceTest { @Test fun `getConfiguredIcon() gets icon from resources when INSTANCE_ICON_SET configuration is false`() { getConfiguredImageTest(ConfigurationType.INSTANCE_ICON_SET, false) { resource -> - val found = service.getConfiguredIcon() + val found = logic.getConfiguredIcon() assertEquals(resource, found) } @@ -267,7 +266,7 @@ class ConfigurationServiceTest { @Test fun `getConfiguredIcon() gets icon from files when INSTANCE_ICON_SET configuration is true`() { getConfiguredImageTest(ConfigurationType.INSTANCE_ICON_SET, true) { resource -> - val found = service.getConfiguredIcon() + val found = logic.getConfiguredIcon() assertEquals(resource, found) } @@ -276,7 +275,7 @@ class ConfigurationServiceTest { @Test fun `getConfiguredLogo() gets logo from resources when INSTANCE_LOGO_SET is false`() { getConfiguredImageTest(ConfigurationType.INSTANCE_LOGO_SET, false) { resource -> - val found = service.getConfiguredLogo() + val found = logic.getConfiguredLogo() assertEquals(resource, found) } @@ -285,7 +284,7 @@ class ConfigurationServiceTest { @Test fun `getConfiguredLogo() gets logo from files when INSTANCE_LOGO_SET is true`() { getConfiguredImageTest(ConfigurationType.INSTANCE_LOGO_SET, true) { resource -> - val found = service.getConfiguredLogo() + val found = logic.getConfiguredLogo() assertEquals(resource, found) } @@ -297,7 +296,7 @@ class ConfigurationServiceTest { every { configurationSource.set(any()) } just runs - service.set(configuration) + logic.set(configuration) verify { configurationSource.set(configuration) @@ -316,7 +315,7 @@ class ConfigurationServiceTest { every { configurationSource.set(any()) } just runs every { content.encrypt(any(), any()) } returns encryptedContent - service.set(configuration) + logic.set(configuration) verify { configurationSource.set(match { @@ -328,8 +327,8 @@ class ConfigurationServiceTest { private fun setConfiguredImageTest(test: (MultipartFile) -> Unit) { val file = mockk() - every { service.set(any()) } just runs - every { fileService.write(any(), any(), any()) } just runs + every { logic.set(any()) } just runs + every { fileLogic.write(any(), any(), any()) } just runs test(file) } @@ -337,10 +336,10 @@ class ConfigurationServiceTest { @Test fun `setConfiguredIcon() sets icon in files`() { setConfiguredImageTest { file -> - service.setConfiguredIcon(file) + logic.setConfiguredIcon(file) verify { - fileService.write(file, any(), true) + fileLogic.write(file, any(), true) } } } @@ -350,10 +349,10 @@ class ConfigurationServiceTest { val type = ConfigurationType.INSTANCE_ICON_SET setConfiguredImageTest { file -> - service.setConfiguredIcon(file) + logic.setConfiguredIcon(file) verify { - service.set(match { + logic.set(match { it.key == type.key && it.content == true.toString() }) } @@ -363,10 +362,10 @@ class ConfigurationServiceTest { @Test fun `setConfiguredLogo() sets logo in files`() { setConfiguredImageTest { file -> - service.setConfiguredLogo(file) + logic.setConfiguredLogo(file) verify { - fileService.write(file, any(), true) + fileLogic.write(file, any(), true) } } } @@ -376,10 +375,10 @@ class ConfigurationServiceTest { val type = ConfigurationType.INSTANCE_LOGO_SET setConfiguredImageTest { file -> - service.setConfiguredLogo(file) + logic.setConfiguredLogo(file) verify { - service.set(match { + logic.set(match { it.key == type.key && it.content == true.toString() }) } diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/InventoryServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogicTest.kt similarity index 77% rename from src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/InventoryServiceTest.kt rename to src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogicTest.kt index 98eb32e..be94bf5 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/InventoryServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogicTest.kt @@ -1,4 +1,4 @@ -package dev.fyloz.colorrecipesexplorer.service +package dev.fyloz.colorrecipesexplorer.logic import com.nhaarman.mockitokotlin2.* import dev.fyloz.colorrecipesexplorer.model.* @@ -10,14 +10,14 @@ import kotlin.test.assertEquals import kotlin.test.assertTrue @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class InventoryServiceTest { - private val materialService: MaterialService = mock() - private val mixService: MixService = mock() - private val service = spy(InventoryServiceImpl(materialService, mixService)) +class InventoryLogicTest { + private val materialLogic: MaterialLogic = mock() + private val mixLogic: MixLogic = mock() + private val logic = spy(DefaultInventoryLogic(materialLogic, mixLogic)) @AfterEach fun afterEach() { - reset(materialService, service) + reset(materialLogic, logic) } // add() @@ -32,13 +32,13 @@ class InventoryServiceTest { ) val storedQuantity = 2000f - doAnswer { storedQuantity + (it.arguments[0] as MaterialQuantityDto).quantity }.whenever(service) + doAnswer { storedQuantity + (it.arguments[0] as MaterialQuantityDto).quantity }.whenever(logic) .add(any()) - val found = service.add(materialQuantities) + val found = logic.add(materialQuantities) materialQuantities.forEach { - verify(service).add(it) + verify(logic).add(it) assertTrue { found.any { updated -> updated.material == it.material && updated.quantity == storedQuantity + it.quantity } } } } @@ -47,11 +47,11 @@ class InventoryServiceTest { fun `add(materialQuantity) updates material's quantity`() { withGivenQuantities(0f, 1000f) { val updatedQuantity = it + this.quantity - whenever(materialService.updateQuantity(any(), eq(this.quantity))).doReturn(updatedQuantity) + whenever(materialLogic.updateQuantity(any(), eq(this.quantity))).doReturn(updatedQuantity) - val found = service.add(this) + val found = logic.add(this) - verify(materialService).updateQuantity( + verify(materialLogic).updateQuantity( argThat { this.id == this@withGivenQuantities.material }, eq(this.quantity) ) @@ -78,16 +78,16 @@ class InventoryServiceTest { 1L to 750f ) - whenever(mixService.getById(mix.id!!)).doReturn(mix) + whenever(mixLogic.getById(mix.id!!)).doReturn(mix) doAnswer { (it.arguments[0] as Collection).map { materialQuantity -> materialQuantityDto(materialId = materialQuantity.material, quantity = 0f) } - }.whenever(service).deduct(any>()) + }.whenever(logic).deduct(any>()) - val found = service.deductMix(mixRatio) + val found = logic.deductMix(mixRatio) - verify(service).deduct(argThat> { + verify(logic).deduct(argThat> { this.all { it.quantity == expectedQuantities[it.material] } }) @@ -106,13 +106,13 @@ class InventoryServiceTest { ) val storedQuantity = 5000f - doAnswer { storedQuantity - (it.arguments[0] as MaterialQuantityDto).quantity }.whenever(service) + doAnswer { storedQuantity - (it.arguments[0] as MaterialQuantityDto).quantity }.whenever(logic) .deduct(any()) - val found = service.deduct(materialQuantities) + val found = logic.deduct(materialQuantities) materialQuantities.forEach { - verify(service).deduct(it) + verify(logic).deduct(it) assertTrue { found.any { updated -> updated.material == it.material && updated.quantity == storedQuantity - it.quantity } } } } @@ -131,18 +131,18 @@ class InventoryServiceTest { withGivenQuantities(inventoryQuantity, it) } - assertThrows { service.deduct(materialQuantities) } + assertThrows { logic.deduct(materialQuantities) } } @Test fun `deduct(materialQuantity) updates material's quantity`() { withGivenQuantities(5000f, 1000f) { val updatedQuantity = it - this.quantity - whenever(materialService.updateQuantity(any(), eq(-this.quantity))).doReturn(updatedQuantity) + whenever(materialLogic.updateQuantity(any(), eq(-this.quantity))).doReturn(updatedQuantity) - val found = service.deduct(this) + val found = logic.deduct(this) - verify(materialService).updateQuantity( + verify(materialLogic).updateQuantity( argThat { this.id == this@withGivenQuantities.material }, eq(-this.quantity) ) @@ -153,7 +153,7 @@ class InventoryServiceTest { @Test fun `deduct(materialQuantity) throws NotEnoughInventoryException when there is not enough inventory of the given material`() { withGivenQuantities(0f, 1000f) { - assertThrows { service.deduct(this) } + assertThrows { logic.deduct(this) } } } @@ -175,7 +175,7 @@ class InventoryServiceTest { ) { val material = material(id = materialQuantity.material, inventoryQuantity = stored) - whenever(materialService.getById(material.id!!)).doReturn(material) + whenever(materialLogic.getById(material.id!!)).doReturn(material) materialQuantity.test(stored) } diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/JwtServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/JwtLogicTest.kt similarity index 90% rename from src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/JwtServiceTest.kt rename to src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/JwtLogicTest.kt index 8a0a241..8d99969 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/JwtServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/JwtLogicTest.kt @@ -1,26 +1,25 @@ -package dev.fyloz.colorrecipesexplorer.service +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.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.service.users.JwtServiceImpl -import dev.fyloz.colorrecipesexplorer.service.users.jwtClaimUser 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.spyk -import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import java.time.Instant import kotlin.test.assertEquals import kotlin.test.assertTrue -class JwtServiceTest { +class JwtLogicTest { private val objectMapper = jacksonObjectMapper() private val securityProperties = CreSecurityProperties().apply { jwtSecret = "XRRm7OflmFuCrOB2Xvmfsercih9DCKom" @@ -33,7 +32,7 @@ class JwtServiceTest { .build() } - private val jwtService = spyk(JwtServiceImpl(objectMapper, securityProperties)) + private val jwtService = spyk(DefaultJwtLogic(objectMapper, securityProperties)) private val user = user() private val userOutputDto = user.toOutputDto() diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogicTest.kt similarity index 79% rename from src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialServiceTest.kt rename to src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogicTest.kt index be7c476..ee326cb 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogicTest.kt @@ -1,10 +1,10 @@ -package dev.fyloz.colorrecipesexplorer.service +package dev.fyloz.colorrecipesexplorer.logic import com.nhaarman.mockitokotlin2.* import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException +import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.repository.MaterialRepository -import dev.fyloz.colorrecipesexplorer.service.files.WriteableFileService import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance @@ -15,15 +15,15 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class MaterialServiceTest : - AbstractExternalNamedModelServiceTest() { +class MaterialLogicTest : + AbstractExternalNamedModelServiceTest() { override val repository: MaterialRepository = mock() - private val recipeService: RecipeService = mock() - private val mixService: MixService = mock() - private val materialTypeService: MaterialTypeService = mock() - private val fileService: WriteableFileService = mock() - override val service: MaterialService = - spy(MaterialServiceImpl(repository, recipeService, mixService, materialTypeService, fileService, mock())) + private val recipeService: RecipeLogic = mock() + private val mixService: MixLogic = mock() + private val materialTypeService: MaterialTypeLogic = mock() + private val fileService: WriteableFileLogic = mock() + override val logic: MaterialLogic = + spy(DefaultMaterialLogic(repository, recipeService, mixService, materialTypeService, fileService, mock())) override val entity: Material = material(id = 0L, name = "material") private val entityOutput = materialOutputDto(entity) @@ -46,7 +46,7 @@ class MaterialServiceTest : fun `existsByMaterialType() returns true when a material with the given material type exists in the repository`() { whenever(repository.existsByMaterialType(materialType)).doReturn(true) - val found = service.existsByMaterialType(materialType) + val found = logic.existsByMaterialType(materialType) assertTrue(found) } @@ -55,7 +55,7 @@ class MaterialServiceTest : fun `existsByMaterialType() returns false when no material with the given material type exists in the repository`() { whenever(repository.existsByMaterialType(materialType)).doReturn(false) - val found = service.existsByMaterialType(materialType) + val found = logic.existsByMaterialType(materialType) assertFalse(found) } @@ -65,9 +65,9 @@ class MaterialServiceTest : @Test fun `hasSimdut() returns false when simdutService_exists() returns false`() { whenever(fileService.exists(any())).doReturn(false) - doReturn(entity).whenever(service).getById(entity.id!!) + doReturn(entity).whenever(logic).getById(entity.id!!) - val found = service.hasSimdut(entity) + val found = logic.hasSimdut(entity) assertFalse(found) } @@ -75,9 +75,9 @@ class MaterialServiceTest : @Test fun `hasSimdut() returns true when simdutService_exists() returns true`() { whenever(fileService.exists(any())).doReturn(true) - doReturn(entity).whenever(service).getById(entity.id!!) + doReturn(entity).whenever(logic).getById(entity.id!!) - val found = service.hasSimdut(entity) + val found = logic.hasSimdut(entity) assertTrue(found) } @@ -90,9 +90,9 @@ class MaterialServiceTest : val mixTypeMaterialOutput = materialOutputDto(mixTypeMaterial) val materialList = listOf(entity, mixTypeMaterial) - doReturn(materialList).whenever(service).getAll() + doReturn(materialList).whenever(logic).getAll() - val found = service.getAllNotMixType() + val found = logic.getAllNotMixType() assertTrue(found.contains(entityOutput)) assertFalse(found.contains(mixTypeMaterialOutput)) @@ -102,15 +102,15 @@ class MaterialServiceTest : @Test fun `save() throws AlreadyExistsException when a material with the given name exists in the repository`() { - doReturn(true).whenever(service).existsByName(entity.name) + doReturn(true).whenever(logic).existsByName(entity.name) - assertThrows { service.save(entity) } + assertThrows { logic.save(entity) } .assertErrorCode("name") } @Test override fun `save(dto) calls and returns save() with the created entity`() { - withBaseSaveDtoTest(entity, entitySaveDto, service, { any() }) + withBaseSaveDtoTest(entity, entitySaveDto, logic, { any() }) } @Test @@ -119,9 +119,9 @@ class MaterialServiceTest : val materialSaveDto = spy(materialSaveDto(simdutFile = mockMultipartFile)) doReturn(false).whenever(mockMultipartFile).isEmpty - doReturn(entity).whenever(service).save(any()) + doReturn(entity).whenever(logic).save(any()) - service.save(materialSaveDto) + logic.save(materialSaveDto) verify(fileService).write(mockMultipartFile, entity.simdutFilePath, false) } @@ -134,9 +134,9 @@ class MaterialServiceTest : val anotherMaterial = material(id = 1L, name = "name") whenever(repository.findByName(material.name)).doReturn(anotherMaterial) - doReturn(entity).whenever(service).getById(material.id!!) + doReturn(entity).whenever(logic).getById(material.id!!) - assertThrows { service.update(material) } + assertThrows { logic.update(material) } .assertErrorCode("name") } @@ -145,11 +145,11 @@ class MaterialServiceTest : val mockSimdutFile = MockMultipartFile("simdut", byteArrayOf(1, 2, 3, 4, 5)) val materialUpdateDto = spy(materialUpdateDto(id = 0L, simdutFile = mockSimdutFile)) - doReturn(entity).whenever(service).getById(any()) - doReturn(entity).whenever(service).update(any()) + doReturn(entity).whenever(logic).getById(any()) + doReturn(entity).whenever(logic).update(any()) doReturn(entity).whenever(materialUpdateDto).toEntity() - service.update(materialUpdateDto) + logic.update(materialUpdateDto) verify(fileService).write(mockSimdutFile, entity.simdutFilePath, true) } @@ -162,7 +162,7 @@ class MaterialServiceTest : val quantity = 1234f val totalQuantity = material.inventoryQuantity + quantity - val found = service.updateQuantity(material, quantity) + val found = logic.updateQuantity(material, quantity) verify(repository).updateInventoryQuantityById(material.id!!, totalQuantity) assertEquals(totalQuantity, found) @@ -180,9 +180,9 @@ class MaterialServiceTest : recipe(id = 0L, mixes = mutableListOf(mix(mixType = mixType(id = 0L, material = mixTypeMaterial)))) whenever(recipeService.getById(recipe.id!!)).doReturn(recipe) - doReturn(materials).whenever(service).getAll() + doReturn(materials).whenever(logic).getAll() - val found = service.getAllForMixCreation(recipe.id!!) + val found = logic.getAllForMixCreation(recipe.id!!) assertTrue(materialOutputDto(normalMaterial) in found) assertTrue(materialOutputDto(mixTypeMaterial) in found) @@ -202,9 +202,9 @@ class MaterialServiceTest : recipe.mixes.add(mix) whenever(mixService.getById(mix.id!!)).doReturn(mix) - doReturn(materials).whenever(service).getAll() + doReturn(materials).whenever(logic).getAll() - val found = service.getAllForMixUpdate(mix.id!!) + val found = logic.getAllForMixUpdate(mix.id!!) assertTrue(materialOutputDto(normalMaterial) in found) assertTrue(materialOutputDto(mixTypeMaterial) in found) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogicTest.kt similarity index 77% rename from src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeServiceTest.kt rename to src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogicTest.kt index ea0a5f4..18611a6 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogicTest.kt @@ -1,4 +1,4 @@ -package dev.fyloz.colorrecipesexplorer.service +package dev.fyloz.colorrecipesexplorer.logic import com.nhaarman.mockitokotlin2.* import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException @@ -16,11 +16,11 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class MaterialTypeServiceTest : - AbstractExternalNamedModelServiceTest() { +class MaterialTypeLogicTest : + AbstractExternalNamedModelServiceTest() { override val repository: MaterialTypeRepository = mock() - private val materialService: MaterialService = mock() - override val service: MaterialTypeService = spy(MaterialTypeServiceImpl(repository, materialService)) + private val materialService: MaterialLogic = mock() + override val logic: MaterialTypeLogic = spy(DefaultMaterialTypeLogic(repository, materialService)) override val entity: MaterialType = materialType(id = 0L, name = "material type", prefix = "MAT") override val anotherEntity: MaterialType = materialType(id = 1L, name = "another material type", prefix = "AMT") override val entityWithEntityName: MaterialType = materialType(2L, name = entity.name, prefix = "EEN") @@ -42,7 +42,7 @@ class MaterialTypeServiceTest : fun `existsByPrefix() returns true when a material type with the given prefix exists`() { whenever(repository.existsByPrefix(entity.prefix)).doReturn(true) - val found = service.existsByPrefix(entity.prefix) + val found = logic.existsByPrefix(entity.prefix) assertTrue(found) } @@ -51,7 +51,7 @@ class MaterialTypeServiceTest : fun `existsByPrefix() returns false when no material type with the given prefix exists`() { whenever(repository.existsByPrefix(entity.prefix)).doReturn(false) - val found = service.existsByPrefix(entity.prefix) + val found = logic.existsByPrefix(entity.prefix) assertFalse(found) } @@ -62,7 +62,7 @@ class MaterialTypeServiceTest : fun `isUsedByMaterial() returns true when materialService_existsByMaterialType() returns true`() { whenever(materialService.existsByMaterialType(entity)).doReturn(true) - val found = service.isUsedByMaterial(entity) + val found = logic.isUsedByMaterial(entity) assertTrue(found) } @@ -71,7 +71,7 @@ class MaterialTypeServiceTest : fun `isUsedByMaterial() returns false when materialService_existsByMaterialType() returns false`() { whenever(materialService.existsByMaterialType(entity)).doReturn(false) - val found = service.isUsedByMaterial(entity) + val found = logic.isUsedByMaterial(entity) assertFalse(found) } @@ -82,7 +82,7 @@ class MaterialTypeServiceTest : fun `getAllSystemTypes() returns all system types`() { whenever(repository.findAllBySystemTypeIs(true)).doReturn(listOf(systemType, anotherSystemType)) - val found = service.getAllSystemTypes() + val found = logic.getAllSystemTypes() assertTrue(found.contains(systemType)) assertTrue(found.contains(anotherSystemType)) @@ -94,7 +94,7 @@ class MaterialTypeServiceTest : fun `getAllNonSystemTypes() returns all non system types`() { whenever(repository.findAllBySystemTypeIs(false)).doReturn(listOf(entity, anotherEntity)) - val found = service.getAllNonSystemType() + val found = logic.getAllNonSystemType() assertTrue(found.contains(entity)) assertTrue(found.contains(anotherEntity)) @@ -104,16 +104,16 @@ class MaterialTypeServiceTest : @Test override fun `save(dto) calls and returns save() with the created entity`() { - withBaseSaveDtoTest(entity, entitySaveDto, service) + withBaseSaveDtoTest(entity, entitySaveDto, logic) } // saveMaterialType() @Test fun `saveMaterialType() throws AlreadyExistsException when a material type with the given prefix already exists`() { - doReturn(true).whenever(service).existsByPrefix(entity.prefix) + doReturn(true).whenever(logic).existsByPrefix(entity.prefix) - assertThrows { service.save(entity) } + assertThrows { logic.save(entity) } .assertErrorCode("prefix") } @@ -121,16 +121,16 @@ class MaterialTypeServiceTest : @Test override fun `update(dto) calls and returns update() with the created entity`() = - withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() }) + withBaseUpdateDtoTest(entity, entityUpdateDto, logic, { any() }) override fun `update() saves in the repository and returns the updated value`() { whenever(repository.save(entity)).doReturn(entity) whenever(repository.findByName(entity.name)).doReturn(null) whenever(repository.findByPrefix(entity.prefix)).doReturn(null) - doReturn(true).whenever(service).existsById(entity.id!!) - doReturn(entity).whenever(service).getById(entity.id!!) + doReturn(true).whenever(logic).existsById(entity.id!!) + doReturn(entity).whenever(logic).getById(entity.id!!) - val found = service.update(entity) + val found = logic.update(entity) verify(repository).save(entity) assertEquals(entity, found) @@ -139,20 +139,20 @@ class MaterialTypeServiceTest : override fun `update() throws NotFoundException when no entity with the given id exists in the repository`() { whenever(repository.findByName(entity.name)).doReturn(null) whenever(repository.findByPrefix(entity.prefix)).doReturn(null) - doReturn(false).whenever(service).existsById(entity.id!!) - doReturn(null).whenever(service).getById(entity.id!!) + doReturn(false).whenever(logic).existsById(entity.id!!) + doReturn(null).whenever(logic).getById(entity.id!!) - assertThrows { service.update(entity) } + assertThrows { logic.update(entity) } .assertErrorCode() } override fun `update() throws AlreadyExistsException when an entity with the updated name exists`() { whenever(repository.findByName(entity.name)).doReturn(entityWithEntityName) whenever(repository.findByPrefix(entity.prefix)).doReturn(null) - doReturn(true).whenever(service).existsById(entity.id!!) - doReturn(entity).whenever(service).getById(entity.id!!) + doReturn(true).whenever(logic).existsById(entity.id!!) + doReturn(entity).whenever(logic).getById(entity.id!!) - assertThrows { service.update(entity) } + assertThrows { logic.update(entity) } .assertErrorCode("name") } @@ -160,9 +160,9 @@ class MaterialTypeServiceTest : fun `update() throws AlreadyExistsException when an entity with the updated prefix exists`() { val anotherMaterialType = materialType(prefix = entity.prefix) whenever(repository.findByPrefix(entity.prefix)).doReturn(anotherMaterialType) - doReturn(entity).whenever(service).getById(entity.id!!) + doReturn(entity).whenever(logic).getById(entity.id!!) - assertThrows { service.update(entity) } + assertThrows { logic.update(entity) } .assertErrorCode("prefix") } @@ -170,7 +170,7 @@ class MaterialTypeServiceTest : fun `update() throws CannotUpdateException when updating a system material type`() { whenever(repository.existsByIdAndSystemTypeIsTrue(systemType.id!!)).doReturn(true) - assertThrows { service.update(systemType) } + assertThrows { logic.update(systemType) } } // delete() @@ -179,7 +179,7 @@ class MaterialTypeServiceTest : fun `delete() throws CannotDeleteException when deleting a system material type`() { whenever(repository.existsByIdAndSystemTypeIsTrue(systemType.id!!)).doReturn(true) - assertThrows { service.delete(systemType) } + assertThrows { logic.delete(systemType) } } override fun `delete() deletes in the repository`() { diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MixServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogicTest.kt similarity index 82% rename from src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MixServiceTest.kt rename to src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogicTest.kt index 707da9f..fba155e 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MixServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogicTest.kt @@ -1,4 +1,4 @@ -package dev.fyloz.colorrecipesexplorer.service +package dev.fyloz.colorrecipesexplorer.logic import com.nhaarman.mockitokotlin2.* import dev.fyloz.colorrecipesexplorer.model.* @@ -10,14 +10,14 @@ import kotlin.test.assertEquals import kotlin.test.assertTrue @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class MixServiceTest : AbstractExternalModelServiceTest() { +class MixLogicTest : AbstractExternalModelServiceTest() { override val repository: MixRepository = mock() - private val recipeService: RecipeService = mock() - private val materialTypeService: MaterialTypeService = mock() - private val mixMaterialService: MixMaterialService = mock() - private val mixTypeService: MixTypeService = mock() - override val service: MixService = - spy(MixServiceImpl(repository, recipeService, materialTypeService, mixMaterialService, mixTypeService)) + private val recipeService: RecipeLogic = mock() + private val materialTypeService: MaterialTypeLogic = mock() + private val mixMaterialService: MixMaterialLogic = mock() + private val mixTypeService: MixTypeLogic = mock() + override val logic: MixLogic = + spy(DefaultMixLogic(repository, recipeService, materialTypeService, mixMaterialService, mixTypeService)) override val entity: Mix = mix(id = 0L, location = "location") override val anotherEntity: Mix = mix(id = 1L) @@ -39,7 +39,7 @@ class MixServiceTest : AbstractExternalModelServiceTest()) + doReturn(true).whenever(logic).existsById(mixWithId.id!!) + doReturn(mixWithId).whenever(logic).save(any()) - val found = service.save(entitySaveDto) + val found = logic.save(entitySaveDto) - verify(service).save(argThat { this.recipe == mix.recipe }) + verify(logic).save(argThat { this.recipe == mix.recipe }) verify(recipeService).addMix(recipe, mixWithId) // Verify if this method is called instead of the MixType's constructor, which does not check if the name is already taken by a material. @@ -92,10 +92,10 @@ class MixServiceTest : AbstractExternalModelServiceTest Unit ) { with(scope) { - doReturn(true).whenever(service).existsById(mix.id!!) - doReturn(mix).whenever(service).getById(mix.id!!) - doReturn(sharedMixType).whenever(service).mixTypeIsShared(mix.mixType) - doAnswer { it.arguments[0] }.whenever(service).update(any()) + doReturn(true).whenever(logic).existsById(mix.id!!) + doReturn(mix).whenever(logic).getById(mix.id!!) + doReturn(sharedMixType).whenever(logic).mixTypeIsShared(mix.mixType) + doAnswer { it.arguments[0] }.whenever(logic).update(any()) if (mixUpdateDto.materialTypeId != null) { whenever(materialTypeService.getById(materialType.id!!)).doReturn(materialType) @@ -115,12 +115,12 @@ class MixServiceTest : AbstractExternalModelServiceTest() { +class MixMaterialLogicTest : AbstractModelServiceTest() { override val repository: MixMaterialRepository = mock() - private val materialService: MaterialService = mock() - override val service: MixMaterialService = spy(MixMaterialServiceImpl(repository, materialService)) + private val materialService: MaterialLogic = mock() + override val logic: MixMaterialLogic = spy(DefaultMixMaterialLogic(repository, materialService)) private val material: Material = material(id = 0L) override val entity: MixMaterial = mixMaterial(id = 0L, material = material, quantity = 1000f) @@ -27,7 +27,7 @@ class MixMaterialServiceTest : AbstractModelServiceTest()) + }.whenever(logic).create(any()) - val found = service.create(mixMaterialDtos) + val found = logic.create(mixMaterialDtos) mixMaterialDtos.forEach { dto -> - verify(service).create(dto) + verify(logic).create(dto) assertTrue { found.any { it.material.id == dto.materialId && it.quantity == dto.quantity && it.position == dto.position @@ -80,7 +80,7 @@ class MixMaterialServiceTest : AbstractModelServiceTest { - service.validateMixMaterials(mixMaterials) + logic.validateMixMaterials(mixMaterials) } } private fun assertInvalidMixMaterialsPositionsException(mixMaterials: Set, errorType: String) { val exception = assertThrows { - service.validateMixMaterials(mixMaterials) + logic.validateMixMaterials(mixMaterials) } assertTrue { exception.errors.size == 1 } diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogicTest.kt similarity index 72% rename from src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeServiceTest.kt rename to src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogicTest.kt index 4e6f246..6d38923 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogicTest.kt @@ -1,4 +1,4 @@ -package dev.fyloz.colorrecipesexplorer.service +package dev.fyloz.colorrecipesexplorer.logic import com.nhaarman.mockitokotlin2.* import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException @@ -13,11 +13,10 @@ import kotlin.test.assertEquals import kotlin.test.assertTrue @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class MixTypeServiceTest : AbstractNamedModelServiceTest() { +class MixTypeLogicTest : AbstractNamedModelServiceTest() { override val repository: MixTypeRepository = mock() - private val materialService: MaterialService = mock() - private val mixService: MixService = mock() - override val service: MixTypeService = spy(MixTypeServiceImpl(repository, materialService, mixService)) + private val materialService: MaterialLogic = mock() + override val logic: MixTypeLogic = spy(DefaultMixTypeLogic(repository, materialService)) private val materialType: MaterialType = materialType() private val material: Material = material(id = 0L, materialType = materialType) @@ -38,7 +37,7 @@ class MixTypeServiceTest : AbstractNamedModelServiceTest { service.getByMaterial(material) } + assertThrows { logic.getByMaterial(material) } .assertErrorCode("name") } @@ -69,7 +68,7 @@ class MixTypeServiceTest : AbstractNamedModelServiceTest { service.save(entity) } + assertThrows { logic.save(entity) } .assertErrorCode("name") } @@ -116,11 +115,11 @@ class MixTypeServiceTest : AbstractNamedModelServiceTest() { +class RecipeLogicTest : + AbstractExternalModelServiceTest() { override val repository: RecipeRepository = mock() - private val companyService: CompanyService = mock() - private val mixService: MixService = mock() - private val groupService: GroupService = mock() - private val recipeStepService: RecipeStepService = mock() - private val configService: ConfigurationService = mock() - override val service: RecipeService = + private val companyLogic: CompanyLogic = mock() + private val mixService: MixLogic = mock() + private val groupService: GroupLogic = mock() + private val recipeStepService: RecipeStepLogic = mock() + private val configService: ConfigurationLogic = mock() + override val logic: RecipeLogic = spy( - RecipeServiceImpl( + DefaultRecipeLogic( repository, - companyService, + companyLogic, mixService, recipeStepService, groupService, @@ -51,7 +51,7 @@ class RecipeServiceTest : @AfterEach override fun afterEach() { - reset(companyService, mixService) + reset(companyLogic, mixService) super.afterEach() } @@ -61,7 +61,7 @@ class RecipeServiceTest : fun `existsByCompany() returns true when at least one recipe exists for the given company`() { whenever(repository.existsByCompany(company)).doReturn(true) - val found = service.existsByCompany(company) + val found = logic.existsByCompany(company) assertTrue(found) } @@ -70,7 +70,7 @@ class RecipeServiceTest : fun `existsByCompany() returns false when no recipe exists for the given company`() { whenever(repository.existsByCompany(company)).doReturn(false) - val found = service.existsByCompany(company) + val found = logic.existsByCompany(company) assertFalse(found) } @@ -82,7 +82,7 @@ class RecipeServiceTest : setOf(true, false).forEach { whenever(repository.existsByNameAndCompany(entity.name, company)).doReturn(it) - val exists = service.existsByNameAndCompany(entity.name, company) + val exists = logic.existsByNameAndCompany(entity.name, company) assertEquals(it, exists) } @@ -97,7 +97,7 @@ class RecipeServiceTest : whenever(configService.getContent(ConfigurationType.RECIPE_APPROBATION_EXPIRATION)).doReturn(period.toString()) - val approbationExpired = service.isApprobationExpired(recipe) + val approbationExpired = logic.isApprobationExpired(recipe) assertNotNull(approbationExpired) assertFalse(approbationExpired) @@ -110,7 +110,7 @@ class RecipeServiceTest : whenever(configService.getContent(ConfigurationType.RECIPE_APPROBATION_EXPIRATION)).doReturn(period.toString()) - val approbationExpired = service.isApprobationExpired(recipe) + val approbationExpired = logic.isApprobationExpired(recipe) assertNotNull(approbationExpired) assertTrue(approbationExpired) @@ -123,7 +123,7 @@ class RecipeServiceTest : whenever(configService.getContent(ConfigurationType.RECIPE_APPROBATION_EXPIRATION)).doReturn(period.toString()) - val approbationExpired = service.isApprobationExpired(recipe) + val approbationExpired = logic.isApprobationExpired(recipe) assertNull(approbationExpired) } @@ -136,7 +136,7 @@ class RecipeServiceTest : whenever(repository.findAllByName(entity.name)).doReturn(recipes) - val found = service.getAllByName(entity.name) + val found = logic.getAllByName(entity.name) assertEquals(recipes, found) } @@ -148,7 +148,7 @@ class RecipeServiceTest : val companies = listOf(entity, anotherEntity) whenever(repository.findAllByCompany(company)).doReturn(companies) - val found = service.getAllByCompany(company) + val found = logic.getAllByCompany(company) assertEquals(companies, found) } @@ -157,17 +157,17 @@ class RecipeServiceTest : @Test override fun `save(dto) calls and returns save() with the created entity`() { - whenever(companyService.getById(company.id!!)).doReturn(company) - doReturn(false).whenever(service).existsByNameAndCompany(entity.name, company) - withBaseSaveDtoTest(entity, entitySaveDto, service, { argThat { this.id == null && this.color == color } }) + whenever(companyLogic.getById(company.id!!)).doReturn(company) + doReturn(false).whenever(logic).existsByNameAndCompany(entity.name, company) + withBaseSaveDtoTest(entity, entitySaveDto, logic, { argThat { this.id == null && this.color == color } }) } @Test fun `save(dto) throw AlreadyExistsException when a recipe with the given name and company exists in the repository`() { - whenever(companyService.getById(company.id!!)).doReturn(company) - doReturn(true).whenever(service).existsByNameAndCompany(entity.name, company) + whenever(companyLogic.getById(company.id!!)).doReturn(company) + doReturn(true).whenever(logic).existsByNameAndCompany(entity.name, company) - with(assertThrows { service.save(entitySaveDto) }) { + with(assertThrows { logic.save(entitySaveDto) }) { this.assertErrorCode("company-name") } } @@ -176,19 +176,19 @@ class RecipeServiceTest : @Test override fun `update(dto) calls and returns update() with the created entity`() { - doReturn(false).whenever(service).existsByNameAndCompany(entity.name, company) - withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() }) + doReturn(false).whenever(logic).existsByNameAndCompany(entity.name, company) + withBaseUpdateDtoTest(entity, entityUpdateDto, logic, { any() }) } @Test fun `update(dto) throws AlreadyExistsException when a recipe exists for the given name and company`() { val name = "another recipe" - doReturn(entity).whenever(service).getById(entity.id!!) - doReturn(true).whenever(service).existsByNameAndCompany(name, company) + doReturn(entity).whenever(logic).getById(entity.id!!) + doReturn(true).whenever(logic).existsByNameAndCompany(name, company) doReturn(name).whenever(entityUpdateDto).name - with(assertThrows { service.update(entityUpdateDto) }) { + with(assertThrows { logic.update(entityUpdateDto) }) { this.assertErrorCode("company-name") } } @@ -210,12 +210,12 @@ class RecipeServiceTest : ) val publicData = recipePublicDataDto(recipeId = recipe.id!!, notes = notes) - doReturn(recipe).whenever(service).getById(recipe.id!!) - doAnswer { it.arguments[0] }.whenever(service).update(any()) + doReturn(recipe).whenever(logic).getById(recipe.id!!) + doAnswer { it.arguments[0] }.whenever(logic).update(any()) - service.updatePublicData(publicData) + logic.updatePublicData(publicData) - verify(service).update(argThat { + verify(logic).update(argThat { assertTrue { this.groupsInformation.first { it.group.id == 1L }.note == notes.first { it.groupId == 1L }.content } assertTrue { this.groupsInformation.first { it.group.id == 2L }.note == notes.first { it.groupId == 2L }.content } assertTrue { this.groupsInformation.any { it.group.id == 3L } && this.groupsInformation.first { it.group.id == 3L }.note == null } @@ -225,7 +225,7 @@ class RecipeServiceTest : } @Test - fun `updatePublicData() update the location of a recipe mixes in the mix service according to the RecipePublicDataDto`() { + fun `updatePublicData() update the location of a recipe mixes in the mix logic according to the RecipePublicDataDto`() { val publicData = recipePublicDataDto( mixesLocation = setOf( mixLocationDto(mixId = 0L, location = "Loc 1"), @@ -233,10 +233,10 @@ class RecipeServiceTest : ) ) - service.updatePublicData(publicData) + logic.updatePublicData(publicData) verify(mixService).updateLocations(publicData.mixesLocation!!) - verify(service, times(0)).update(any()) + verify(logic, times(0)).update(any()) } // addMix() @@ -246,11 +246,11 @@ class RecipeServiceTest : val mix = mix(id = 0L) val recipe = recipe(id = 0L, mixes = mutableListOf()) - doAnswer { it.arguments[0] }.whenever(service).update(any()) + doAnswer { it.arguments[0] }.whenever(logic).update(any()) - val found = service.addMix(recipe, mix) + val found = logic.addMix(recipe, mix) - verify(service).update(any()) + verify(logic).update(any()) assertEquals(recipe.id, found.id) assertTrue(found.mixes.contains(mix)) @@ -264,11 +264,11 @@ class RecipeServiceTest : val mix = mix(id = 0L, recipe = recipe) recipe.mixes.add(mix) - doAnswer { it.arguments[0] }.whenever(service).update(any()) + doAnswer { it.arguments[0] }.whenever(logic).update(any()) - val found = service.removeMix(mix) + val found = logic.removeMix(mix) - verify(service).update(any()) + verify(logic).update(any()) assertEquals(recipe.id, found.id) assertFalse(found.mixes.contains(mix)) @@ -276,11 +276,11 @@ class RecipeServiceTest : } private class RecipeImageServiceTestContext { - val fileService = mockk { + val fileService = mockk { every { write(any(), any(), any()) } just Runs every { delete(any()) } just Runs } - val recipeImageService = spyk(RecipeImageServiceImpl(fileService)) + val recipeImageService = spyk(DefaultRecipeImageLogic(fileService)) val recipe = spyk(recipe()) val recipeImagesIds = setOf(1L, 10L, 21L) val recipeImagesNames = recipeImagesIds.map { it.imageName }.toSet() @@ -293,7 +293,7 @@ private class RecipeImageServiceTestContext { get() = "${recipe.imagesDirectoryPath}/$this$RECIPE_IMAGE_EXTENSION" } -class RecipeImageServiceTest { +class RecipeImageLogicTest { @AfterEach internal fun afterEach() { clearAllMocks() diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogicTest.kt similarity index 87% rename from src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepServiceTest.kt rename to src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogicTest.kt index 31933b0..43b2767 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogicTest.kt @@ -1,4 +1,4 @@ -package dev.fyloz.colorrecipesexplorer.service +package dev.fyloz.colorrecipesexplorer.logic import com.nhaarman.mockitokotlin2.* import dev.fyloz.colorrecipesexplorer.model.RecipeGroupInformation @@ -13,10 +13,10 @@ import org.junit.jupiter.api.assertThrows import kotlin.test.assertTrue @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class RecipeStepServiceTest : - AbstractModelServiceTest() { +class RecipeStepLogicTest : + AbstractModelServiceTest() { override val repository: RecipeStepRepository = mock() - override val service: RecipeStepService = spy(RecipeStepServiceImpl(repository)) + override val logic: RecipeStepLogic = spy(DefaultRecipeStepLogic(repository)) override val entity: RecipeStep = recipeStep(id = 0L, message = "message") override val anotherEntity: RecipeStep = recipeStep(id = 1L, message = "another message") @@ -26,19 +26,19 @@ class RecipeStepServiceTest : @Test fun `validateGroupInformationSteps() calls validateSteps() with the given RecipeGroupInformation steps`() { withGroupInformation { - service.validateGroupInformationSteps(this) + logic.validateGroupInformationSteps(this) - verify(service).validateSteps(this.steps!!) + verify(logic).validateSteps(this.steps!!) } } @Test fun `validateGroupInformationSteps() throws InvalidGroupStepsPositionsException when validateSteps() throws an InvalidStepsPositionsException`() { withGroupInformation { - doAnswer { throw InvalidStepsPositionsException(setOf()) }.whenever(service).validateSteps(this.steps!!) + doAnswer { throw InvalidStepsPositionsException(setOf()) }.whenever(logic).validateSteps(this.steps!!) assertThrows { - service.validateGroupInformationSteps(this) + logic.validateGroupInformationSteps(this) } } } @@ -100,7 +100,7 @@ class RecipeStepServiceTest : private fun assertInvalidStepsPositionsException(steps: MutableSet, errorType: String) { val exception = assertThrows { - service.validateSteps(steps) + logic.validateSteps(steps) } assertTrue { exception.errors.size == 1 } diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/TouchUpKitServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/TouchUpKitLogicTest.kt similarity index 90% rename from src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/TouchUpKitServiceTest.kt rename to src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/TouchUpKitLogicTest.kt index 51ef288..2bf5256 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/TouchUpKitServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/TouchUpKitLogicTest.kt @@ -1,10 +1,10 @@ -package dev.fyloz.colorrecipesexplorer.service +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.service.config.ConfigurationService -import dev.fyloz.colorrecipesexplorer.service.files.WriteableFileService import dev.fyloz.colorrecipesexplorer.utils.PdfDocument import dev.fyloz.colorrecipesexplorer.utils.toByteArrayResource import io.mockk.* @@ -15,11 +15,11 @@ import kotlin.test.assertEquals private class TouchUpKitServiceTestContext { val touchUpKitRepository = mockk() - val fileService = mockk { + val fileService = mockk { every { write(any(), any(), any()) } just Runs } - val configService = mockk(relaxed = true) - val touchUpKitService = spyk(TouchUpKitServiceImpl(fileService, configService, touchUpKitRepository)) + val configService = mockk(relaxed = true) + val touchUpKitService = spyk(DefaultTouchUpKitLogic(fileService, configService, touchUpKitRepository)) val pdfDocumentData = mockk() val pdfDocument = mockk { mockkStatic(PdfDocument::toByteArrayResource) @@ -28,7 +28,7 @@ private class TouchUpKitServiceTestContext { } } -class TouchUpKitServiceTest { +class TouchUpKitLogicTest { private val job = "job" @AfterEach diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/DefaultFileCacheTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/files/DefaultFileCacheTest.kt similarity index 99% rename from src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/DefaultFileCacheTest.kt rename to src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/files/DefaultFileCacheTest.kt index c35cf96..4e79298 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/DefaultFileCacheTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/files/DefaultFileCacheTest.kt @@ -1,4 +1,4 @@ -package dev.fyloz.colorrecipesexplorer.service.files +package dev.fyloz.colorrecipesexplorer.logic.files import dev.fyloz.colorrecipesexplorer.utils.FilePath import dev.fyloz.memorycache.MemoryCache diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/files/FileLogicTest.kt similarity index 98% rename from src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileServiceTest.kt rename to src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/files/FileLogicTest.kt index 0f02ee4..ce8e5d1 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/files/FileLogicTest.kt @@ -1,4 +1,4 @@ -package dev.fyloz.colorrecipesexplorer.service.files +package dev.fyloz.colorrecipesexplorer.logic.files import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties import dev.fyloz.colorrecipesexplorer.utils.File @@ -21,11 +21,11 @@ private const val mockFilePath = "existingFile" private val mockFilePathPath = Path.of(mockFilePath) private val mockFileData = byteArrayOf(0x1, 0x8, 0xa, 0xf) -class FileServiceTest { +class FileLogicTest { private val fileCacheMock = mockk { every { setExists(any(), any()) } just runs } - private val fileService = spyk(FileServiceImpl(fileCacheMock, creProperties)) + private val fileService = spyk(DefaultFileLogic(fileCacheMock, creProperties)) private val mockFile = mockk { every { file } returns mockk() diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/ResourceFileServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/files/ResourceFileLogicTest.kt similarity index 85% rename from src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/ResourceFileServiceTest.kt rename to src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/files/ResourceFileLogicTest.kt index ac806df..801359a 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/ResourceFileServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/files/ResourceFileLogicTest.kt @@ -1,4 +1,4 @@ -package dev.fyloz.colorrecipesexplorer.service.files +package dev.fyloz.colorrecipesexplorer.logic.files import dev.fyloz.colorrecipesexplorer.utils.FilePath import io.mockk.clearAllMocks @@ -14,10 +14,10 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue -class ResourceFileServiceTest { +class ResourceFileLogicTest { private val resourceLoader = mockk() - private val service = spyk(ResourceFileService(resourceLoader)) + private val logic = spyk(ResourceFileLogic(resourceLoader)) @AfterEach fun afterEach() { @@ -26,7 +26,7 @@ class ResourceFileServiceTest { private fun existsTest(shouldExists: Boolean, test: (String) -> Unit) { val path = "unit_test_resource" - with(service) { + with(logic) { every { fullPath(path) } returns mockk { every { resource } returns mockk { every { exists() } returns shouldExists @@ -40,7 +40,7 @@ class ResourceFileServiceTest { @Test fun `exists() returns true when a resource exists at the given path`() { existsTest(true) { path -> - val found = service.exists(path) + val found = logic.exists(path) assertTrue { found } } @@ -49,7 +49,7 @@ class ResourceFileServiceTest { @Test fun `exists() returns false when no resource exists at the given path`() { existsTest(false) { path -> - val found = service.exists(path) + val found = logic.exists(path) assertFalse { found } } @@ -60,7 +60,7 @@ class ResourceFileServiceTest { every { exists() } returns shouldExists } val path = "unit_test_path" - with(service) { + with(logic) { every { fullPath(path) } returns mockk { every { resource } returns mockResource } @@ -72,7 +72,7 @@ class ResourceFileServiceTest { @Test fun `read() returns the resource at the given path`() { readTest(true) { resource, path -> - val found = service.read(path) + val found = logic.read(path) assertEquals(resource, found) } @@ -82,7 +82,7 @@ class ResourceFileServiceTest { fun `read() throws FileNotFoundException when no resource exists at the given path`() { readTest(false) { _, path -> assertThrows { - service.read(path) + logic.read(path) } } } @@ -92,7 +92,7 @@ class ResourceFileServiceTest { val path = "unit_test_path" val expectedPath = "classpath:$path" - val found = service.fullPath(path) + val found = logic.fullPath(path) assertEquals(expectedPath, found.value) } @@ -104,7 +104,7 @@ class ResourceFileServiceTest { every { resourceLoader.getResource(filePath.value) } returns resource - with(service) { + with(logic) { val found = filePath.resource assertEquals(resource, found) -- 2.40.1 From cb355c9e0d3b5ec0b1319e90387aac6a1081cfa0 Mon Sep 17 00:00:00 2001 From: FyloZ Date: Tue, 15 Feb 2022 23:34:14 -0500 Subject: [PATCH 02/11] #25 Migrate companies to new logic --- .../config/annotations/Components.kt | 15 ++ .../colorrecipesexplorer/dtos/CompanyDto.kt | 10 + .../colorrecipesexplorer/dtos/EntityDto.kt | 5 + .../logic/CompanyLogic.kt | 72 +++----- .../fyloz/colorrecipesexplorer/logic/Logic.kt | 93 ++++++++++ .../logic/{Service.kt => OldService.kt} | 34 ++-- .../colorrecipesexplorer/logic/RecipeLogic.kt | 2 +- .../colorrecipesexplorer/model/Company.kt | 95 +--------- .../colorrecipesexplorer/model/Material.kt | 5 +- .../model/MaterialType.kt | 5 +- .../fyloz/colorrecipesexplorer/model/Mix.kt | 2 +- .../colorrecipesexplorer/model/MixMaterial.kt | 3 +- .../colorrecipesexplorer/model/MixType.kt | 2 +- .../model/{Model.kt => ModelEntity.kt} | 6 +- .../colorrecipesexplorer/model/Recipe.kt | 6 +- .../colorrecipesexplorer/model/RecipeStep.kt | 2 +- .../model/account/Group.kt | 7 +- .../model/account/User.kt | 6 +- .../model/touchupkit/TouchUpKit.kt | 8 +- .../repository/CompanyRepository.kt | 17 +- .../repository/Repository.kt | 4 +- .../rest/CompanyController.kt | 37 ++-- .../colorrecipesexplorer/rest/RestUtils.kt | 17 +- .../service/CompanyService.kt | 27 +++ .../colorrecipesexplorer/service/Service.kt | 64 +++++++ .../colorrecipesexplorer/utils/Collections.kt | 6 +- .../logic/AbstractServiceTest.kt | 18 +- .../logic/BaseLogicTest.kt | 173 ++++++++++++++++++ .../logic/CompanyLogicTest.kt | 90 --------- .../logic/DefaultCompanyLogicTest.kt | 55 ++++++ .../logic/RecipeLogicTest.kt | 4 +- 31 files changed, 581 insertions(+), 309 deletions(-) create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/annotations/Components.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/CompanyDto.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/EntityDto.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/{Service.kt => OldService.kt} (79%) rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/{Model.kt => ModelEntity.kt} (69%) create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/Service.kt create mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/BaseLogicTest.kt delete mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogicTest.kt create mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultCompanyLogicTest.kt diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/annotations/Components.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/annotations/Components.kt new file mode 100644 index 0000000..c7d0927 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/annotations/Components.kt @@ -0,0 +1,15 @@ +package dev.fyloz.colorrecipesexplorer.config.annotations + +import org.springframework.stereotype.Service + +@Service +@RequireDatabase +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class ServiceComponent + +@Service +@RequireDatabase +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class LogicComponent \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/CompanyDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/CompanyDto.kt new file mode 100644 index 0000000..3596d65 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/CompanyDto.kt @@ -0,0 +1,10 @@ +package dev.fyloz.colorrecipesexplorer.dtos + +import javax.validation.constraints.NotBlank + +data class CompanyDto( + override val id: Long = 0L, + + @NotBlank + val name: String +) : EntityDto \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/EntityDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/EntityDto.kt new file mode 100644 index 0000000..43fbb19 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/EntityDto.kt @@ -0,0 +1,5 @@ +package dev.fyloz.colorrecipesexplorer.dtos + +interface EntityDto { + val id: Long +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogic.kt index 0bb2e2b..fe2b04e 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogic.kt @@ -1,50 +1,38 @@ package dev.fyloz.colorrecipesexplorer.logic -import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase -import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.repository.CompanyRepository -import org.springframework.context.annotation.Lazy -import org.springframework.stereotype.Service +import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent +import dev.fyloz.colorrecipesexplorer.dtos.CompanyDto +import dev.fyloz.colorrecipesexplorer.model.Company +import dev.fyloz.colorrecipesexplorer.service.CompanyService -interface CompanyLogic : - ExternalNamedModelService { - /** Checks if the given [company] is used by one or more recipes. */ - fun isLinkedToRecipes(company: Company): Boolean -} +interface CompanyLogic : Logic -@Service -@RequireDatabase -class DefaultCompanyLogic( - companyRepository: CompanyRepository, - @Lazy val recipeLogic: RecipeLogic -) : - AbstractExternalNamedModelService( - companyRepository - ), - CompanyLogic { - override fun idNotFoundException(id: Long) = companyIdNotFoundException(id) - override fun idAlreadyExistsException(id: Long) = companyIdAlreadyExistsException(id) - override fun nameNotFoundException(name: String) = companyNameNotFoundException(name) - override fun nameAlreadyExistsException(name: String) = companyNameAlreadyExistsException(name) +@LogicComponent +class DefaultCompanyLogic(service: CompanyService) : + BaseLogic(service, Company::class.simpleName!!), CompanyLogic { + override fun save(dto: CompanyDto): CompanyDto { + throwIfNameAlreadyExists(dto.name) - override fun Company.toOutput() = this - - override fun isLinkedToRecipes(company: Company): Boolean = recipeLogic.existsByCompany(company) - - override fun update(entity: CompanyUpdateDto): Company { - // Lazy loaded to prevent checking the database when not necessary - val persistedCompany by lazy { getById(entity.id) } - - return update(with(entity) { - company( - id = id, - name = if (name != null && name.isNotBlank()) name else persistedCompany.name - ) - }) + return super.save(dto) } - override fun delete(entity: Company) { - if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteCompany(entity) - super.delete(entity) + override fun update(dto: CompanyDto): CompanyDto { + throwIfNameAlreadyExists(dto.name, dto.id) + + return super.update(dto) } -} + + override fun deleteById(id: Long) { + if (service.recipesDependsOnCompanyById(id)) { + throw cannotDeleteException("Cannot delete the company with the id '$id' because one or more recipes depends on it") + } + + super.deleteById(id) + } + + private fun throwIfNameAlreadyExists(name: String, id: Long? = null) { + if (service.existsByName(name, id)) { + throw alreadyExistsException(value = name) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt new file mode 100644 index 0000000..647d6de --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt @@ -0,0 +1,93 @@ +package dev.fyloz.colorrecipesexplorer.logic + +import dev.fyloz.colorrecipesexplorer.dtos.EntityDto +import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException +import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException +import dev.fyloz.colorrecipesexplorer.exception.NotFoundException +import dev.fyloz.colorrecipesexplorer.service.Service + +/** + * Represents the logic for a DTO type. + * + * @param D The type of the DTO. + * @param S The service for the DTO. + */ +interface Logic> { + /** Checks if a DTO with the given [id] exists. */ + fun existsById(id: Long): Boolean + + /** Get all DTOs. */ + fun getAll(): Collection + + /** Get the DTO for the given [id]. */ + fun getById(id: Long): D + + /** Saves the given [dto]. */ + fun save(dto: D): D + + /** Updates the given [dto]. */ + fun update(dto: D): D + + /** Deletes the dto with the given [id]. */ + fun deleteById(id: Long) +} + +abstract class BaseLogic>( + protected val service: S, + protected val typeName: String +) : Logic { + protected val typeNameLowerCase = typeName.lowercase() + + override fun existsById(id: Long) = + service.existsById(id) + + override fun getAll() = + service.getAll() + + override fun getById(id: Long) = + service.getById(id) ?: throw notFoundException(value = id) + + override fun save(dto: D) = + service.save(dto) + + override fun update(dto: D): D { + if (!existsById(dto.id)) { + throw notFoundException(value = dto.id) + } + + return service.save(dto) + } + + override fun deleteById(id: Long) = + service.deleteById(id) + + protected fun notFoundException(identifierName: String = idIdentifierName, value: Any) = + NotFoundException( + typeNameLowerCase, + "$typeName not found", + "A $typeNameLowerCase with the $identifierName '$value' could not be found", + value, + identifierName + ) + + protected fun alreadyExistsException(identifierName: String = nameIdentifierName, value: Any) = + AlreadyExistsException( + typeNameLowerCase, + "$typeName already exists", + "A $typeNameLowerCase with the $identifierName '$value' already exists", + value, + identifierName + ) + + protected fun cannotDeleteException(details: String) = + CannotDeleteException( + typeNameLowerCase, + "Cannot delete $typeNameLowerCase", + details + ) + + companion object { + const val idIdentifierName = "id" + const val nameIdentifierName = "name" + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Service.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/OldService.kt similarity index 79% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Service.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/OldService.kt index 314d93b..25e08b4 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Service.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/OldService.kt @@ -3,8 +3,8 @@ package dev.fyloz.colorrecipesexplorer.logic import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.model.EntityDto -import dev.fyloz.colorrecipesexplorer.model.Model -import dev.fyloz.colorrecipesexplorer.model.NamedModel +import dev.fyloz.colorrecipesexplorer.model.ModelEntity +import dev.fyloz.colorrecipesexplorer.model.NamedModelEntity import dev.fyloz.colorrecipesexplorer.repository.NamedJpaRepository import io.jsonwebtoken.lang.Assert import org.springframework.data.jpa.repository.JpaRepository @@ -16,7 +16,7 @@ import org.springframework.data.repository.findByIdOrNull * @param E The entity type * @param R The entity repository type */ -interface Service> { +interface OldService> { val repository: R /** Gets all entities. */ @@ -32,8 +32,8 @@ interface Service> { fun delete(entity: E) } -/** A service for entities implementing the [Model] interface. This service add supports for numeric identifiers. */ -interface ModelService> : Service { +/** A service for entities implementing the [ModelEntity] interface. This service add supports for numeric identifiers. */ +interface ModelService> : OldService { /** Checks if an entity with the given [id] exists. */ fun existsById(id: Long): Boolean @@ -44,8 +44,8 @@ interface ModelService> : Service { fun deleteById(id: Long) } -/** A service for entities implementing the [NamedModel] interface. This service add supports for name identifiers. */ -interface NamedModelService> : ModelService { +/** A service for entities implementing the [NamedModelEntity] interface. This service add supports for name identifiers. */ +interface NamedModelService> : ModelService { /** Checks if an entity with the given [name] exists. */ fun existsByName(name: String): Boolean @@ -54,14 +54,14 @@ interface NamedModelService> : ModelServ } -abstract class AbstractService>(override val repository: R) : Service { +abstract class AbstractService>(override val repository: R) : OldService { override fun getAll(): Collection = repository.findAll() override fun save(entity: E): E = repository.save(entity) override fun update(entity: E): E = repository.save(entity) override fun delete(entity: E) = repository.delete(entity) } -abstract class AbstractModelService>(repository: R) : +abstract class AbstractModelService>(repository: R) : AbstractService(repository), ModelService { protected abstract fun idNotFoundException(id: Long): NotFoundException protected abstract fun idAlreadyExistsException(id: Long): AlreadyExistsException @@ -90,7 +90,7 @@ abstract class AbstractModelService>(repos } } -abstract class AbstractNamedModelService>(repository: R) : +abstract class AbstractNamedModelService>(repository: R) : AbstractModelService(repository), NamedModelService { protected abstract fun nameNotFoundException(name: String): NotFoundException protected abstract fun nameAlreadyExistsException(name: String): AlreadyExistsException @@ -126,7 +126,7 @@ abstract class AbstractNamedModelService, U : EntityDto, O, R : JpaRepository> : Service { +interface ExternalService, U : EntityDto, O, R : JpaRepository> : OldService { /** Gets all entities mapped to their output model. */ fun getAllForOutput(): Collection @@ -140,15 +140,15 @@ interface ExternalService, U : EntityDto, O, R : JpaRepos fun E.toOutput(): O } -/** An [ExternalService] for entities implementing the [Model] interface. */ -interface ExternalModelService, U : EntityDto, O, R : JpaRepository> : +/** An [ExternalService] for entities implementing the [ModelEntity] interface. */ +interface ExternalModelService, U : EntityDto, O, R : JpaRepository> : ModelService, ExternalService { /** Gets the entity with the given [id] mapped to its output model. */ fun getByIdForOutput(id: Long): O } -/** An [ExternalService] for entities implementing the [NamedModel] interface. */ -interface ExternalNamedModelService, U : EntityDto, O, R : JpaRepository> : +/** An [ExternalService] for entities implementing the [NamedModelEntity] interface. */ +interface ExternalNamedModelService, U : EntityDto, O, R : JpaRepository> : NamedModelService, ExternalModelService /** An [AbstractService] with the functionalities of a [ExternalService]. */ @@ -160,7 +160,7 @@ abstract class AbstractExternalService, U : EntityDto, O, } /** An [AbstractModelService] with the functionalities of a [ExternalService]. */ -abstract class AbstractExternalModelService, U : EntityDto, O, R : JpaRepository>( +abstract class AbstractExternalModelService, U : EntityDto, O, R : JpaRepository>( repository: R ) : AbstractModelService(repository), ExternalModelService { override fun getAllForOutput() = @@ -171,7 +171,7 @@ abstract class AbstractExternalModelService, U : Ent } /** An [AbstractNamedModelService] with the functionalities of a [ExternalService]. */ -abstract class AbstractExternalNamedModelService, U : EntityDto, O, R : NamedJpaRepository>( +abstract class AbstractExternalNamedModelService, U : EntityDto, O, R : NamedJpaRepository>( repository: R ) : AbstractNamedModelService(repository), ExternalNamedModelService { override fun getAllForOutput() = diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt index ddc8cb6..775b64f 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt @@ -96,7 +96,7 @@ class DefaultRecipeLogic( override fun getAllByCompany(company: Company) = repository.findAllByCompany(company) override fun save(entity: RecipeSaveDto): Recipe { - val company = companyLogic.getById(entity.companyId) + val company = company(companyLogic.getById(entity.companyId)) if (existsByNameAndCompany(entity.name, company)) { throw recipeNameAlreadyExistsForCompanyException(entity.name, company) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt index d2bcba7..b2e1158 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt @@ -1,12 +1,7 @@ package dev.fyloz.colorrecipesexplorer.model -import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException -import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException -import dev.fyloz.colorrecipesexplorer.exception.NotFoundException -import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank +import dev.fyloz.colorrecipesexplorer.dtos.CompanyDto import javax.persistence.* -import javax.validation.constraints.NotBlank -import javax.validation.constraints.NotNull @Entity @Table(name = "company") @@ -16,30 +11,8 @@ data class Company( override val id: Long?, @Column(unique = true) - override val name: String -) : NamedModel { - override fun toString(): String { - return name - } -} - - -open class CompanySaveDto( - @field:NotBlank val name: String -) : EntityDto { - override fun toEntity(): Company = Company(null, name) -} - - -open class CompanyUpdateDto( - val id: Long, - - @field:NotBlank - val name: String? -) : EntityDto { - override fun toEntity(): Company = Company(id, name ?: "") -} +) : ModelEntity // ==== DSL ==== fun company( @@ -48,60 +21,12 @@ fun company( op: Company.() -> Unit = {} ) = Company(id, name).apply(op) -fun companySaveDto( - name: String = "name", - op: CompanySaveDto.() -> Unit = {} -) = CompanySaveDto(name).apply(op) +@Deprecated("Temporary DSL for transition") +fun company( + dto: CompanyDto +) = Company(dto.id, dto.name) -fun companyUpdateDto( - id: Long = 0L, - name: String? = "name", - op: CompanyUpdateDto.() -> Unit = {} -) = CompanyUpdateDto(id, name).apply(op) - -// ==== Exceptions ==== -private const val COMPANY_NOT_FOUND_EXCEPTION_TITLE = "Company not found" -private const val COMPANY_ALREADY_EXISTS_EXCEPTION_TITLE = "Company already exists" -private const val COMPANY_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete company" -private const val COMPANY_EXCEPTION_ERROR_CODE = "company" - -fun companyIdNotFoundException(id: Long) = - NotFoundException( - COMPANY_EXCEPTION_ERROR_CODE, - COMPANY_NOT_FOUND_EXCEPTION_TITLE, - "A company with the id $id could not be found", - id - ) - -fun companyNameNotFoundException(name: String) = - NotFoundException( - COMPANY_EXCEPTION_ERROR_CODE, - COMPANY_NOT_FOUND_EXCEPTION_TITLE, - "A company with the name $name could not be found", - name, - "name" - ) - -fun companyIdAlreadyExistsException(id: Long) = - AlreadyExistsException( - COMPANY_EXCEPTION_ERROR_CODE, - COMPANY_ALREADY_EXISTS_EXCEPTION_TITLE, - "A company with the id $id already exists", - id - ) - -fun companyNameAlreadyExistsException(name: String) = - AlreadyExistsException( - COMPANY_EXCEPTION_ERROR_CODE, - COMPANY_ALREADY_EXISTS_EXCEPTION_TITLE, - "A company with the name $name already exists", - name, - "name" - ) - -fun cannotDeleteCompany(company: Company) = - CannotDeleteException( - COMPANY_EXCEPTION_ERROR_CODE, - COMPANY_CANNOT_DELETE_EXCEPTION_TITLE, - "Cannot delete the company ${company.name} because one or more recipes depends on it" - ) +@Deprecated("Temporary DSL for transition") +fun companyDto( + entity: Company +) = CompanyDto(entity.id!!, entity.name) \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt index 76f505a..3513570 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt @@ -8,7 +8,6 @@ import org.springframework.web.multipart.MultipartFile import javax.persistence.* import javax.validation.constraints.Min import javax.validation.constraints.NotBlank -import javax.validation.constraints.Size const val SIMDUT_FILES_PATH = "pdf/simdut" @@ -31,7 +30,7 @@ data class Material( @ManyToOne @JoinColumn(name = "material_type_id") var materialType: MaterialType? -) : NamedModel { +) : NamedModelEntity { val simdutFilePath @JsonIgnore @Transient @@ -71,7 +70,7 @@ data class MaterialOutputDto( val isMixType: Boolean, val materialType: MaterialType, val simdutUrl: String? -) : Model +) : ModelEntity data class MaterialQuantityDto( val material: Long, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt index 7abc3b7..a99033f 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt @@ -4,12 +4,9 @@ import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException import dev.fyloz.colorrecipesexplorer.exception.CannotUpdateException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException -import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank -import dev.fyloz.colorrecipesexplorer.model.validation.NullOrSize import org.hibernate.annotations.ColumnDefault import javax.persistence.* import javax.validation.constraints.NotBlank -import javax.validation.constraints.NotNull import javax.validation.constraints.Size private const val VALIDATION_PREFIX_SIZE = "Must contains exactly 3 characters" @@ -34,7 +31,7 @@ data class MaterialType( @Column(name = "system_type") @ColumnDefault("false") val systemType: Boolean = false -) : NamedModel +) : NamedModelEntity open class MaterialTypeSaveDto( @field:NotBlank diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt index 3622343..d7f0053 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt @@ -30,7 +30,7 @@ data class Mix( @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true) @JoinColumn(name = "mix_id") var mixMaterials: MutableSet, -) : Model +) : ModelEntity open class MixSaveDto( @field:NotBlank diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt index c48316b..afe56b2 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt @@ -4,7 +4,6 @@ import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import javax.persistence.* import javax.validation.constraints.Min -import javax.validation.constraints.NotNull @Entity @Table(name = "mix_material") @@ -20,7 +19,7 @@ data class MixMaterial( var quantity: Float, var position: Int -) : Model +) : ModelEntity data class MixMaterialDto( val materialId: Long, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixType.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixType.kt index 6281099..9953f80 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixType.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixType.kt @@ -20,7 +20,7 @@ data class MixType( @OneToOne(cascade = [CascadeType.ALL]) @JoinColumn(name = "material_id") var material: Material -) : NamedModel +) : NamedModelEntity // ==== DSL ==== fun mixType( diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Model.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/ModelEntity.kt similarity index 69% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Model.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/ModelEntity.kt index 147285f..6b790dc 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Model.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/ModelEntity.kt @@ -1,11 +1,11 @@ package dev.fyloz.colorrecipesexplorer.model -/** The model of a stored entity. Each model should implements its own equals and hashCode methods to keep compatibility with the legacy Java and Thymeleaf code. */ -interface Model { +/** Represents an entity, named differently to prevent conflicts with the JPA annotation. */ +interface ModelEntity { val id: Long? } -interface NamedModel : Model { +interface NamedModelEntity : ModelEntity { val name: String } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt index a78ba28..2e56962 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt @@ -52,7 +52,7 @@ data class Recipe( @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true) @JoinColumn(name = "recipe_id") val groupsInformation: Set -) : Model { +) : ModelEntity { /** The mix types contained in this recipe. */ val mixTypes: Collection @JsonIgnore @@ -150,7 +150,7 @@ data class RecipeOutputDto( val mixes: Set, val groupsInformation: Set, var imagesUrls: Set -) : Model +) : ModelEntity @Entity @Table(name = "recipe_group_information") @@ -168,7 +168,7 @@ data class RecipeGroupInformation( @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true) @JoinColumn(name = "recipe_group_information_id") var steps: MutableSet? -) : Model +) : ModelEntity data class RecipeStepsDto( val groupId: Long, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/RecipeStep.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/RecipeStep.kt index 1f5a3a7..51f9377 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/RecipeStep.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/RecipeStep.kt @@ -14,7 +14,7 @@ data class RecipeStep( val position: Int, val message: String -) : Model +) : ModelEntity // ==== DSL ==== fun recipeStep( diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/Group.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/Group.kt index 1169c5e..6f6b24c 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/Group.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/Group.kt @@ -4,14 +4,13 @@ import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.exception.RestException import dev.fyloz.colorrecipesexplorer.model.* +import dev.fyloz.colorrecipesexplorer.model.ModelEntity import org.hibernate.annotations.Fetch import org.hibernate.annotations.FetchMode import org.springframework.http.HttpStatus import javax.persistence.* import javax.validation.constraints.NotBlank import javax.validation.constraints.NotEmpty -import javax.validation.constraints.NotNull -import javax.validation.constraints.Size @Entity @Table(name = "user_group") @@ -29,7 +28,7 @@ data class Group( @Column(name = "permission") @Fetch(FetchMode.SUBSELECT) val permissions: MutableSet = mutableSetOf(), -) : NamedModel { +) : NamedModelEntity { val flatPermissions: Set get() = this.permissions .flatMap { it.flat() } @@ -66,7 +65,7 @@ data class GroupOutputDto( val name: String, val permissions: Set, val explicitPermissions: Set -): Model +): ModelEntity fun group( id: Long? = null, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/User.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/User.kt index ac6f5d6..633a1a4 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/User.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/User.kt @@ -4,7 +4,7 @@ import dev.fyloz.colorrecipesexplorer.SpringUserDetails import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.model.EntityDto -import dev.fyloz.colorrecipesexplorer.model.Model +import dev.fyloz.colorrecipesexplorer.model.ModelEntity import org.hibernate.annotations.Fetch import org.hibernate.annotations.FetchMode import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder @@ -50,7 +50,7 @@ data class User( @Column(name = "last_login_time") var lastLoginTime: LocalDateTime? = null -) : Model { +) : ModelEntity { val flatPermissions: Set get() = permissions .flatMap { it.flat() } @@ -103,7 +103,7 @@ data class UserOutputDto( val permissions: Set, val explicitPermissions: Set, val lastLoginTime: LocalDateTime? -) : Model +) : ModelEntity data class UserLoginRequest(val id: Long, val password: String) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt index 1a90530..b96738b 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt @@ -3,7 +3,7 @@ package dev.fyloz.colorrecipesexplorer.model.touchupkit import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.model.EntityDto -import dev.fyloz.colorrecipesexplorer.model.Model +import dev.fyloz.colorrecipesexplorer.model.ModelEntity import dev.fyloz.colorrecipesexplorer.model.VALIDATION_SIZE_GE_ONE import java.time.LocalDate import javax.persistence.* @@ -43,7 +43,7 @@ data class TouchUpKit( @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true) @JoinColumn(name = "touch_up_kit_id") val content: Set -) : Model { +) : ModelEntity { val finish get() = finishConcatenated.split(TOUCH_UP_KIT_DELIMITER) @@ -68,7 +68,7 @@ data class TouchUpKitProduct( val quantity: Float, val ready: Boolean -) : Model +) : ModelEntity data class TouchUpKitSaveDto( @field:NotBlank @@ -140,7 +140,7 @@ data class TouchUpKitOutputDto( val material: List, val content: Set, val pdfUrl: String -) : Model +) : ModelEntity data class TouchUpKitProductDto( val name: String, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/CompanyRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/CompanyRepository.kt index b0b4142..963e80d 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/CompanyRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/CompanyRepository.kt @@ -1,18 +1,21 @@ package dev.fyloz.colorrecipesexplorer.repository import dev.fyloz.colorrecipesexplorer.model.Company +import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository @Repository -interface CompanyRepository : NamedJpaRepository { +interface CompanyRepository : JpaRepository { + /** Checks if a company with the given [name] and an id different from the given [id] exists. */ + fun existsByNameAndIdNot(name: String, id: Long): Boolean + + /** Checks if a recipe depends on the company with the given [id]. */ @Query( - """ - select case when(count(r.id) > 0) then false else true end - from Company c - left join Recipe r on c.id = r.company.id - where c.id = :id + """ + select case when(count(r) > 0) then true else false end + from Recipe r where r.company.id = :id """ ) - fun canBeDeleted(id: Long): Boolean + fun recipesDependsOnCompanyById(id: Long): Boolean } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/Repository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/Repository.kt index 59ebe12..5e0843c 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/Repository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/Repository.kt @@ -1,12 +1,12 @@ package dev.fyloz.colorrecipesexplorer.repository -import dev.fyloz.colorrecipesexplorer.model.NamedModel +import dev.fyloz.colorrecipesexplorer.model.NamedModelEntity import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.repository.NoRepositoryBean /** Adds support for entities using a name identifier. */ @NoRepositoryBean -interface NamedJpaRepository : JpaRepository { +interface NamedJpaRepository : JpaRepository { /** Checks if an entity with the given [name]. */ fun existsByName(name: String): Boolean diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt index 3375cbd..e2a00f7 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt @@ -1,10 +1,9 @@ package dev.fyloz.colorrecipesexplorer.rest import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewCatalog -import dev.fyloz.colorrecipesexplorer.model.Company -import dev.fyloz.colorrecipesexplorer.model.CompanySaveDto -import dev.fyloz.colorrecipesexplorer.model.CompanyUpdateDto -import org.springframework.context.annotation.Profile +import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase +import dev.fyloz.colorrecipesexplorer.dtos.CompanyDto +import dev.fyloz.colorrecipesexplorer.logic.CompanyLogic import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.* import javax.validation.Valid @@ -13,35 +12,35 @@ private const val COMPANY_CONTROLLER_PATH = "api/company" @RestController @RequestMapping(COMPANY_CONTROLLER_PATH) -@Profile("!emergency") +@RequireDatabase @PreAuthorizeViewCatalog -class CompanyController(private val companyLogic: dev.fyloz.colorrecipesexplorer.logic.CompanyLogic) { +class CompanyController(private val companyLogic: CompanyLogic) { @GetMapping fun getAll() = - ok(companyLogic.getAllForOutput()) + ok(companyLogic.getAll()) @GetMapping("{id}") fun getById(@PathVariable id: Long) = - ok(companyLogic.getByIdForOutput(id)) + ok(companyLogic.getById(id)) @PostMapping @PreAuthorize("hasAuthority('EDIT_COMPANIES')") - fun save(@Valid @RequestBody company: CompanySaveDto) = - created(COMPANY_CONTROLLER_PATH) { - companyLogic.save(company) - } + fun save(@Valid @RequestBody company: CompanyDto) = + created(COMPANY_CONTROLLER_PATH) { + companyLogic.save(company) + } @PutMapping @PreAuthorize("hasAuthority('EDIT_COMPANIES')") - fun update(@Valid @RequestBody company: CompanyUpdateDto) = - noContent { - companyLogic.update(company) - } + fun update(@Valid @RequestBody company: CompanyDto) = + noContent { + companyLogic.update(company) + } @DeleteMapping("{id}") @PreAuthorize("hasAuthority('EDIT_COMPANIES')") fun deleteById(@PathVariable id: Long) = - noContent { - companyLogic.deleteById(id) - } + noContent { + companyLogic.deleteById(id) + } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt index 7147aa0..2c1e2f5 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt @@ -1,7 +1,8 @@ package dev.fyloz.colorrecipesexplorer.rest import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties -import dev.fyloz.colorrecipesexplorer.model.Model +import dev.fyloz.colorrecipesexplorer.dtos.EntityDto +import dev.fyloz.colorrecipesexplorer.model.ModelEntity import org.springframework.core.io.Resource import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus @@ -35,11 +36,21 @@ fun okFile(file: Resource, mediaType: String? = null): ResponseEntity .body(file) /** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */ -fun created(controllerPath: String, body: T): ResponseEntity = +fun created(controllerPath: String, body: T): ResponseEntity = created(controllerPath, body, body.id!!) +/** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */ +@JvmName("createdDto") +fun created(controllerPath: String, body: T): ResponseEntity = + created(controllerPath, body, body.id) + /** Creates a HTTP CREATED [ResponseEntity] with the result of the given [producer] as its body. */ -fun created(controllerPath: String, producer: () -> T): ResponseEntity = +fun created(controllerPath: String, producer: () -> T): ResponseEntity = + created(controllerPath, producer()) + +/** Creates a HTTP CREATED [ResponseEntity] with the result of the given [producer] as its body. */ +@JvmName("createdDto") +fun created(controllerPath: String, producer: () -> T): ResponseEntity = created(controllerPath, producer()) /** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */ diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt new file mode 100644 index 0000000..c65b91c --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt @@ -0,0 +1,27 @@ +package dev.fyloz.colorrecipesexplorer.service + +import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent +import dev.fyloz.colorrecipesexplorer.dtos.CompanyDto +import dev.fyloz.colorrecipesexplorer.model.Company +import dev.fyloz.colorrecipesexplorer.repository.CompanyRepository + +interface CompanyService : Service { + /** Checks if a company with the given [name] exists. */ + fun existsByName(name: String, id: Long?): Boolean + + /** Checks if a recipe depends on the company with the given [id]. */ + fun recipesDependsOnCompanyById(id: Long): Boolean +} + +@ServiceComponent +class DefaultCompanyService(repository: CompanyRepository) : + BaseService(repository), CompanyService { + override fun existsByName(name: String, id: Long?) = repository.existsByNameAndIdNot(name, id ?: 0) + override fun recipesDependsOnCompanyById(id: Long) = repository.recipesDependsOnCompanyById(id) + + override fun toDto(entity: Company) = + CompanyDto(entity.id!!, entity.name) + + override fun toEntity(dto: CompanyDto) = + Company(dto.id, dto.name) +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/Service.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/Service.kt new file mode 100644 index 0000000..bfcc7c6 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/Service.kt @@ -0,0 +1,64 @@ +package dev.fyloz.colorrecipesexplorer.service + +import dev.fyloz.colorrecipesexplorer.dtos.EntityDto +import dev.fyloz.colorrecipesexplorer.model.ModelEntity +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.repository.findByIdOrNull + +/** + * Represents a service between the logic and the repository. + * Gives access to the repository using a DTO. + * + * @param D The type of the entity DTO. + * @param E The type of the entity. + * @param R The repository of the entity. + */ +interface Service> { + /** Checks if an entity with the given [id] exists. */ + fun existsById(id: Long): Boolean + + /** Gets all entities as DTOs. */ + fun getAll(): Collection + + /** Gets the entity DTO with the given [id].*/ + fun getById(id: Long): D? + + /** Saves the given [dto]. */ + fun save(dto: D): D + + /** Deletes the given [dto]. */ + fun delete(dto: D) + + /** Deletes the entity with the given [id]. */ + fun deleteById(id: Long) +} + +abstract class BaseService>(protected val repository: R) : + Service { + override fun existsById(id: Long) = + repository.existsById(id) + + override fun getAll() = + repository.findAll().map(this::toDto) + + override fun getById(id: Long): D? { + val entity = repository.findByIdOrNull(id) ?: return null + return toDto(entity) + } + + override fun save(dto: D): D { + val entity = repository.save(toEntity(dto)) + return toDto(entity) + } + + override fun delete(dto: D) { + repository.delete(toEntity(dto)) + } + + override fun deleteById(id: Long) { + repository.deleteById(id) + } + + abstract fun toDto(entity: E): D + abstract fun toEntity(dto: D): E +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt index 00b853c..3aadbe8 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt @@ -1,6 +1,6 @@ package dev.fyloz.colorrecipesexplorer.utils -import dev.fyloz.colorrecipesexplorer.model.Model +import dev.fyloz.colorrecipesexplorer.model.ModelEntity /** Returns a list containing the result of the given [transform] applied to each item of the [Iterable]. If the given [transform] throws, the [Throwable] will be passed to the given [throwableConsumer]. */ inline fun Iterable.mapMayThrow( @@ -46,8 +46,8 @@ inline fun MutableCollection.excludeAll(predicate: (T) -> Boolean): Itera return matching } -/** Merge to [Model] [Iterable]s and prevent id duplication. */ -fun Iterable.merge(other: Iterable) = +/** Merge to [ModelEntity] [Iterable]s and prevent id duplication. */ +fun Iterable.merge(other: Iterable) = this .filter { model -> other.all { it.id != model.id } } .plus(other) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/AbstractServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/AbstractServiceTest.kt index a37cf7b..968aa7b 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/AbstractServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/AbstractServiceTest.kt @@ -5,8 +5,8 @@ import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.exception.RestException import dev.fyloz.colorrecipesexplorer.model.EntityDto -import dev.fyloz.colorrecipesexplorer.model.Model -import dev.fyloz.colorrecipesexplorer.model.NamedModel +import dev.fyloz.colorrecipesexplorer.model.ModelEntity +import dev.fyloz.colorrecipesexplorer.model.NamedModelEntity import dev.fyloz.colorrecipesexplorer.repository.NamedJpaRepository import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test @@ -18,7 +18,7 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue import dev.fyloz.colorrecipesexplorer.logic.AbstractServiceTest as AbstractServiceTest1 -abstract class AbstractServiceTest, R : JpaRepository> { +abstract class AbstractServiceTest, R : JpaRepository> { protected abstract val repository: R protected abstract val logic: S @@ -90,7 +90,7 @@ abstract class AbstractServiceTest, R : JpaRepository } } -abstract class AbstractModelServiceTest, R : JpaRepository> : +abstract class AbstractModelServiceTest, R : JpaRepository> : AbstractServiceTest1() { // existsById() @@ -176,7 +176,7 @@ abstract class AbstractModelServiceTest, R : J } } -abstract class AbstractNamedModelServiceTest, R : NamedJpaRepository> : +abstract class AbstractNamedModelServiceTest, R : NamedJpaRepository> : AbstractModelServiceTest() { protected abstract val entityWithEntityName: E @@ -269,7 +269,7 @@ interface ExternalModelServiceTest { // ==== IMPLEMENTATIONS FOR EXTERNAL SERVICES ==== // Lots of code duplication but I don't have a better solution for now -abstract class AbstractExternalModelServiceTest, U : EntityDto, S : ExternalModelService, R : JpaRepository> : +abstract class AbstractExternalModelServiceTest, U : EntityDto, S : ExternalModelService, R : JpaRepository> : AbstractModelServiceTest(), ExternalModelServiceTest { protected abstract val entitySaveDto: N protected abstract val entityUpdateDto: U @@ -281,7 +281,7 @@ abstract class AbstractExternalModelServiceTest, U : } } -abstract class AbstractExternalNamedModelServiceTest, U : EntityDto, S : ExternalNamedModelService, R : NamedJpaRepository> : +abstract class AbstractExternalNamedModelServiceTest, U : EntityDto, S : ExternalNamedModelService, R : NamedJpaRepository> : AbstractNamedModelServiceTest(), ExternalModelServiceTest { protected abstract val entitySaveDto: N protected abstract val entityUpdateDto: U @@ -310,7 +310,7 @@ fun RestException.assertErrorCode(errorCode: String) { assertEquals(errorCode, this.errorCode) } -fun > withBaseSaveDtoTest( +fun > withBaseSaveDtoTest( entity: E, entitySaveDto: N, service: ExternalService, @@ -328,7 +328,7 @@ fun > withBaseSaveDtoTest( op() } -fun > withBaseUpdateDtoTest( +fun > withBaseUpdateDtoTest( entity: E, entityUpdateDto: U, service: ExternalModelService, diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/BaseLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/BaseLogicTest.kt new file mode 100644 index 0000000..bebab7d --- /dev/null +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/BaseLogicTest.kt @@ -0,0 +1,173 @@ +package dev.fyloz.colorrecipesexplorer.logic + +import dev.fyloz.colorrecipesexplorer.dtos.EntityDto +import dev.fyloz.colorrecipesexplorer.exception.NotFoundException +import dev.fyloz.colorrecipesexplorer.model.ModelEntity +import dev.fyloz.colorrecipesexplorer.service.Service +import io.mockk.* +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.data.jpa.repository.JpaRepository +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class BaseLogicTest { + private val serviceMock = mockk>>() + + private val baseLogic = spyk(TestBaseLogic(serviceMock)) + + private val dto = TestEntityDto(id = 1L) + + @AfterEach + internal fun afterEach() { + clearAllMocks() + } + + @Test + fun existsById_normalBehavior_returnsTrue() { + // Arrange + every { serviceMock.existsById(any()) } returns true + + // Act + val exists = baseLogic.existsById(dto.id) + + // Assert + assertTrue(exists) + } + + @Test + fun exists_notFound_returnsFalse() { + // Arrange + every { serviceMock.existsById(any()) } returns false + + // Act + val exists = baseLogic.existsById(dto.id) + + // Assert + assertFalse(exists) + } + + @Test + fun getAll_normalBehavior_returnsAllDtos() { + // Arrange + val expectedDtos = listOf(dto) + + every { serviceMock.getAll() } returns expectedDtos + + // Act + val actualDtos = baseLogic.getAll() + + // Assert + assertEquals(expectedDtos, actualDtos) + } + + @Test + fun getById_normalBehavior_returnsDtoWithGivenId() { + // Arrange + every { serviceMock.getById(any()) } returns dto + + // Act + val dtoById = baseLogic.getById(dto.id) + + // Assert + assertEquals(dto, dtoById) + } + + @Test + fun getById_notFound_throwsNotFoundException() { + // Arrange + every { serviceMock.getById(any()) } returns null + + // Act + // Assert + assertThrows { baseLogic.getById(dto.id) } + } + + @Test + fun save_normalBehavior_callsServiceSave() { + // Arrange + every { serviceMock.save(any()) } returns dto + + // Act + baseLogic.save(dto) + + // Assert + verify { + serviceMock.save(dto) + } + confirmVerified(serviceMock) + } + + @Test + fun save_normalBehavior_returnsSavedDto() { + // Arrange + every { serviceMock.save(any()) } returns dto + + // Act + val savedDto = baseLogic.save(dto) + + // Assert + assertEquals(dto, savedDto) + } + + @Test + fun update_normalBehavior_callsServiceSave() { + // Arrange + every { serviceMock.save(any()) } returns dto + every { baseLogic.existsById(any()) } returns true + + // Act + baseLogic.update(dto) + + // Assert + verify { + serviceMock.save(dto) + } + confirmVerified(serviceMock) + } + + @Test + fun update_normalBehavior_returnsUpdatedDto() { + // Arrange + every { serviceMock.save(any()) } returns dto + every { baseLogic.existsById(any()) } returns true + + // Act + val updatedDto = baseLogic.update(dto) + + // Assert + assertEquals(dto, updatedDto) + } + + @Test + fun update_notFound_throwsNotFoundException() { + // Arrange + every { serviceMock.save(any()) } returns dto + every { baseLogic.existsById(any()) } returns false + + // Act + // Assert + assertThrows { baseLogic.update(dto) } + } + + @Test + fun deleteById_normalBehavior_callsServiceDeleteById() { + // Arrange + every { serviceMock.deleteById(any()) } just runs + + // Act + baseLogic.deleteById(dto.id) + + // Assert + verify { + serviceMock.deleteById(dto.id) + } + confirmVerified(serviceMock) + } +} + +private data class TestEntityDto(override val id: Long) : EntityDto +private class TestBaseLogic>(service: S) : + BaseLogic(service, "UnitTestType") \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogicTest.kt deleted file mode 100644 index be6dba4..0000000 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogicTest.kt +++ /dev/null @@ -1,90 +0,0 @@ -package dev.fyloz.colorrecipesexplorer.logic - -import com.nhaarman.mockitokotlin2.* -import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.repository.CompanyRepository -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class CompanyLogicTest : - AbstractExternalNamedModelServiceTest() { - private val recipeLogic: RecipeLogic = mock() - override val repository: CompanyRepository = mock() - override val logic: CompanyLogic = spy( - DefaultCompanyLogic( - repository, - recipeLogic - ) - ) - - override val entity: Company = company(id = 0L, name = "company") - override val anotherEntity: Company = company(id = 1L, name = "another company") - override val entityWithEntityName: Company = company(id = 2L, name = entity.name) - override val entitySaveDto: CompanySaveDto = spy(companySaveDto()) - override val entityUpdateDto: CompanyUpdateDto = spy(companyUpdateDto(id = entity.id!!, name = null)) - - @AfterEach - override fun afterEach() { - reset(recipeLogic) - super.afterEach() - } - - // isLinkedToRecipes - - @Test - fun `isLinkedToRecipes() returns true when a given company is linked to one or more recipes`() { - whenever(recipeLogic.existsByCompany(entity)).doReturn(true) - - val found = logic.isLinkedToRecipes(entity) - - assertTrue(found) - } - - @Test - fun `isLinkedToRecipes() returns false when a given company is not linked to any recipe`() { - whenever(recipeLogic.existsByCompany(entity)).doReturn(false) - - val found = logic.isLinkedToRecipes(entity) - - assertFalse(found) - } - - // save() - - @Test - override fun `save(dto) calls and returns save() with the created entity`() { - withBaseSaveDtoTest(entity, entitySaveDto, logic) - } - - // update() - - @Test - override fun `update(dto) calls and returns update() with the created entity`() = - withBaseUpdateDtoTest(entity, entityUpdateDto, logic, { any() }) - - // delete() - - override fun `delete() deletes in the repository`() { - whenCanBeDeleted { - super.`delete() deletes in the repository`() - } - } - - // deleteById() - - override fun `deleteById() deletes the entity with the given id in the repository`() { - whenCanBeDeleted { - super.`deleteById() deletes the entity with the given id in the repository`() - } - } - - private fun whenCanBeDeleted(id: Long = any(), test: () -> Unit) { - whenever(repository.canBeDeleted(id)).doReturn(true) - - test() - } -} diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultCompanyLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultCompanyLogicTest.kt new file mode 100644 index 0000000..6049ae9 --- /dev/null +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultCompanyLogicTest.kt @@ -0,0 +1,55 @@ +package dev.fyloz.colorrecipesexplorer.logic + +import dev.fyloz.colorrecipesexplorer.dtos.CompanyDto +import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException +import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException +import dev.fyloz.colorrecipesexplorer.service.CompanyService +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class DefaultCompanyLogicTest { + private val companyServiceMock = mockk() + + private val companyLogic = DefaultCompanyLogic(companyServiceMock) + + private val company = CompanyDto(id = 1L, name = "UnitTestCompany") + + @AfterEach + internal fun afterEach() { + clearAllMocks() + } + + @Test + fun save_nameExists_throwsAlreadyExistsException() { + // Arrange + every { companyServiceMock.existsByName(any(), any()) } returns true + + // Act + // Assert + assertThrows { companyLogic.save(company) } + } + + @Test + fun update_nameExists_throwsAlreadyExistsException() { + // Arrange + every { companyServiceMock.existsByName(any(), any()) } returns true + + // Act + // Assert + assertThrows { companyLogic.update(company) } + } + + @Test + fun deleteById_recipesDependsOnCompany_throwsCannotDeleteException() { + // Arrange + every { companyServiceMock.recipesDependsOnCompanyById(company.id) } returns true + + // Act + // Assert + assertThrows { companyLogic.deleteById(company.id) } + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogicTest.kt index 6c0d371..5be796c 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogicTest.kt @@ -157,14 +157,14 @@ class RecipeLogicTest : @Test override fun `save(dto) calls and returns save() with the created entity`() { - whenever(companyLogic.getById(company.id!!)).doReturn(company) + whenever(companyLogic.getById(company.id!!)).doReturn(companyDto(company)) doReturn(false).whenever(logic).existsByNameAndCompany(entity.name, company) withBaseSaveDtoTest(entity, entitySaveDto, logic, { argThat { this.id == null && this.color == color } }) } @Test fun `save(dto) throw AlreadyExistsException when a recipe with the given name and company exists in the repository`() { - whenever(companyLogic.getById(company.id!!)).doReturn(company) + whenever(companyLogic.getById(company.id!!)).doReturn(companyDto(company)) doReturn(true).whenever(logic).existsByNameAndCompany(entity.name, company) with(assertThrows { logic.save(entitySaveDto) }) { -- 2.40.1 From b59865259471c7426c6f54a5921e8ce4f4c011c9 Mon Sep 17 00:00:00 2001 From: FyloZ Date: Thu, 17 Feb 2022 23:34:08 -0500 Subject: [PATCH 03/11] #25 Migrate materials to new logic --- .../fyloz/colorrecipesexplorer/Constants.kt | 14 + .../colorrecipesexplorer/dtos/CompanyDto.kt | 2 +- .../colorrecipesexplorer/dtos/MaterialDto.kt | 34 ++ .../logic/CompanyLogic.kt | 2 +- .../logic/InventoryLogic.kt | 3 +- .../logic/MaterialLogic.kt | 211 +++++----- .../logic/MaterialTypeLogic.kt | 7 +- .../logic/MixMaterialLogic.kt | 4 +- .../logic/MixTypeLogic.kt | 44 +- .../colorrecipesexplorer/model/Material.kt | 79 +--- .../colorrecipesexplorer/model/MixMaterial.kt | 2 +- .../colorrecipesexplorer/model/Recipe.kt | 9 +- .../repository/CompanyRepository.kt | 4 +- .../repository/MaterialRepository.kt | 22 +- .../rest/FileController.kt | 7 +- .../rest/MaterialController.kt | 37 +- .../service/CompanyService.kt | 4 +- .../service/MaterialService.kt | 58 +++ .../logic/DefaultCompanyLogicTest.kt | 2 +- .../logic/DefaultMaterialLogicTest.kt | 382 ++++++++++++++++++ .../logic/InventoryLogicTest.kt | 2 +- .../logic/MaterialLogicTest.kt | 249 ------------ .../logic/MaterialTypeLogicTest.kt | 22 +- .../logic/MixMaterialLogicTest.kt | 2 +- 24 files changed, 667 insertions(+), 535 deletions(-) create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt create mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialLogicTest.kt delete mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogicTest.kt diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt new file mode 100644 index 0000000..6694fed --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt @@ -0,0 +1,14 @@ +package dev.fyloz.colorrecipesexplorer + +object Constants { + object ControllerPaths { + const val file = "/api/file" + const val material = "/api/material" + } + + object FilePaths { + const val pdfs = "pdf" + + const val simdut = "$pdfs/simdut" + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/CompanyDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/CompanyDto.kt index 3596d65..520f738 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/CompanyDto.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/CompanyDto.kt @@ -5,6 +5,6 @@ import javax.validation.constraints.NotBlank data class CompanyDto( override val id: Long = 0L, - @NotBlank + @field:NotBlank val name: String ) : EntityDto \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt new file mode 100644 index 0000000..45c6b15 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt @@ -0,0 +1,34 @@ +package dev.fyloz.colorrecipesexplorer.dtos + +import dev.fyloz.colorrecipesexplorer.model.MaterialType +import org.springframework.web.multipart.MultipartFile +import javax.validation.constraints.Min +import javax.validation.constraints.NotBlank + +data class MaterialDto( + override val id: Long = 0L, + + val name: String, + + val inventoryQuantity: Float, + + val isMixType: Boolean, + + val materialType: MaterialType, + + val simdutUrl: String? = null +) : EntityDto + +data class MaterialSaveDto( + override val id: Long = 0L, + + @field:NotBlank + val name: String, + + @field:Min(0) + val inventoryQuantity: Float, + + val materialTypeId: Long, + + val simdutFile: MultipartFile? +) : EntityDto diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogic.kt index fe2b04e..ea323ad 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogic.kt @@ -23,7 +23,7 @@ class DefaultCompanyLogic(service: CompanyService) : } override fun deleteById(id: Long) { - if (service.recipesDependsOnCompanyById(id)) { + if (service.isUsedByRecipe(id)) { throw cannotDeleteException("Cannot delete the company with the id '$id' because one or more recipes depends on it") } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogic.kt index ea7bafe..f43f1e6 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogic.kt @@ -1,6 +1,7 @@ package dev.fyloz.colorrecipesexplorer.logic import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase +import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto import dev.fyloz.colorrecipesexplorer.exception.RestException import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.utils.mapMayThrow @@ -89,7 +90,7 @@ class DefaultInventoryLogic( } } -class NotEnoughInventoryException(quantity: Float, material: Material) : +class NotEnoughInventoryException(quantity: Float, material: MaterialDto) : RestException( "notenoughinventory", "Not enough inventory", diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogic.kt index 5f877cb..7f3027f 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogic.kt @@ -1,145 +1,120 @@ package dev.fyloz.colorrecipesexplorer.logic -import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase -import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic +import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent +import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto +import dev.fyloz.colorrecipesexplorer.dtos.MaterialSaveDto import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic -import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.repository.MaterialRepository -import dev.fyloz.colorrecipesexplorer.rest.FILE_CONTROLLER_PATH -import io.jsonwebtoken.lang.Assert -import org.springframework.context.annotation.Lazy -import org.springframework.stereotype.Service -import java.net.URLEncoder -import java.nio.charset.StandardCharsets +import dev.fyloz.colorrecipesexplorer.model.Material +import dev.fyloz.colorrecipesexplorer.service.MaterialService -interface MaterialLogic : - ExternalNamedModelService { - /** Checks if a material with the given [materialType] exists. */ - fun existsByMaterialType(materialType: MaterialType): Boolean - - /** Checks if the given [material] has a SIMDUT file. */ - fun hasSimdut(material: Material): Boolean +interface MaterialLogic : Logic { + /** Checks if a material with the given [name] exists. */ + fun existsByName(name: String): Boolean /** Gets all materials that are not a mix type. */ - fun getAllNotMixType(): Collection + fun getAllNotMixType(): Collection - /** Gets all materials available for the creation of a mix for the recipe with the given [recipeId], including normal materials and materials from [MixType]s included in the said recipe. */ - fun getAllForMixCreation(recipeId: Long): Collection + /** + * Gets all materials available for the creation of a mix for the recipe with the given [recipeId], + * including normal materials and materials from mix types included in the said recipe. + */ + fun getAllForMixCreation(recipeId: Long): Collection - /** Gets all materials available for updating the mix with the given [mixId], including normal materials and materials from [MixType]s included in the mix recipe, excluding the material of the [MixType] of the said mix. */ - fun getAllForMixUpdate(mixId: Long): Collection + /** + * Gets all materials available for updating the mix with the given [mixId], + * including normal materials and materials from mix types included in the mix recipe + * and excluding the material of the mix type of the said mix. + */ + fun getAllForMixUpdate(mixId: Long): Collection + + /** Saves the given [dto]. */ + fun save(dto: MaterialSaveDto): MaterialDto + + /** Updates the given [dto]. */ + fun update(dto: MaterialSaveDto): MaterialDto /** Updates the quantity of the given [material] with the given [factor] and returns the updated quantity. */ - fun updateQuantity(material: Material, factor: Float): Float + fun updateQuantity(material: MaterialDto, factor: Float): Float } -@Service -@RequireDatabase +@LogicComponent class DefaultMaterialLogic( - materialRepository: MaterialRepository, + service: MaterialService, val recipeLogic: RecipeLogic, val mixLogic: MixLogic, - @Lazy val materialTypeLogic: MaterialTypeLogic, - val fileService: WriteableFileLogic, - val configService: ConfigurationLogic -) : - AbstractExternalNamedModelService( - materialRepository - ), - MaterialLogic { - override fun idNotFoundException(id: Long) = materialIdNotFoundException(id) - override fun idAlreadyExistsException(id: Long) = materialIdAlreadyExistsException(id) - override fun nameNotFoundException(name: String) = materialNameNotFoundException(name) - override fun nameAlreadyExistsException(name: String) = materialNameAlreadyExistsException(name) + val materialTypeLogic: MaterialTypeLogic, + val fileLogic: WriteableFileLogic +) : BaseLogic(service, Material::class.simpleName!!), MaterialLogic { + override fun existsByName(name: String) = service.existsByName(name, null) + override fun getAllNotMixType() = service.getAllNotMixType() - override fun Material.toOutput(): MaterialOutputDto = - MaterialOutputDto( - id = this.id!!, - name = this.name, - inventoryQuantity = this.inventoryQuantity, - isMixType = this.isMixType, - materialType = this.materialType!!, - simdutUrl = if (fileService.exists(this.simdutFilePath)) - "${configService.getContent(ConfigurationType.INSTANCE_URL)}$FILE_CONTROLLER_PATH?path=${ - URLEncoder.encode( - this.simdutFilePath, - StandardCharsets.UTF_8 - ) - }" - else null - ) - - override fun existsByMaterialType(materialType: MaterialType): Boolean = - repository.existsByMaterialType(materialType) - - override fun hasSimdut(material: Material): Boolean = fileService.exists(material.simdutFilePath) - override fun getAllNotMixType(): Collection = getAllForOutput().filter { !it.isMixType } - - override fun save(entity: MaterialSaveDto): Material = - save(with(entity) { - material( - name = entity.name, - inventoryQuantity = entity.inventoryQuantity, - materialType = materialTypeLogic.getById(materialTypeId), - isMixType = false - ) - }).apply { - if (entity.simdutFile != null && !entity.simdutFile.isEmpty) fileService.write( - entity.simdutFile, - this.simdutFilePath, - false - ) - } - - override fun update(entity: MaterialUpdateDto): Material { - val persistedMaterial by lazy { - getById(entity.id).apply { assertPersistedMaterial(this) } - } - - return update(with(entity) { - material( - id = id, - name = if (name != null && name.isNotBlank()) name else persistedMaterial.name, - inventoryQuantity = if (inventoryQuantity != null && inventoryQuantity != Float.MIN_VALUE) inventoryQuantity else persistedMaterial.inventoryQuantity, - isMixType = persistedMaterial.isMixType, - materialType = if (materialTypeId != null) materialTypeLogic.getById(materialTypeId) else persistedMaterial.materialType - ) - }).apply { - if (entity.simdutFile != null && !entity.simdutFile.isEmpty) fileService.write( - entity.simdutFile, - this.simdutFilePath, - true - ) - } - } - - override fun updateQuantity(material: Material, factor: Float) = with(material) { - val updatedQuantity = this.inventoryQuantity + factor - repository.updateInventoryQuantityById(this.id!!, updatedQuantity) - updatedQuantity - } - - override fun getAllForMixCreation(recipeId: Long): Collection { + override fun getAllForMixCreation(recipeId: Long): Collection { val recipesMixTypes = recipeLogic.getById(recipeId).mixTypes - return getAllForOutput() - .filter { !it.isMixType || recipesMixTypes.any { mixType -> mixType.material.id == it.id } } + + return getAll().filter { !it.isMixType || recipesMixTypes.any { mixType -> mixType.material.id == it.id } } } - override fun getAllForMixUpdate(mixId: Long): Collection { + override fun getAllForMixUpdate(mixId: Long): Collection { val mix = mixLogic.getById(mixId) val recipesMixTypes = mix.recipe.mixTypes - return getAllForOutput() - .filter { !it.isMixType || recipesMixTypes.any { mixType -> mixType.material.id == it.id } } + + return getAll().filter { !it.isMixType || recipesMixTypes.any { mixType -> mixType.material.id == it.id } } .filter { it.id != mix.mixType.material.id } } - private fun assertPersistedMaterial(material: Material) { - Assert.notNull(material.name, "The persisted material with the id ${material.id} has a null name") + override fun save(dto: MaterialSaveDto) = save(saveDtoToDto(dto, false)).also { saveSimdutFile(dto, false) } + override fun save(dto: MaterialDto): MaterialDto { + throwIfNameAlreadyExists(dto.name) + + return super.save(dto) } - override fun delete(entity: Material) { - if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteMaterialException(entity) - if (fileService.exists(entity.simdutFilePath)) fileService.delete(entity.simdutFilePath) - super.delete(entity) + override fun update(dto: MaterialSaveDto) = update(saveDtoToDto(dto, true)).also { saveSimdutFile(dto, true) } + override fun update(dto: MaterialDto): MaterialDto { + throwIfNameAlreadyExists(dto.name, dto.id) + + return super.update(dto) } -} + + override fun updateQuantity(material: MaterialDto, factor: Float): Float { + val updatedQuantity = material.inventoryQuantity + factor + service.updateInventoryQuantityById(material.id, updatedQuantity) + + return updatedQuantity + } + + override fun deleteById(id: Long) { + if (service.isUsedByMixMaterialOrMixType(id)) { + throw cannotDeleteException("Cannot delete the material with the id '$id' because mix types and/or recipes depends on it") + } + + val material = getById(id) + val simdutPath = Material.getSimdutFilePath(material.name) + if (fileLogic.exists(simdutPath)) { + fileLogic.delete(simdutPath) + } + + super.deleteById(id) + } + + private fun saveDtoToDto(saveDto: MaterialSaveDto, updating: Boolean): MaterialDto { + val isMixType = !updating || getById(saveDto.id).isMixType + val materialType = materialTypeLogic.getById(saveDto.materialTypeId) + + return MaterialDto(saveDto.id, saveDto.name, saveDto.inventoryQuantity, isMixType, materialType, null) + } + + private fun saveSimdutFile(dto: MaterialSaveDto, updating: Boolean) { + val file = dto.simdutFile + + if (file != null && !file.isEmpty) { + fileLogic.write(file, Material.getSimdutFilePath(dto.name), updating) + } + } + + 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/MaterialTypeLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogic.kt index b3cb0eb..833afc6 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogic.kt @@ -11,9 +11,6 @@ interface MaterialTypeLogic : /** Checks if a material type with the given [prefix] exists. */ fun existsByPrefix(prefix: String): Boolean - /** Checks if the given [materialType] is used by one or more materials. */ - fun isUsedByMaterial(materialType: MaterialType): Boolean - /** Gets all system material types. */ fun getAllSystemTypes(): Collection @@ -26,7 +23,7 @@ interface MaterialTypeLogic : @Service @RequireDatabase -class DefaultMaterialTypeLogic(repository: MaterialTypeRepository, private val materialLogic: MaterialLogic) : +class DefaultMaterialTypeLogic(repository: MaterialTypeRepository) : AbstractExternalNamedModelService( repository ), MaterialTypeLogic { @@ -38,8 +35,6 @@ class DefaultMaterialTypeLogic(repository: MaterialTypeRepository, private val m override fun MaterialType.toOutput() = this override fun existsByPrefix(prefix: String): Boolean = repository.existsByPrefix(prefix) - override fun isUsedByMaterial(materialType: MaterialType): Boolean = - materialLogic.existsByMaterialType(materialType) override fun getAllSystemTypes(): Collection = repository.findAllBySystemTypeIs(true) override fun getAllNonSystemType(): Collection = repository.findAllBySystemTypeIs(false) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt index 3aea9df..913016a 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt @@ -44,7 +44,7 @@ class DefaultMixMaterialLogic( override fun MixMaterial.toOutput() = MixMaterialOutputDto( this.id!!, - with(materialLogic) { this@toOutput.material.toOutput() }, + this.material, this.quantity, this.position ) @@ -55,7 +55,7 @@ class DefaultMixMaterialLogic( override fun create(mixMaterial: MixMaterialDto): MixMaterial = mixMaterial( - material = materialLogic.getById(mixMaterial.materialId), + material = material(materialLogic.getById(mixMaterial.materialId)), quantity = mixMaterial.quantity, position = mixMaterial.position ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt index bbc241a..3ec9b08 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt @@ -32,27 +32,27 @@ class DefaultMixTypeLogic( mixTypeRepository: MixTypeRepository, @Lazy val materialLogic: MaterialLogic ) : - AbstractNamedModelService(mixTypeRepository), MixTypeLogic { + AbstractNamedModelService(mixTypeRepository), MixTypeLogic { override fun idNotFoundException(id: Long) = mixTypeIdNotFoundException(id) override fun idAlreadyExistsException(id: Long) = mixTypeIdAlreadyExistsException(id) override fun nameNotFoundException(name: String) = mixTypeNameNotFoundException(name) override fun nameAlreadyExistsException(name: String) = mixTypeNameAlreadyExistsException(name) override fun existsByNameAndMaterialType(name: String, materialType: MaterialType): Boolean = - repository.existsByNameAndMaterialType(name, materialType) + repository.existsByNameAndMaterialType(name, materialType) override fun getByMaterial(material: Material): MixType = - repository.findByMaterial(material) ?: throw nameNotFoundException(material.name) + repository.findByMaterial(material) ?: throw nameNotFoundException(material.name) override fun getByNameAndMaterialType(name: String, materialType: MaterialType): MixType = - repository.findByNameAndMaterialType(name, materialType) - ?: throw MixTypeNameAndMaterialTypeNotFoundException(name, materialType) + repository.findByNameAndMaterialType(name, materialType) + ?: throw MixTypeNameAndMaterialTypeNotFoundException(name, materialType) override fun getOrCreateForNameAndMaterialType(name: String, materialType: MaterialType): MixType = - if (existsByNameAndMaterialType(name, materialType)) - getByNameAndMaterialType(name, materialType) - else - saveForNameAndMaterialType(name, materialType) + if (existsByNameAndMaterialType(name, materialType)) + getByNameAndMaterialType(name, materialType) + else + saveForNameAndMaterialType(name, materialType) override fun save(entity: MixType): MixType { if (materialLogic.existsByName(entity.name)) @@ -61,24 +61,20 @@ class DefaultMixTypeLogic( } override fun saveForNameAndMaterialType(name: String, materialType: MaterialType): MixType = - save( - mixType( - name = name, - material = material( - name = name, - inventoryQuantity = Float.MIN_VALUE, - isMixType = true, - materialType = materialType - ) + save( + mixType( + name = name, + material = material( + name = name, + inventoryQuantity = Float.MIN_VALUE, + isMixType = true, + materialType = materialType + ) + ) ) - ) override fun updateForNameAndMaterialType(mixType: MixType, name: String, materialType: MaterialType): MixType = - update(mixType.apply { - this.name = name - material.name = name - material.materialType = materialType - }) + update(mixType.copy(material = mixType.material.copy(name = name, materialType = materialType))) override fun delete(entity: MixType) { if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteMixTypeException(entity) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt index 3513570..bab6b2c 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt @@ -1,13 +1,12 @@ package dev.fyloz.colorrecipesexplorer.model -import com.fasterxml.jackson.annotation.JsonIgnore +import dev.fyloz.colorrecipesexplorer.Constants +import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException -import org.springframework.web.multipart.MultipartFile import javax.persistence.* import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank const val SIMDUT_FILES_PATH = "pdf/simdut" @@ -19,59 +18,24 @@ data class Material( override val id: Long?, @Column(unique = true) - override var name: String, + val name: String, @Column(name = "inventory_quantity") - var inventoryQuantity: Float, + val inventoryQuantity: Float, @Column(name = "mix_type") val isMixType: Boolean, @ManyToOne @JoinColumn(name = "material_type_id") - var materialType: MaterialType? -) : NamedModelEntity { - val simdutFilePath - @JsonIgnore - @Transient - get() = "$SIMDUT_FILES_PATH/$name.pdf" + val materialType: MaterialType? +) : ModelEntity { + companion object { + fun getSimdutFilePath(name: String) = + "${Constants.FilePaths.simdut}/$name.pdf" + } } -open class MaterialSaveDto( - @field:NotBlank - val name: String, - - @field:Min(0, message = VALIDATION_SIZE_GE_ZERO) - val inventoryQuantity: Float, - - val materialTypeId: Long, - - val simdutFile: MultipartFile? = null -) : EntityDto - -open class MaterialUpdateDto( - val id: Long, - - @field:NotBlank - val name: String?, - - @field:Min(0, message = VALIDATION_SIZE_GE_ZERO) - val inventoryQuantity: Float?, - - val materialTypeId: Long?, - - val simdutFile: MultipartFile? = null -) : EntityDto - -data class MaterialOutputDto( - override val id: Long, - val name: String, - val inventoryQuantity: Float, - val isMixType: Boolean, - val materialType: MaterialType, - val simdutUrl: String? -) : ModelEntity - data class MaterialQuantityDto( val material: Long, @@ -99,22 +63,15 @@ fun material( ?: material.name, material.inventoryQuantity, material.isMixType, material.materialType ) -fun materialSaveDto( - name: String = "name", - inventoryQuantity: Float = 0f, - materialTypeId: Long = 0L, - simdutFile: MultipartFile? = null, - op: MaterialSaveDto.() -> Unit = {} -) = MaterialSaveDto(name, inventoryQuantity, materialTypeId, simdutFile).apply(op) +@Deprecated("Temporary DSL for transition") +fun material( + dto: MaterialDto +) = Material(dto.id, dto.name, dto.inventoryQuantity, dto.isMixType, dto.materialType) -fun materialUpdateDto( - id: Long = 0L, - name: String? = "name", - inventoryQuantity: Float? = 0f, - materialTypeId: Long? = 0L, - simdutFile: MultipartFile? = null, - op: MaterialUpdateDto.() -> Unit = {} -) = MaterialUpdateDto(id, name, inventoryQuantity, materialTypeId, simdutFile).apply(op) +@Deprecated("Temporary DSL for transition") +fun materialDto( + entity: Material +) = MaterialDto(entity.id!!, entity.name, entity.inventoryQuantity, entity.isMixType, entity.materialType!!) fun materialQuantityDto( materialId: Long, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt index afe56b2..7a91003 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt @@ -32,7 +32,7 @@ data class MixMaterialDto( data class MixMaterialOutputDto( val id: Long, - val material: MaterialOutputDto, + val material: Material, // TODO move to MaterialDto val quantity: Float, val position: Int ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt index 2e56962..dfdee70 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt @@ -1,16 +1,19 @@ package dev.fyloz.colorrecipesexplorer.model import com.fasterxml.jackson.annotation.JsonIgnore +import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.model.account.Group import dev.fyloz.colorrecipesexplorer.model.account.group -import dev.fyloz.colorrecipesexplorer.rest.FILE_CONTROLLER_PATH import java.net.URLEncoder import java.nio.charset.StandardCharsets import java.time.LocalDate import javax.persistence.* -import javax.validation.constraints.* +import javax.validation.constraints.Max +import javax.validation.constraints.Min +import javax.validation.constraints.NotBlank +import javax.validation.constraints.Pattern private const val VALIDATION_COLOR_PATTERN = "^#([0-9a-f]{6})$" @@ -67,7 +70,7 @@ data class Recipe( groupsInformation.firstOrNull { it.group.id == groupId } fun imageUrl(deploymentUrl: String, name: String) = - "$deploymentUrl$FILE_CONTROLLER_PATH?path=${ + "$deploymentUrl${Constants.ControllerPaths.file}?path=${ URLEncoder.encode( "${this.imagesDirectoryPath}/$name", StandardCharsets.UTF_8 diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/CompanyRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/CompanyRepository.kt index 963e80d..484bd4d 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/CompanyRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/CompanyRepository.kt @@ -7,7 +7,7 @@ import org.springframework.stereotype.Repository @Repository interface CompanyRepository : JpaRepository { - /** Checks if a company with the given [name] and an id different from the given [id] exists. */ + /** Checks if a company with the given [name] and a different [id] exists. */ fun existsByNameAndIdNot(name: String, id: Long): Boolean /** Checks if a recipe depends on the company with the given [id]. */ @@ -17,5 +17,5 @@ interface CompanyRepository : JpaRepository { from Recipe r where r.company.id = :id """ ) - fun recipesDependsOnCompanyById(id: Long): Boolean + fun isUsedByRecipe(id: Long): Boolean } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MaterialRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MaterialRepository.kt index 03ed9c3..5125087 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MaterialRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MaterialRepository.kt @@ -1,29 +1,33 @@ package dev.fyloz.colorrecipesexplorer.repository import dev.fyloz.colorrecipesexplorer.model.Material -import dev.fyloz.colorrecipesexplorer.model.MaterialType +import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository @Repository -interface MaterialRepository : NamedJpaRepository { - /** Checks if one or more materials have the given [materialType]. */ - fun existsByMaterialType(materialType: MaterialType): Boolean +interface MaterialRepository : JpaRepository { + /** Checks if a material with the given [name] and a different [id] exists. */ + fun existsByNameAndIdNot(name: String, id: Long): Boolean + + /** Gets all non mix type materials. */ + fun getAllByIsMixTypeIsFalse(): Collection /** Updates the [inventoryQuantity] of the [Material] with the given [id]. */ @Modifying @Query("UPDATE Material m SET m.inventoryQuantity = :inventoryQuantity WHERE m.id = :id") fun updateInventoryQuantityById(id: Long, inventoryQuantity: Float) + /** Checks if a mix material or a mix type depends on the material with the given [id]. */ @Query( - """ + """ select case when(count(mm.id) + count(mt.id) > 0) then false else true end from Material m - left join MixMaterial mm on m.id = mm.material.id - left join MixType mt on m.id = mt.material.id + left join MixMaterial mm on mm.material.id = m.id + left join MixType mt on mt.material.id = m.id where m.id = :id - """ + """ ) - fun canBeDeleted(id: Long): Boolean + fun isUsedByMixMaterialOrMixType(id: Long): Boolean } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/FileController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/FileController.kt index 5d6aa84..346b195 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/FileController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/FileController.kt @@ -1,5 +1,6 @@ package dev.fyloz.colorrecipesexplorer.rest +import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic import dev.fyloz.colorrecipesexplorer.model.ConfigurationType @@ -10,10 +11,8 @@ import org.springframework.web.bind.annotation.* import org.springframework.web.multipart.MultipartFile import java.net.URI -const val FILE_CONTROLLER_PATH = "/api/file" - @RestController -@RequestMapping(FILE_CONTROLLER_PATH) +@RequestMapping(Constants.ControllerPaths.file) class FileController( private val fileLogic: WriteableFileLogic, private val configurationLogic: ConfigurationLogic @@ -44,6 +43,6 @@ class FileController( private fun created(path: String): ResponseEntity = ResponseEntity - .created(URI.create("${configurationLogic.get(ConfigurationType.INSTANCE_URL)}$FILE_CONTROLLER_PATH?path=$path")) + .created(URI.create("${configurationLogic.get(ConfigurationType.INSTANCE_URL)}${Constants.ControllerPaths.file}?path=$path")) .build() } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt index 9401f28..eb29623 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt @@ -1,8 +1,10 @@ package dev.fyloz.colorrecipesexplorer.rest +import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewCatalog +import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto +import dev.fyloz.colorrecipesexplorer.dtos.MaterialSaveDto import dev.fyloz.colorrecipesexplorer.logic.MaterialLogic -import dev.fyloz.colorrecipesexplorer.model.* import org.springframework.context.annotation.Profile import org.springframework.http.MediaType import org.springframework.security.access.prepost.PreAuthorize @@ -10,10 +12,8 @@ import org.springframework.web.bind.annotation.* import org.springframework.web.multipart.MultipartFile import javax.validation.Valid -private const val MATERIAL_CONTROLLER_PATH = "api/material" - @RestController -@RequestMapping(MATERIAL_CONTROLLER_PATH) +@RequestMapping(Constants.ControllerPaths.material) @Profile("!emergency") @PreAuthorizeViewCatalog class MaterialController( @@ -21,7 +21,7 @@ class MaterialController( ) { @GetMapping fun getAll() = - ok(materialLogic.getAllForOutput()) + ok(materialLogic.getAll()) @GetMapping("notmixtype") fun getAllNotMixType() = @@ -29,37 +29,20 @@ class MaterialController( @GetMapping("{id}") fun getById(@PathVariable id: Long) = - ok(materialLogic.getByIdForOutput(id)) + ok(materialLogic.getById(id)) @PostMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) @PreAuthorize("hasAuthority('EDIT_MATERIALS')") fun save(@Valid material: MaterialSaveDto, simdutFile: MultipartFile?) = - created(MATERIAL_CONTROLLER_PATH) { - with(materialLogic) { - save( - materialSaveDto( - name = material.name, - inventoryQuantity = material.inventoryQuantity, - materialTypeId = material.materialTypeId, - simdutFile = simdutFile - ) - ).toOutput() - } + created(Constants.ControllerPaths.material) { + materialLogic.save(material.copy(simdutFile = simdutFile)) } @PutMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) @PreAuthorize("hasAuthority('EDIT_MATERIALS')") - fun update(@Valid material: MaterialUpdateDto, simdutFile: MultipartFile?) = + fun update(@Valid material: MaterialSaveDto, simdutFile: MultipartFile?) = noContent { - materialLogic.update( - materialUpdateDto( - id = material.id, - name = material.name, - inventoryQuantity = material.inventoryQuantity, - materialTypeId = material.materialTypeId, - simdutFile = simdutFile - ) - ) + materialLogic.update(material.copy(simdutFile = simdutFile)) } @DeleteMapping("{id}") diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt index c65b91c..70057e4 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt @@ -10,14 +10,14 @@ interface CompanyService : Service { fun existsByName(name: String, id: Long?): Boolean /** Checks if a recipe depends on the company with the given [id]. */ - fun recipesDependsOnCompanyById(id: Long): Boolean + fun isUsedByRecipe(id: Long): Boolean } @ServiceComponent class DefaultCompanyService(repository: CompanyRepository) : BaseService(repository), CompanyService { override fun existsByName(name: String, id: Long?) = repository.existsByNameAndIdNot(name, id ?: 0) - override fun recipesDependsOnCompanyById(id: Long) = repository.recipesDependsOnCompanyById(id) + override fun isUsedByRecipe(id: Long) = repository.isUsedByRecipe(id) override fun toDto(entity: Company) = CompanyDto(entity.id!!, entity.name) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt new file mode 100644 index 0000000..1f3af3b --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt @@ -0,0 +1,58 @@ +package dev.fyloz.colorrecipesexplorer.service + +import dev.fyloz.colorrecipesexplorer.Constants +import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent +import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto +import dev.fyloz.colorrecipesexplorer.logic.files.FileLogic +import dev.fyloz.colorrecipesexplorer.model.Material +import dev.fyloz.colorrecipesexplorer.repository.MaterialRepository +import org.springframework.beans.factory.annotation.Qualifier +import java.net.URLEncoder +import java.nio.charset.StandardCharsets + +interface MaterialService : Service { + /** Checks if a material with the given [name] and a different [id] exists. */ + fun existsByName(name: String, id: Long?): Boolean + + /** Gets all non mix type materials. */ + fun getAllNotMixType(): Collection + + /** Updates the [inventoryQuantity] of the [Material] with the given [id]. */ + fun updateInventoryQuantityById(id: Long, inventoryQuantity: Float) + + /** Checks if a mix material or a mix type depends on the material with the given [id]. */ + fun isUsedByMixMaterialOrMixType(id: Long): Boolean +} + +@ServiceComponent +class DefaultMaterialService(repository: MaterialRepository, @Qualifier("defaultFileLogic") val fileLogic: FileLogic) : + BaseService(repository), MaterialService { + override fun existsByName(name: String, id: Long?) = repository.existsByNameAndIdNot(name, id ?: 0) + override fun getAllNotMixType() = repository.getAllByIsMixTypeIsFalse().map(this::toDto) + override fun updateInventoryQuantityById(id: Long, inventoryQuantity: Float) = repository.updateInventoryQuantityById(id, inventoryQuantity) + override fun isUsedByMixMaterialOrMixType(id: Long) = repository.isUsedByMixMaterialOrMixType(id) + + override fun toDto(entity: Material) = + MaterialDto( + entity.id!!, + entity.name, + entity.inventoryQuantity, + entity.isMixType, + entity.materialType!!, + getSimdutUrl(entity) + ) + + override fun toEntity(dto: MaterialDto) = + Material(dto.id, dto.name, dto.inventoryQuantity, dto.isMixType, dto.materialType) + + private fun getSimdutUrl(material: Material): String? { + val filePath = "${Constants.FilePaths.simdut}/${material.name}.pdf" + + if (!fileLogic.exists(filePath)) { + return null + } + + val encodedPath = URLEncoder.encode(filePath, StandardCharsets.UTF_8) + return "${Constants.ControllerPaths.file}?path=$encodedPath" + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultCompanyLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultCompanyLogicTest.kt index 6049ae9..0cde4a3 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultCompanyLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultCompanyLogicTest.kt @@ -46,7 +46,7 @@ class DefaultCompanyLogicTest { @Test fun deleteById_recipesDependsOnCompany_throwsCannotDeleteException() { // Arrange - every { companyServiceMock.recipesDependsOnCompanyById(company.id) } returns true + every { companyServiceMock.isUsedByRecipe(company.id) } returns true // Act // Assert diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialLogicTest.kt new file mode 100644 index 0000000..7cba942 --- /dev/null +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialLogicTest.kt @@ -0,0 +1,382 @@ +package dev.fyloz.colorrecipesexplorer.logic + +import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto +import dev.fyloz.colorrecipesexplorer.dtos.MaterialSaveDto +import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException +import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException +import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic +import dev.fyloz.colorrecipesexplorer.model.* +import dev.fyloz.colorrecipesexplorer.service.MaterialService +import io.mockk.* +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.mock.web.MockMultipartFile +import org.springframework.web.multipart.MultipartFile +import kotlin.test.assertContains +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class DefaultMaterialLogicTest { + private val materialServiceMock = mockk() + private val recipeLogicMock = mockk() + private val mixLogicMock = mockk() + private val materialTypeLogicMock = mockk() + private val fileLogicMock = mockk() + + private val materialLogic = spyk( + DefaultMaterialLogic( + materialServiceMock, recipeLogicMock, mixLogicMock, materialTypeLogicMock, fileLogicMock + ) + ) + + private val materialType = MaterialType( + 1L, "Unit test material type", "UNT", usePercentages = false, systemType = false + ) // TODO move to DTO + private val material = MaterialDto(1L, "Unit test material", 1000f, false, materialType) + private val materialMixType = material.copy(id = 2L, isMixType = true) + private val materialMixType2 = material.copy(id = 3L, isMixType = true) + private val company = Company(1L, "Unit test company") + private val recipe = Recipe( + 1L, + "Unit test recipe", + "Unit test recipe", + "#FFFFFF", + 0, + 123, + null, + "A remark", + company, + mutableListOf(), + setOf() + ) + private val mix = Mix( + 1L, "location", recipe, mixType = MixType(1L, "Unit test mix type", material(materialMixType)), mutableSetOf() + ) + private val mix2 = mix.copy(id = 2L, mixType = mix.mixType.copy(id = 2L, material = material(materialMixType2))) + + private val simdutFileMock = MockMultipartFile( + "Unit test SIMDUT", + byteArrayOf(1, 2, 3, 4) + ) // Put some content in the mock file so it is not ignored + private val materialSaveDto = MaterialSaveDto(1L, "Unit test material", 1000f, materialType.id!!, simdutFileMock) + + init { + recipe.mixes.addAll(listOf(mix, mix2)) + } + + @AfterEach + internal fun afterEach() { + clearAllMocks() + } + + @Test + fun existsByName_normalBehavior_returnsTrue() { + // Arrange + every { materialServiceMock.existsByName(any(), any()) } returns true + + // Act + val exists = materialLogic.existsByName(material.name) + + // Assert + assertTrue(exists) + } + + @Test + fun existsByName_notFound_returnsFalse() { + // Arrange + every { materialServiceMock.existsByName(any(), any()) } returns false + + // Act + val exists = materialLogic.existsByName(material.name) + + // Assert + assertFalse(exists) + } + + @Test + fun getAllNotMixType_normalBehavior_returnsMaterialsFromService() { + // Arrange + every { materialServiceMock.getAllNotMixType() } returns listOf(material) + + // Act + val materials = materialLogic.getAllNotMixType() + + // Assert + assertContains(materials, material) + } + + @Test + fun getAllForMixCreation_normalBehavior_returnsNonMixTypeMaterials() { + // Arrange + every { materialLogic.getAll() } returns listOf(material, materialMixType2) + every { recipeLogicMock.getById(any()) } returns recipe + + // Act + val materials = materialLogic.getAllForMixCreation(recipe.id!!) + + // Assert + assertContains(materials, material) + } + + @Test + fun getAllForMixCreation_normalBehavior_returnsRecipeMixTypesMaterials() { + // Arrange + every { materialLogic.getAll() } returns listOf(material, materialMixType2) + every { recipeLogicMock.getById(any()) } returns recipe + + // Act + val materials = materialLogic.getAllForMixCreation(recipe.id!!) + + // Assert + assertContains(materials, materialMixType2) + } + + @Test + fun getAllForMixUpdate_normalBehavior_returnsNonMixTypeMaterials() { + // Arrange + every { materialLogic.getAll() } returns listOf(material, materialMixType, materialMixType2) + every { mixLogicMock.getById(any()) } returns mix + + // Act + val materials = materialLogic.getAllForMixUpdate(mix.id!!) + + // Assert + assertContains(materials, material) + } + + @Test + fun getAllForMixUpdate_normalBehavior_returnsRecipeMixTypesMaterials() { + // Arrange + every { materialLogic.getAll() } returns listOf(material, materialMixType, materialMixType2) + every { mixLogicMock.getById(any()) } returns mix + + // Act + val materials = materialLogic.getAllForMixUpdate(mix.id!!) + + // Assert + assertContains(materials, materialMixType2) + } + + @Test + fun getAllForMixUpdate_normalBehavior_excludesGivenMixTypeMaterial() { + // Arrange + every { materialLogic.getAll() } returns listOf(material, materialMixType, materialMixType2) + every { mixLogicMock.getById(any()) } returns mix + + // Act + val materials = materialLogic.getAllForMixUpdate(mix.id!!) + + // Assert + assertFalse { materialMixType in materials } + } + + @Test + fun save_materialSaveDto_normalBehavior_callsSave() { + // Arrange + every { materialLogic.save(any()) } returns material + every { materialTypeLogicMock.getById(any()) } returns materialType + every { fileLogicMock.write(any(), any(), any()) } just runs + + // Act + materialLogic.save(materialSaveDto) + + // Assert + verify { + materialLogic.save(any()) + } + } + + @Test + fun save_materialSaveDto_normalBehavior_callsWriteInFileService() { + // Arrange + every { materialLogic.save(any()) } returns material + every { materialTypeLogicMock.getById(any()) } returns materialType + every { fileLogicMock.write(any(), any(), any()) } just runs + + // Act + materialLogic.save(materialSaveDto) + + // Assert + verify { + fileLogicMock.write(simdutFileMock, any(), false) + } + confirmVerified(fileLogicMock) + } + + @Test + fun save_materialSaveDto_noSimdutFile_doesNotCallWriteInFileService() { + // Arrange + every { materialLogic.save(any()) } returns material + every { materialTypeLogicMock.getById(any()) } returns materialType + every { fileLogicMock.write(any(), any(), any()) } just runs + + val saveDto = materialSaveDto.copy(simdutFile = null) + + // Act + materialLogic.save(saveDto) + + // Assert + verify(exactly = 0) { + fileLogicMock.write(simdutFileMock, any(), false) + } + confirmVerified(fileLogicMock) + } + + @Test + fun save_nameExists_throwsNameAlreadyExists() { + // Arrange + every { materialServiceMock.existsByName(any(), any()) } returns true + + // Act + // Assert + assertThrows { materialLogic.save(material) } + } + + @Test + fun update_saveDto_normalBehavior_callsUpdate() { + // Arrange + every { materialLogic.getById(any()) } returns material + every { materialLogic.update(any()) } returns material + every { materialTypeLogicMock.getById(any()) } returns materialType + every { fileLogicMock.write(any(), any(), any()) } just runs + + // Act + materialLogic.update(materialSaveDto) + + // Assert + verify { + materialLogic.update(any()) + } + } + + @Test + fun update_materialSaveDto_normalBehavior_callsWriteInFileService() { + // Arrange + every { materialLogic.getById(any()) } returns material + every { materialLogic.update(any()) } returns material + every { materialTypeLogicMock.getById(any()) } returns materialType + every { fileLogicMock.write(any(), any(), any()) } just runs + + // Act + materialLogic.update(materialSaveDto) + + // Assert + verify { + fileLogicMock.write(simdutFileMock, any(), true) + } + confirmVerified(fileLogicMock) + } + + @Test + fun update_materialSaveDto_noSimdutFile_doesNotCallWriteInFileService() { + // Arrange + every { materialLogic.getById(any()) } returns material + every { materialLogic.update(any()) } returns material + every { materialTypeLogicMock.getById(any()) } returns materialType + every { fileLogicMock.write(any(), any(), any()) } just runs + + val saveDto = materialSaveDto.copy(simdutFile = null) + + // Act + materialLogic.update(saveDto) + + // Assert + verify(exactly = 0) { + fileLogicMock.write(simdutFileMock, any(), true) + } + confirmVerified(fileLogicMock) + } + + @Test + fun updateQuantity_normalBehavior_callsUpdateInventoryQuantityByIdInService() { + // Arrange + every { materialServiceMock.updateInventoryQuantityById(any(), any()) } just runs + + val factor = 3f + + // Act + materialLogic.updateQuantity(material, factor) + + // Assert + verify { + materialServiceMock.updateInventoryQuantityById(material.id, material.inventoryQuantity + factor) + } + confirmVerified(materialServiceMock) + } + + @Test + fun updateQuantity_normalBehavior_returnsUpdatedQuantity() { + // Arrange + every { materialServiceMock.updateInventoryQuantityById(any(), any()) } just runs + + val factor = 3f + + // Act + val updatedQuantity = materialLogic.updateQuantity(material, factor) + + // Assert + assertEquals(material.inventoryQuantity + factor, updatedQuantity) + } + + @Test + fun deleteById_normalBehavior_callsDeleteInFileLogic() { + // Arrange + every { materialLogic.getById(any()) } returns material + every { materialServiceMock.isUsedByMixMaterialOrMixType(any()) } returns false + every { materialServiceMock.deleteById(any()) } just runs + every { fileLogicMock.exists(any()) } returns true + every { fileLogicMock.delete(any()) } just runs + + val simdutPath = Material.getSimdutFilePath(material.name) + + // Act + materialLogic.deleteById(material.id) + + // Assert + verify { + fileLogicMock.exists(simdutPath) + fileLogicMock.delete(simdutPath) + } + confirmVerified(fileLogicMock) + } + + @Test + fun deleteById_simdutFileNotExists_doesNotCallDeleteInFileLogic() { + // Arrange + every { materialLogic.getById(any()) } returns material + every { materialServiceMock.isUsedByMixMaterialOrMixType(any()) } returns false + every { materialServiceMock.deleteById(any()) } just runs + every { fileLogicMock.exists(any()) } returns false + every { fileLogicMock.delete(any()) } just runs + + val simdutPath = Material.getSimdutFilePath(material.name) + + // Act + materialLogic.deleteById(material.id) + + // Assert + verify { + fileLogicMock.exists(simdutPath) + } + verify(exactly = 0) { + fileLogicMock.delete(simdutPath) + } + confirmVerified(fileLogicMock) + } + + @Test + fun deleteById_usedByMixMaterialOrMixType_throwsCannotDeleteException() { + // Arrange + every { materialLogic.getById(any()) } returns material + every { materialServiceMock.isUsedByMixMaterialOrMixType(any()) } returns true + every { materialServiceMock.deleteById(any()) } just runs + every { fileLogicMock.exists(any()) } returns false + every { fileLogicMock.delete(any()) } just runs + + // Act + // Assert + assertThrows { materialLogic.deleteById(material.id) } + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogicTest.kt index be94bf5..96a416c 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogicTest.kt @@ -175,7 +175,7 @@ class InventoryLogicTest { ) { val material = material(id = materialQuantity.material, inventoryQuantity = stored) - whenever(materialLogic.getById(material.id!!)).doReturn(material) + whenever(materialLogic.getById(material.id!!)).doReturn(materialDto(material)) materialQuantity.test(stored) } diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogicTest.kt deleted file mode 100644 index ee326cb..0000000 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogicTest.kt +++ /dev/null @@ -1,249 +0,0 @@ -package dev.fyloz.colorrecipesexplorer.logic - -import com.nhaarman.mockitokotlin2.* -import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException -import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic -import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.repository.MaterialRepository -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance -import org.junit.jupiter.api.assertThrows -import org.springframework.mock.web.MockMultipartFile -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class MaterialLogicTest : - AbstractExternalNamedModelServiceTest() { - override val repository: MaterialRepository = mock() - private val recipeService: RecipeLogic = mock() - private val mixService: MixLogic = mock() - private val materialTypeService: MaterialTypeLogic = mock() - private val fileService: WriteableFileLogic = mock() - override val logic: MaterialLogic = - spy(DefaultMaterialLogic(repository, recipeService, mixService, materialTypeService, fileService, mock())) - - override val entity: Material = material(id = 0L, name = "material") - private val entityOutput = materialOutputDto(entity) - override val anotherEntity: Material = material(id = 1L, name = "another material") - override val entityWithEntityName: Material = material(id = 2L, name = "material") - override val entitySaveDto: MaterialSaveDto = spy(materialSaveDto()) - override val entityUpdateDto: MaterialUpdateDto = spy(materialUpdateDto(id = 0L)) - - private val materialType = materialType() - - @AfterEach - override fun afterEach() { - reset(recipeService, mixService, materialTypeService, fileService) - super.afterEach() - } - - // existsByMaterialType - - @Test - fun `existsByMaterialType() returns true when a material with the given material type exists in the repository`() { - whenever(repository.existsByMaterialType(materialType)).doReturn(true) - - val found = logic.existsByMaterialType(materialType) - - assertTrue(found) - } - - @Test - fun `existsByMaterialType() returns false when no material with the given material type exists in the repository`() { - whenever(repository.existsByMaterialType(materialType)).doReturn(false) - - val found = logic.existsByMaterialType(materialType) - - assertFalse(found) - } - - // hasSimdut() - - @Test - fun `hasSimdut() returns false when simdutService_exists() returns false`() { - whenever(fileService.exists(any())).doReturn(false) - doReturn(entity).whenever(logic).getById(entity.id!!) - - val found = logic.hasSimdut(entity) - - assertFalse(found) - } - - @Test - fun `hasSimdut() returns true when simdutService_exists() returns true`() { - whenever(fileService.exists(any())).doReturn(true) - doReturn(entity).whenever(logic).getById(entity.id!!) - - val found = logic.hasSimdut(entity) - - assertTrue(found) - } - - // getAllNotMixType() - - @Test - fun `getAllNotMixType() returns a list containing every material that are not a mix type`() { - val mixTypeMaterial = material(id = 1L, name = "mix type material", isMixType = true) - val mixTypeMaterialOutput = materialOutputDto(mixTypeMaterial) - val materialList = listOf(entity, mixTypeMaterial) - - doReturn(materialList).whenever(logic).getAll() - - val found = logic.getAllNotMixType() - - assertTrue(found.contains(entityOutput)) - assertFalse(found.contains(mixTypeMaterialOutput)) - } - - // save() - - @Test - fun `save() throws AlreadyExistsException when a material with the given name exists in the repository`() { - doReturn(true).whenever(logic).existsByName(entity.name) - - assertThrows { logic.save(entity) } - .assertErrorCode("name") - } - - @Test - override fun `save(dto) calls and returns save() with the created entity`() { - withBaseSaveDtoTest(entity, entitySaveDto, logic, { any() }) - } - - @Test - fun `save(dto) calls simdutService_write() with the saved entity`() { - val mockMultipartFile = spy(MockMultipartFile("simdut", byteArrayOf())) - val materialSaveDto = spy(materialSaveDto(simdutFile = mockMultipartFile)) - - doReturn(false).whenever(mockMultipartFile).isEmpty - doReturn(entity).whenever(logic).save(any()) - - logic.save(materialSaveDto) - - verify(fileService).write(mockMultipartFile, entity.simdutFilePath, false) - } - - // update() - - @Test - fun `update() throws AlreadyExistsException when another material with the updated name exists in the repository`() { - val material = material(id = 0L, name = "name") - val anotherMaterial = material(id = 1L, name = "name") - - whenever(repository.findByName(material.name)).doReturn(anotherMaterial) - doReturn(entity).whenever(logic).getById(material.id!!) - - assertThrows { logic.update(material) } - .assertErrorCode("name") - } - - @Test - override fun `update(dto) calls and returns update() with the created entity`() { - val mockSimdutFile = MockMultipartFile("simdut", byteArrayOf(1, 2, 3, 4, 5)) - val materialUpdateDto = spy(materialUpdateDto(id = 0L, simdutFile = mockSimdutFile)) - - doReturn(entity).whenever(logic).getById(any()) - doReturn(entity).whenever(logic).update(any()) - doReturn(entity).whenever(materialUpdateDto).toEntity() - - logic.update(materialUpdateDto) - - verify(fileService).write(mockSimdutFile, entity.simdutFilePath, true) - } - - // updateQuantity() - - @Test - fun `updateQuantity() updates the quantity of the the given material in the repository`() { - val material = material(id = 0L, inventoryQuantity = 4321f) - val quantity = 1234f - val totalQuantity = material.inventoryQuantity + quantity - - val found = logic.updateQuantity(material, quantity) - - verify(repository).updateInventoryQuantityById(material.id!!, totalQuantity) - assertEquals(totalQuantity, found) - } - - // getAllForMixCreation() - - @Test - fun `getAllForMixCreation() returns all normal materials and all mix type materials for the given recipe`() { - val normalMaterial = material(id = 0L, isMixType = false) - val mixTypeMaterial = material(id = 1L, isMixType = true) - val anotherMixTypeMaterial = material(id = 2L, isMixType = true) - val materials = listOf(normalMaterial, mixTypeMaterial, anotherMixTypeMaterial) - val recipe = - recipe(id = 0L, mixes = mutableListOf(mix(mixType = mixType(id = 0L, material = mixTypeMaterial)))) - - whenever(recipeService.getById(recipe.id!!)).doReturn(recipe) - doReturn(materials).whenever(logic).getAll() - - val found = logic.getAllForMixCreation(recipe.id!!) - - assertTrue(materialOutputDto(normalMaterial) in found) - assertTrue(materialOutputDto(mixTypeMaterial) in found) - assertFalse(materialOutputDto(anotherMixTypeMaterial) in found) - } - - // getAllForMixUpdate() - - @Test - fun `getAllForMixUpdate() returns all normal materials and all mix type materials for the recipe of the given mix without the mix type of the said mix`() { - val normalMaterial = material(id = 0L, isMixType = false) - val mixTypeMaterial = material(id = 1L, isMixType = true) - val anotherMixTypeMaterial = material(id = 2L, isMixType = true) - val materials = listOf(normalMaterial, mixTypeMaterial, anotherMixTypeMaterial) - val recipe = recipe(id = 0L, mixes = mutableListOf(mix(mixType = mixType(material = mixTypeMaterial)))) - val mix = mix(id = 1L, recipe = recipe, mixType = mixType(material = anotherMixTypeMaterial)) - recipe.mixes.add(mix) - - whenever(mixService.getById(mix.id!!)).doReturn(mix) - doReturn(materials).whenever(logic).getAll() - - val found = logic.getAllForMixUpdate(mix.id!!) - - assertTrue(materialOutputDto(normalMaterial) in found) - assertTrue(materialOutputDto(mixTypeMaterial) in found) - assertFalse(materialOutputDto(anotherMixTypeMaterial) in found) - } - - - // delete() - - override fun `delete() deletes in the repository`() { - whenCanBeDeleted { - super.`delete() deletes in the repository`() - } - } - - // deleteById() - - override fun `deleteById() deletes the entity with the given id in the repository`() { - whenCanBeDeleted { - super.`deleteById() deletes the entity with the given id in the repository`() - } - } - - /** Utility property to check if the identifier of the given [Material] is even. */ - private val Material.evenId: Boolean - get() = this.id!! % 2 == 0L - - private fun whenCanBeDeleted(id: Long = any(), test: () -> Unit) { - whenever(repository.canBeDeleted(id)).doReturn(true) - - test() - } - - private fun materialOutputDto(material: Material) = MaterialOutputDto( - id = material.id!!, - name = material.name, - inventoryQuantity = material.inventoryQuantity, - isMixType = material.isMixType, - materialType = material.materialType!!, - simdutUrl = null - ) -} diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogicTest.kt index 18611a6..b1e84d3 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogicTest.kt @@ -20,7 +20,7 @@ class MaterialTypeLogicTest : AbstractExternalNamedModelServiceTest() { override val repository: MaterialTypeRepository = mock() private val materialService: MaterialLogic = mock() - override val logic: MaterialTypeLogic = spy(DefaultMaterialTypeLogic(repository, materialService)) + override val logic: MaterialTypeLogic = spy(DefaultMaterialTypeLogic(repository)) override val entity: MaterialType = materialType(id = 0L, name = "material type", prefix = "MAT") override val anotherEntity: MaterialType = materialType(id = 1L, name = "another material type", prefix = "AMT") override val entityWithEntityName: MaterialType = materialType(2L, name = entity.name, prefix = "EEN") @@ -56,26 +56,6 @@ class MaterialTypeLogicTest : assertFalse(found) } - // isUsedByMaterial() - - @Test - fun `isUsedByMaterial() returns true when materialService_existsByMaterialType() returns true`() { - whenever(materialService.existsByMaterialType(entity)).doReturn(true) - - val found = logic.isUsedByMaterial(entity) - - assertTrue(found) - } - - @Test - fun `isUsedByMaterial() returns false when materialService_existsByMaterialType() returns false`() { - whenever(materialService.existsByMaterialType(entity)).doReturn(false) - - val found = logic.isUsedByMaterial(entity) - - assertFalse(found) - } - // getAllSystemTypes() @Test diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogicTest.kt index 88ee333..2b54a53 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogicTest.kt @@ -78,7 +78,7 @@ class MixMaterialLogicTest : AbstractModelServiceTest Date: Sun, 27 Feb 2022 20:46:56 -0500 Subject: [PATCH 04/11] #25 Migrate recipe steps to new logic --- .../dtos/RecipeStepDto.kt | 15 + .../fyloz/colorrecipesexplorer/logic/Logic.kt | 8 +- .../logic/RecipeStepLogic.kt | 143 +++++----- .../colorrecipesexplorer/model/RecipeStep.kt | 35 +-- .../service/RecipeStepService.kt | 18 ++ .../logic/DefaultRecipeStepLogicTest.kt | 257 ++++++++++++++++++ .../logic/RecipeStepLogicTest.kt | 109 -------- 7 files changed, 373 insertions(+), 212 deletions(-) create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/RecipeStepDto.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt create mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeStepLogicTest.kt delete mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogicTest.kt diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/RecipeStepDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/RecipeStepDto.kt new file mode 100644 index 0000000..f1fe4eb --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/RecipeStepDto.kt @@ -0,0 +1,15 @@ +package dev.fyloz.colorrecipesexplorer.dtos + +data class RecipeStepDto( + override val id: Long = 0L, + + val position: Int, + + val message: String +) : EntityDto { + companion object { + const val VALIDATION_ERROR_CODE_INVALID_FIRST_STEP = "first" + const val VALIDATION_ERROR_CODE_DUPLICATED_STEPS_POSITION = "duplicated" + const val VALIDATION_ERROR_CODE_GAP_BETWEEN_STEPS_POSITIONS = "gap" + } +} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt index 647d6de..3578c0f 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt @@ -61,7 +61,7 @@ abstract class BaseLogic>( override fun deleteById(id: Long) = service.deleteById(id) - protected fun notFoundException(identifierName: String = idIdentifierName, value: Any) = + protected fun notFoundException(identifierName: String = ID_IDENTIFIER_NAME, value: Any) = NotFoundException( typeNameLowerCase, "$typeName not found", @@ -70,7 +70,7 @@ abstract class BaseLogic>( identifierName ) - protected fun alreadyExistsException(identifierName: String = nameIdentifierName, value: Any) = + protected fun alreadyExistsException(identifierName: String = NAME_IDENTIFIER_NAME, value: Any) = AlreadyExistsException( typeNameLowerCase, "$typeName already exists", @@ -87,7 +87,7 @@ abstract class BaseLogic>( ) companion object { - const val idIdentifierName = "id" - const val nameIdentifierName = "name" + const val ID_IDENTIFIER_NAME = "id" + const val NAME_IDENTIFIER_NAME = "name" } } \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt index 3bc0d2c..3323ff8 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt @@ -1,19 +1,18 @@ package dev.fyloz.colorrecipesexplorer.logic -import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase +import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent +import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto import dev.fyloz.colorrecipesexplorer.exception.RestException import dev.fyloz.colorrecipesexplorer.model.RecipeGroupInformation import dev.fyloz.colorrecipesexplorer.model.RecipeStep import dev.fyloz.colorrecipesexplorer.model.account.Group -import dev.fyloz.colorrecipesexplorer.model.recipeStepIdAlreadyExistsException -import dev.fyloz.colorrecipesexplorer.model.recipeStepIdNotFoundException -import dev.fyloz.colorrecipesexplorer.repository.RecipeStepRepository +import dev.fyloz.colorrecipesexplorer.model.recipeStepDto +import dev.fyloz.colorrecipesexplorer.service.RecipeStepService import dev.fyloz.colorrecipesexplorer.utils.findDuplicated import dev.fyloz.colorrecipesexplorer.utils.hasGaps import org.springframework.http.HttpStatus -import org.springframework.stereotype.Service -interface RecipeStepLogic : ModelService { +interface RecipeStepLogic : Logic { /** Validates the steps of the given [groupInformation], according to the criteria of [validateSteps]. */ fun validateGroupInformationSteps(groupInformation: RecipeGroupInformation) @@ -22,106 +21,112 @@ interface RecipeStepLogic : ModelService { * There must also be no gap between the positions. * If any of those criteria are not met, an [InvalidGroupStepsPositionsException] will be thrown. */ - fun validateSteps(steps: Set) + fun validateSteps(steps: Set) } -@Service -@RequireDatabase -class DefaultRecipeStepLogic(recipeStepRepository: RecipeStepRepository) : - AbstractModelService(recipeStepRepository), - RecipeStepLogic { - override fun idNotFoundException(id: Long) = recipeStepIdNotFoundException(id) - override fun idAlreadyExistsException(id: Long) = recipeStepIdAlreadyExistsException(id) - +@LogicComponent +class DefaultRecipeStepLogic(recipeStepService: RecipeStepService) : + BaseLogic(recipeStepService, RecipeStep::class.simpleName!!), RecipeStepLogic { override fun validateGroupInformationSteps(groupInformation: RecipeGroupInformation) { if (groupInformation.steps == null) return try { - validateSteps(groupInformation.steps!!) + validateSteps(groupInformation.steps!!.map { recipeStepDto(it) }.toSet()) } catch (validationException: InvalidStepsPositionsException) { throw InvalidGroupStepsPositionsException(groupInformation.group, validationException) } } - override fun validateSteps(steps: Set) { + override fun validateSteps(steps: Set) { if (steps.isEmpty()) return val sortedSteps = steps.sortedBy { it.position } val errors = mutableSetOf() // Check if the first step position is 1 - fun isFirstStepPositionInvalid() = - sortedSteps[0].position != 1 + validateFirstStepPosition(sortedSteps, errors) // Check if any position is duplicated - fun getDuplicatedPositionsErrors() = - sortedSteps - .findDuplicated { it.position } - .map { duplicatedStepsPositions(it) } + validateDuplicatedStepsPositions(sortedSteps, errors) + + // Check for gaps between positions + validateGapsInStepsPositions(sortedSteps, errors) - // Find all errors and throw if there is any - if (isFirstStepPositionInvalid()) errors += invalidFirstStepPosition(sortedSteps[0]) - errors += getDuplicatedPositionsErrors() - if (errors.isEmpty() && steps.hasGaps { it.position }) errors += gapBetweenStepsPositions() if (errors.isNotEmpty()) { throw InvalidStepsPositionsException(errors) } } + + private fun validateFirstStepPosition( + steps: List, + errors: MutableSet + ) { + if (steps[0].position != 1) { + errors += InvalidStepsPositionsError( + RecipeStepDto.VALIDATION_ERROR_CODE_INVALID_FIRST_STEP, + "The first step must be at position 1" + ) + } + } + + private fun validateDuplicatedStepsPositions( + steps: List, + errors: MutableSet + ) { + errors += steps + .findDuplicated { it.position } + .map { + InvalidStepsPositionsError( + RecipeStepDto.VALIDATION_ERROR_CODE_DUPLICATED_STEPS_POSITION, + "The position $it is duplicated" + ) + } + } + + private fun validateGapsInStepsPositions( + steps: List, + errors: MutableSet + ) { + if (errors.isEmpty() && steps.hasGaps { it.position }) { + errors += InvalidStepsPositionsError( + RecipeStepDto.VALIDATION_ERROR_CODE_GAP_BETWEEN_STEPS_POSITIONS, + "There is a gap between steps positions" + ) + } + } } data class InvalidStepsPositionsError( - val type: String, - val details: String + val type: String, + val details: String ) class InvalidStepsPositionsException( - val errors: Set + val errors: Set ) : RestException( - "invalid-recipestep-position", - "Invalid steps positions", - HttpStatus.BAD_REQUEST, - "The position of steps are invalid", - mapOf( - "invalidSteps" to errors - ) + "invalid-recipestep-position", + "Invalid steps positions", + HttpStatus.BAD_REQUEST, + "The position of steps are invalid", + mapOf( + "invalidSteps" to errors + ) ) class InvalidGroupStepsPositionsException( val group: Group, val exception: InvalidStepsPositionsException ) : RestException( - "invalid-groupinformation-recipestep-position", - "Invalid steps positions", - HttpStatus.BAD_REQUEST, - "The position of steps for the group ${group.name} are invalid", - mapOf( - "group" to group.name, - "groupId" to group.id!!, - "invalidSteps" to exception.errors - ) + "invalid-groupinformation-recipestep-position", + "Invalid steps positions", + HttpStatus.BAD_REQUEST, + "The position of steps for the group ${group.name} are invalid", + mapOf( + "group" to group.name, + "groupId" to group.id!!, + "invalidSteps" to exception.errors + ) ) { val errors: Set get() = exception.errors -} - -const val INVALID_FIRST_STEP_POSITION_ERROR_CODE = "first" -const val DUPLICATED_STEPS_POSITIONS_ERROR_CODE = "duplicated" -const val GAP_BETWEEN_STEPS_POSITIONS_ERROR_CODE = "gap" - -private fun invalidFirstStepPosition(step: RecipeStep) = - InvalidStepsPositionsError( - INVALID_FIRST_STEP_POSITION_ERROR_CODE, - "The position ${step.position} is under the minimum of 1" - ) - -private fun duplicatedStepsPositions(position: Int) = - InvalidStepsPositionsError( - DUPLICATED_STEPS_POSITIONS_ERROR_CODE, - "The position $position is duplicated" - ) - -private fun gapBetweenStepsPositions() = - InvalidStepsPositionsError( - GAP_BETWEEN_STEPS_POSITIONS_ERROR_CODE, - "There is a gap between steps positions" - ) +} \ 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 51f9377..ddc6eac 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/RecipeStep.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/RecipeStep.kt @@ -1,7 +1,6 @@ package dev.fyloz.colorrecipesexplorer.model -import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException -import dev.fyloz.colorrecipesexplorer.exception.NotFoundException +import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto import javax.persistence.* @Entity @@ -16,31 +15,7 @@ data class RecipeStep( val message: String ) : ModelEntity -// ==== DSL ==== -fun recipeStep( - id: Long? = null, - position: Int = 0, - message: String = "message", - op: RecipeStep.() -> Unit = {} -) = RecipeStep(id, position, message).apply(op) - -// ==== Exceptions ==== -private const val RECIPE_STEP_NOT_FOUND_EXCEPTION_TITLE = "Recipe step not found" -private const val RECIPE_STEP_ALREADY_EXISTS_EXCEPTION_TITLE = "Recipe step already exists" -private const val RECIPE_STEP_EXCEPTION_ERROR_CODE = "recipestep" - -fun recipeStepIdNotFoundException(id: Long) = - NotFoundException( - RECIPE_STEP_EXCEPTION_ERROR_CODE, - RECIPE_STEP_NOT_FOUND_EXCEPTION_TITLE, - "A recipe step with the id $id could not be found", - id - ) - -fun recipeStepIdAlreadyExistsException(id: Long) = - AlreadyExistsException( - RECIPE_STEP_EXCEPTION_ERROR_CODE, - RECIPE_STEP_ALREADY_EXISTS_EXCEPTION_TITLE, - "A recipe step with the id $id already exists", - id - ) +@Deprecated("Temporary DSL for transition") +fun recipeStepDto( + entity: RecipeStep +) = RecipeStepDto(entity.id!!, entity.position, entity.message) \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt new file mode 100644 index 0000000..57a8777 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt @@ -0,0 +1,18 @@ +package dev.fyloz.colorrecipesexplorer.service + +import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent +import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto +import dev.fyloz.colorrecipesexplorer.model.RecipeStep +import dev.fyloz.colorrecipesexplorer.repository.RecipeStepRepository + +interface RecipeStepService : Service + +@ServiceComponent +class DefaultRecipeStepService(repository: RecipeStepRepository) : + BaseService(repository), RecipeStepService { + override fun toDto(entity: RecipeStep) = + RecipeStepDto(entity.id!!, entity.position, entity.message) + + override fun toEntity(dto: RecipeStepDto) = + RecipeStep(dto.id, dto.position, dto.message) +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeStepLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeStepLogicTest.kt new file mode 100644 index 0000000..f606abd --- /dev/null +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeStepLogicTest.kt @@ -0,0 +1,257 @@ +package dev.fyloz.colorrecipesexplorer.logic + +import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto +import dev.fyloz.colorrecipesexplorer.model.RecipeGroupInformation +import dev.fyloz.colorrecipesexplorer.model.RecipeStep +import dev.fyloz.colorrecipesexplorer.model.account.Group +import dev.fyloz.colorrecipesexplorer.service.RecipeStepService +import io.mockk.* +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertTrue + +class DefaultRecipeStepLogicTest { + private val recipeStepServiceMock = mockk() + + private val recipeStepLogic = spyk(DefaultRecipeStepLogic(recipeStepServiceMock)) + + @AfterEach + internal fun afterEach() { + clearAllMocks() + } + + @Test + fun validateGroupInformationSteps_normalBehavior_callsValidateSteps() { + // Arrange + every { recipeStepLogic.validateSteps(any()) } just runs + + val group = Group(1L, "Unit test group") + val steps = mutableSetOf(RecipeStep(1L, 1, "A message")) + val groupInfo = RecipeGroupInformation(1L, group, "A note", steps) + + // Act + recipeStepLogic.validateGroupInformationSteps(groupInfo) + + // Assert + verify { + recipeStepLogic.validateSteps(any()) // TODO replace with actual steps dtos when RecipeGroupInformation updated + } + } + + @Test + fun validateGroupInformationSteps_stepSetIsNull_doesNothing() { + // Arrange + every { recipeStepLogic.validateSteps(any()) } just runs + + val group = Group(1L, "Unit test group") + val groupInfo = RecipeGroupInformation(1L, group, "A note", null) + + // Act + recipeStepLogic.validateGroupInformationSteps(groupInfo) + + // Assert + verify(exactly = 0) { + recipeStepLogic.validateSteps(any()) // TODO replace with actual steps dtos when RecipeGroupInformation updated + } + } + + @Test + fun validateGroupInformationSteps_invalidSteps_throwsInvalidGroupStepsPositionsException() { + // Arrange + val errors = setOf(InvalidStepsPositionsError("error", "An unit test error")) + every { recipeStepLogic.validateSteps(any()) } throws InvalidStepsPositionsException(errors) + + val group = Group(1L, "Unit test group") + val steps = mutableSetOf(RecipeStep(1L, 1, "A message")) + val groupInfo = RecipeGroupInformation(1L, group, "A note", steps) + + // Act + // Assert + assertThrows { recipeStepLogic.validateGroupInformationSteps(groupInfo) } + } + + @Test + fun validateSteps_normalBehavior_doesNothing() { + // Arrange + val recipeSteps = setOf( + RecipeStepDto(1L, 1, "A message"), + RecipeStepDto(2L, 2, "Another message") + ) + + // Act + // Assert + assertDoesNotThrow { recipeStepLogic.validateSteps(recipeSteps) } + } + + @Test + fun validateSteps_emptyStepSet_doesNothing() { + // Arrange + val recipeSteps = setOf() + + // Act + // Assert + assertDoesNotThrow { recipeStepLogic.validateSteps(recipeSteps) } + } + + @Test + fun validateSteps_hasInvalidPositions_throwsInvalidStepsPositionsException() { + // Arrange + val recipeSteps = setOf( + RecipeStepDto(1L, 2, "A message"), + RecipeStepDto(2L, 3, "Another message") + ) + + // Act + // Assert + assertThrows { recipeStepLogic.validateSteps(recipeSteps) } + } + + @Test + fun validateSteps_firstStepPositionInvalid_returnsInvalidStepValidationError() { + // Arrange + val recipeSteps = setOf( + RecipeStepDto(1L, 2, "A message"), + RecipeStepDto(2L, 3, "Another message") + ) + + // Act + val exception = assertThrows { recipeStepLogic.validateSteps(recipeSteps) } + + // Assert + assertTrue { + exception.errors.any { it.type == RecipeStepDto.VALIDATION_ERROR_CODE_INVALID_FIRST_STEP } + } + } + + @Test + fun validateSteps_duplicatedPositions_returnsInvalidStepValidationError() { + // Arrange + val recipeSteps = setOf( + RecipeStepDto(1L, 1, "A message"), + RecipeStepDto(2L, 1, "Another message") + ) + + // Act + val exception = assertThrows { recipeStepLogic.validateSteps(recipeSteps) } + + // Assert + assertTrue { + exception.errors.any { it.type == RecipeStepDto.VALIDATION_ERROR_CODE_DUPLICATED_STEPS_POSITION } + } + } + + @Test + fun validateSteps_gapsInPositions_returnsInvalidStepValidationError() { + // Arrange + val recipeSteps = setOf( + RecipeStepDto(1L, 1, "A message"), + RecipeStepDto(2L, 3, "Another message") + ) + + // Act + val exception = assertThrows { recipeStepLogic.validateSteps(recipeSteps) } + + // Assert + assertTrue { + exception.errors.any { it.type == RecipeStepDto.VALIDATION_ERROR_CODE_GAP_BETWEEN_STEPS_POSITIONS } + } + } +} + +//@TestInstance(TestInstance.Lifecycle.PER_CLASS) +//class RecipeStepLogicTest : +// AbstractModelServiceTest() { +// override val repository: RecipeStepRepository = mock() +// override val logic: RecipeStepLogic = spy(DefaultRecipeStepLogic(repository)) +// +// override val entity: RecipeStep = recipeStep(id = 0L, message = "message") +// override val anotherEntity: RecipeStep = recipeStep(id = 1L, message = "another message") +// +// // validateGroupInformationSteps() +// +// @Test +// fun `validateGroupInformationSteps() calls validateSteps() with the given RecipeGroupInformation steps`() { +// withGroupInformation { +// logic.validateGroupInformationSteps(this) +// +// verify(logic).validateSteps(this.steps!!) +// } +// } +// +// @Test +// fun `validateGroupInformationSteps() throws InvalidGroupStepsPositionsException when validateSteps() throws an InvalidStepsPositionsException`() { +// withGroupInformation { +// doAnswer { throw InvalidStepsPositionsException(setOf()) }.whenever(logic).validateSteps(this.steps!!) +// +// assertThrows { +// logic.validateGroupInformationSteps(this) +// } +// } +// } +// +// // validateSteps() +// +// @Test +// fun `validateSteps() throws an InvalidStepsPositionsException when the position of the first step of the given groupInformation is not 1`() { +// assertInvalidStepsPositionsException( +// mutableSetOf( +// recipeStep(id = 0L, position = 0), +// recipeStep(id = 1L, position = 1), +// recipeStep(id = 2L, position = 2), +// recipeStep(id = 3L, position = 3) +// ), +// INVALID_FIRST_STEP_POSITION_ERROR_CODE +// ) +// } +// +// @Test +// fun `validateSteps() throws an InvalidStepsPositionsException when steps positions are duplicated in the given groupInformation`() { +// assertInvalidStepsPositionsException( +// mutableSetOf( +// recipeStep(id = 0L, position = 1), +// recipeStep(id = 1L, position = 2), +// recipeStep(id = 2L, position = 2), +// recipeStep(id = 3L, position = 3) +// ), +// DUPLICATED_STEPS_POSITIONS_ERROR_CODE +// ) +// } +// +// @Test +// fun `validateSteps() throws an InvalidStepsPositionsException when there is a gap between steps positions in the given groupInformation`() { +// assertInvalidStepsPositionsException( +// mutableSetOf( +// recipeStep(id = 0L, position = 1), +// recipeStep(id = 1L, position = 2), +// recipeStep(id = 2L, position = 4), +// recipeStep(id = 3L, position = 5) +// ), +// GAP_BETWEEN_STEPS_POSITIONS_ERROR_CODE +// ) +// } +// +// private fun withGroupInformation(steps: MutableSet? = null, test: RecipeGroupInformation.() -> Unit) { +// recipeGroupInformation( +// group = group(id = 0L), +// steps = steps ?: mutableSetOf( +// recipeStep(id = 0L, position = 1), +// recipeStep(id = 1L, position = 2), +// recipeStep(id = 2L, position = 3), +// recipeStep(id = 3L, position = 4) +// ) +// ) { +// test() +// } +// } +// +// private fun assertInvalidStepsPositionsException(steps: MutableSet, errorType: String) { +// val exception = assertThrows { +// logic.validateSteps(steps) +// } +// +// assertTrue { exception.errors.size == 1 } +// assertTrue { exception.errors.first().type == errorType } +// } +//} diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogicTest.kt deleted file mode 100644 index 43b2767..0000000 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogicTest.kt +++ /dev/null @@ -1,109 +0,0 @@ -package dev.fyloz.colorrecipesexplorer.logic - -import com.nhaarman.mockitokotlin2.* -import dev.fyloz.colorrecipesexplorer.model.RecipeGroupInformation -import dev.fyloz.colorrecipesexplorer.model.RecipeStep -import dev.fyloz.colorrecipesexplorer.model.account.group -import dev.fyloz.colorrecipesexplorer.model.recipeGroupInformation -import dev.fyloz.colorrecipesexplorer.model.recipeStep -import dev.fyloz.colorrecipesexplorer.repository.RecipeStepRepository -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance -import org.junit.jupiter.api.assertThrows -import kotlin.test.assertTrue - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class RecipeStepLogicTest : - AbstractModelServiceTest() { - override val repository: RecipeStepRepository = mock() - override val logic: RecipeStepLogic = spy(DefaultRecipeStepLogic(repository)) - - override val entity: RecipeStep = recipeStep(id = 0L, message = "message") - override val anotherEntity: RecipeStep = recipeStep(id = 1L, message = "another message") - - // validateGroupInformationSteps() - - @Test - fun `validateGroupInformationSteps() calls validateSteps() with the given RecipeGroupInformation steps`() { - withGroupInformation { - logic.validateGroupInformationSteps(this) - - verify(logic).validateSteps(this.steps!!) - } - } - - @Test - fun `validateGroupInformationSteps() throws InvalidGroupStepsPositionsException when validateSteps() throws an InvalidStepsPositionsException`() { - withGroupInformation { - doAnswer { throw InvalidStepsPositionsException(setOf()) }.whenever(logic).validateSteps(this.steps!!) - - assertThrows { - logic.validateGroupInformationSteps(this) - } - } - } - - // validateSteps() - - @Test - fun `validateSteps() throws an InvalidStepsPositionsException when the position of the first step of the given groupInformation is not 1`() { - assertInvalidStepsPositionsException( - mutableSetOf( - recipeStep(id = 0L, position = 0), - recipeStep(id = 1L, position = 1), - recipeStep(id = 2L, position = 2), - recipeStep(id = 3L, position = 3) - ), - INVALID_FIRST_STEP_POSITION_ERROR_CODE - ) - } - - @Test - fun `validateSteps() throws an InvalidStepsPositionsException when steps positions are duplicated in the given groupInformation`() { - assertInvalidStepsPositionsException( - mutableSetOf( - recipeStep(id = 0L, position = 1), - recipeStep(id = 1L, position = 2), - recipeStep(id = 2L, position = 2), - recipeStep(id = 3L, position = 3) - ), - DUPLICATED_STEPS_POSITIONS_ERROR_CODE - ) - } - - @Test - fun `validateSteps() throws an InvalidStepsPositionsException when there is a gap between steps positions in the given groupInformation`() { - assertInvalidStepsPositionsException( - mutableSetOf( - recipeStep(id = 0L, position = 1), - recipeStep(id = 1L, position = 2), - recipeStep(id = 2L, position = 4), - recipeStep(id = 3L, position = 5) - ), - GAP_BETWEEN_STEPS_POSITIONS_ERROR_CODE - ) - } - - private fun withGroupInformation(steps: MutableSet? = null, test: RecipeGroupInformation.() -> Unit) { - recipeGroupInformation( - group = group(id = 0L), - steps = steps ?: mutableSetOf( - recipeStep(id = 0L, position = 1), - recipeStep(id = 1L, position = 2), - recipeStep(id = 2L, position = 3), - recipeStep(id = 3L, position = 4) - ) - ) { - test() - } - } - - private fun assertInvalidStepsPositionsException(steps: MutableSet, errorType: String) { - val exception = assertThrows { - logic.validateSteps(steps) - } - - assertTrue { exception.errors.size == 1 } - assertTrue { exception.errors.first().type == errorType } - } -} -- 2.40.1 From d785cfdbe764c60048674e7ef23ed6bf9a686b9e Mon Sep 17 00:00:00 2001 From: FyloZ Date: Tue, 1 Mar 2022 22:47:15 -0500 Subject: [PATCH 05/11] #25 Migrate material types to new logic --- .../fyloz/colorrecipesexplorer/Constants.kt | 9 +- .../initializers/MaterialTypeInitializer.kt | 23 +- .../properties/MaterialTypeProperties.kt | 5 +- .../colorrecipesexplorer/dtos/MaterialDto.kt | 3 +- .../dtos/MaterialTypeDto.kt | 13 + .../fyloz/colorrecipesexplorer/logic/Logic.kt | 4 +- .../logic/MaterialTypeLogic.kt | 131 +++---- .../colorrecipesexplorer/logic/MixLogic.kt | 4 +- .../colorrecipesexplorer/model/Material.kt | 6 +- .../model/MaterialType.kt | 185 ++------- .../colorrecipesexplorer/model/Recipe.kt | 2 +- .../repository/MaterialRepository.kt | 2 +- .../repository/MaterialTypeRepository.kt | 35 +- .../rest/FileController.kt | 4 +- .../rest/MaterialController.kt | 4 +- .../rest/MaterialTypeController.kt | 35 +- .../service/MaterialService.kt | 20 +- .../service/MaterialTypeService.kt | 44 +++ .../colorrecipesexplorer/service/Service.kt | 9 +- .../logic/DefaultMaterialLogicTest.kt | 9 +- .../logic/DefaultMaterialTypeLogicTest.kt | 146 +++++++ .../logic/MaterialTypeLogicTest.kt | 364 +++++++++--------- .../logic/MixLogicTest.kt | 4 +- 23 files changed, 575 insertions(+), 486 deletions(-) create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialTypeDto.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeService.kt create mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialTypeLogicTest.kt diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt index 6694fed..a1bf68c 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt @@ -2,13 +2,14 @@ package dev.fyloz.colorrecipesexplorer object Constants { object ControllerPaths { - const val file = "/api/file" - const val material = "/api/material" + const val FILE = "/api/file" + const val MATERIAL = "/api/material" + const val MATERIAL_TYPE = "/api/materialtype" } object FilePaths { - const val pdfs = "pdf" + const val PDF = "pdf" - const val simdut = "$pdfs/simdut" + const val SIMDUT = "$PDF/simdut" } } \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MaterialTypeInitializer.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MaterialTypeInitializer.kt index 99f0a3f..8431c25 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MaterialTypeInitializer.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MaterialTypeInitializer.kt @@ -2,8 +2,8 @@ package dev.fyloz.colorrecipesexplorer.config.initializers import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase import dev.fyloz.colorrecipesexplorer.config.properties.MaterialTypeProperties +import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto import dev.fyloz.colorrecipesexplorer.logic.MaterialTypeLogic -import dev.fyloz.colorrecipesexplorer.model.MaterialType import mu.KotlinLogging import org.springframework.context.annotation.Configuration @@ -23,17 +23,16 @@ class MaterialTypeInitializer( private fun ensureSystemMaterialTypesExists() { val systemTypes = materialTypeProperties.systemTypes.map { it.toMaterialType() } - val oldSystemTypes = materialTypeLogic.getAllSystemTypes().toMutableSet() + val oldSystemTypes = materialTypeLogic.getAll(true).toMutableSet() - fun saveOrUpdateSystemType(type: MaterialType) { - if (materialTypeLogic.existsByName(type.name)) { - with(materialTypeLogic.getByName(type.name)) { - if (!this.systemType) { - logger.info("Material type '${type.name}' already exists and will be flagged as a system type") - materialTypeLogic.update(this.copy(systemType = true)) - } else { - logger.debug("System material type '${type.name}' already exists") - } + fun saveOrUpdateSystemType(type: MaterialTypeDto) { + val storedMaterialType = materialTypeLogic.getByName(type.name) + if (storedMaterialType != null) { + if (!storedMaterialType.systemType) { + logger.info("Material type '${type.name}' already exists and will be flagged as a system type") + materialTypeLogic.update(storedMaterialType.copy(systemType = true)) + } else { + logger.debug("System material type '${type.name}' already exists") } } else { logger.info("System material type '${type.name}' will be created") @@ -50,7 +49,7 @@ class MaterialTypeInitializer( // Remove old system types oldSystemTypes.forEach { logger.info("Material type '${it.name}' is not a system type anymore") - materialTypeLogic.updateSystemType(it.copy(systemType = false)) + materialTypeLogic.updateNonSystemType(it.copy(systemType = false)) } } } \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/properties/MaterialTypeProperties.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/properties/MaterialTypeProperties.kt index 226e9d3..736cd91 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/properties/MaterialTypeProperties.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/properties/MaterialTypeProperties.kt @@ -1,5 +1,6 @@ package dev.fyloz.colorrecipesexplorer.config.properties +import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto import dev.fyloz.colorrecipesexplorer.model.MaterialType import dev.fyloz.colorrecipesexplorer.model.materialType import org.springframework.boot.context.properties.ConfigurationProperties @@ -16,9 +17,9 @@ class MaterialTypeProperties { var prefix: String = "", var usePercentages: Boolean = false ) { - fun toMaterialType(): MaterialType { + fun toMaterialType(): MaterialTypeDto { Assert.hasText(name, "A system material type has an empty name") - return materialType(name = name, prefix = prefix, usePercentages = usePercentages, systemType = true) + return MaterialTypeDto(name = name, prefix = prefix, usePercentages = usePercentages, systemType = true) } } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt index 45c6b15..251d71b 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt @@ -1,6 +1,5 @@ package dev.fyloz.colorrecipesexplorer.dtos -import dev.fyloz.colorrecipesexplorer.model.MaterialType import org.springframework.web.multipart.MultipartFile import javax.validation.constraints.Min import javax.validation.constraints.NotBlank @@ -14,7 +13,7 @@ data class MaterialDto( val isMixType: Boolean, - val materialType: MaterialType, + val materialType: MaterialTypeDto, val simdutUrl: String? = null ) : EntityDto diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialTypeDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialTypeDto.kt new file mode 100644 index 0000000..eb2b197 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialTypeDto.kt @@ -0,0 +1,13 @@ +package dev.fyloz.colorrecipesexplorer.dtos + +data class MaterialTypeDto( + override val id: Long = 0L, + + val name: String, + + val prefix: String, + + val usePercentages: Boolean, + + val systemType: Boolean = false +) : EntityDto \ 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 3578c0f..4b1971e 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt @@ -19,13 +19,13 @@ interface Logic> { /** Get all DTOs. */ fun getAll(): Collection - /** Get the DTO for the given [id]. */ + /** Get the DTO for the given [id]. Throws if no DTO were found. */ fun getById(id: Long): D /** Saves the given [dto]. */ fun save(dto: D): D - /** Updates the given [dto]. */ + /** Updates the given [dto]. Throws if no DTO with the same id exists. */ fun update(dto: D): D /** Deletes the dto with the given [id]. */ diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogic.kt index 833afc6..e4d5c46 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogic.kt @@ -1,88 +1,75 @@ package dev.fyloz.colorrecipesexplorer.logic -import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase -import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.model.validation.isNotNullAndNotBlank -import dev.fyloz.colorrecipesexplorer.repository.MaterialTypeRepository -import org.springframework.stereotype.Service +import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent +import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto +import dev.fyloz.colorrecipesexplorer.exception.CannotUpdateException +import dev.fyloz.colorrecipesexplorer.model.MaterialType +import dev.fyloz.colorrecipesexplorer.service.MaterialTypeService -interface MaterialTypeLogic : - ExternalNamedModelService { - /** Checks if a material type with the given [prefix] exists. */ - fun existsByPrefix(prefix: String): Boolean +interface MaterialTypeLogic : Logic { + /** Gets all material types which are or not [systemType]s. */ + fun getAll(systemType: Boolean): Collection - /** Gets all system material types. */ - fun getAllSystemTypes(): Collection + /** Gets the material type with the given [name]. */ + fun getByName(name: String): MaterialTypeDto? - /** Gets all material types who are not a system type. */ - fun getAllNonSystemType(): Collection - - /** Allows to update the given system [materialType], should not be exposed to users. */ - fun updateSystemType(materialType: MaterialType): MaterialType + /** Updates the given [dto], and throws if it is a system types. */ + fun updateNonSystemType(dto: MaterialTypeDto) } -@Service -@RequireDatabase -class DefaultMaterialTypeLogic(repository: MaterialTypeRepository) : - AbstractExternalNamedModelService( - repository - ), MaterialTypeLogic { - override fun idNotFoundException(id: Long) = materialTypeIdNotFoundException(id) - override fun idAlreadyExistsException(id: Long) = materialIdAlreadyExistsException(id) - override fun nameNotFoundException(name: String) = materialTypeNameNotFoundException(name) - override fun nameAlreadyExistsException(name: String) = materialTypeNameAlreadyExistsException(name) +@LogicComponent +class DefaultMaterialTypeLogic(service: MaterialTypeService) : + BaseLogic(service, MaterialType::class.simpleName!!), MaterialTypeLogic { + override fun getAll(systemType: Boolean) = service.getAll(systemType) + override fun getByName(name: String) = service.getByName(name) - override fun MaterialType.toOutput() = this - - override fun existsByPrefix(prefix: String): Boolean = repository.existsByPrefix(prefix) - - override fun getAllSystemTypes(): Collection = repository.findAllBySystemTypeIs(true) - override fun getAllNonSystemType(): Collection = repository.findAllBySystemTypeIs(false) - - override fun save(entity: MaterialType): MaterialType { - if (existsByPrefix(entity.prefix)) - throw materialTypePrefixAlreadyExistsException(entity.prefix) - return super.save(entity) - } - - override fun update(entity: MaterialTypeUpdateDto): MaterialType { - val persistedMaterialType by lazy { getById(entity.id) } - - return update(with(entity) { - MaterialType( - id = id, - name = if (isNotNullAndNotBlank(name)) name else persistedMaterialType.name, - prefix = if (isNotNullAndNotBlank(prefix)) prefix else persistedMaterialType.prefix, - systemType = false + override fun updateNonSystemType(dto: MaterialTypeDto) { + if (service.existsById(dto.id, true)) { + throw CannotUpdateException( + typeNameLowerCase, + "Cannot update $typeNameLowerCase", + "Cannot update material type '${dto.name}' because it is a system material type" ) - }) - } - - override fun updateSystemType(materialType: MaterialType) = - update(materialType, true) - - override fun update(entity: MaterialType) = - update(entity, false) - - private fun update(entity: MaterialType, allowSystemTypes: Boolean): MaterialType { - if (!allowSystemTypes && repository.existsByIdAndSystemTypeIsTrue(entity.id!!)) { - throw cannotUpdateSystemMaterialTypeException(entity) } - with(repository.findByPrefix(entity.prefix)) { - if (this != null && id != entity.id) - throw materialTypePrefixAlreadyExistsException(entity.prefix) - } - - return super.update(entity) + update(dto) } - override fun delete(entity: MaterialType) { - if (repository.existsByIdAndSystemTypeIsTrue(entity.id!!)) { - throw cannotDeleteSystemMaterialTypeException(entity) + override fun save(dto: MaterialTypeDto): MaterialTypeDto { + throwIfNameAlreadyExists(dto.name) + throwIfPrefixAlreadyExists(dto.prefix) + + return super.save(dto) + } + + override fun update(dto: MaterialTypeDto): MaterialTypeDto { + throwIfNameAlreadyExists(dto.name, dto.id) + throwIfPrefixAlreadyExists(dto.prefix, dto.id) + + return super.update(dto) + } + + override fun deleteById(id: Long) { + if (service.isUsedByMaterial(id)) { + throw cannotDeleteException("Cannot delete material type with the id '$id' because one or more materials depends on it") } - if (!repository.canBeDeleted(entity.id)) throw cannotDeleteMaterialTypeException(entity) - super.delete(entity) + super.deleteById(id) } -} + + private fun throwIfNameAlreadyExists(name: String, id: Long? = null) { + if (service.existsByName(name, id)) { + throw alreadyExistsException(value = name) + } + } + + private fun throwIfPrefixAlreadyExists(prefix: String, id: Long? = null) { + if (service.existsByPrefix(prefix, id)) { + throw alreadyExistsException(PREFIX_IDENTIFIER_NAME, prefix) + } + } + + companion object { + const val PREFIX_IDENTIFIER_NAME = "prefix" + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt index 8de0da0..8e843cf 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt @@ -53,7 +53,7 @@ class DefaultMixLogic( override fun save(entity: MixSaveDto): Mix { val recipe = recipeLogic.getById(entity.recipeId) val materialType = materialTypeLogic.getById(entity.materialTypeId) - val mixType = mixTypeLogic.getOrCreateForNameAndMaterialType(entity.name, materialType) + val mixType = mixTypeLogic.getOrCreateForNameAndMaterialType(entity.name, materialType(materialType)) val mixMaterials = if (entity.mixMaterials != null) mixMaterialLogic.create(entity.mixMaterials) else setOf() mixMaterialLogic.validateMixMaterials(mixMaterials) @@ -72,7 +72,7 @@ class DefaultMixLogic( if (entity.name != null || entity.materialTypeId != null) { val name = entity.name ?: mix.mixType.name val materialType = if (entity.materialTypeId != null) - materialTypeLogic.getById(entity.materialTypeId) + materialType(materialTypeLogic.getById(entity.materialTypeId)) else mix.mixType.material.materialType!! diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt index bab6b2c..02bfce6 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt @@ -32,7 +32,7 @@ data class Material( ) : ModelEntity { companion object { fun getSimdutFilePath(name: String) = - "${Constants.FilePaths.simdut}/$name.pdf" + "${Constants.FilePaths.SIMDUT}/$name.pdf" } } @@ -66,12 +66,12 @@ fun material( @Deprecated("Temporary DSL for transition") fun material( dto: MaterialDto -) = Material(dto.id, dto.name, dto.inventoryQuantity, dto.isMixType, dto.materialType) +) = Material(dto.id, dto.name, dto.inventoryQuantity, dto.isMixType, materialType(dto.materialType)) @Deprecated("Temporary DSL for transition") fun materialDto( entity: Material -) = MaterialDto(entity.id!!, entity.name, entity.inventoryQuantity, entity.isMixType, entity.materialType!!) +) = MaterialDto(entity.id!!, entity.name, entity.inventoryQuantity, entity.isMixType, materialTypeDto(entity.materialType!!)) fun materialQuantityDto( materialId: Long, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt index a99033f..279c9c4 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt @@ -1,171 +1,62 @@ package dev.fyloz.colorrecipesexplorer.model -import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException -import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException -import dev.fyloz.colorrecipesexplorer.exception.CannotUpdateException -import dev.fyloz.colorrecipesexplorer.exception.NotFoundException +import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto import org.hibernate.annotations.ColumnDefault import javax.persistence.* -import javax.validation.constraints.NotBlank -import javax.validation.constraints.Size - -private const val VALIDATION_PREFIX_SIZE = "Must contains exactly 3 characters" @Entity @Table(name = "material_type") data class MaterialType( - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - override val id: Long? = null, + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + override val id: Long? = null, - @Column(unique = true) - override val name: String = "", + @Column(unique = true) + val name: String = "", - @Column(unique = true) - val prefix: String = "", + @Column(unique = true) + val prefix: String = "", - @Column(name = "use_percentages") - @ColumnDefault("false") - val usePercentages: Boolean = false, + @Column(name = "use_percentages") + @ColumnDefault("false") + val usePercentages: Boolean = false, - @Column(name = "system_type") - @ColumnDefault("false") - val systemType: Boolean = false -) : NamedModelEntity - -open class MaterialTypeSaveDto( - @field:NotBlank - val name: String, - - @field:NotBlank - @field:Size(min = 3, max = 3, message = VALIDATION_PREFIX_SIZE) - val prefix: String, - - val usePercentages: Boolean = false -) : EntityDto { - override fun toEntity(): MaterialType = - MaterialType(null, name, prefix, usePercentages) -} - -open class MaterialTypeUpdateDto( - val id: Long, - - @field:NotBlank - val name: String?, - - @field:Size(min = 3, max = 3, message = VALIDATION_PREFIX_SIZE) - val prefix: String? -) : EntityDto { - override fun toEntity(): MaterialType = - MaterialType(id, name ?: "", prefix ?: "") -} + @Column(name = "system_type") + @ColumnDefault("false") + val systemType: Boolean = false +) : ModelEntity // ==== DSL ==== fun materialType( - id: Long? = null, - name: String = "name", - prefix: String = "PRE", - usePercentages: Boolean = false, - systemType: Boolean = false, - op: MaterialType.() -> Unit = {} + id: Long? = null, + name: String = "name", + prefix: String = "PRE", + usePercentages: Boolean = false, + systemType: Boolean = false, + op: MaterialType.() -> Unit = {} ) = MaterialType(id, name, prefix, usePercentages, systemType).apply(op) fun materialType( - materialType: MaterialType, - newId: Long? = null, - newName: String? = null, - newSystemType: Boolean? = null + materialType: MaterialType, + newId: Long? = null, + newName: String? = null, + newSystemType: Boolean? = null ) = with(materialType) { MaterialType( - newId ?: id, - newName ?: name, - prefix, - usePercentages, - newSystemType ?: systemType + newId ?: id, + newName ?: name, + prefix, + usePercentages, + newSystemType ?: systemType ) } -fun materialTypeSaveDto( - name: String = "name", - prefix: String = "PRE", - usePercentages: Boolean = false, - op: MaterialTypeSaveDto.() -> Unit = {} -) = MaterialTypeSaveDto(name, prefix, usePercentages).apply(op) +@Deprecated("Temporary DSL for transition") +fun materialType( + dto: MaterialTypeDto +) = MaterialType(dto.id, dto.name, dto.prefix, dto.usePercentages, dto.systemType) -fun materialTypeUpdateDto( - id: Long = 0L, - name: String? = null, - prefix: String? = null, - op: MaterialTypeUpdateDto.() -> Unit = {} -) = MaterialTypeUpdateDto(id, name, prefix).apply(op) - -// ==== Exceptions ==== -private const val MATERIAL_TYPE_NOT_FOUND_EXCEPTION_TITLE = "Material type not found" -private const val MATERIAL_TYPE_ALREADY_EXISTS_EXCEPTION_TITLE = "Material type already exists" -private const val MATERIAL_TYPE_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete material type" -private const val MATERIAL_TYPE_CANNOT_UPDATE_EXCEPTION_TITLE = "Cannot update material type" -private const val MATERIAL_TYPE_EXCEPTION_ERROR_CODE = "materialtype" - -fun materialTypeIdNotFoundException(id: Long) = - NotFoundException( - MATERIAL_TYPE_EXCEPTION_ERROR_CODE, - MATERIAL_TYPE_NOT_FOUND_EXCEPTION_TITLE, - "A material type with the id $id could not be found", - id - ) - -fun materialTypeNameNotFoundException(name: String) = - NotFoundException( - MATERIAL_TYPE_EXCEPTION_ERROR_CODE, - MATERIAL_TYPE_NOT_FOUND_EXCEPTION_TITLE, - "A material type with the name $name could not be found", - name, - "name" - ) - -fun materialTypeIdAlreadyExistsException(id: Long) = - AlreadyExistsException( - MATERIAL_TYPE_EXCEPTION_ERROR_CODE, - MATERIAL_TYPE_ALREADY_EXISTS_EXCEPTION_TITLE, - "A material type with the id $id already exists", - id - ) - -fun materialTypeNameAlreadyExistsException(name: String) = - AlreadyExistsException( - MATERIAL_TYPE_EXCEPTION_ERROR_CODE, - MATERIAL_TYPE_ALREADY_EXISTS_EXCEPTION_TITLE, - "A material type with the name $name already exists", - name, - "name" - ) - -fun materialTypePrefixAlreadyExistsException(prefix: String) = - AlreadyExistsException( - MATERIAL_TYPE_EXCEPTION_ERROR_CODE, - MATERIAL_TYPE_ALREADY_EXISTS_EXCEPTION_TITLE, - "A material type with the prefix $prefix already exists", - prefix, - "prefix" - ) - -fun cannotUpdateSystemMaterialTypeException(materialType: MaterialType) = - CannotUpdateException( - MATERIAL_TYPE_EXCEPTION_ERROR_CODE, - MATERIAL_TYPE_CANNOT_UPDATE_EXCEPTION_TITLE, - "Cannot update material type ${materialType.name} because it is a system material type" - ) - -fun cannotDeleteMaterialTypeException(materialType: MaterialType) = - CannotDeleteException( - MATERIAL_TYPE_EXCEPTION_ERROR_CODE, - MATERIAL_TYPE_CANNOT_DELETE_EXCEPTION_TITLE, - "Cannot delete material type ${materialType.name} because one or more materials depends on it" - ) - -fun cannotDeleteSystemMaterialTypeException(materialType: MaterialType) = - CannotDeleteException( - MATERIAL_TYPE_EXCEPTION_ERROR_CODE, - MATERIAL_TYPE_CANNOT_DELETE_EXCEPTION_TITLE, - "Cannot delete material type ${materialType.name} because it is a system material type" - ) +@Deprecated("Temporary DSL for transition") +fun materialTypeDto( + entity: MaterialType +) = MaterialTypeDto(entity.id!!, entity.name, entity.prefix, entity.usePercentages, entity.systemType) \ 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 dfdee70..b9cb4d3 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt @@ -70,7 +70,7 @@ data class Recipe( groupsInformation.firstOrNull { it.group.id == groupId } fun imageUrl(deploymentUrl: String, name: String) = - "$deploymentUrl${Constants.ControllerPaths.file}?path=${ + "$deploymentUrl${Constants.ControllerPaths.FILE}?path=${ URLEncoder.encode( "${this.imagesDirectoryPath}/$name", StandardCharsets.UTF_8 diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MaterialRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MaterialRepository.kt index 5125087..2cc5ada 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MaterialRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MaterialRepository.kt @@ -12,7 +12,7 @@ interface MaterialRepository : JpaRepository { fun existsByNameAndIdNot(name: String, id: Long): Boolean /** Gets all non mix type materials. */ - fun getAllByIsMixTypeIsFalse(): Collection + fun findAllByIsMixTypeIsFalse(): Collection /** Updates the [inventoryQuantity] of the [Material] with the given [id]. */ @Modifying diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MaterialTypeRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MaterialTypeRepository.kt index d90b51e..c2d4a22 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MaterialTypeRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MaterialTypeRepository.kt @@ -1,30 +1,33 @@ package dev.fyloz.colorrecipesexplorer.repository import dev.fyloz.colorrecipesexplorer.model.MaterialType +import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository @Repository -interface MaterialTypeRepository : NamedJpaRepository { - /** Checks if a material type exists with the given [prefix]. */ - fun existsByPrefix(prefix: String): Boolean - +interface MaterialTypeRepository : JpaRepository { /** Checks if a system material type with the given [id] exists. */ - fun existsByIdAndSystemTypeIsTrue(id: Long): Boolean + fun existsByIdAndSystemTypeIs(id: Long, systemType: Boolean): Boolean - /** Gets all material types which are not system types. */ - fun findAllBySystemTypeIs(value: Boolean): Collection + /** Checks if a material type with the given [name] and a different [id] exists. */ + fun existsByNameAndIdNot(name: String, id: Long): Boolean - /** Gets the material type with the given [prefix]. */ - fun findByPrefix(prefix: String): MaterialType? + /** Checks if a material type with the given [prefix] and a different [id] exists. */ + fun existsByPrefixAndIdNot(prefix: String, id: Long): Boolean + /** Find all material types which are or not [systemType]s. */ + fun findAllBySystemTypeIs(systemType: Boolean): Collection + + /** Find the material type with the given [name]. */ + fun findByName(name: String): MaterialType? + + /** Checks if a material depends on the material type with the given [id]. */ @Query( - """ - select case when(count(m.id) > 0) then false else true end - from MaterialType t - left join Material m on t.id = m.materialType.id - where t.id = :id - """ + """ + select case when(count(m) > 0) then true else false end + from Material m where m.materialType.id = :id + """ ) - fun canBeDeleted(id: Long): Boolean + fun isUsedByMaterial(id: Long): Boolean } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/FileController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/FileController.kt index 346b195..579bf39 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/FileController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/FileController.kt @@ -12,7 +12,7 @@ import org.springframework.web.multipart.MultipartFile import java.net.URI @RestController -@RequestMapping(Constants.ControllerPaths.file) +@RequestMapping(Constants.ControllerPaths.FILE) class FileController( private val fileLogic: WriteableFileLogic, private val configurationLogic: ConfigurationLogic @@ -43,6 +43,6 @@ class FileController( private fun created(path: String): ResponseEntity = ResponseEntity - .created(URI.create("${configurationLogic.get(ConfigurationType.INSTANCE_URL)}${Constants.ControllerPaths.file}?path=$path")) + .created(URI.create("${configurationLogic.get(ConfigurationType.INSTANCE_URL)}${Constants.ControllerPaths.FILE}?path=$path")) .build() } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt index eb29623..5674227 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt @@ -13,7 +13,7 @@ import org.springframework.web.multipart.MultipartFile import javax.validation.Valid @RestController -@RequestMapping(Constants.ControllerPaths.material) +@RequestMapping(Constants.ControllerPaths.MATERIAL) @Profile("!emergency") @PreAuthorizeViewCatalog class MaterialController( @@ -34,7 +34,7 @@ class MaterialController( @PostMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) @PreAuthorize("hasAuthority('EDIT_MATERIALS')") fun save(@Valid material: MaterialSaveDto, simdutFile: MultipartFile?) = - created(Constants.ControllerPaths.material) { + created(Constants.ControllerPaths.MATERIAL) { materialLogic.save(material.copy(simdutFile = simdutFile)) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialTypeController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialTypeController.kt index f0d5e90..4e40cf6 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialTypeController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialTypeController.kt @@ -1,49 +1,46 @@ package dev.fyloz.colorrecipesexplorer.rest +import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewCatalog +import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto import dev.fyloz.colorrecipesexplorer.logic.MaterialTypeLogic -import dev.fyloz.colorrecipesexplorer.model.MaterialType -import dev.fyloz.colorrecipesexplorer.model.MaterialTypeSaveDto -import dev.fyloz.colorrecipesexplorer.model.MaterialTypeUpdateDto import org.springframework.context.annotation.Profile import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.* import javax.validation.Valid -private const val MATERIAL_TYPE_CONTROLLER_PATH = "api/materialtype" - @RestController -@RequestMapping(MATERIAL_TYPE_CONTROLLER_PATH) +@RequestMapping(Constants.ControllerPaths.MATERIAL_TYPE) @Profile("!emergency") @PreAuthorizeViewCatalog class MaterialTypeController(private val materialTypeLogic: MaterialTypeLogic) { @GetMapping fun getAll() = - ok(materialTypeLogic.getAllForOutput()) + ok(materialTypeLogic.getAll()) @GetMapping("{id}") fun getById(@PathVariable id: Long) = - ok(materialTypeLogic.getByIdForOutput(id)) + ok(materialTypeLogic.getById(id)) @PostMapping @PreAuthorize("hasAuthority('EDIT_MATERIAL_TYPES')") - fun save(@Valid @RequestBody materialType: MaterialTypeSaveDto) = - created(MATERIAL_TYPE_CONTROLLER_PATH) { - materialTypeLogic.save(materialType) - } + fun save(@Valid @RequestBody materialType: MaterialTypeDto) = + created(Constants.ControllerPaths.MATERIAL_TYPE) { + materialTypeLogic.save(materialType) + } @PutMapping @PreAuthorize("hasAuthority('EDIT_MATERIAL_TYPES')") - fun update(@Valid @RequestBody materialType: MaterialTypeUpdateDto) = - noContent { - materialTypeLogic.update(materialType) - } + fun update(@Valid @RequestBody materialType: MaterialTypeDto) = + noContent { + materialTypeLogic.updateNonSystemType(materialType) + } @DeleteMapping("{id}") @PreAuthorize("hasAuthority('EDIT_MATERIAL_TYPES')") fun deleteById(@PathVariable id: Long) = - noContent { - materialTypeLogic.deleteById(id) - } + noContent { + materialTypeLogic.deleteById(id) + } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt index 1f3af3b..8930bf9 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt @@ -25,11 +25,17 @@ interface MaterialService : Service { } @ServiceComponent -class DefaultMaterialService(repository: MaterialRepository, @Qualifier("defaultFileLogic") val fileLogic: FileLogic) : +class DefaultMaterialService( + repository: MaterialRepository, + private val materialTypeService: MaterialTypeService, + @Qualifier("defaultFileLogic") val fileLogic: FileLogic +) : BaseService(repository), MaterialService { override fun existsByName(name: String, id: Long?) = repository.existsByNameAndIdNot(name, id ?: 0) - override fun getAllNotMixType() = repository.getAllByIsMixTypeIsFalse().map(this::toDto) - override fun updateInventoryQuantityById(id: Long, inventoryQuantity: Float) = repository.updateInventoryQuantityById(id, inventoryQuantity) + override fun getAllNotMixType() = repository.findAllByIsMixTypeIsFalse().map(::toDto) + override fun updateInventoryQuantityById(id: Long, inventoryQuantity: Float) = + repository.updateInventoryQuantityById(id, inventoryQuantity) + override fun isUsedByMixMaterialOrMixType(id: Long) = repository.isUsedByMixMaterialOrMixType(id) override fun toDto(entity: Material) = @@ -38,21 +44,21 @@ class DefaultMaterialService(repository: MaterialRepository, @Qualifier("default entity.name, entity.inventoryQuantity, entity.isMixType, - entity.materialType!!, + materialTypeService.toDto(entity.materialType!!), getSimdutUrl(entity) ) override fun toEntity(dto: MaterialDto) = - Material(dto.id, dto.name, dto.inventoryQuantity, dto.isMixType, dto.materialType) + Material(dto.id, dto.name, dto.inventoryQuantity, dto.isMixType, materialTypeService.toEntity(dto.materialType)) private fun getSimdutUrl(material: Material): String? { - val filePath = "${Constants.FilePaths.simdut}/${material.name}.pdf" + val filePath = "${Constants.FilePaths.SIMDUT}/${material.name}.pdf" if (!fileLogic.exists(filePath)) { return null } val encodedPath = URLEncoder.encode(filePath, StandardCharsets.UTF_8) - return "${Constants.ControllerPaths.file}?path=$encodedPath" + return "${Constants.ControllerPaths.FILE}?path=$encodedPath" } } \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeService.kt new file mode 100644 index 0000000..74820c4 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeService.kt @@ -0,0 +1,44 @@ +package dev.fyloz.colorrecipesexplorer.service + +import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent +import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto +import dev.fyloz.colorrecipesexplorer.model.MaterialType +import dev.fyloz.colorrecipesexplorer.repository.MaterialTypeRepository + +interface MaterialTypeService : Service { + /** Checks if a system material type with the given [id] exists. */ + fun existsById(id: Long, systemType: Boolean): Boolean + + /** Checks if a material type with the given [name] and a different [id] exists. */ + fun existsByName(name: String, id: Long?): Boolean + + /** Checks if a material type with the given [prefix] and a different [id] exists. */ + fun existsByPrefix(prefix: String, id: Long?): Boolean + + /** Gets all material types which are or not a [systemType]. */ + fun getAll(systemType: Boolean): Collection + + /** Gets the material type with the given [name]. */ + fun getByName(name: String): MaterialTypeDto? + + /** Checks if a material depends on the material type with the given [id]. */ + fun isUsedByMaterial(id: Long): Boolean +} + +@ServiceComponent +class DefaultMaterialTypeService(repository: MaterialTypeRepository) : + BaseService(repository), MaterialTypeService { + override fun existsById(id: Long, systemType: Boolean) = repository.existsByIdAndSystemTypeIs(id, systemType) + override fun existsByName(name: String, id: Long?) = repository.existsByNameAndIdNot(name, id ?: 0) + override fun existsByPrefix(prefix: String, id: Long?) = repository.existsByPrefixAndIdNot(prefix, id ?: 0) + override fun getAll(systemType: Boolean) = repository.findAllBySystemTypeIs(systemType).map(::toDto) + override fun getByName(name: String) = repository.findByName(name)?.let(::toDto) + + override fun isUsedByMaterial(id: Long) = repository.isUsedByMaterial(id) + + override fun toDto(entity: MaterialType) = + 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) +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/Service.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/Service.kt index bfcc7c6..cfd846a 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/Service.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/Service.kt @@ -31,6 +31,12 @@ interface Service> { /** Deletes the entity with the given [id]. */ fun deleteById(id: Long) + + /** Converts the given [entity] to a DTO. */ + fun toDto(entity: E): D + + /** Converts the given [dto] to an entity. */ + fun toEntity(dto: D): E } abstract class BaseService>(protected val repository: R) : @@ -58,7 +64,4 @@ abstract class BaseService() + + private val materialTypeLogic = spyk(DefaultMaterialTypeLogic(materialTypeServiceMock)) + + private val materialType = MaterialTypeDto(1L, "Unit test material type", "UTMT", false) + private val systemMaterialType = + MaterialTypeDto(2L, "Unit test system material type", "UTSMT", false, systemType = true) + + @AfterEach + internal fun afterEach() { + clearAllMocks() + } + + @Test + fun getAll_normalBehavior_returnsFromService() { + // Arrange + val expectedMaterialTypes = listOf(materialType, systemMaterialType) + + every { materialTypeServiceMock.getAll(any()) } returns expectedMaterialTypes + + // Act + val actualMaterialTypes = materialTypeLogic.getAll(true) + + // Assert + assertEquals(expectedMaterialTypes, actualMaterialTypes) + } + + @Test + fun getByName_normalBehavior_returnsMaterialType() { + // Arrange + every { materialTypeServiceMock.getByName(any()) } returns materialType + + // Act + val actualMaterialType = materialTypeLogic.getByName(materialType.name) + + // Assert + assertEquals(materialType, actualMaterialType) + } + + @Test + fun getByName_nameNotFound_returnsNull() { + // Arrange + every { materialTypeServiceMock.getByName(any()) } returns null + + // Act + val actualMaterialType = materialTypeLogic.getByName(materialType.name) + + // Assert + assertNull(actualMaterialType) + } + + @Test + fun updateNonSystemType_normalBehavior_callsUpdate() { + // Arrange + every { materialTypeServiceMock.existsById(any(), any()) } returns false + every { materialTypeLogic.update(any()) } returnsArgument 0 + + // Act + materialTypeLogic.updateNonSystemType(materialType) + + // Assert + verify { + materialTypeLogic.update(materialType) + } + } + + @Test + fun updateNonSystemType_isSystemType_throwsCannotUpdateException() { + // Arrange + every { materialTypeServiceMock.existsById(any(), any()) } returns true + every { materialTypeLogic.update(any()) } returnsArgument 0 + + // Act + // Assert + assertThrows { materialTypeLogic.updateNonSystemType(materialType) } + } + + @Test + fun save_nameExists_throwsAlreadyExistsException() { + // Arrange + every { materialTypeServiceMock.existsByName(any(), any()) } returns true + every { materialTypeServiceMock.existsByPrefix(any(), any()) } returns false + + // Act + // Assert + assertThrows { materialTypeLogic.save(materialType) } + } + + @Test + fun save_prefixExists_throwsAlreadyExistsException() { + // Arrange + every { materialTypeServiceMock.existsByName(any(), any()) } returns false + every { materialTypeServiceMock.existsByPrefix(any(), any()) } returns true + + // Act + // Assert + assertThrows { materialTypeLogic.save(materialType) } + } + + @Test + fun update_nameExists_throwsAlreadyExistsException() { + // Arrange + every { materialTypeServiceMock.existsByName(any(), any()) } returns true + every { materialTypeServiceMock.existsByPrefix(any(), any()) } returns false + + // Act + // Assert + assertThrows { materialTypeLogic.update(materialType) } + } + + @Test + fun update_prefixExists_throwsAlreadyExistsException() { + // Arrange + every { materialTypeServiceMock.existsByName(any(), any()) } returns false + every { materialTypeServiceMock.existsByPrefix(any(), any()) } returns true + + // Act + // Assert + assertThrows { materialTypeLogic.update(materialType) } + } + + @Test + fun deleteById_usedByMaterial_throwsCannotDeleteException() { + // Arrange + every { materialTypeServiceMock.isUsedByMaterial(any()) } returns true + + // Act + // Assert + assertThrows { materialTypeLogic.deleteById(materialType.id) } + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogicTest.kt index b1e84d3..d9b69e5 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogicTest.kt @@ -1,182 +1,182 @@ -package dev.fyloz.colorrecipesexplorer.logic - -import com.nhaarman.mockitokotlin2.* -import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException -import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException -import dev.fyloz.colorrecipesexplorer.exception.CannotUpdateException -import dev.fyloz.colorrecipesexplorer.exception.NotFoundException -import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.repository.MaterialTypeRepository -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance -import org.junit.jupiter.api.assertThrows -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class MaterialTypeLogicTest : - AbstractExternalNamedModelServiceTest() { - override val repository: MaterialTypeRepository = mock() - private val materialService: MaterialLogic = mock() - override val logic: MaterialTypeLogic = spy(DefaultMaterialTypeLogic(repository)) - override val entity: MaterialType = materialType(id = 0L, name = "material type", prefix = "MAT") - override val anotherEntity: MaterialType = materialType(id = 1L, name = "another material type", prefix = "AMT") - override val entityWithEntityName: MaterialType = materialType(2L, name = entity.name, prefix = "EEN") - private val systemType = materialType(id = 3L, name = "systype", prefix = "SYS", systemType = true) - private val anotherSystemType = materialType(id = 4L, name = "another systype", prefix = "ASY", systemType = true) - override val entitySaveDto: MaterialTypeSaveDto = spy(materialTypeSaveDto(name = "material type", prefix = "MAT")) - override val entityUpdateDto: MaterialTypeUpdateDto = - spy(materialTypeUpdateDto(id = 0L, name = "material type", prefix = "MAT")) - - @AfterEach - override fun afterEach() { - reset(materialService) - super.afterEach() - } - - // existsByPrefix() - - @Test - fun `existsByPrefix() returns true when a material type with the given prefix exists`() { - whenever(repository.existsByPrefix(entity.prefix)).doReturn(true) - - val found = logic.existsByPrefix(entity.prefix) - - assertTrue(found) - } - - @Test - fun `existsByPrefix() returns false when no material type with the given prefix exists`() { - whenever(repository.existsByPrefix(entity.prefix)).doReturn(false) - - val found = logic.existsByPrefix(entity.prefix) - - assertFalse(found) - } - - // getAllSystemTypes() - - @Test - fun `getAllSystemTypes() returns all system types`() { - whenever(repository.findAllBySystemTypeIs(true)).doReturn(listOf(systemType, anotherSystemType)) - - val found = logic.getAllSystemTypes() - - assertTrue(found.contains(systemType)) - assertTrue(found.contains(anotherSystemType)) - } - - // getAllNonSystemTypes() - - @Test - fun `getAllNonSystemTypes() returns all non system types`() { - whenever(repository.findAllBySystemTypeIs(false)).doReturn(listOf(entity, anotherEntity)) - - val found = logic.getAllNonSystemType() - - assertTrue(found.contains(entity)) - assertTrue(found.contains(anotherEntity)) - } - - // save() - - @Test - override fun `save(dto) calls and returns save() with the created entity`() { - withBaseSaveDtoTest(entity, entitySaveDto, logic) - } - - // saveMaterialType() - - @Test - fun `saveMaterialType() throws AlreadyExistsException when a material type with the given prefix already exists`() { - doReturn(true).whenever(logic).existsByPrefix(entity.prefix) - - assertThrows { logic.save(entity) } - .assertErrorCode("prefix") - } - - // update() - - @Test - override fun `update(dto) calls and returns update() with the created entity`() = - withBaseUpdateDtoTest(entity, entityUpdateDto, logic, { any() }) - - override fun `update() saves in the repository and returns the updated value`() { - whenever(repository.save(entity)).doReturn(entity) - whenever(repository.findByName(entity.name)).doReturn(null) - whenever(repository.findByPrefix(entity.prefix)).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) - } - - override fun `update() throws NotFoundException when no entity with the given id exists in the repository`() { - whenever(repository.findByName(entity.name)).doReturn(null) - whenever(repository.findByPrefix(entity.prefix)).doReturn(null) - doReturn(false).whenever(logic).existsById(entity.id!!) - doReturn(null).whenever(logic).getById(entity.id!!) - - assertThrows { logic.update(entity) } - .assertErrorCode() - } - - override fun `update() throws AlreadyExistsException when an entity with the updated name exists`() { - whenever(repository.findByName(entity.name)).doReturn(entityWithEntityName) - whenever(repository.findByPrefix(entity.prefix)).doReturn(null) - doReturn(true).whenever(logic).existsById(entity.id!!) - doReturn(entity).whenever(logic).getById(entity.id!!) - - assertThrows { logic.update(entity) } - .assertErrorCode("name") - } - - @Test - fun `update() throws AlreadyExistsException when an entity with the updated prefix exists`() { - val anotherMaterialType = materialType(prefix = entity.prefix) - whenever(repository.findByPrefix(entity.prefix)).doReturn(anotherMaterialType) - doReturn(entity).whenever(logic).getById(entity.id!!) - - assertThrows { logic.update(entity) } - .assertErrorCode("prefix") - } - - @Test - fun `update() throws CannotUpdateException when updating a system material type`() { - whenever(repository.existsByIdAndSystemTypeIsTrue(systemType.id!!)).doReturn(true) - - assertThrows { logic.update(systemType) } - } - - // delete() - - @Test - fun `delete() throws CannotDeleteException when deleting a system material type`() { - whenever(repository.existsByIdAndSystemTypeIsTrue(systemType.id!!)).doReturn(true) - - assertThrows { logic.delete(systemType) } - } - - override fun `delete() deletes in the repository`() { - whenCanBeDeleted { - super.`delete() deletes in the repository`() - } - } - - override fun `deleteById() deletes the entity with the given id in the repository`() { - whenCanBeDeleted { - super.`deleteById() deletes the entity with the given id in the repository`() - } - } - - private fun whenCanBeDeleted(id: Long = any(), test: () -> Unit) { - whenever(repository.canBeDeleted(id)).doReturn(true) - - test() - } -} +//package dev.fyloz.colorrecipesexplorer.logic +// +//import com.nhaarman.mockitokotlin2.* +//import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException +//import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException +//import dev.fyloz.colorrecipesexplorer.exception.CannotUpdateException +//import dev.fyloz.colorrecipesexplorer.exception.NotFoundException +//import dev.fyloz.colorrecipesexplorer.model.* +//import dev.fyloz.colorrecipesexplorer.repository.MaterialTypeRepository +//import org.junit.jupiter.api.AfterEach +//import org.junit.jupiter.api.Test +//import org.junit.jupiter.api.TestInstance +//import org.junit.jupiter.api.assertThrows +//import kotlin.test.assertEquals +//import kotlin.test.assertFalse +//import kotlin.test.assertTrue +// +//@TestInstance(TestInstance.Lifecycle.PER_CLASS) +//class MaterialTypeLogicTest : +// AbstractExternalNamedModelServiceTest() { +// override val repository: MaterialTypeRepository = mock() +// private val materialService: MaterialLogic = mock() +// override val logic: MaterialTypeLogic = spy(DefaultMaterialTypeLogic(repository)) +// override val entity: MaterialType = materialType(id = 0L, name = "material type", prefix = "MAT") +// override val anotherEntity: MaterialType = materialType(id = 1L, name = "another material type", prefix = "AMT") +// override val entityWithEntityName: MaterialType = materialType(2L, name = entity.name, prefix = "EEN") +// private val systemType = materialType(id = 3L, name = "systype", prefix = "SYS", systemType = true) +// private val anotherSystemType = materialType(id = 4L, name = "another systype", prefix = "ASY", systemType = true) +// override val entitySaveDto: MaterialTypeSaveDto = spy(materialTypeSaveDto(name = "material type", prefix = "MAT")) +// override val entityUpdateDto: MaterialTypeUpdateDto = +// spy(materialTypeUpdateDto(id = 0L, name = "material type", prefix = "MAT")) +// +// @AfterEach +// override fun afterEach() { +// reset(materialService) +// super.afterEach() +// } +// +// // existsByPrefix() +// +// @Test +// fun `existsByPrefix() returns true when a material type with the given prefix exists`() { +// whenever(repository.existsByPrefix(entity.prefix)).doReturn(true) +// +// val found = logic.existsByPrefix(entity.prefix) +// +// assertTrue(found) +// } +// +// @Test +// fun `existsByPrefix() returns false when no material type with the given prefix exists`() { +// whenever(repository.existsByPrefix(entity.prefix)).doReturn(false) +// +// val found = logic.existsByPrefix(entity.prefix) +// +// assertFalse(found) +// } +// +// // getAllSystemTypes() +// +// @Test +// fun `getAllSystemTypes() returns all system types`() { +// whenever(repository.findAllBySystemTypeIs(true)).doReturn(listOf(systemType, anotherSystemType)) +// +// val found = logic.getAllSystemTypes() +// +// assertTrue(found.contains(systemType)) +// assertTrue(found.contains(anotherSystemType)) +// } +// +// // getAllNonSystemTypes() +// +// @Test +// fun `getAllNonSystemTypes() returns all non system types`() { +// whenever(repository.findAllBySystemTypeIs(false)).doReturn(listOf(entity, anotherEntity)) +// +// val found = logic.getAllNonSystemType() +// +// assertTrue(found.contains(entity)) +// assertTrue(found.contains(anotherEntity)) +// } +// +// // save() +// +// @Test +// override fun `save(dto) calls and returns save() with the created entity`() { +// withBaseSaveDtoTest(entity, entitySaveDto, logic) +// } +// +// // saveMaterialType() +// +// @Test +// fun `saveMaterialType() throws AlreadyExistsException when a material type with the given prefix already exists`() { +// doReturn(true).whenever(logic).existsByPrefix(entity.prefix) +// +// assertThrows { logic.save(entity) } +// .assertErrorCode("prefix") +// } +// +// // update() +// +// @Test +// override fun `update(dto) calls and returns update() with the created entity`() = +// withBaseUpdateDtoTest(entity, entityUpdateDto, logic, { any() }) +// +// override fun `update() saves in the repository and returns the updated value`() { +// whenever(repository.save(entity)).doReturn(entity) +// whenever(repository.findByName(entity.name)).doReturn(null) +// whenever(repository.findByPrefix(entity.prefix)).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) +// } +// +// override fun `update() throws NotFoundException when no entity with the given id exists in the repository`() { +// whenever(repository.findByName(entity.name)).doReturn(null) +// whenever(repository.findByPrefix(entity.prefix)).doReturn(null) +// doReturn(false).whenever(logic).existsById(entity.id!!) +// doReturn(null).whenever(logic).getById(entity.id!!) +// +// assertThrows { logic.update(entity) } +// .assertErrorCode() +// } +// +// override fun `update() throws AlreadyExistsException when an entity with the updated name exists`() { +// whenever(repository.findByName(entity.name)).doReturn(entityWithEntityName) +// whenever(repository.findByPrefix(entity.prefix)).doReturn(null) +// doReturn(true).whenever(logic).existsById(entity.id!!) +// doReturn(entity).whenever(logic).getById(entity.id!!) +// +// assertThrows { logic.update(entity) } +// .assertErrorCode("name") +// } +// +// @Test +// fun `update() throws AlreadyExistsException when an entity with the updated prefix exists`() { +// val anotherMaterialType = materialType(prefix = entity.prefix) +// whenever(repository.findByPrefix(entity.prefix)).doReturn(anotherMaterialType) +// doReturn(entity).whenever(logic).getById(entity.id!!) +// +// assertThrows { logic.update(entity) } +// .assertErrorCode("prefix") +// } +// +// @Test +// fun `update() throws CannotUpdateException when updating a system material type`() { +// whenever(repository.existsByIdAndSystemTypeIsTrue(systemType.id!!)).doReturn(true) +// +// assertThrows { logic.update(systemType) } +// } +// +// // delete() +// +// @Test +// fun `delete() throws CannotDeleteException when deleting a system material type`() { +// whenever(repository.existsByIdAndSystemTypeIsTrue(systemType.id!!)).doReturn(true) +// +// assertThrows { logic.delete(systemType) } +// } +// +// override fun `delete() deletes in the repository`() { +// whenCanBeDeleted { +// super.`delete() deletes in the repository`() +// } +// } +// +// override fun `deleteById() deletes the entity with the given id in the repository`() { +// whenCanBeDeleted { +// super.`deleteById() deletes the entity with the given id in the repository`() +// } +// } +// +// private fun whenCanBeDeleted(id: Long = any(), test: () -> Unit) { +// whenever(repository.canBeDeleted(id)).doReturn(true) +// +// test() +// } +//} diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogicTest.kt index fba155e..d611a92 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogicTest.kt @@ -62,7 +62,7 @@ class MixLogicTest : AbstractExternalModelServiceTest()) if (mixUpdateDto.materialTypeId != null) { - whenever(materialTypeService.getById(materialType.id!!)).doReturn(materialType) + whenever(materialTypeService.getById(materialType.id!!)).doReturn(materialTypeDto(materialType)) } op() -- 2.40.1 From 618ef6c77aac6da1a95c9d6b5f982c4752be80bf Mon Sep 17 00:00:00 2001 From: FyloZ Date: Thu, 3 Mar 2022 23:24:55 -0500 Subject: [PATCH 06/11] #25 Migrate mix materials to new logic --- .../fyloz/colorrecipesexplorer/Constants.kt | 6 + .../colorrecipesexplorer/dtos/MaterialDto.kt | 3 +- .../dtos/MixMaterialDto.kt | 25 + .../dtos/RecipeStepDto.kt | 8 +- .../exception/InvalidPositionsException.kt | 15 + .../fyloz/colorrecipesexplorer/logic/Logic.kt | 6 + .../colorrecipesexplorer/logic/MixLogic.kt | 13 +- .../logic/MixMaterialLogic.kt | 136 +---- .../logic/MixTypeLogic.kt | 3 +- .../logic/RecipeStepLogic.kt | 100 +--- .../colorrecipesexplorer/model/Material.kt | 52 +- .../fyloz/colorrecipesexplorer/model/Mix.kt | 6 +- .../colorrecipesexplorer/model/MixMaterial.kt | 52 +- .../colorrecipesexplorer/model/ModelEntity.kt | 7 +- .../colorrecipesexplorer/model/Recipe.kt | 12 +- .../model/touchupkit/TouchUpKit.kt | 6 +- .../repository/MixMaterialRepository.kt | 5 +- .../service/MixMaterialService.kt | 23 + .../colorrecipesexplorer/utils/Collections.kt | 16 +- .../utils/PositionUtils.kt | 50 ++ .../logic/DefaultMixMaterialLogicTest.kt | 79 +++ .../logic/DefaultRecipeStepLogicTest.kt | 209 +------- .../logic/MaterialTypeLogicTest.kt | 182 ------- .../logic/MixLogicTest.kt | 496 +++++++++--------- .../logic/MixMaterialLogicTest.kt | 171 ------ .../utils/PositionUtilsTest.kt | 88 ++++ 26 files changed, 633 insertions(+), 1136 deletions(-) create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixMaterialDto.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/exception/InvalidPositionsException.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialService.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/PositionUtils.kt create mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixMaterialLogicTest.kt delete mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogicTest.kt delete mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogicTest.kt create mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/utils/PositionUtilsTest.kt diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt index a1bf68c..18ff917 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt @@ -12,4 +12,10 @@ object Constants { const val SIMDUT = "$PDF/simdut" } + + 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" + } } \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt index 251d71b..b5367bc 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt @@ -1,5 +1,6 @@ package dev.fyloz.colorrecipesexplorer.dtos +import dev.fyloz.colorrecipesexplorer.Constants import org.springframework.web.multipart.MultipartFile import javax.validation.constraints.Min import javax.validation.constraints.NotBlank @@ -24,7 +25,7 @@ data class MaterialSaveDto( @field:NotBlank val name: String, - @field:Min(0) + @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO) val inventoryQuantity: Float, val materialTypeId: Long, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixMaterialDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixMaterialDto.kt new file mode 100644 index 0000000..7ba94ce --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixMaterialDto.kt @@ -0,0 +1,25 @@ +package dev.fyloz.colorrecipesexplorer.dtos + +import dev.fyloz.colorrecipesexplorer.Constants +import javax.validation.constraints.Min + +data class MixMaterialDto( + override val id: Long = 0L, + + val material: MaterialDto, + + val quantity: Float, + + val position: Int +) : EntityDto + +data class MixMaterialSaveDto( + override val id: Long = 0L, + + val materialId: Long, + + @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO) + val quantity: Float, + + val position: Int +) : EntityDto \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/RecipeStepDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/RecipeStepDto.kt index f1fe4eb..e597927 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/RecipeStepDto.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/RecipeStepDto.kt @@ -6,10 +6,4 @@ data class RecipeStepDto( val position: Int, val message: String -) : EntityDto { - companion object { - const val VALIDATION_ERROR_CODE_INVALID_FIRST_STEP = "first" - const val VALIDATION_ERROR_CODE_DUPLICATED_STEPS_POSITION = "duplicated" - const val VALIDATION_ERROR_CODE_GAP_BETWEEN_STEPS_POSITIONS = "gap" - } -} +) : EntityDto \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/exception/InvalidPositionsException.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/exception/InvalidPositionsException.kt new file mode 100644 index 0000000..c53c0aa --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/exception/InvalidPositionsException.kt @@ -0,0 +1,15 @@ +package dev.fyloz.colorrecipesexplorer.exception + +import org.springframework.http.HttpStatus + +class InvalidPositionsException(val errors: Set) : RestException( + "invalid-positions", + "Invalid positions", + HttpStatus.BAD_REQUEST, + "The positions are invalid", + mapOf( + "errors" to errors + ) +) + +data class InvalidPositionError(val type: String, val details: String) \ 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 4b1971e..1214c9e 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt @@ -25,6 +25,9 @@ interface Logic> { /** Saves the given [dto]. */ fun save(dto: D): D + /** Saves all the given [dtos]. */ + fun saveAll(dtos: Collection): Collection + /** Updates the given [dto]. Throws if no DTO with the same id exists. */ fun update(dto: D): D @@ -50,6 +53,9 @@ abstract class BaseLogic>( override fun save(dto: D) = service.save(dto) + override fun saveAll(dtos: Collection) = + dtos.map(::save) + override fun update(dto: D): D { if (!existsById(dto.id)) { throw notFoundException(value = dto.id) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt index 8e843cf..e2687f6 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt @@ -42,11 +42,7 @@ class DefaultMixLogic( this.id!!, this.location, this.mixType, - this.mixMaterials.map { - with(mixMaterialLogic) { - return@with it.toOutput() - } - }.toSet() + this.mixMaterials.map { mixMaterialDto(it) }.toSet() ) @Transactional @@ -55,10 +51,11 @@ class DefaultMixLogic( val materialType = materialTypeLogic.getById(entity.materialTypeId) val mixType = mixTypeLogic.getOrCreateForNameAndMaterialType(entity.name, materialType(materialType)) - val mixMaterials = if (entity.mixMaterials != null) mixMaterialLogic.create(entity.mixMaterials) else setOf() + val mixMaterials = + if (entity.mixMaterials != null) mixMaterialLogic.saveAll(entity.mixMaterials).toSet() else setOf() mixMaterialLogic.validateMixMaterials(mixMaterials) - var mix = mix(recipe = recipe, mixType = mixType, mixMaterials = mixMaterials.toMutableSet()) + var mix = mix(recipe = recipe, mixType = mixType, mixMaterials = mixMaterials.map(::mixMaterial).toMutableSet()) mix = save(mix) recipeLogic.addMix(recipe, mix) @@ -83,7 +80,7 @@ class DefaultMixLogic( } } if (entity.mixMaterials != null) { - mix.mixMaterials.setAll(mixMaterialLogic.create(entity.mixMaterials!!).toMutableSet()) + mix.mixMaterials.setAll(mixMaterialLogic.saveAll(entity.mixMaterials!!).map(::mixMaterial).toMutableSet()) } return update(mix) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt index 913016a..2cedbe8 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt @@ -1,112 +1,47 @@ package dev.fyloz.colorrecipesexplorer.logic +import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent +import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto +import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError +import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException import dev.fyloz.colorrecipesexplorer.exception.RestException -import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.repository.MixMaterialRepository -import dev.fyloz.colorrecipesexplorer.utils.findDuplicated -import dev.fyloz.colorrecipesexplorer.utils.hasGaps -import org.springframework.context.annotation.Lazy -import org.springframework.context.annotation.Profile +import dev.fyloz.colorrecipesexplorer.model.MixMaterial +import dev.fyloz.colorrecipesexplorer.service.MixMaterialService +import dev.fyloz.colorrecipesexplorer.utils.PositionUtils import org.springframework.http.HttpStatus -import org.springframework.stereotype.Service - -interface MixMaterialLogic : ModelService { - /** Checks if one or more mix materials have the given [material]. */ - fun existsByMaterial(material: Material): Boolean - - /** Creates [MixMaterial]s from the givens [MixMaterialDto]. */ - fun create(mixMaterials: Set): Set - - /** Creates a [MixMaterial] from a given [MixMaterialDto]. */ - fun create(mixMaterial: MixMaterialDto): MixMaterial - - /** Updates the [quantity] of the given [mixMaterial]. */ - fun updateQuantity(mixMaterial: MixMaterial, quantity: Float): MixMaterial +interface MixMaterialLogic : Logic { /** * Validates if the given [mixMaterials]. To be valid, the position of each mix material must be greater or equals to 1 and unique in the set. * There must also be no gap between the positions. Also, the quantity of the first mix material in the set must not be expressed in percentages. * If any of those criteria are not met, an [InvalidGroupStepsPositionsException] will be thrown. */ - fun validateMixMaterials(mixMaterials: Set) - - fun MixMaterial.toOutput(): MixMaterialOutputDto + fun validateMixMaterials(mixMaterials: Set) } -@Service -@Profile("!emergency") -class DefaultMixMaterialLogic( - mixMaterialRepository: MixMaterialRepository, - @Lazy val materialLogic: MaterialLogic -) : AbstractModelService(mixMaterialRepository), MixMaterialLogic { - override fun idNotFoundException(id: Long) = mixMaterialIdNotFoundException(id) - override fun idAlreadyExistsException(id: Long) = mixMaterialIdAlreadyExistsException(id) - - override fun MixMaterial.toOutput() = MixMaterialOutputDto( - this.id!!, - this.material, - this.quantity, - this.position - ) - - override fun existsByMaterial(material: Material): Boolean = repository.existsByMaterial(material) - override fun create(mixMaterials: Set): Set = - mixMaterials.map(::create).toSet() - - override fun create(mixMaterial: MixMaterialDto): MixMaterial = - mixMaterial( - material = material(materialLogic.getById(mixMaterial.materialId)), - quantity = mixMaterial.quantity, - position = mixMaterial.position - ) - - override fun updateQuantity(mixMaterial: MixMaterial, quantity: Float) = - update(mixMaterial.apply { - this.quantity = quantity - }) - - override fun validateMixMaterials(mixMaterials: Set) { +@LogicComponent +class DefaultMixMaterialLogic(service: MixMaterialService) : + BaseLogic(service, MixMaterial::class.simpleName!!), MixMaterialLogic { + override fun validateMixMaterials(mixMaterials: Set) { if (mixMaterials.isEmpty()) return val sortedMixMaterials = mixMaterials.sortedBy { it.position } - val firstMixMaterial = sortedMixMaterials[0] - val errors = mutableSetOf() - // Check if the first mix material position is 1 - fun isFirstMixMaterialPositionInvalid() = - sortedMixMaterials[0].position != 1 - - // Check if the first mix material is expressed in percents - fun isFirstMixMaterialPercentages() = - sortedMixMaterials[0].material.materialType!!.usePercentages - - // Check if any positions is duplicated - fun getDuplicatedPositionsErrors() = - sortedMixMaterials - .findDuplicated { it.position } - .map { duplicatedMixMaterialsPositions(it) } - - // Find all errors and throw if there is any - if (isFirstMixMaterialPositionInvalid()) errors += invalidFirstMixMaterialPosition(sortedMixMaterials[0]) - errors += getDuplicatedPositionsErrors() - if (errors.isEmpty() && mixMaterials.hasGaps { it.position }) errors += gapBetweenStepsPositions() - if (errors.isNotEmpty()) { - throw InvalidMixMaterialsPositionsException(errors) + try { + PositionUtils.validate(sortedMixMaterials.map { it.position }) + } catch (ex: InvalidPositionsException) { + throw InvalidMixMaterialsPositionsException(ex.errors) } - if (isFirstMixMaterialPercentages()) { - throw InvalidFirstMixMaterial(firstMixMaterial) + if (sortedMixMaterials[0].material.materialType.usePercentages) { + throw InvalidFirstMixMaterialException(sortedMixMaterials[0]) } } } -class InvalidMixMaterialsPositionsError( - val type: String, - val details: String -) - +// TODO check if required class InvalidMixMaterialsPositionsException( - val errors: Set + val errors: Set ) : RestException( "invalid-mixmaterial-position", "Invalid mix materials positions", @@ -117,8 +52,8 @@ class InvalidMixMaterialsPositionsException( ) ) -class InvalidFirstMixMaterial( - val mixMaterial: MixMaterial +class InvalidFirstMixMaterialException( + val mixMaterial: MixMaterialDto ) : RestException( "invalid-mixmaterial-first", "Invalid first mix material", @@ -127,27 +62,4 @@ class InvalidFirstMixMaterial( mapOf( "mixMaterial" to mixMaterial ) -) - -const val INVALID_FIRST_MIX_MATERIAL_POSITION_ERROR_CODE = "first" -const val DUPLICATED_MIX_MATERIALS_POSITIONS_ERROR_CODE = "duplicated" -const val GAP_BETWEEN_MIX_MATERIALS_POSITIONS_ERROR_CODE = "gap" - -private fun invalidFirstMixMaterialPosition(mixMaterial: MixMaterial) = - InvalidMixMaterialsPositionsError( - INVALID_FIRST_MIX_MATERIAL_POSITION_ERROR_CODE, - "The position ${mixMaterial.position} is under the minimum of 1" - ) - -private fun duplicatedMixMaterialsPositions(position: Int) = - InvalidMixMaterialsPositionsError( - DUPLICATED_MIX_MATERIALS_POSITIONS_ERROR_CODE, - "The position $position is duplicated" - ) - -private fun gapBetweenStepsPositions() = - InvalidMixMaterialsPositionsError( - GAP_BETWEEN_MIX_MATERIALS_POSITIONS_ERROR_CODE, - "There is a gap between mix materials positions" - ) - +) \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt index 3ec9b08..6b7d2ac 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt @@ -1,6 +1,7 @@ package dev.fyloz.colorrecipesexplorer.logic import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase +import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.repository.MixTypeRepository import org.springframework.context.annotation.Lazy @@ -56,7 +57,7 @@ class DefaultMixTypeLogic( override fun save(entity: MixType): MixType { if (materialLogic.existsByName(entity.name)) - throw materialNameAlreadyExistsException(entity.name) + throw AlreadyExistsException("material", "material already exists", "material already exists details (TODO)", entity.name) // TODO return super.save(entity) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt index 3323ff8..b7852fa 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt @@ -2,26 +2,19 @@ package dev.fyloz.colorrecipesexplorer.logic import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto +import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError +import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException import dev.fyloz.colorrecipesexplorer.exception.RestException import dev.fyloz.colorrecipesexplorer.model.RecipeGroupInformation import dev.fyloz.colorrecipesexplorer.model.RecipeStep import dev.fyloz.colorrecipesexplorer.model.account.Group -import dev.fyloz.colorrecipesexplorer.model.recipeStepDto import dev.fyloz.colorrecipesexplorer.service.RecipeStepService -import dev.fyloz.colorrecipesexplorer.utils.findDuplicated -import dev.fyloz.colorrecipesexplorer.utils.hasGaps +import dev.fyloz.colorrecipesexplorer.utils.PositionUtils import org.springframework.http.HttpStatus interface RecipeStepLogic : Logic { - /** Validates the steps of the given [groupInformation], according to the criteria of [validateSteps]. */ + /** Validates the steps of the given [groupInformation], according to the criteria of [PositionUtils.validate]. */ fun validateGroupInformationSteps(groupInformation: RecipeGroupInformation) - - /** - * Validates if the given [steps]. To be valid, the position of each step must be greater or equals to 1 and unique in the set. - * There must also be no gap between the positions. - * If any of those criteria are not met, an [InvalidGroupStepsPositionsException] will be thrown. - */ - fun validateSteps(steps: Set) } @LogicComponent @@ -31,91 +24,16 @@ class DefaultRecipeStepLogic(recipeStepService: RecipeStepService) : if (groupInformation.steps == null) return try { - validateSteps(groupInformation.steps!!.map { recipeStepDto(it) }.toSet()) - } catch (validationException: InvalidStepsPositionsException) { - throw InvalidGroupStepsPositionsException(groupInformation.group, validationException) - } - } - - override fun validateSteps(steps: Set) { - if (steps.isEmpty()) return - - val sortedSteps = steps.sortedBy { it.position } - val errors = mutableSetOf() - - // Check if the first step position is 1 - validateFirstStepPosition(sortedSteps, errors) - - // Check if any position is duplicated - validateDuplicatedStepsPositions(sortedSteps, errors) - - // Check for gaps between positions - validateGapsInStepsPositions(sortedSteps, errors) - - if (errors.isNotEmpty()) { - throw InvalidStepsPositionsException(errors) - } - } - - private fun validateFirstStepPosition( - steps: List, - errors: MutableSet - ) { - if (steps[0].position != 1) { - errors += InvalidStepsPositionsError( - RecipeStepDto.VALIDATION_ERROR_CODE_INVALID_FIRST_STEP, - "The first step must be at position 1" - ) - } - } - - private fun validateDuplicatedStepsPositions( - steps: List, - errors: MutableSet - ) { - errors += steps - .findDuplicated { it.position } - .map { - InvalidStepsPositionsError( - RecipeStepDto.VALIDATION_ERROR_CODE_DUPLICATED_STEPS_POSITION, - "The position $it is duplicated" - ) - } - } - - private fun validateGapsInStepsPositions( - steps: List, - errors: MutableSet - ) { - if (errors.isEmpty() && steps.hasGaps { it.position }) { - errors += InvalidStepsPositionsError( - RecipeStepDto.VALIDATION_ERROR_CODE_GAP_BETWEEN_STEPS_POSITIONS, - "There is a gap between steps positions" - ) + PositionUtils.validate(groupInformation.steps!!.map { it.position }.toList()) + } catch (ex: InvalidPositionsException) { + throw InvalidGroupStepsPositionsException(groupInformation.group, ex) } } } -data class InvalidStepsPositionsError( - val type: String, - val details: String -) - -class InvalidStepsPositionsException( - val errors: Set -) : RestException( - "invalid-recipestep-position", - "Invalid steps positions", - HttpStatus.BAD_REQUEST, - "The position of steps are invalid", - mapOf( - "invalidSteps" to errors - ) -) - class InvalidGroupStepsPositionsException( val group: Group, - val exception: InvalidStepsPositionsException + val exception: InvalidPositionsException ) : RestException( "invalid-groupinformation-recipestep-position", "Invalid steps positions", @@ -127,6 +45,6 @@ class InvalidGroupStepsPositionsException( "invalidSteps" to exception.errors ) ) { - val errors: Set + val errors: Set get() = exception.errors } \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt index 02bfce6..23fd00c 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt @@ -39,7 +39,7 @@ data class Material( data class MaterialQuantityDto( val material: Long, - @field:Min(0, message = VALIDATION_SIZE_GE_ZERO) + @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO) val quantity: Float ) @@ -77,52 +77,4 @@ fun materialQuantityDto( materialId: Long, quantity: Float, op: MaterialQuantityDto.() -> Unit = {} -) = MaterialQuantityDto(materialId, quantity).apply(op) - -// ==== Exceptions ==== -private const -val MATERIAL_NOT_FOUND_EXCEPTION_TITLE = "Material not found" -private const val MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE = "Material already exists" -private const val MATERIAL_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete material" -private const val MATERIAL_EXCEPTION_ERROR_CODE = "material" - -fun materialIdNotFoundException(id: Long) = - NotFoundException( - MATERIAL_EXCEPTION_ERROR_CODE, - MATERIAL_NOT_FOUND_EXCEPTION_TITLE, - "A material with the id $id could not be found", - id - ) - -fun materialNameNotFoundException(name: String) = - NotFoundException( - MATERIAL_EXCEPTION_ERROR_CODE, - MATERIAL_NOT_FOUND_EXCEPTION_TITLE, - "A material with the name $name could not be found", - name, - "name" - ) - -fun materialIdAlreadyExistsException(id: Long) = - AlreadyExistsException( - MATERIAL_EXCEPTION_ERROR_CODE, - MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE, - "A material with the id $id already exists", - id - ) - -fun materialNameAlreadyExistsException(name: String) = - AlreadyExistsException( - MATERIAL_EXCEPTION_ERROR_CODE, - MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE, - "A material with the name $name already exists", - name, - "name" - ) - -fun cannotDeleteMaterialException(material: Material) = - CannotDeleteException( - MATERIAL_EXCEPTION_ERROR_CODE, - MATERIAL_CANNOT_DELETE_EXCEPTION_TITLE, - "Cannot delete the material ${material.name} because one or more recipes depends on it" - ) +) = MaterialQuantityDto(materialId, quantity).apply(op) \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt index d7f0053..2670501 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt @@ -1,6 +1,8 @@ package dev.fyloz.colorrecipesexplorer.model import com.fasterxml.jackson.annotation.JsonIgnore +import dev.fyloz.colorrecipesexplorer.Constants +import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException @@ -58,13 +60,13 @@ data class MixOutputDto( val id: Long, val location: String?, val mixType: MixType, - val mixMaterials: Set + val mixMaterials: Set ) data class MixDeductDto( val id: Long, - @field:Min(0, message = VALIDATION_SIZE_GE_ZERO) + @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO) val ratio: Float ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt index 7a91003..6ac7569 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt @@ -1,9 +1,7 @@ package dev.fyloz.colorrecipesexplorer.model -import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException -import dev.fyloz.colorrecipesexplorer.exception.NotFoundException +import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto import javax.persistence.* -import javax.validation.constraints.Min @Entity @Table(name = "mix_material") @@ -21,22 +19,6 @@ data class MixMaterial( var position: Int ) : ModelEntity -data class MixMaterialDto( - val materialId: Long, - - @field:Min(0, message = VALIDATION_SIZE_GE_ZERO) - val quantity: Float, - - val position: Int -) - -data class MixMaterialOutputDto( - val id: Long, - val material: Material, // TODO move to MaterialDto - val quantity: Float, - val position: Int -) - // ==== DSL ==== fun mixMaterial( id: Long? = null, @@ -46,30 +28,12 @@ fun mixMaterial( op: MixMaterial.() -> Unit = {} ) = MixMaterial(id, material, quantity, position).apply(op) +@Deprecated("Temporary DSL for transition") fun mixMaterialDto( - materialId: Long = 0L, - quantity: Float = 0f, - position: Int = 0, - op: MixMaterialDto.() -> Unit = {} -) = MixMaterialDto(materialId, quantity, position).apply(op) + entity: MixMaterial +) = MixMaterialDto(entity.id!!, materialDto(entity.material), entity.quantity, entity.position) -// ==== Exceptions ==== -private const val MIX_MATERIAL_NOT_FOUND_EXCEPTION_TITLE = "Mix material not found" -private const val MIX_MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE = "Mix material already exists" -private const val MIX_MATERIAL_EXCEPTION_ERROR_CODE = "mixmaterial" - -fun mixMaterialIdNotFoundException(id: Long) = - NotFoundException( - MIX_MATERIAL_EXCEPTION_ERROR_CODE, - MIX_MATERIAL_NOT_FOUND_EXCEPTION_TITLE, - "A mix material with the id $id could not be found", - id - ) - -fun mixMaterialIdAlreadyExistsException(id: Long) = - AlreadyExistsException( - MIX_MATERIAL_EXCEPTION_ERROR_CODE, - MIX_MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE, - "A mix material with the id $id already exists", - id - ) +@Deprecated("Temporary DSL for transition") +fun mixMaterial( + dto: MixMaterialDto +) = MixMaterial(dto.id, material(dto.material), dto.quantity, dto.position) \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/ModelEntity.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/ModelEntity.kt index 6b790dc..4185cac 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/ModelEntity.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/ModelEntity.kt @@ -14,9 +14,4 @@ interface EntityDto { fun toEntity(): E { throw UnsupportedOperationException() } -} - -// GENERAL VALIDATION MESSAGES -const val VALIDATION_SIZE_GE_ZERO = "Must be greater or equals to 0" -const val VALIDATION_SIZE_GE_ONE = "Must be greater or equals to 1" -const val VALIDATION_RANGE_PERCENTS = "Must be between 0 and 100" +} \ 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 b9cb4d3..af8142c 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt @@ -89,11 +89,11 @@ open class RecipeSaveDto( @field:Pattern(regexp = VALIDATION_COLOR_PATTERN) val color: String, - @field:Min(0, message = VALIDATION_RANGE_PERCENTS) - @field:Max(100, message = VALIDATION_RANGE_PERCENTS) + @field:Min(0, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS) + @field:Max(100, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS) val gloss: Byte, - @field:Min(0, message = VALIDATION_SIZE_GE_ZERO) + @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO) val sample: Int?, val approbationDate: LocalDate?, @@ -125,11 +125,11 @@ open class RecipeUpdateDto( @field:Pattern(regexp = VALIDATION_COLOR_PATTERN) val color: String?, - @field:Min(0, message = VALIDATION_RANGE_PERCENTS) - @field:Max(100, message = VALIDATION_RANGE_PERCENTS) + @field:Min(0, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS) + @field:Max(100, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS) val gloss: Byte?, - @field:Min(0, message = VALIDATION_SIZE_GE_ZERO) + @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO) val sample: Int?, val approbationDate: LocalDate?, 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 b96738b..623e92d 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt @@ -1,10 +1,10 @@ package dev.fyloz.colorrecipesexplorer.model.touchupkit +import dev.fyloz.colorrecipesexplorer.Constants 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.VALIDATION_SIZE_GE_ONE import java.time.LocalDate import javax.persistence.* import javax.validation.constraints.Min @@ -80,7 +80,7 @@ data class TouchUpKitSaveDto( @field:NotBlank val company: String, - @field:Min(1, message = VALIDATION_SIZE_GE_ONE) + @field:Min(1, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ONE) val quantity: Int, val shippingDate: LocalDate, @@ -109,7 +109,7 @@ data class TouchUpKitUpdateDto( @field:NotBlank val company: String?, - @field:Min(1, message = VALIDATION_SIZE_GE_ONE) + @field:Min(1, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ONE) val quantity: Int?, val shippingDate: LocalDate?, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixMaterialRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixMaterialRepository.kt index 6bd9aa9..ae20f67 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixMaterialRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixMaterialRepository.kt @@ -1,12 +1,11 @@ package dev.fyloz.colorrecipesexplorer.repository -import dev.fyloz.colorrecipesexplorer.model.Material import dev.fyloz.colorrecipesexplorer.model.MixMaterial import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @Repository interface MixMaterialRepository : JpaRepository { - /** Checks if one or more mix materials have the given [material]. */ - fun existsByMaterial(material: Material): Boolean + /** Checks if one or more mix materials have the given [materialId]. */ + fun existsByMaterialId(materialId: Long): Boolean } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialService.kt new file mode 100644 index 0000000..491763b --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialService.kt @@ -0,0 +1,23 @@ +package dev.fyloz.colorrecipesexplorer.service + +import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent +import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto +import dev.fyloz.colorrecipesexplorer.model.MixMaterial +import dev.fyloz.colorrecipesexplorer.repository.MixMaterialRepository + +interface MixMaterialService : Service { + /** Checks if a mix material with the given [materialId] exists. */ + fun existsByMaterialId(materialId: Long): Boolean +} + +@ServiceComponent +class DefaultMixMaterialService(repository: MixMaterialRepository, private val materialService: MaterialService) : + BaseService(repository), MixMaterialService { + override fun existsByMaterialId(materialId: Long) = repository.existsByMaterialId(materialId) + + override fun toDto(entity: MixMaterial) = + 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) +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt index 3aadbe8..2d525d1 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt @@ -19,13 +19,19 @@ inline fun Iterable.mapMayThrow( } } -/** Find duplicated in the given [Iterable] from keys obtained from the given [keySelector]. */ +/** Find duplicated keys in the given [Iterable], using keys obtained from the given [keySelector]. */ inline fun Iterable.findDuplicated(keySelector: (T) -> K) = this.groupBy(keySelector) .filter { it.value.count() > 1 } .map { it.key } -/** Check if the given [Iterable] has gaps between each items, using keys obtained from the given [keySelector]. */ +/** Find duplicated elements in the given [Iterable]. */ +fun Iterable.findDuplicated() = + this.groupBy { it } + .filter { it.value.count() > 1 } + .map { it.key } + +/** Check if the given [Iterable] has gaps between each element, using keys obtained from the given [keySelector]. */ inline fun Iterable.hasGaps(keySelector: (T) -> Int) = this.map(keySelector) .toIntArray() @@ -33,6 +39,12 @@ inline fun Iterable.hasGaps(keySelector: (T) -> Int) = .filterIndexed { index, it -> it != index + 1 } .isNotEmpty() +/** Check if the given [Int] [Iterable] has gaps between each element. */ +fun Iterable.hasGaps() = + this.sorted() + .filterIndexed { index, it -> it != index + 1 } + .isNotEmpty() + /** Clears and fills the given [MutableCollection] with the given [elements]. */ fun MutableCollection.setAll(elements: Collection) { this.clear() diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/PositionUtils.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/PositionUtils.kt new file mode 100644 index 0000000..858a280 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/PositionUtils.kt @@ -0,0 +1,50 @@ +package dev.fyloz.colorrecipesexplorer.utils + +import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError +import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException + +object PositionUtils { + const val INVALID_FIRST_POSITION_ERROR_CODE = "first" + const val DUPLICATED_POSITION_ERROR_CODE = "duplicated" + const val GAP_BETWEEN_POSITIONS_ERROR_CODE = "gap" + + private const val FIRST_POSITION = 1 + + fun validate(positions: List) { + if (positions.isEmpty()) { + return + } + + val sortedPositions = positions.sorted() + val errors = mutableSetOf() + + validateFirstPosition(sortedPositions[0], errors) + validateDuplicatedPositions(sortedPositions, errors) + validateGapsInPositions(sortedPositions, errors) + + if (errors.isNotEmpty()) { + throw InvalidPositionsException(errors) + } + } + + private fun validateFirstPosition(position: Int, errors: MutableSet) { + if (position == FIRST_POSITION) { + return + } + + errors += InvalidPositionError(INVALID_FIRST_POSITION_ERROR_CODE, "The first position must be $FIRST_POSITION") + } + + private fun validateDuplicatedPositions(positions: List, errors: MutableSet) { + errors += positions.findDuplicated() + .map { InvalidPositionError(DUPLICATED_POSITION_ERROR_CODE, "The position $it is duplicated") } + } + + private fun validateGapsInPositions(positions: List, errors: MutableSet) { + if (!positions.hasGaps()) { + return + } + + errors += InvalidPositionError(GAP_BETWEEN_POSITIONS_ERROR_CODE, "There is a gap between the positions") + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixMaterialLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixMaterialLogicTest.kt new file mode 100644 index 0000000..c27434c --- /dev/null +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixMaterialLogicTest.kt @@ -0,0 +1,79 @@ +package dev.fyloz.colorrecipesexplorer.logic + +import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto +import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto +import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto +import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError +import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException +import dev.fyloz.colorrecipesexplorer.service.MixMaterialService +import dev.fyloz.colorrecipesexplorer.utils.PositionUtils +import io.mockk.* +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class DefaultMixMaterialLogicTest { + private val mixMaterialServiceMock = mockk() + + private val mixMaterialLogic = DefaultMixMaterialLogic(mixMaterialServiceMock) + + @AfterEach + internal fun afterEach() { + clearAllMocks() + } + + @Test + fun validateMixMaterials_normalBehavior_doesNothing() { + // Arrange + val materialType = MaterialTypeDto(1L, "Unit test material type", "UTMT", false) + val material = MaterialDto(1L, "Unit test material", 1000f, false, materialType) + val mixMaterial = MixMaterialDto(1L, material, 100f, 1) + + mockkObject(PositionUtils) + every { PositionUtils.validate(any()) } just runs + + // Act + // Assert + mixMaterialLogic.validateMixMaterials(setOf(mixMaterial)) + } + + @Test + fun validateMixMaterials_emptySet_doesNothing() { + // Arrange + // Act + // Assert + mixMaterialLogic.validateMixMaterials(setOf()) + } + + @Test + fun validateMixMaterials_firstUsesPercents_throwsInvalidFirstMixMaterialException() { + // Arrange + val materialType = MaterialTypeDto(1L, "Unit test material type", "UTMT", true) + val material = MaterialDto(1L, "Unit test material", 1000f, false, materialType) + val mixMaterial = MixMaterialDto(1L, material, 100f, 1) + + mockkObject(PositionUtils) + every { PositionUtils.validate(any()) } just runs + + // Act + // Assert + assertThrows { mixMaterialLogic.validateMixMaterials(setOf(mixMaterial)) } + } + + @Test + fun validateMixMaterials_invalidPositions_throwsInvalidMixMaterialsPositionsException() { + // Arrange + val materialType = MaterialTypeDto(1L, "Unit test material type", "UTMT", false) + val material = MaterialDto(1L, "Unit test material", 1000f, false, materialType) + val mixMaterial = MixMaterialDto(1L, material, 100f, 1) + + val errors = setOf(InvalidPositionError("error", "An unit test error")) + + mockkObject(PositionUtils) + every { PositionUtils.validate(any()) } throws InvalidPositionsException(errors) + + // Act + // Assert + assertThrows { mixMaterialLogic.validateMixMaterials(setOf(mixMaterial)) } + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeStepLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeStepLogicTest.kt index f606abd..c9defbc 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeStepLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeStepLogicTest.kt @@ -1,21 +1,21 @@ package dev.fyloz.colorrecipesexplorer.logic -import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto +import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError +import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException import dev.fyloz.colorrecipesexplorer.model.RecipeGroupInformation import dev.fyloz.colorrecipesexplorer.model.RecipeStep import dev.fyloz.colorrecipesexplorer.model.account.Group import dev.fyloz.colorrecipesexplorer.service.RecipeStepService +import dev.fyloz.colorrecipesexplorer.utils.PositionUtils import io.mockk.* import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows -import kotlin.test.assertTrue class DefaultRecipeStepLogicTest { private val recipeStepServiceMock = mockk() - private val recipeStepLogic = spyk(DefaultRecipeStepLogic(recipeStepServiceMock)) + private val recipeStepLogic = DefaultRecipeStepLogic(recipeStepServiceMock) @AfterEach internal fun afterEach() { @@ -25,7 +25,8 @@ class DefaultRecipeStepLogicTest { @Test fun validateGroupInformationSteps_normalBehavior_callsValidateSteps() { // Arrange - every { recipeStepLogic.validateSteps(any()) } just runs + mockkObject(PositionUtils) + every { PositionUtils.validate(any()) } just runs val group = Group(1L, "Unit test group") val steps = mutableSetOf(RecipeStep(1L, 1, "A message")) @@ -36,14 +37,15 @@ class DefaultRecipeStepLogicTest { // Assert verify { - recipeStepLogic.validateSteps(any()) // TODO replace with actual steps dtos when RecipeGroupInformation updated + PositionUtils.validate(steps.map { it.position }) } } @Test fun validateGroupInformationSteps_stepSetIsNull_doesNothing() { // Arrange - every { recipeStepLogic.validateSteps(any()) } just runs + mockkObject(PositionUtils) + every { PositionUtils.validate(any()) } just runs val group = Group(1L, "Unit test group") val groupInfo = RecipeGroupInformation(1L, group, "A note", null) @@ -53,15 +55,17 @@ class DefaultRecipeStepLogicTest { // Assert verify(exactly = 0) { - recipeStepLogic.validateSteps(any()) // TODO replace with actual steps dtos when RecipeGroupInformation updated + PositionUtils.validate(any()) } } @Test fun validateGroupInformationSteps_invalidSteps_throwsInvalidGroupStepsPositionsException() { // Arrange - val errors = setOf(InvalidStepsPositionsError("error", "An unit test error")) - every { recipeStepLogic.validateSteps(any()) } throws InvalidStepsPositionsException(errors) + val errors = setOf(InvalidPositionError("error", "An unit test error")) + + mockkObject(PositionUtils) + every { PositionUtils.validate(any()) } throws InvalidPositionsException(errors) val group = Group(1L, "Unit test group") val steps = mutableSetOf(RecipeStep(1L, 1, "A message")) @@ -71,187 +75,4 @@ class DefaultRecipeStepLogicTest { // Assert assertThrows { recipeStepLogic.validateGroupInformationSteps(groupInfo) } } - - @Test - fun validateSteps_normalBehavior_doesNothing() { - // Arrange - val recipeSteps = setOf( - RecipeStepDto(1L, 1, "A message"), - RecipeStepDto(2L, 2, "Another message") - ) - - // Act - // Assert - assertDoesNotThrow { recipeStepLogic.validateSteps(recipeSteps) } - } - - @Test - fun validateSteps_emptyStepSet_doesNothing() { - // Arrange - val recipeSteps = setOf() - - // Act - // Assert - assertDoesNotThrow { recipeStepLogic.validateSteps(recipeSteps) } - } - - @Test - fun validateSteps_hasInvalidPositions_throwsInvalidStepsPositionsException() { - // Arrange - val recipeSteps = setOf( - RecipeStepDto(1L, 2, "A message"), - RecipeStepDto(2L, 3, "Another message") - ) - - // Act - // Assert - assertThrows { recipeStepLogic.validateSteps(recipeSteps) } - } - - @Test - fun validateSteps_firstStepPositionInvalid_returnsInvalidStepValidationError() { - // Arrange - val recipeSteps = setOf( - RecipeStepDto(1L, 2, "A message"), - RecipeStepDto(2L, 3, "Another message") - ) - - // Act - val exception = assertThrows { recipeStepLogic.validateSteps(recipeSteps) } - - // Assert - assertTrue { - exception.errors.any { it.type == RecipeStepDto.VALIDATION_ERROR_CODE_INVALID_FIRST_STEP } - } - } - - @Test - fun validateSteps_duplicatedPositions_returnsInvalidStepValidationError() { - // Arrange - val recipeSteps = setOf( - RecipeStepDto(1L, 1, "A message"), - RecipeStepDto(2L, 1, "Another message") - ) - - // Act - val exception = assertThrows { recipeStepLogic.validateSteps(recipeSteps) } - - // Assert - assertTrue { - exception.errors.any { it.type == RecipeStepDto.VALIDATION_ERROR_CODE_DUPLICATED_STEPS_POSITION } - } - } - - @Test - fun validateSteps_gapsInPositions_returnsInvalidStepValidationError() { - // Arrange - val recipeSteps = setOf( - RecipeStepDto(1L, 1, "A message"), - RecipeStepDto(2L, 3, "Another message") - ) - - // Act - val exception = assertThrows { recipeStepLogic.validateSteps(recipeSteps) } - - // Assert - assertTrue { - exception.errors.any { it.type == RecipeStepDto.VALIDATION_ERROR_CODE_GAP_BETWEEN_STEPS_POSITIONS } - } - } -} - -//@TestInstance(TestInstance.Lifecycle.PER_CLASS) -//class RecipeStepLogicTest : -// AbstractModelServiceTest() { -// override val repository: RecipeStepRepository = mock() -// override val logic: RecipeStepLogic = spy(DefaultRecipeStepLogic(repository)) -// -// override val entity: RecipeStep = recipeStep(id = 0L, message = "message") -// override val anotherEntity: RecipeStep = recipeStep(id = 1L, message = "another message") -// -// // validateGroupInformationSteps() -// -// @Test -// fun `validateGroupInformationSteps() calls validateSteps() with the given RecipeGroupInformation steps`() { -// withGroupInformation { -// logic.validateGroupInformationSteps(this) -// -// verify(logic).validateSteps(this.steps!!) -// } -// } -// -// @Test -// fun `validateGroupInformationSteps() throws InvalidGroupStepsPositionsException when validateSteps() throws an InvalidStepsPositionsException`() { -// withGroupInformation { -// doAnswer { throw InvalidStepsPositionsException(setOf()) }.whenever(logic).validateSteps(this.steps!!) -// -// assertThrows { -// logic.validateGroupInformationSteps(this) -// } -// } -// } -// -// // validateSteps() -// -// @Test -// fun `validateSteps() throws an InvalidStepsPositionsException when the position of the first step of the given groupInformation is not 1`() { -// assertInvalidStepsPositionsException( -// mutableSetOf( -// recipeStep(id = 0L, position = 0), -// recipeStep(id = 1L, position = 1), -// recipeStep(id = 2L, position = 2), -// recipeStep(id = 3L, position = 3) -// ), -// INVALID_FIRST_STEP_POSITION_ERROR_CODE -// ) -// } -// -// @Test -// fun `validateSteps() throws an InvalidStepsPositionsException when steps positions are duplicated in the given groupInformation`() { -// assertInvalidStepsPositionsException( -// mutableSetOf( -// recipeStep(id = 0L, position = 1), -// recipeStep(id = 1L, position = 2), -// recipeStep(id = 2L, position = 2), -// recipeStep(id = 3L, position = 3) -// ), -// DUPLICATED_STEPS_POSITIONS_ERROR_CODE -// ) -// } -// -// @Test -// fun `validateSteps() throws an InvalidStepsPositionsException when there is a gap between steps positions in the given groupInformation`() { -// assertInvalidStepsPositionsException( -// mutableSetOf( -// recipeStep(id = 0L, position = 1), -// recipeStep(id = 1L, position = 2), -// recipeStep(id = 2L, position = 4), -// recipeStep(id = 3L, position = 5) -// ), -// GAP_BETWEEN_STEPS_POSITIONS_ERROR_CODE -// ) -// } -// -// private fun withGroupInformation(steps: MutableSet? = null, test: RecipeGroupInformation.() -> Unit) { -// recipeGroupInformation( -// group = group(id = 0L), -// steps = steps ?: mutableSetOf( -// recipeStep(id = 0L, position = 1), -// recipeStep(id = 1L, position = 2), -// recipeStep(id = 2L, position = 3), -// recipeStep(id = 3L, position = 4) -// ) -// ) { -// test() -// } -// } -// -// private fun assertInvalidStepsPositionsException(steps: MutableSet, errorType: String) { -// val exception = assertThrows { -// logic.validateSteps(steps) -// } -// -// assertTrue { exception.errors.size == 1 } -// assertTrue { exception.errors.first().type == errorType } -// } -//} +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogicTest.kt deleted file mode 100644 index d9b69e5..0000000 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogicTest.kt +++ /dev/null @@ -1,182 +0,0 @@ -//package dev.fyloz.colorrecipesexplorer.logic -// -//import com.nhaarman.mockitokotlin2.* -//import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException -//import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException -//import dev.fyloz.colorrecipesexplorer.exception.CannotUpdateException -//import dev.fyloz.colorrecipesexplorer.exception.NotFoundException -//import dev.fyloz.colorrecipesexplorer.model.* -//import dev.fyloz.colorrecipesexplorer.repository.MaterialTypeRepository -//import org.junit.jupiter.api.AfterEach -//import org.junit.jupiter.api.Test -//import org.junit.jupiter.api.TestInstance -//import org.junit.jupiter.api.assertThrows -//import kotlin.test.assertEquals -//import kotlin.test.assertFalse -//import kotlin.test.assertTrue -// -//@TestInstance(TestInstance.Lifecycle.PER_CLASS) -//class MaterialTypeLogicTest : -// AbstractExternalNamedModelServiceTest() { -// override val repository: MaterialTypeRepository = mock() -// private val materialService: MaterialLogic = mock() -// override val logic: MaterialTypeLogic = spy(DefaultMaterialTypeLogic(repository)) -// override val entity: MaterialType = materialType(id = 0L, name = "material type", prefix = "MAT") -// override val anotherEntity: MaterialType = materialType(id = 1L, name = "another material type", prefix = "AMT") -// override val entityWithEntityName: MaterialType = materialType(2L, name = entity.name, prefix = "EEN") -// private val systemType = materialType(id = 3L, name = "systype", prefix = "SYS", systemType = true) -// private val anotherSystemType = materialType(id = 4L, name = "another systype", prefix = "ASY", systemType = true) -// override val entitySaveDto: MaterialTypeSaveDto = spy(materialTypeSaveDto(name = "material type", prefix = "MAT")) -// override val entityUpdateDto: MaterialTypeUpdateDto = -// spy(materialTypeUpdateDto(id = 0L, name = "material type", prefix = "MAT")) -// -// @AfterEach -// override fun afterEach() { -// reset(materialService) -// super.afterEach() -// } -// -// // existsByPrefix() -// -// @Test -// fun `existsByPrefix() returns true when a material type with the given prefix exists`() { -// whenever(repository.existsByPrefix(entity.prefix)).doReturn(true) -// -// val found = logic.existsByPrefix(entity.prefix) -// -// assertTrue(found) -// } -// -// @Test -// fun `existsByPrefix() returns false when no material type with the given prefix exists`() { -// whenever(repository.existsByPrefix(entity.prefix)).doReturn(false) -// -// val found = logic.existsByPrefix(entity.prefix) -// -// assertFalse(found) -// } -// -// // getAllSystemTypes() -// -// @Test -// fun `getAllSystemTypes() returns all system types`() { -// whenever(repository.findAllBySystemTypeIs(true)).doReturn(listOf(systemType, anotherSystemType)) -// -// val found = logic.getAllSystemTypes() -// -// assertTrue(found.contains(systemType)) -// assertTrue(found.contains(anotherSystemType)) -// } -// -// // getAllNonSystemTypes() -// -// @Test -// fun `getAllNonSystemTypes() returns all non system types`() { -// whenever(repository.findAllBySystemTypeIs(false)).doReturn(listOf(entity, anotherEntity)) -// -// val found = logic.getAllNonSystemType() -// -// assertTrue(found.contains(entity)) -// assertTrue(found.contains(anotherEntity)) -// } -// -// // save() -// -// @Test -// override fun `save(dto) calls and returns save() with the created entity`() { -// withBaseSaveDtoTest(entity, entitySaveDto, logic) -// } -// -// // saveMaterialType() -// -// @Test -// fun `saveMaterialType() throws AlreadyExistsException when a material type with the given prefix already exists`() { -// doReturn(true).whenever(logic).existsByPrefix(entity.prefix) -// -// assertThrows { logic.save(entity) } -// .assertErrorCode("prefix") -// } -// -// // update() -// -// @Test -// override fun `update(dto) calls and returns update() with the created entity`() = -// withBaseUpdateDtoTest(entity, entityUpdateDto, logic, { any() }) -// -// override fun `update() saves in the repository and returns the updated value`() { -// whenever(repository.save(entity)).doReturn(entity) -// whenever(repository.findByName(entity.name)).doReturn(null) -// whenever(repository.findByPrefix(entity.prefix)).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) -// } -// -// override fun `update() throws NotFoundException when no entity with the given id exists in the repository`() { -// whenever(repository.findByName(entity.name)).doReturn(null) -// whenever(repository.findByPrefix(entity.prefix)).doReturn(null) -// doReturn(false).whenever(logic).existsById(entity.id!!) -// doReturn(null).whenever(logic).getById(entity.id!!) -// -// assertThrows { logic.update(entity) } -// .assertErrorCode() -// } -// -// override fun `update() throws AlreadyExistsException when an entity with the updated name exists`() { -// whenever(repository.findByName(entity.name)).doReturn(entityWithEntityName) -// whenever(repository.findByPrefix(entity.prefix)).doReturn(null) -// doReturn(true).whenever(logic).existsById(entity.id!!) -// doReturn(entity).whenever(logic).getById(entity.id!!) -// -// assertThrows { logic.update(entity) } -// .assertErrorCode("name") -// } -// -// @Test -// fun `update() throws AlreadyExistsException when an entity with the updated prefix exists`() { -// val anotherMaterialType = materialType(prefix = entity.prefix) -// whenever(repository.findByPrefix(entity.prefix)).doReturn(anotherMaterialType) -// doReturn(entity).whenever(logic).getById(entity.id!!) -// -// assertThrows { logic.update(entity) } -// .assertErrorCode("prefix") -// } -// -// @Test -// fun `update() throws CannotUpdateException when updating a system material type`() { -// whenever(repository.existsByIdAndSystemTypeIsTrue(systemType.id!!)).doReturn(true) -// -// assertThrows { logic.update(systemType) } -// } -// -// // delete() -// -// @Test -// fun `delete() throws CannotDeleteException when deleting a system material type`() { -// whenever(repository.existsByIdAndSystemTypeIsTrue(systemType.id!!)).doReturn(true) -// -// assertThrows { logic.delete(systemType) } -// } -// -// override fun `delete() deletes in the repository`() { -// whenCanBeDeleted { -// super.`delete() deletes in the repository`() -// } -// } -// -// override fun `deleteById() deletes the entity with the given id in the repository`() { -// whenCanBeDeleted { -// super.`deleteById() deletes the entity with the given id in the repository`() -// } -// } -// -// private fun whenCanBeDeleted(id: Long = any(), test: () -> Unit) { -// whenever(repository.canBeDeleted(id)).doReturn(true) -// -// test() -// } -//} diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogicTest.kt index d611a92..d76215b 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogicTest.kt @@ -1,255 +1,245 @@ package dev.fyloz.colorrecipesexplorer.logic -import com.nhaarman.mockitokotlin2.* -import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.repository.MixRepository -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class MixLogicTest : AbstractExternalModelServiceTest() { - override val repository: MixRepository = mock() - private val recipeService: RecipeLogic = mock() - private val materialTypeService: MaterialTypeLogic = mock() - private val mixMaterialService: MixMaterialLogic = mock() - private val mixTypeService: MixTypeLogic = mock() - override val logic: MixLogic = - spy(DefaultMixLogic(repository, recipeService, materialTypeService, mixMaterialService, mixTypeService)) - - override val entity: Mix = mix(id = 0L, location = "location") - override val anotherEntity: Mix = mix(id = 1L) - override val entitySaveDto: MixSaveDto = - spy(mixSaveDto(mixMaterials = setOf(mixMaterialDto(materialId = 1L, quantity = 1000f, position = 0)))) - override val entityUpdateDto: MixUpdateDto = spy(mixUpdateDto(id = entity.id!!)) - - @AfterEach - override fun afterEach() { - super.afterEach() - reset(recipeService, materialTypeService, mixMaterialService, mixTypeService) - } - - // getAllByMixType() - - @Test - fun `getAllByMixType() returns all mixes with the given mix type`() { - val mixType = mixType(id = 0L) - - whenever(repository.findAllByMixType(mixType)).doReturn(entityList) - - val found = logic.getAllByMixType(mixType) - - assertEquals(entityList, found) - } - - // save() - - @Test - override fun `save(dto) calls and returns save() with the created entity`() { - val recipe = recipe(id = entitySaveDto.recipeId) - val materialType = materialType(id = entitySaveDto.materialTypeId) - val material = material( - name = entitySaveDto.name, - inventoryQuantity = Float.MIN_VALUE, - isMixType = true, - materialType = materialType - ) - val mixType = mixType(name = entitySaveDto.name, material = material) - val mix = mix(recipe = recipe, mixType = mixType) - val mixWithId = mix(id = 0L, recipe = recipe, mixType = mixType) - val mixMaterials = setOf(mixMaterial(material = material(id = 1L), quantity = 1000f)) - - whenever(recipeService.getById(recipe.id!!)).doReturn(recipe) - whenever(materialTypeService.getById(materialType.id!!)).doReturn(materialTypeDto(materialType)) - whenever(mixMaterialService.create(entitySaveDto.mixMaterials!!)).doReturn(mixMaterials) - whenever( - mixTypeService.getOrCreateForNameAndMaterialType( - mixType.name, - mixType.material.materialType!! - ) - ).doReturn(mixType) - doReturn(true).whenever(logic).existsById(mixWithId.id!!) - doReturn(mixWithId).whenever(logic).save(any()) - - val found = logic.save(entitySaveDto) - - verify(logic).save(argThat { this.recipe == mix.recipe }) - verify(recipeService).addMix(recipe, mixWithId) - - // Verify if this method is called instead of the MixType's constructor, which does not check if the name is already taken by a material. - verify(mixTypeService).getOrCreateForNameAndMaterialType(mixType.name, mixType.material.materialType!!) - - assertEquals(mixWithId, found) - } - - // update() - - private fun mixUpdateDtoTest( - scope: MixUpdateDtoTestScope = MixUpdateDtoTestScope(), - sharedMixType: Boolean = false, - op: MixUpdateDtoTestScope.() -> Unit - ) { - with(scope) { - doReturn(true).whenever(logic).existsById(mix.id!!) - doReturn(mix).whenever(logic).getById(mix.id!!) - doReturn(sharedMixType).whenever(logic).mixTypeIsShared(mix.mixType) - doAnswer { it.arguments[0] }.whenever(logic).update(any()) - - if (mixUpdateDto.materialTypeId != null) { - whenever(materialTypeService.getById(materialType.id!!)).doReturn(materialTypeDto(materialType)) - } - - op() - } - } - - private fun mixUpdateDtoMixTypeTest(sharedMixType: Boolean = false, op: MixUpdateDtoTestScope.() -> Unit) { - with(MixUpdateDtoTestScope(mixUpdateDto = mixUpdateDto(id = 0L, name = "name", materialTypeId = 0L))) { - mixUpdateDtoTest(this, sharedMixType, op) - } - } - - @Test - override fun `update(dto) calls and returns update() with the created entity`() { - val mixUpdateDto = spy(mixUpdateDto(id = 0L, name = null, materialTypeId = null)) - - doReturn(entity).whenever(logic).getById(any()) - doReturn(entity).whenever(logic).update(entity) - - val found = logic.update(mixUpdateDto) - - verify(logic).update(entity) - - assertEquals(entity, found) - } - - @Test - fun `update(dto) calls MixTypeService saveForNameAndMaterialType() when mix type is shared`() { - mixUpdateDtoMixTypeTest(sharedMixType = true) { - whenever(mixTypeService.saveForNameAndMaterialType(mixUpdateDto.name!!, materialType)) - .doReturn(newMixType) - - val found = logic.update(mixUpdateDto) - - verify(mixTypeService).saveForNameAndMaterialType(mixUpdateDto.name!!, materialType) - - assertEquals(newMixType, found.mixType) - } - } - - @Test - fun `update(dto) calls MixTypeService updateForNameAndMaterialType() when mix type is not shared`() { - mixUpdateDtoMixTypeTest { - whenever(mixTypeService.updateForNameAndMaterialType(mixType, mixUpdateDto.name!!, materialType)) - .doReturn(newMixType) - - val found = logic.update(mixUpdateDto) - - verify(mixTypeService).updateForNameAndMaterialType(mixType, mixUpdateDto.name!!, materialType) - - assertEquals(newMixType, found.mixType) - } - } - - @Test - fun `update(dto) update, create and delete mix materials according to the given mix materials map`() { - mixUpdateDtoTest { - val mixMaterials = setOf( - mixMaterialDto(materialId = 0L, quantity = 100f, position = 0), - mixMaterialDto(materialId = 1L, quantity = 200f, position = 1), - mixMaterialDto(materialId = 2L, quantity = 300f, position = 2), - mixMaterialDto(materialId = 3L, quantity = 400f, position = 3), - ) - mixUpdateDto.mixMaterials = mixMaterials - - whenever(mixMaterialService.create(any>())).doAnswer { - (it.arguments[0] as Set).map { dto -> - mixMaterial( - material = material(id = dto.materialId), - quantity = dto.quantity, - position = dto.position - ) - }.toSet() - } - - val found = logic.update(mixUpdateDto) - - mixMaterials.forEach { - assertTrue { - found.mixMaterials.any { mixMaterial -> - mixMaterial.material.id == it.materialId && mixMaterial.quantity == it.quantity && mixMaterial.position == it.position - } - } - } - } - } - - // updateLocations() - - @Test - fun `updateLocations() calls updateLocation() for each given MixLocationDto`() { - val locations = setOf( - mixLocationDto(mixId = 0, location = "Loc 0"), - mixLocationDto(mixId = 1, location = "Loc 1"), - mixLocationDto(mixId = 2, location = "Loc 2"), - mixLocationDto(mixId = 3, location = "Loc 3") - ) - - logic.updateLocations(locations) - - locations.forEach { - verify(logic).updateLocation(it) - } - } - - // updateLocation() - - @Test - fun `updateLocation() updates the location of a mix in the repository according to the given MixLocationDto`() { - val locationDto = mixLocationDto(mixId = 0L, location = "Location") - - logic.updateLocation(locationDto) - - verify(repository).updateLocationById(locationDto.mixId, locationDto.location) - } - - // delete() - - override fun `delete() deletes in the repository`() { - whenCanBeDeleted { - super.`delete() deletes in the repository`() - } - } - - // deleteById() - - @Test - override fun `deleteById() deletes the entity with the given id in the repository`() { - whenCanBeDeleted { - super.`deleteById() deletes the entity with the given id in the repository`() - } - } - - private fun whenCanBeDeleted(id: Long = any(), test: () -> Unit) { - whenever(repository.canBeDeleted(id)).doReturn(true) - - test() - } -} - -data class MixUpdateDtoTestScope( - val mixType: MixType = mixType(name = "mix type"), - val newMixType: MixType = mixType(name = "another mix type"), - val materialType: MaterialType = materialType(id = 0L), - val mix: Mix = mix(id = 0L, mixType = mixType), - val mixUpdateDto: MixUpdateDto = spy( - mixUpdateDto( - id = 0L, - name = null, - materialTypeId = null, - mixMaterials = setOf() - ) - ) -) +//@TestInstance(TestInstance.Lifecycle.PER_CLASS) +//class MixLogicTest : AbstractExternalModelServiceTest() { +// override val repository: MixRepository = mock() +// private val recipeService: RecipeLogic = mock() +// private val materialTypeService: MaterialTypeLogic = mock() +// private val mixMaterialService: MixMaterialLogic = mock() +// private val mixTypeService: MixTypeLogic = mock() +// override val logic: MixLogic = +// spy(DefaultMixLogic(repository, recipeService, materialTypeService, mixMaterialService, mixTypeService)) +// +// override val entity: Mix = mix(id = 0L, location = "location") +// override val anotherEntity: Mix = mix(id = 1L) +// override val entitySaveDto: MixSaveDto = spy(mixSaveDto(mixMaterials = setOf())) +// override val entityUpdateDto: MixUpdateDto = spy(mixUpdateDto(id = entity.id!!)) +// +// @AfterEach +// override fun afterEach() { +// super.afterEach() +// reset(recipeService, materialTypeService, mixMaterialService, mixTypeService) +// } +// +// // getAllByMixType() +// +//// @Test +//// fun `getAllByMixType() returns all mixes with the given mix type`() { +//// val mixType = mixType(id = 0L) +//// +//// whenever(repository.findAllByMixType(mixType)).doReturn(entityList) +//// +//// val found = logic.getAllByMixType(mixType) +//// +//// assertEquals(entityList, found) +//// } +//// +//// // save() +//// +//// @Test +//// override fun `save(dto) calls and returns save() with the created entity`() { +//// val recipe = recipe(id = entitySaveDto.recipeId) +//// val materialType = materialType(id = entitySaveDto.materialTypeId) +//// val material = material( +//// name = entitySaveDto.name, +//// inventoryQuantity = Float.MIN_VALUE, +//// isMixType = true, +//// materialType = materialType +//// ) +//// val mixType = mixType(name = entitySaveDto.name, material = material) +//// val mix = mix(recipe = recipe, mixType = mixType) +//// val mixWithId = mix(id = 0L, recipe = recipe, mixType = mixType) +//// val mixMaterials = setOf(mixMaterial(material = material(id = 1L), quantity = 1000f)) +//// +//// whenever(recipeService.getById(recipe.id!!)).doReturn(recipe) +//// whenever(materialTypeService.getById(materialType.id!!)).doReturn(materialTypeDto(materialType)) +//// whenever(mixMaterialService.create(entitySaveDto.mixMaterials!!)).doReturn(mixMaterials) +//// whenever( +//// mixTypeService.getOrCreateForNameAndMaterialType( +//// mixType.name, +//// mixType.material.materialType!! +//// ) +//// ).doReturn(mixType) +//// doReturn(true).whenever(logic).existsById(mixWithId.id!!) +//// doReturn(mixWithId).whenever(logic).save(any()) +//// +//// val found = logic.save(entitySaveDto) +//// +//// verify(logic).save(argThat { this.recipe == mix.recipe }) +//// verify(recipeService).addMix(recipe, mixWithId) +//// +//// // Verify if this method is called instead of the MixType's constructor, which does not check if the name is already taken by a material. +//// verify(mixTypeService).getOrCreateForNameAndMaterialType(mixType.name, mixType.material.materialType!!) +//// +//// assertEquals(mixWithId, found) +//// } +//// +//// // update() +//// +//// private fun mixUpdateDtoTest( +//// scope: MixUpdateDtoTestScope = MixUpdateDtoTestScope(), +//// sharedMixType: Boolean = false, +//// op: MixUpdateDtoTestScope.() -> Unit +//// ) { +//// with(scope) { +//// doReturn(true).whenever(logic).existsById(mix.id!!) +//// doReturn(mix).whenever(logic).getById(mix.id!!) +//// doReturn(sharedMixType).whenever(logic).mixTypeIsShared(mix.mixType) +//// doAnswer { it.arguments[0] }.whenever(logic).update(any()) +//// +//// if (mixUpdateDto.materialTypeId != null) { +//// whenever(materialTypeService.getById(materialType.id!!)).doReturn(materialTypeDto(materialType)) +//// } +//// +//// op() +//// } +//// } +//// +//// private fun mixUpdateDtoMixTypeTest(sharedMixType: Boolean = false, op: MixUpdateDtoTestScope.() -> Unit) { +//// with(MixUpdateDtoTestScope(mixUpdateDto = mixUpdateDto(id = 0L, name = "name", materialTypeId = 0L))) { +//// mixUpdateDtoTest(this, sharedMixType, op) +//// } +//// } +//// +//// @Test +//// override fun `update(dto) calls and returns update() with the created entity`() { +//// val mixUpdateDto = spy(mixUpdateDto(id = 0L, name = null, materialTypeId = null)) +//// +//// doReturn(entity).whenever(logic).getById(any()) +//// doReturn(entity).whenever(logic).update(entity) +//// +//// val found = logic.update(mixUpdateDto) +//// +//// verify(logic).update(entity) +//// +//// assertEquals(entity, found) +//// } +//// +//// @Test +//// fun `update(dto) calls MixTypeService saveForNameAndMaterialType() when mix type is shared`() { +//// mixUpdateDtoMixTypeTest(sharedMixType = true) { +//// whenever(mixTypeService.saveForNameAndMaterialType(mixUpdateDto.name!!, materialType)) +//// .doReturn(newMixType) +//// +//// val found = logic.update(mixUpdateDto) +//// +//// verify(mixTypeService).saveForNameAndMaterialType(mixUpdateDto.name!!, materialType) +//// +//// assertEquals(newMixType, found.mixType) +//// } +//// } +//// +//// @Test +//// fun `update(dto) calls MixTypeService updateForNameAndMaterialType() when mix type is not shared`() { +//// mixUpdateDtoMixTypeTest { +//// whenever(mixTypeService.updateForNameAndMaterialType(mixType, mixUpdateDto.name!!, materialType)) +//// .doReturn(newMixType) +//// +//// val found = logic.update(mixUpdateDto) +//// +//// verify(mixTypeService).updateForNameAndMaterialType(mixType, mixUpdateDto.name!!, materialType) +//// +//// assertEquals(newMixType, found.mixType) +//// } +//// } +//// +//// @Test +//// fun `update(dto) update, create and delete mix materials according to the given mix materials map`() { +//// mixUpdateDtoTest { +//// val mixMaterials = setOf( +//// mixMaterialDto(materialId = 0L, quantity = 100f, position = 0), +//// mixMaterialDto(materialId = 1L, quantity = 200f, position = 1), +//// mixMaterialDto(materialId = 2L, quantity = 300f, position = 2), +//// mixMaterialDto(materialId = 3L, quantity = 400f, position = 3), +//// ) +//// mixUpdateDto.mixMaterials = mixMaterials +//// +//// whenever(mixMaterialService.create(any>())).doAnswer { +//// (it.arguments[0] as Set).map { dto -> +//// mixMaterial( +//// material = material(id = dto.materialId), +//// quantity = dto.quantity, +//// position = dto.position +//// ) +//// }.toSet() +//// } +//// +//// val found = logic.update(mixUpdateDto) +//// +//// mixMaterials.forEach { +//// assertTrue { +//// found.mixMaterials.any { mixMaterial -> +//// mixMaterial.material.id == it.materialId && mixMaterial.quantity == it.quantity && mixMaterial.position == it.position +//// } +//// } +//// } +//// } +//// } +//// +//// // updateLocations() +//// +//// @Test +//// fun `updateLocations() calls updateLocation() for each given MixLocationDto`() { +//// val locations = setOf( +//// mixLocationDto(mixId = 0, location = "Loc 0"), +//// mixLocationDto(mixId = 1, location = "Loc 1"), +//// mixLocationDto(mixId = 2, location = "Loc 2"), +//// mixLocationDto(mixId = 3, location = "Loc 3") +//// ) +//// +//// logic.updateLocations(locations) +//// +//// locations.forEach { +//// verify(logic).updateLocation(it) +//// } +//// } +//// +//// // updateLocation() +//// +//// @Test +//// fun `updateLocation() updates the location of a mix in the repository according to the given MixLocationDto`() { +//// val locationDto = mixLocationDto(mixId = 0L, location = "Location") +//// +//// logic.updateLocation(locationDto) +//// +//// verify(repository).updateLocationById(locationDto.mixId, locationDto.location) +//// } +//// +//// // delete() +//// +//// override fun `delete() deletes in the repository`() { +//// whenCanBeDeleted { +//// super.`delete() deletes in the repository`() +//// } +//// } +//// +//// // deleteById() +//// +//// @Test +//// override fun `deleteById() deletes the entity with the given id in the repository`() { +//// whenCanBeDeleted { +//// super.`deleteById() deletes the entity with the given id in the repository`() +//// } +//// } +//// +//// private fun whenCanBeDeleted(id: Long = any(), test: () -> Unit) { +//// whenever(repository.canBeDeleted(id)).doReturn(true) +//// +//// test() +//// } +//} +// +//data class MixUpdateDtoTestScope( +// val mixType: MixType = mixType(name = "mix type"), +// val newMixType: MixType = mixType(name = "another mix type"), +// val materialType: MaterialType = materialType(id = 0L), +// val mix: Mix = mix(id = 0L, mixType = mixType), +// val mixUpdateDto: MixUpdateDto = spy( +// mixUpdateDto( +// id = 0L, +// name = null, +// materialTypeId = null, +// mixMaterials = setOf() +// ) +// ) +//) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogicTest.kt deleted file mode 100644 index 2b54a53..0000000 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogicTest.kt +++ /dev/null @@ -1,171 +0,0 @@ -package dev.fyloz.colorrecipesexplorer.logic - -import com.nhaarman.mockitokotlin2.* -import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.repository.MixMaterialRepository -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance -import org.junit.jupiter.api.assertThrows -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotEquals -import kotlin.test.assertTrue - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class MixMaterialLogicTest : AbstractModelServiceTest() { - override val repository: MixMaterialRepository = mock() - private val materialService: MaterialLogic = mock() - override val logic: MixMaterialLogic = spy(DefaultMixMaterialLogic(repository, materialService)) - - private val material: Material = material(id = 0L) - override val entity: MixMaterial = mixMaterial(id = 0L, material = material, quantity = 1000f) - override val anotherEntity: MixMaterial = mixMaterial(id = 1L, material = material) - - // existsByMaterial() - - @Test - fun `existsByMaterial() returns true when a mix material with the given material exists`() { - whenever(repository.existsByMaterial(material)).doReturn(true) - - val found = logic.existsByMaterial(material) - - assertTrue(found) - } - - @Test - fun `existsByMaterial() returns false when no mix material with the given material exists`() { - whenever(repository.existsByMaterial(material)).doReturn(false) - - val found = logic.existsByMaterial(material) - - assertFalse(found) - } - - // create() - - @Test - fun `create(set) calls create() for each MixMaterialDto`() { - val mixMaterialDtos = setOf( - mixMaterialDto(materialId = 0L, quantity = 1000f, position = 1), - mixMaterialDto(materialId = 1L, quantity = 2000f, position = 2), - mixMaterialDto(materialId = 2L, quantity = 3000f, position = 3), - mixMaterialDto(materialId = 3L, quantity = 4000f, position = 4) - ) - - doAnswer { - with(it.arguments[0] as MixMaterialDto) { - mixMaterial( - material = material(id = this.materialId), - quantity = this.quantity, - position = this.position - ) - } - }.whenever(logic).create(any()) - - val found = logic.create(mixMaterialDtos) - - mixMaterialDtos.forEach { dto -> - verify(logic).create(dto) - assertTrue { - found.any { - it.material.id == dto.materialId && it.quantity == dto.quantity && it.position == dto.position - } - } - } - } - - @Test - fun `create() creates a mix material according to the given MixUpdateDto`() { - val mixMaterialDto = mixMaterialDto(materialId = 0L, quantity = 1000f, position = 1) - - whenever(materialService.getById(mixMaterialDto.materialId)).doAnswer { materialDto(material(id = it.arguments[0] as Long)) } - - val found = logic.create(mixMaterialDto) - - assertTrue { - found.material.id == mixMaterialDto.materialId && - found.quantity == mixMaterialDto.quantity && - found.position == mixMaterialDto.position - } - } - - // updateQuantity() - - @Test - fun `updateQuantity() updates the given mix material with the given quantity`() { - val quantity = 5000f - assertNotEquals(quantity, entity.quantity, message = "Quantities must not be equals for this test to works") - - doAnswer { it.arguments[0] }.whenever(logic).update(any()) - - val found = logic.updateQuantity(entity, quantity) - - assertEquals(found.quantity, quantity) - } - - // validateMixMaterials() - - @Test - fun `validateMixMaterials() throws InvalidMixMaterialsPositionsException when the position of the first mix material is not 1`() { - assertInvalidMixMaterialsPositionsException( - setOf( - mixMaterial(id = 0L, position = 0), - mixMaterial(id = 1L, position = 1), - mixMaterial(id = 2L, position = 2), - mixMaterial(id = 3L, position = 3) - ), - INVALID_FIRST_MIX_MATERIAL_POSITION_ERROR_CODE - ) - } - - @Test - fun `validateMixMaterials() throws InvalidMixMaterialsPositionsException when positions are duplicated`() { - assertInvalidMixMaterialsPositionsException( - setOf( - mixMaterial(id = 0L, position = 1), - mixMaterial(id = 1L, position = 2), - mixMaterial(id = 2L, position = 2), - mixMaterial(id = 3L, position = 3) - ), - DUPLICATED_MIX_MATERIALS_POSITIONS_ERROR_CODE - ) - } - - @Test - fun `validateMixMaterials() throws InvalidMixMaterialsPositionsException when there is a gap between positions`() { - assertInvalidMixMaterialsPositionsException( - setOf( - mixMaterial(id = 0L, position = 1), - mixMaterial(id = 1L, position = 2), - mixMaterial(id = 2L, position = 4), - mixMaterial(id = 3L, position = 5) - ), - GAP_BETWEEN_MIX_MATERIALS_POSITIONS_ERROR_CODE - ) - } - - @Test - fun `validateMixMaterials() throws InvalidFirstMixMaterial when the first mix material's quantity is expressed in percents`() { - val normalMaterial = material(materialType = materialType(usePercentages = false)) - val percentsMaterial = material(materialType = materialType(usePercentages = true)) - val mixMaterials = setOf( - mixMaterial(id = 0L, position = 1, material = percentsMaterial), - mixMaterial(id = 1L, position = 2, material = normalMaterial), - mixMaterial(id = 2L, position = 3, material = normalMaterial), - mixMaterial(id = 3L, position = 4, material = normalMaterial) - ) - - assertThrows { - logic.validateMixMaterials(mixMaterials) - } - } - - private fun assertInvalidMixMaterialsPositionsException(mixMaterials: Set, errorType: String) { - val exception = assertThrows { - logic.validateMixMaterials(mixMaterials) - } - - assertTrue { exception.errors.size == 1 } - assertTrue { exception.errors.first().type == errorType } - } -} diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/utils/PositionUtilsTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/utils/PositionUtilsTest.kt new file mode 100644 index 0000000..89809c7 --- /dev/null +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/utils/PositionUtilsTest.kt @@ -0,0 +1,88 @@ +package dev.fyloz.colorrecipesexplorer.utils + +import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException +import io.mockk.clearAllMocks +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertTrue + +class PositionUtilsTest { + @AfterEach + internal fun afterEach() { + clearAllMocks() + } + + @Test + fun validateSteps_normalBehavior_doesNothing() { + // Arrange + val positions = listOf(1, 2) + + // Act + // Assert + assertDoesNotThrow { PositionUtils.validate(positions) } + } + + @Test + fun validateSteps_emptyStepSet_doesNothing() { + // Arrange + val positions = listOf() + + // Act + // Assert + assertDoesNotThrow { PositionUtils.validate(positions) } + } + + @Test + fun validateSteps_hasInvalidPositions_throwsInvalidStepsPositionsException() { + // Arrange + val positions = listOf(2, 3) + + // Act + // Assert + assertThrows { PositionUtils.validate(positions) } + } + + @Test + fun validateSteps_firstStepPositionInvalid_returnsInvalidStepValidationError() { + // Arrange + val positions = listOf(2, 3) + + // Act + val exception = assertThrows { PositionUtils.validate(positions) } + + // Assert + assertTrue { + exception.errors.any { it.type == PositionUtils.INVALID_FIRST_POSITION_ERROR_CODE } + } + } + + @Test + fun validateSteps_duplicatedPositions_returnsInvalidStepValidationError() { + // Arrange + val positions = listOf(1, 1) + + // Act + val exception = assertThrows { PositionUtils.validate(positions) } + + // Assert + assertTrue { + exception.errors.any { it.type == PositionUtils.DUPLICATED_POSITION_ERROR_CODE } + } + } + + @Test + fun validateSteps_gapsInPositions_returnsInvalidStepValidationError() { + // Arrange + val positions = listOf(1, 3) + + // Act + val exception = assertThrows { PositionUtils.validate(positions) } + + // Assert + assertTrue { + exception.errors.any { it.type == PositionUtils.GAP_BETWEEN_POSITIONS_ERROR_CODE } + } + } +} \ No newline at end of file -- 2.40.1 From 956db504f55a8084224d8cd44dab0a06a1ee3140 Mon Sep 17 00:00:00 2001 From: FyloZ Date: Sun, 6 Mar 2022 19:12:57 -0500 Subject: [PATCH 07/11] #25 Migrate mix types to new logic --- .../colorrecipesexplorer/dtos/MixTypeDto.kt | 9 + .../colorrecipesexplorer/logic/MixLogic.kt | 8 +- .../logic/MixTypeLogic.kt | 112 +++++------- .../colorrecipesexplorer/model/MixType.kt | 104 +++-------- .../repository/MixTypeRepository.kt | 29 ++- .../service/MixTypeService.kt | 36 ++++ .../logic/DefaultMixTypeLogicTest.kt | 131 +++++++++++++ .../logic/MixTypeLogicTest.kt | 172 ------------------ 8 files changed, 264 insertions(+), 337 deletions(-) create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixTypeDto.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeService.kt create mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixTypeLogicTest.kt delete mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogicTest.kt diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixTypeDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixTypeDto.kt new file mode 100644 index 0000000..3d26a6c --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixTypeDto.kt @@ -0,0 +1,9 @@ +package dev.fyloz.colorrecipesexplorer.dtos + +data class MixTypeDto( + override val id: Long = 0L, + + val name: String, + + val material: MaterialDto +) : EntityDto diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt index e2687f6..9de3733 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt @@ -49,13 +49,13 @@ class DefaultMixLogic( override fun save(entity: MixSaveDto): Mix { val recipe = recipeLogic.getById(entity.recipeId) val materialType = materialTypeLogic.getById(entity.materialTypeId) - val mixType = mixTypeLogic.getOrCreateForNameAndMaterialType(entity.name, materialType(materialType)) + val mixType = mixTypeLogic.getOrCreateForNameAndMaterialType(entity.name, materialType) val mixMaterials = if (entity.mixMaterials != null) mixMaterialLogic.saveAll(entity.mixMaterials).toSet() else setOf() mixMaterialLogic.validateMixMaterials(mixMaterials) - var mix = mix(recipe = recipe, mixType = mixType, mixMaterials = mixMaterials.map(::mixMaterial).toMutableSet()) + var mix = mix(recipe = recipe, mixType = mixType(mixType), mixMaterials = mixMaterials.map(::mixMaterial).toMutableSet()) mix = save(mix) recipeLogic.addMix(recipe, mix) @@ -74,9 +74,9 @@ class DefaultMixLogic( mix.mixType.material.materialType!! mix.mixType = if (mixTypeIsShared(mix.mixType)) { - mixTypeLogic.saveForNameAndMaterialType(name, materialType) + mixType(mixTypeLogic.saveForNameAndMaterialType(name, materialTypeDto(materialType))) } else { - mixTypeLogic.updateForNameAndMaterialType(mix.mixType, name, materialType) + mixType(mixTypeLogic.updateForNameAndMaterialType(mixTypeDto(mix.mixType), name, materialTypeDto(materialType))) } } if (entity.mixMaterials != null) { diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt index 6b7d2ac..79eb5f9 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt @@ -1,84 +1,58 @@ package dev.fyloz.colorrecipesexplorer.logic -import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase -import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException -import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.repository.MixTypeRepository -import org.springframework.context.annotation.Lazy -import org.springframework.stereotype.Service +import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent +import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto +import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto +import dev.fyloz.colorrecipesexplorer.dtos.MixTypeDto +import dev.fyloz.colorrecipesexplorer.model.MixType +import dev.fyloz.colorrecipesexplorer.service.MixTypeService +import org.springframework.transaction.annotation.Transactional -interface MixTypeLogic : NamedModelService { - /** Checks if a [MixType] with the given [name] and [materialType] exists. */ - fun existsByNameAndMaterialType(name: String, materialType: MaterialType): Boolean - - /** Gets the mix type with the given [material]. */ - fun getByMaterial(material: Material): MixType - - /** Gets the [MixType] with the given [name] and [materialType]. */ - fun getByNameAndMaterialType(name: String, materialType: MaterialType): MixType - - /** Returns a [MixType] for the given [name] and [materialType]. If a mix type with these does not already exists, it will be created. */ - fun getOrCreateForNameAndMaterialType(name: String, materialType: MaterialType): MixType +interface MixTypeLogic : Logic { + /** Returns a [MixType] for the given [name] and [materialType]. If this mix type does not already exist, it will be created. */ + fun getOrCreateForNameAndMaterialType(name: String, materialType: MaterialTypeDto): MixTypeDto /** Returns a new and persisted [MixType] with the given [name] and [materialType]. */ - fun saveForNameAndMaterialType(name: String, materialType: MaterialType): MixType + fun saveForNameAndMaterialType(name: String, materialType: MaterialTypeDto): MixTypeDto /** Returns the given [mixType] updated with the given [name] and [materialType]. */ - fun updateForNameAndMaterialType(mixType: MixType, name: String, materialType: MaterialType): MixType + fun updateForNameAndMaterialType(mixType: MixTypeDto, name: String, materialType: MaterialTypeDto): MixTypeDto } -@Service -@RequireDatabase -class DefaultMixTypeLogic( - mixTypeRepository: MixTypeRepository, - @Lazy val materialLogic: MaterialLogic -) : - AbstractNamedModelService(mixTypeRepository), MixTypeLogic { - override fun idNotFoundException(id: Long) = mixTypeIdNotFoundException(id) - override fun idAlreadyExistsException(id: Long) = mixTypeIdAlreadyExistsException(id) - override fun nameNotFoundException(name: String) = mixTypeNameNotFoundException(name) - override fun nameAlreadyExistsException(name: String) = mixTypeNameAlreadyExistsException(name) +@LogicComponent +class DefaultMixTypeLogic(service: MixTypeService, private val materialLogic: MaterialLogic) : + BaseLogic(service, MixType::class.simpleName!!), MixTypeLogic { + override fun getOrCreateForNameAndMaterialType(name: String, materialType: MaterialTypeDto) = + service.getByNameAndMaterialType(name, materialType.id) ?: saveForNameAndMaterialType(name, materialType) - override fun existsByNameAndMaterialType(name: String, materialType: MaterialType): Boolean = - repository.existsByNameAndMaterialType(name, materialType) - - override fun getByMaterial(material: Material): MixType = - repository.findByMaterial(material) ?: throw nameNotFoundException(material.name) - - override fun getByNameAndMaterialType(name: String, materialType: MaterialType): MixType = - repository.findByNameAndMaterialType(name, materialType) - ?: throw MixTypeNameAndMaterialTypeNotFoundException(name, materialType) - - override fun getOrCreateForNameAndMaterialType(name: String, materialType: MaterialType): MixType = - if (existsByNameAndMaterialType(name, materialType)) - getByNameAndMaterialType(name, materialType) - else - saveForNameAndMaterialType(name, materialType) - - override fun save(entity: MixType): MixType { - if (materialLogic.existsByName(entity.name)) - throw AlreadyExistsException("material", "material already exists", "material already exists details (TODO)", entity.name) // TODO - return super.save(entity) - } - - override fun saveForNameAndMaterialType(name: String, materialType: MaterialType): MixType = - save( - mixType( + @Transactional + override fun saveForNameAndMaterialType(name: String, materialType: MaterialTypeDto): MixTypeDto { + val material = materialLogic.save( + MaterialDto( name = name, - material = material( - name = name, - inventoryQuantity = Float.MIN_VALUE, - isMixType = true, - materialType = materialType - ) + inventoryQuantity = Float.MIN_VALUE, + isMixType = true, + materialType = materialType ) ) - override fun updateForNameAndMaterialType(mixType: MixType, name: String, materialType: MaterialType): MixType = - update(mixType.copy(material = mixType.material.copy(name = name, materialType = materialType))) - - override fun delete(entity: MixType) { - if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteMixTypeException(entity) - super.delete(entity) + return save(MixTypeDto(name = name, material = material)) } -} + + override fun updateForNameAndMaterialType( + mixType: MixTypeDto, + name: String, + materialType: MaterialTypeDto + ): MixTypeDto { + val material = materialLogic.update(mixType.material.copy(name = name, materialType = materialType)) + return update(mixType.copy(name = name, material = material)) + } + + override fun deleteById(id: Long) { + if (service.isUsedByMixes(id)) { + throw cannotDeleteException("Cannot delete the mix type with the id '$id' because one or more mixes depends on it") + } + + super.deleteById(id) + } +} \ 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 9953f80..8b25b67 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixType.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixType.kt @@ -1,5 +1,6 @@ package dev.fyloz.colorrecipesexplorer.model +import dev.fyloz.colorrecipesexplorer.dtos.MixTypeDto import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException @@ -10,91 +11,42 @@ import javax.persistence.* @Entity @Table(name = "mix_type") data class MixType( - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - override val id: Long?, + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + override val id: Long?, - @Column(unique = true) - override var name: String, + @Column(unique = true) + val name: String, - @OneToOne(cascade = [CascadeType.ALL]) - @JoinColumn(name = "material_id") - var material: Material -) : NamedModelEntity + @OneToOne(cascade = [CascadeType.ALL]) + @JoinColumn(name = "material_id") + var material: Material +) : ModelEntity // ==== DSL ==== fun mixType( - id: Long? = null, - name: String = "name", - material: Material = material(), - op: MixType.() -> Unit = {} + id: Long? = null, + name: String = "name", + material: Material = material(), + op: MixType.() -> Unit = {} ) = MixType(id, name, material).apply(op) fun mixType( - name: String = "name", - materialType: MaterialType = materialType(), - op: MixType.() -> Unit = {} + name: String = "name", + materialType: MaterialType = materialType(), + op: MixType.() -> Unit = {} ) = mixType( - id = null, - name, - material = material(name = name, inventoryQuantity = 0f, isMixType = true, materialType = materialType) + id = null, + name, + material = material(name = name, inventoryQuantity = 0f, isMixType = true, materialType = materialType) ).apply(op) -// ==== Exceptions ==== -private const val MIX_TYPE_NOT_FOUND_EXCEPTION_TITLE = "Mix type not found" -private const val MIX_TYPE_ALREADY_EXISTS_EXCEPTION_TITLE = "Mix type already exists" -private const val MIX_TYPE_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete mix type" -private const val MIX_TYPE_EXCEPTION_ERROR_CODE = "mixtype" +@Deprecated("Temporary DSL for transition") +fun mixTypeDto( + entity: MixType +) = MixTypeDto(entity.id!!, entity.name, materialDto(entity.material)) -class MixTypeNameAndMaterialTypeNotFoundException(name: String, materialType: MaterialType) : - RestException( - "notfound-mixtype-namematerialtype", - MIX_TYPE_NOT_FOUND_EXCEPTION_TITLE, - HttpStatus.NOT_FOUND, - "A mix type with the name $name and material type ${materialType.name} could not be found", - mapOf( - "name" to name, - "materialType" to materialType.name - ) - ) - -fun mixTypeIdNotFoundException(id: Long) = - NotFoundException( - MIX_TYPE_EXCEPTION_ERROR_CODE, - MIX_TYPE_NOT_FOUND_EXCEPTION_TITLE, - "A mix type with the id $id could not be found", - id - ) - -fun mixTypeIdAlreadyExistsException(id: Long) = - AlreadyExistsException( - MIX_TYPE_EXCEPTION_ERROR_CODE, - MIX_TYPE_ALREADY_EXISTS_EXCEPTION_TITLE, - "A mix type with the id $id already exists", - id - ) - -fun mixTypeNameNotFoundException(name: String) = - NotFoundException( - MIX_TYPE_EXCEPTION_ERROR_CODE, - MIX_TYPE_NOT_FOUND_EXCEPTION_TITLE, - "A mix type with the name $name could not be found", - name, - "name" - ) - -fun mixTypeNameAlreadyExistsException(name: String) = - AlreadyExistsException( - MIX_TYPE_EXCEPTION_ERROR_CODE, - MIX_TYPE_ALREADY_EXISTS_EXCEPTION_TITLE, - "A mix type with the name $name already exists", - name, - "name" - ) - -fun cannotDeleteMixTypeException(mixType: MixType) = - CannotDeleteException( - MIX_TYPE_EXCEPTION_ERROR_CODE, - MIX_TYPE_CANNOT_DELETE_EXCEPTION_TITLE, - "Cannot delete the mix type ${mixType.name} because one or more mixes depends on it" - ) +@Deprecated("Temporary DSL for transition") +fun mixType( + dto: MixTypeDto +) = MixType(dto.id, dto.name, material(dto.material)) \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixTypeRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixTypeRepository.kt index 7298eb6..ac9e643 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixTypeRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixTypeRepository.kt @@ -1,30 +1,27 @@ package dev.fyloz.colorrecipesexplorer.repository -import dev.fyloz.colorrecipesexplorer.model.Material import dev.fyloz.colorrecipesexplorer.model.MaterialType import dev.fyloz.colorrecipesexplorer.model.MixType +import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository @Repository -interface MixTypeRepository : NamedJpaRepository { - @Query("select case when(count(m) > 0) then true else false end from MixType m where m.name = :name and m.material.materialType = :materialType") - fun existsByNameAndMaterialType(name: String, materialType: MaterialType): Boolean +interface MixTypeRepository : JpaRepository { + /** Checks if a mix type with the given [name], [materialTypeId] and a different [id] exists. */ + @Query("select case when(count(m) > 0) then true else false end from MixType m where m.name = :name and m.material.materialType.id = :materialTypeId and m.id <> :id") + fun existsByNameAndMaterialType(name: String, materialTypeId: Long, id: Long): Boolean - /** Gets the mix type with the given [material]. */ - fun findByMaterial(material: Material): MixType? - - /** Gets the [MixType] with the given [name] and [materialType]. */ - @Query("select m from MixType m where m.name = :name and m.material.materialType = :materialType") - fun findByNameAndMaterialType(name: String, materialType: MaterialType): MixType? + /** Finds the mix type with the given [name] and [materialTypeId]. */ + @Query("select m from MixType m where m.name = :name and m.material.materialType.id = :materialTypeId") + fun findByNameAndMaterialType(name: String, materialTypeId: Long): MixType? + /** Checks if a mix depends on the mix type with the given [id]. */ @Query( - """ + """ select case when(count(m.id) > 0) then false else true end - from MixType t - left join Mix m on t.id = m.mixType.id - where t.id = :id - """ + from Mix m where m.mixType.id = :id + """ ) - fun canBeDeleted(id: Long): Boolean + fun isUsedByMixes(id: Long): Boolean } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeService.kt new file mode 100644 index 0000000..6d196f6 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeService.kt @@ -0,0 +1,36 @@ +package dev.fyloz.colorrecipesexplorer.service + +import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent +import dev.fyloz.colorrecipesexplorer.dtos.MixTypeDto +import dev.fyloz.colorrecipesexplorer.model.MixType +import dev.fyloz.colorrecipesexplorer.repository.MixTypeRepository + +interface MixTypeService : Service { + /** Checks if a mix type with the given [name], [materialTypeId] and a different [id] exists. */ + fun existsByNameAndMaterialType(name: String, materialTypeId: Long, id: Long? = null): Boolean + + /** Finds the mix type with the given [name] and [materialTypeId]. */ + fun getByNameAndMaterialType(name: String, materialTypeId: Long): MixTypeDto? + + /** Checks if a mix depends on the mix type with the given [id]. */ + fun isUsedByMixes(id: Long): Boolean +} + +@ServiceComponent +class DefaultMixTypeService(repository: MixTypeRepository, val materialService: MaterialService) : + BaseService(repository), MixTypeService { + override fun existsByNameAndMaterialType(name: String, materialTypeId: Long, id: Long?) = + repository.existsByNameAndMaterialType(name, materialTypeId, id ?: 0L) + + override fun getByNameAndMaterialType(name: String, materialTypeId: Long) = + repository.findByNameAndMaterialType(name, materialTypeId)?.let(::toDto) + + override fun isUsedByMixes(id: Long) = + repository.isUsedByMixes(id) + + override fun toDto(entity: MixType) = + MixTypeDto(entity.id!!, entity.name, materialService.toDto(entity.material)) + + override fun toEntity(dto: MixTypeDto) = + MixType(dto.id, dto.name, materialService.toEntity(dto.material)) +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixTypeLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixTypeLogicTest.kt new file mode 100644 index 0000000..4c90086 --- /dev/null +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixTypeLogicTest.kt @@ -0,0 +1,131 @@ +package dev.fyloz.colorrecipesexplorer.logic + +import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto +import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto +import dev.fyloz.colorrecipesexplorer.dtos.MixTypeDto +import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException +import dev.fyloz.colorrecipesexplorer.service.MixTypeService +import io.mockk.* +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertEquals + +class DefaultMixTypeLogicTest { + private val mixTypeServiceMock = mockk() + private val materialLogicMock = mockk() + + private val mixTypeLogic = spyk(DefaultMixTypeLogic(mixTypeServiceMock, materialLogicMock)) + + private val materialType = MaterialTypeDto(1L, "Unit test material type", "UTMT", false) + private val material = MaterialDto(1L, "Unit test material", 1000f, true, materialType) + private val mixType = MixTypeDto(id = 1L, name = "Unit test mix type", material) + + @AfterEach + fun afterEach() { + clearAllMocks() + } + + @Test + fun getOrCreateForNameAndMaterialType_normalBehavior_returnsFromService() { + // Arrange + every { mixTypeServiceMock.getByNameAndMaterialType(any(), any()) } returns mixType + + // Act + val actualMixType = mixTypeLogic.getOrCreateForNameAndMaterialType(mixType.name, materialType) + + // Assert + assertEquals(mixType, actualMixType) + } + + @Test + fun getOrCreateForNameAndMaterialType_notFound_returnsFromSaveForNameAndMaterialType() { + // Arrange + every { mixTypeServiceMock.getByNameAndMaterialType(any(), any()) } returns null + every { mixTypeLogic.saveForNameAndMaterialType(any(), any()) } returns mixType + + // Act + val actualMixType = mixTypeLogic.getOrCreateForNameAndMaterialType(mixType.name, materialType) + + // Assert + assertEquals(mixType, actualMixType) + } + + @Test + fun saveForNameAndMaterialType_normalBehavior_callsSavesInMaterialLogic() { + // Arrange + every { materialLogicMock.save(any()) } returnsArgument 0 + every { mixTypeLogic.save(any()) } returnsArgument 0 + + // Act + mixTypeLogic.saveForNameAndMaterialType(mixType.name, materialType) + + // Assert + verify { + materialLogicMock.save(match { it.name == mixType.name && it.materialType == materialType }) + } + confirmVerified(materialLogicMock) + } + + @Test + fun saveForNameAndMaterialType_normalBehavior_callsSave() { + // Arrange + every { materialLogicMock.save(any()) } returnsArgument 0 + every { mixTypeLogic.save(any()) } returnsArgument 0 + + // Act + mixTypeLogic.saveForNameAndMaterialType(mixType.name, materialType) + + // Assert + verify { + mixTypeLogic.save(match { it.name == mixType.name && it.material.name == mixType.name && it.material.materialType == materialType }) + } + } + + @Test + fun updateForNameAndMaterialType_normalBehavior_callsSavesInMaterialLogic() { + // Arrange + val updatedName = mixType.name + " updated" + val updatedMaterialType = materialType.copy(id = 2L) + + every { materialLogicMock.update(any()) } returnsArgument 0 + every { mixTypeLogic.update(any()) } returnsArgument 0 + + // Act + mixTypeLogic.updateForNameAndMaterialType(mixType, updatedName, updatedMaterialType) + + // Assert + verify { + materialLogicMock.update(match { it.name == updatedName && it.materialType == updatedMaterialType }) + } + confirmVerified(materialLogicMock) + } + + @Test + fun updateForNameAndMaterialType_normalBehavior_callsSave() { + // Arrange + val updatedName = mixType.name + " updated" + val updatedMaterialType = materialType.copy(id = 2L) + + every { materialLogicMock.update(any()) } returnsArgument 0 + every { mixTypeLogic.update(any()) } returnsArgument 0 + + // Act + mixTypeLogic.updateForNameAndMaterialType(mixType, updatedName, updatedMaterialType) + + // Assert + verify { + mixTypeLogic.update(match { it.name == updatedName && it.material.name == updatedName && it.material.materialType == updatedMaterialType }) + } + } + + @Test + fun deleteById_usedByMix_throwsCannotDeleteException() { + // Arrange + every { mixTypeServiceMock.isUsedByMixes(any()) } returns true + + // Act + // Assert + assertThrows { mixTypeLogic.deleteById(mixType.id) } + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogicTest.kt deleted file mode 100644 index 6d38923..0000000 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogicTest.kt +++ /dev/null @@ -1,172 +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.model.* -import dev.fyloz.colorrecipesexplorer.repository.MixTypeRepository -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance -import org.junit.jupiter.api.assertThrows -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class MixTypeLogicTest : AbstractNamedModelServiceTest() { - override val repository: MixTypeRepository = mock() - private val materialService: MaterialLogic = mock() - override val logic: MixTypeLogic = spy(DefaultMixTypeLogic(repository, materialService)) - - private val materialType: MaterialType = materialType() - private val material: Material = material(id = 0L, materialType = materialType) - override val entity: MixType = mixType(id = 0L, name = "mix type", material = material) - override val anotherEntity: MixType = mixType(id = 1L, name = "another mix type") - override val entityWithEntityName: MixType = mixType(id = 2L, name = entity.name) - - @AfterEach - override fun afterEach() { - reset(materialService) - super.afterEach() - } - - // existsByNameAndMaterialType - - @Test - fun `existsByNameAndMaterialType() returns repository's answer`() { - setOf(true, false).forEach { - whenever(repository.existsByNameAndMaterialType(entity.name, materialType)).doReturn(it) - - val found = logic.existsByNameAndMaterialType(entity.name, materialType) - - assertTrue { found == it } - } - } - - // getByMaterial() - - @Test - fun `getByMaterial() returns the mix type with the given material`() { - whenever(repository.findByMaterial(material)).doReturn(entity) - - val found = logic.getByMaterial(material) - - assertEquals(entity, found) - } - - @Test - fun `getByMaterial() throws NotFoundException when no mix type with the given material exists`() { - whenever(repository.findByMaterial(material)).doReturn(null) - - assertThrows { logic.getByMaterial(material) } - .assertErrorCode("name") - } - - // getByNameAndMaterialType() - - @Test - fun `getByNameAndMaterialType() returns the mix type with the given name and material type`() { - whenever(repository.findByNameAndMaterialType(entity.name, materialType)).doReturn(entity) - - val found = logic.getByNameAndMaterialType(entity.name, materialType) - - assertEquals(entity, found) - } - - // getOrCreateForNameAndMaterialType() - @Test - fun `getOrCreateForNameAndMaterialType() calls getForNameAndMaterialType() when a mix type with the given name and material type exists`() { - doReturn(true).whenever(logic).existsByNameAndMaterialType(entity.name, materialType) - doReturn(entity).whenever(logic).getByNameAndMaterialType(entity.name, materialType) - - val found = logic.getOrCreateForNameAndMaterialType(entity.name, materialType) - - verify(logic).getByNameAndMaterialType(entity.name, materialType) - - assertEquals(entity, found) - } - - @Test - fun `getOrCreateForNameAndMaterialType() calls saveForNameAndMaterialType() when no mix type with the given name and material type exists`() { - doReturn(false).whenever(logic).existsByNameAndMaterialType(entity.name, materialType) - doReturn(entity).whenever(logic).saveForNameAndMaterialType(entity.name, materialType) - - val found = logic.getOrCreateForNameAndMaterialType(entity.name, materialType) - - verify(logic).saveForNameAndMaterialType(entity.name, materialType) - - assertEquals(entity, found) - } - - // save() - - @Test - fun `save() throws AlreadyExistsException when a material with the name of the new mix type exists`() { - whenever(materialService.existsByName(entity.name)).doReturn(true) - - assertThrows { logic.save(entity) } - .assertErrorCode("name") - } - - // saveForNameAndMaterialType() - - @Test - fun `saveForNameAndMaterialType() creates a save a valid mix type with the given name and material type`() { - val name = entity.name - val materialType = materialType() - - doAnswer { it.arguments[0] }.whenever(logic).save(any()) - - val found = logic.saveForNameAndMaterialType(name, materialType) - - verify(logic).save(any()) - - assertEquals(name, found.name) - assertEquals(name, found.material.name) - assertEquals(materialType, found.material.materialType) - assertTrue(found.material.isMixType) - } - - // updateForNameAndMaterialType() - - @Test - fun `updateForNameAndMaterialType() updates the given mix type with the given name and material type`() { - val mixType = mixType(id = 1L, material = material(isMixType = true)) - val name = entity.name - val materialType = materialType() - - doAnswer { it.arguments[0] }.whenever(logic).update(any()) - - val found = logic.updateForNameAndMaterialType(mixType, name, materialType) - - verify(logic).update(any()) - - assertEquals(mixType.id, found.id) - assertEquals(name, found.name) - assertEquals(name, found.material.name) - assertEquals(materialType, found.material.materialType) - assertTrue(found.material.isMixType) - } - - // delete() - - override fun `delete() deletes in the repository`() { - whenCanBeDeleted { - super.`delete() deletes in the repository`() - } - } - - // deleteById() - - override fun `deleteById() deletes the entity with the given id in the repository`() { - whenCanBeDeleted { - super.`deleteById() deletes the entity with the given id in the repository`() - } - } - - private fun whenCanBeDeleted(id: Long = any(), test: () -> Unit) { - whenever(repository.canBeDeleted(id)).doReturn(true) - - test() - } -} -- 2.40.1 From efac09a76bb079761a3c9a365973668e481a640a Mon Sep 17 00:00:00 2001 From: FyloZ Date: Sat, 19 Mar 2022 21:26:01 -0400 Subject: [PATCH 08/11] #25 Migrate mixes to new logic --- .../fyloz/colorrecipesexplorer/Constants.kt | 1 + .../config/initializers/MixInitializer.kt | 14 +- .../config/initializers/RecipeInitializer.kt | 4 +- .../colorrecipesexplorer/dtos/MaterialDto.kt | 7 + .../fyloz/colorrecipesexplorer/dtos/MixDto.kt | 46 ++++ .../logic/InventoryLogic.kt | 113 ++++---- .../colorrecipesexplorer/logic/MixLogic.kt | 124 ++++----- .../logic/MixMaterialLogic.kt | 22 +- .../logic/MixTypeLogic.kt | 51 ++-- .../colorrecipesexplorer/logic/RecipeLogic.kt | 6 +- .../colorrecipesexplorer/model/Material.kt | 21 +- .../fyloz/colorrecipesexplorer/model/Mix.kt | 116 +------- .../colorrecipesexplorer/model/Recipe.kt | 4 +- .../repository/MixRepository.kt | 16 +- .../repository/MixTypeRepository.kt | 10 +- .../rest/InventoryController.kt | 4 +- .../rest/MixController.kt | 42 +++ .../rest/RecipeController.kt | 32 --- .../service/MixService.kt | 42 +++ .../service/MixTypeService.kt | 7 +- .../colorrecipesexplorer/utils/Collections.kt | 36 ++- .../resources/application-debug.properties | 1 + .../logic/DefaultInventoryLogicTest.kt | 247 ++++++++++++++++++ .../logic/DefaultMaterialLogicTest.kt | 21 +- .../logic/DefaultMixLogicTest.kt | 180 +++++++++++++ .../logic/DefaultMixMaterialLogicTest.kt | 3 +- .../logic/DefaultMixTypeLogicTest.kt | 100 ++++--- .../logic/InventoryLogicTest.kt | 182 ------------- .../logic/MixLogicTest.kt | 245 ----------------- .../logic/RecipeLogicTest.kt | 5 +- 30 files changed, 861 insertions(+), 841 deletions(-) create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixDto.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MixController.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt create mode 100644 src/main/resources/application-debug.properties create mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultInventoryLogicTest.kt create mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixLogicTest.kt delete mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogicTest.kt delete mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogicTest.kt diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt index 18ff917..4cb51dc 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt @@ -5,6 +5,7 @@ object Constants { const val FILE = "/api/file" const val MATERIAL = "/api/material" const val MATERIAL_TYPE = "/api/materialtype" + const val MIX = "/api/recipe/mix" } object FilePaths { diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MixInitializer.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MixInitializer.kt index 1781762..bc48f94 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MixInitializer.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MixInitializer.kt @@ -1,9 +1,9 @@ package dev.fyloz.colorrecipesexplorer.config.initializers import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase +import dev.fyloz.colorrecipesexplorer.dtos.MixDto +import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto import dev.fyloz.colorrecipesexplorer.logic.MixLogic -import dev.fyloz.colorrecipesexplorer.model.Mix -import dev.fyloz.colorrecipesexplorer.model.MixMaterial import dev.fyloz.colorrecipesexplorer.utils.merge import mu.KotlinLogging import org.springframework.context.annotation.Configuration @@ -31,12 +31,12 @@ class MixInitializer( logger.debug("Mix materials positions are valid!") } - private fun fixMixPositions(mix: Mix) { + private fun fixMixPositions(mix: MixDto) { val maxPosition = mix.mixMaterials.maxOf { it.position } logger.warn("Mix ${mix.id} (${mix.mixType.name}, ${mix.recipe.name}) has invalid positions:") - val invalidMixMaterials: Collection = with(mix.mixMaterials.filter { it.position == 0 }) { + val invalidMixMaterials: Collection = with(mix.mixMaterials.filter { it.position == 0 }) { if (maxPosition == 0 && this.size > 1) { orderMixMaterials(this) } else { @@ -52,16 +52,16 @@ class MixInitializer( } } - private fun increaseMixMaterialsPosition(mixMaterials: Iterable, firstPosition: Int) = + private fun increaseMixMaterialsPosition(mixMaterials: Iterable, firstPosition: Int) = mixMaterials .mapIndexed { index, mixMaterial -> mixMaterial.copy(position = firstPosition + index) } .onEach { logger.info("\tPosition of material ${it.material.id} (${it.material.name}) has been set to ${it.position}") } - private fun orderMixMaterials(mixMaterials: Collection) = + private fun orderMixMaterials(mixMaterials: Collection) = LinkedList(mixMaterials).apply { - while (this.peek().material.materialType?.usePercentages == true) { + while (this.peek().material.materialType.usePercentages) { // The first mix material can't use percents, so move it to the end of the queue val pop = this.pop() this.add(pop) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/RecipeInitializer.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/RecipeInitializer.kt index 1491d76..5890b57 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/RecipeInitializer.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/RecipeInitializer.kt @@ -36,7 +36,7 @@ class RecipeInitializer( .filter { groupInfo -> groupInfo.steps!!.any { it.position == 0 } } .map { fixGroupInformationPositions(recipe, it) } - val updatedGroupInformation = recipe.groupsInformation.merge(fixedGroupInformation) + val updatedGroupInformation = recipe.groupsInformation.merge(fixedGroupInformation) { it.id } with(recipe.copy(groupsInformation = updatedGroupInformation.toMutableSet())) { recipeLogic.update(this) @@ -54,7 +54,7 @@ class RecipeInitializer( val invalidRecipeSteps = steps.filter { it.position == 0 } val fixedRecipeSteps = increaseRecipeStepsPosition(groupInformation, invalidRecipeSteps, maxPosition + 1) - val updatedRecipeSteps = steps.merge(fixedRecipeSteps) + val updatedRecipeSteps = steps.merge(fixedRecipeSteps) { it.id } return groupInformation.copy(steps = updatedRecipeSteps.toMutableSet()) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt index b5367bc..f45aa91 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt @@ -32,3 +32,10 @@ data class MaterialSaveDto( val simdutFile: MultipartFile? ) : EntityDto + +data class MaterialQuantityDto( + val materialId: Long, + + @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO) + val quantity: Float +) \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixDto.kt new file mode 100644 index 0000000..957a59f --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixDto.kt @@ -0,0 +1,46 @@ +package dev.fyloz.colorrecipesexplorer.dtos + +import com.fasterxml.jackson.annotation.JsonIgnore +import dev.fyloz.colorrecipesexplorer.Constants +import dev.fyloz.colorrecipesexplorer.model.Recipe +import javax.validation.constraints.Min +import javax.validation.constraints.NotBlank + +data class MixDto( + override val id: Long = 0L, + + val location: String? = null, + + @JsonIgnore + val recipe: Recipe, // TODO change to dto + + val mixType: MixTypeDto, + + val mixMaterials: Set +) : EntityDto + +data class MixSaveDto( + val id: Long = 0L, + + @field:NotBlank + val name: String, + + val recipeId: Long = 0L, + + val materialTypeId: Long, + + val mixMaterials: Set +) + +data class MixDeductDto( + val id: Long, + + @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO) + val ratio: Float +) + +data class MixLocationDto( + val mixId: Long, + + val location: String? +) \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogic.kt index f43f1e6..d938cde 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogic.kt @@ -2,8 +2,11 @@ package dev.fyloz.colorrecipesexplorer.logic import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto +import dev.fyloz.colorrecipesexplorer.dtos.MaterialQuantityDto +import dev.fyloz.colorrecipesexplorer.dtos.MixDeductDto +import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto import dev.fyloz.colorrecipesexplorer.exception.RestException -import dev.fyloz.colorrecipesexplorer.model.* +import dev.fyloz.colorrecipesexplorer.model.Material import dev.fyloz.colorrecipesexplorer.utils.mapMayThrow import org.springframework.http.HttpStatus import org.springframework.stereotype.Service @@ -34,83 +37,87 @@ class DefaultInventoryLogic( ) : InventoryLogic { @Transactional override fun add(materialQuantities: Collection) = - materialQuantities.map { - materialQuantityDto(materialId = it.material, quantity = add(it)) - } + materialQuantities.map { MaterialQuantityDto(it.materialId, add(it)) } override fun add(materialQuantity: MaterialQuantityDto) = - materialLogic.updateQuantity( - materialLogic.getById(materialQuantity.material), - materialQuantity.quantity - ) + materialLogic.updateQuantity( + materialLogic.getById(materialQuantity.materialId), + materialQuantity.quantity + ) @Transactional override fun deductMix(mixRatio: MixDeductDto): Collection { val mix = mixLogic.getById(mixRatio.id) - val firstMixMaterial = mix.mixMaterials.first() - val adjustedFirstMaterialQuantity = firstMixMaterial.quantity * mixRatio.ratio - fun adjustQuantity(mixMaterial: MixMaterial): Float = - if (!mixMaterial.material.materialType!!.usePercentages) - mixMaterial.quantity * mixRatio.ratio - else - (mixMaterial.quantity * adjustedFirstMaterialQuantity) / 100f - - return deduct(mix.mixMaterials.map { - materialQuantityDto( - materialId = it.material.id!!, - quantity = adjustQuantity(it) - ) - }) + return deduct(getMaterialsWithAdjustedQuantities(mix.mixMaterials, mixRatio)) } @Transactional override fun deduct(materialQuantities: Collection): Collection { val thrown = mutableListOf() + val updatedQuantities = - materialQuantities.mapMayThrow( - { thrown.add(it) } - ) { - materialQuantityDto(materialId = it.material, quantity = deduct(it)) - } + materialQuantities.mapMayThrow( + { thrown.add(it) } + ) { + MaterialQuantityDto(it.materialId, deduct(it)) + } if (thrown.isNotEmpty()) { throw MultiplesNotEnoughInventoryException(thrown) } + return updatedQuantities } override fun deduct(materialQuantity: MaterialQuantityDto): Float = - with(materialLogic.getById(materialQuantity.material)) { - if (this.inventoryQuantity >= materialQuantity.quantity) { - materialLogic.updateQuantity(this, -materialQuantity.quantity) - } else { - throw NotEnoughInventoryException(materialQuantity.quantity, this) - } - } + with(materialLogic.getById(materialQuantity.materialId)) { + if (this.inventoryQuantity >= materialQuantity.quantity) { + materialLogic.updateQuantity(this, -materialQuantity.quantity) + } else { + throw NotEnoughInventoryException(materialQuantity.quantity, this) + } + } + + private fun getMaterialsWithAdjustedQuantities( + mixMaterials: Collection, + mixRatio: MixDeductDto + ): Collection { + val adjustedFirstMaterialQuantity = mixMaterials.first().quantity * mixRatio.ratio + + fun getAdjustedQuantity(material: MaterialDto, quantity: Float) = + if (!material.materialType.usePercentages) + quantity * mixRatio.ratio // Simply multiply the quantity by the ratio + else + (quantity * adjustedFirstMaterialQuantity) / 100f // Percents quantities are a ratio of the first material + + return mixMaterials.associate { it.material to it.quantity } + .mapValues { getAdjustedQuantity(it.key, it.value) } + .map { MaterialQuantityDto(it.key.id, it.value) } + } } class NotEnoughInventoryException(quantity: Float, material: MaterialDto) : - RestException( - "notenoughinventory", - "Not enough inventory", - HttpStatus.BAD_REQUEST, - "Cannot deduct ${quantity}mL of ${material.name} because there is only ${material.inventoryQuantity}mL in inventory", - mapOf( - "material" to material.name, - "materialId" to material.id.toString(), - "requestQuantity" to quantity, - "availableQuantity" to material.inventoryQuantity + RestException( + "notenoughinventory", + "Not enough inventory", + HttpStatus.BAD_REQUEST, + "Cannot deduct ${quantity}mL of ${material.name} because there is only ${material.inventoryQuantity}mL in inventory", + mapOf( + "material" to material.name, + "materialId" to material.id.toString(), + "requestQuantity" to quantity, + "availableQuantity" to material.inventoryQuantity + ) ) - ) class MultiplesNotEnoughInventoryException(exceptions: List) : - RestException( - "notenoughinventory-multiple", - "Not enough inventory", - HttpStatus.BAD_REQUEST, - "Cannot deduct requested quantities because there is no enough of them in inventory", - mapOf( - "lowQuantities" to exceptions.map { it.extensions } + RestException( + "notenoughinventory-multiple", + "Not enough inventory", + HttpStatus.BAD_REQUEST, + "Cannot deduct requested quantities because there is no enough of them in inventory", + mapOf( + "lowQuantities" to exceptions.map { it.extensions } + ) ) - ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt index 9de3733..f719422 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt @@ -1,102 +1,66 @@ package dev.fyloz.colorrecipesexplorer.logic -import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase -import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.repository.MixRepository -import dev.fyloz.colorrecipesexplorer.utils.setAll +import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent +import dev.fyloz.colorrecipesexplorer.dtos.MixDto +import dev.fyloz.colorrecipesexplorer.dtos.MixLocationDto +import dev.fyloz.colorrecipesexplorer.dtos.MixSaveDto +import dev.fyloz.colorrecipesexplorer.model.Mix +import dev.fyloz.colorrecipesexplorer.service.MixService import org.springframework.context.annotation.Lazy -import org.springframework.stereotype.Service -import javax.transaction.Transactional +import org.springframework.transaction.annotation.Transactional -interface MixLogic : ExternalModelService { - /** Gets all mixes with the given [mixType]. */ - fun getAllByMixType(mixType: MixType): Collection +interface MixLogic : Logic { + /** Saves the given [dto]. */ + fun save(dto: MixSaveDto): MixDto - /** Checks if a [MixType] is shared by several [Mix]es or not. */ - fun mixTypeIsShared(mixType: MixType): Boolean + /** Updates the given [dto]. */ + fun update(dto: MixSaveDto): MixDto - /** Updates the location of each [Mix] in the given [MixLocationDto]s. */ + /** Updates the location of each mix in the given [updatedLocations]. */ fun updateLocations(updatedLocations: Collection) - - /** Updates the location of a given [Mix] to the given [MixLocationDto]. */ - fun updateLocation(updatedLocation: MixLocationDto) } -@Service -@RequireDatabase +@LogicComponent class DefaultMixLogic( - mixRepository: MixRepository, - @Lazy val recipeLogic: RecipeLogic, - @Lazy val materialTypeLogic: MaterialTypeLogic, - val mixMaterialLogic: MixMaterialLogic, - val mixTypeLogic: MixTypeLogic -) : AbstractExternalModelService(mixRepository), - MixLogic { - override fun idNotFoundException(id: Long) = mixIdNotFoundException(id) - override fun idAlreadyExistsException(id: Long) = mixIdAlreadyExistsException(id) - - override fun getAllByMixType(mixType: MixType): Collection = repository.findAllByMixType(mixType) - override fun mixTypeIsShared(mixType: MixType): Boolean = getAllByMixType(mixType).count() > 1 - - override fun Mix.toOutput() = MixOutputDto( - this.id!!, - this.location, - this.mixType, - this.mixMaterials.map { mixMaterialDto(it) }.toSet() - ) - + service: MixService, + @Lazy private val recipeLogic: RecipeLogic, + @Lazy private val materialTypeLogic: MaterialTypeLogic, + private val mixTypeLogic: MixTypeLogic, + private val mixMaterialLogic: MixMaterialLogic +) : BaseLogic(service, Mix::class.simpleName!!), MixLogic { @Transactional - override fun save(entity: MixSaveDto): Mix { - val recipe = recipeLogic.getById(entity.recipeId) - val materialType = materialTypeLogic.getById(entity.materialTypeId) - val mixType = mixTypeLogic.getOrCreateForNameAndMaterialType(entity.name, materialType) + override fun save(dto: MixSaveDto): MixDto { + val recipe = recipeLogic.getById(dto.recipeId) + val materialType = materialTypeLogic.getById(dto.materialTypeId) - val mixMaterials = - if (entity.mixMaterials != null) mixMaterialLogic.saveAll(entity.mixMaterials).toSet() else setOf() - mixMaterialLogic.validateMixMaterials(mixMaterials) + val mix = MixDto( + recipe = recipe, + mixType = mixTypeLogic.getOrCreateForNameAndMaterialType(dto.name, materialType), + mixMaterials = mixMaterialLogic.validateAndSaveAll(dto.mixMaterials).toSet() + ) - var mix = mix(recipe = recipe, mixType = mixType(mixType), mixMaterials = mixMaterials.map(::mixMaterial).toMutableSet()) - mix = save(mix) - - recipeLogic.addMix(recipe, mix) - - return mix + return save(mix) } @Transactional - override fun update(entity: MixUpdateDto): Mix { - val mix = getById(entity.id) - if (entity.name != null || entity.materialTypeId != null) { - val name = entity.name ?: mix.mixType.name - val materialType = if (entity.materialTypeId != null) - materialType(materialTypeLogic.getById(entity.materialTypeId)) - else - mix.mixType.material.materialType!! + override fun update(dto: MixSaveDto): MixDto { + val materialType = materialTypeLogic.getById(dto.materialTypeId) + val mix = getById(dto.id) - mix.mixType = if (mixTypeIsShared(mix.mixType)) { - mixType(mixTypeLogic.saveForNameAndMaterialType(name, materialTypeDto(materialType))) - } else { - mixType(mixTypeLogic.updateForNameAndMaterialType(mixTypeDto(mix.mixType), name, materialTypeDto(materialType))) - } - } - if (entity.mixMaterials != null) { - mix.mixMaterials.setAll(mixMaterialLogic.saveAll(entity.mixMaterials!!).map(::mixMaterial).toMutableSet()) - } - return update(mix) + return update( + MixDto( + id = dto.id, + recipe = recipeLogic.getById(dto.recipeId), + mixType = mixTypeLogic.updateOrCreateForNameAndMaterialType(mix.mixType, dto.name, materialType), + mixMaterials = mixMaterialLogic.validateAndSaveAll(dto.mixMaterials).toSet() + ) + ) } - override fun updateLocations(updatedLocations: Collection) { + override fun updateLocations(updatedLocations: Collection) = updatedLocations.forEach(::updateLocation) - } - override fun updateLocation(updatedLocation: MixLocationDto) { - repository.updateLocationById(updatedLocation.mixId, updatedLocation.location) + private fun updateLocation(updatedLocation: MixLocationDto) { + service.updateLocationById(updatedLocation.mixId, updatedLocation.location) } - - @Transactional - override fun delete(entity: Mix) { - if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteMixException(entity) - recipeLogic.removeMix(entity) - super.delete(entity) - } -} +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt index 2cedbe8..b6ddf38 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt @@ -2,12 +2,14 @@ package dev.fyloz.colorrecipesexplorer.logic import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto +import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialSaveDto import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException import dev.fyloz.colorrecipesexplorer.exception.RestException import dev.fyloz.colorrecipesexplorer.model.MixMaterial import dev.fyloz.colorrecipesexplorer.service.MixMaterialService import dev.fyloz.colorrecipesexplorer.utils.PositionUtils +import org.springframework.context.annotation.Lazy import org.springframework.http.HttpStatus interface MixMaterialLogic : Logic { @@ -17,10 +19,13 @@ interface MixMaterialLogic : Logic { * If any of those criteria are not met, an [InvalidGroupStepsPositionsException] will be thrown. */ fun validateMixMaterials(mixMaterials: Set) + + /** Validates the given mix materials [dtos] and save them. */ + fun validateAndSaveAll(dtos: Collection): Collection } @LogicComponent -class DefaultMixMaterialLogic(service: MixMaterialService) : +class DefaultMixMaterialLogic(service: MixMaterialService, @Lazy private val materialLogic: MaterialLogic) : BaseLogic(service, MixMaterial::class.simpleName!!), MixMaterialLogic { override fun validateMixMaterials(mixMaterials: Set) { if (mixMaterials.isEmpty()) return @@ -37,6 +42,21 @@ class DefaultMixMaterialLogic(service: MixMaterialService) : throw InvalidFirstMixMaterialException(sortedMixMaterials[0]) } } + + override fun validateAndSaveAll(dtos: Collection): Collection { + val dtosWithMaterials = dtos.map { + MixMaterialDto( + id = it.id, + material = materialLogic.getById(it.materialId), + quantity = it.quantity, + position = it.position + ) + }.toSet() + + validateMixMaterials(dtosWithMaterials) + + return dtosWithMaterials.map(::save) + } } // TODO check if required diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt index 79eb5f9..3226392 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt @@ -6,27 +6,50 @@ import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto import dev.fyloz.colorrecipesexplorer.dtos.MixTypeDto import dev.fyloz.colorrecipesexplorer.model.MixType import dev.fyloz.colorrecipesexplorer.service.MixTypeService +import org.springframework.context.annotation.Lazy import org.springframework.transaction.annotation.Transactional interface MixTypeLogic : Logic { - /** Returns a [MixType] for the given [name] and [materialType]. If this mix type does not already exist, it will be created. */ + /** Returns a mix type for the given [name] and [materialType]. If this mix type does not already exist, it will be created. */ fun getOrCreateForNameAndMaterialType(name: String, materialType: MaterialTypeDto): MixTypeDto - /** Returns a new and persisted [MixType] with the given [name] and [materialType]. */ - fun saveForNameAndMaterialType(name: String, materialType: MaterialTypeDto): MixTypeDto - - /** Returns the given [mixType] updated with the given [name] and [materialType]. */ - fun updateForNameAndMaterialType(mixType: MixTypeDto, name: String, materialType: MaterialTypeDto): MixTypeDto + /** Updates the [mixType] with the given [name] and [materialType], or create a new one if it is shared with other mixes. */ + fun updateOrCreateForNameAndMaterialType( + mixType: MixTypeDto, + name: String, + materialType: MaterialTypeDto + ): MixTypeDto } @LogicComponent -class DefaultMixTypeLogic(service: MixTypeService, private val materialLogic: MaterialLogic) : +class DefaultMixTypeLogic( + service: MixTypeService, + @Lazy private val materialLogic: MaterialLogic +) : BaseLogic(service, MixType::class.simpleName!!), MixTypeLogic { + @Transactional override fun getOrCreateForNameAndMaterialType(name: String, materialType: MaterialTypeDto) = service.getByNameAndMaterialType(name, materialType.id) ?: saveForNameAndMaterialType(name, materialType) - @Transactional - override fun saveForNameAndMaterialType(name: String, materialType: MaterialTypeDto): MixTypeDto { + override fun updateOrCreateForNameAndMaterialType( + mixType: MixTypeDto, + name: String, + materialType: MaterialTypeDto + ) = if (service.isShared(mixType.id)) { + saveForNameAndMaterialType(name, materialType) + } else { + updateForNameAndMaterialType(mixType, name, materialType) + } + + override fun deleteById(id: Long) { + if (service.isUsedByMixes(id)) { + throw cannotDeleteException("Cannot delete the mix type with the id '$id' because one or more mixes depends on it") + } + + super.deleteById(id) + } + + private fun saveForNameAndMaterialType(name: String, materialType: MaterialTypeDto): MixTypeDto { val material = materialLogic.save( MaterialDto( name = name, @@ -39,7 +62,7 @@ class DefaultMixTypeLogic(service: MixTypeService, private val materialLogic: Ma return save(MixTypeDto(name = name, material = material)) } - override fun updateForNameAndMaterialType( + private fun updateForNameAndMaterialType( mixType: MixTypeDto, name: String, materialType: MaterialTypeDto @@ -47,12 +70,4 @@ class DefaultMixTypeLogic(service: MixTypeService, private val materialLogic: Ma val material = materialLogic.update(mixType.material.copy(name = name, materialType = materialType)) return update(mixType.copy(name = name, material = material)) } - - override fun deleteById(id: Long) { - if (service.isUsedByMixes(id)) { - throw cannotDeleteException("Cannot delete the mix type with the id '$id' because one or more mixes depends on it") - } - - super.deleteById(id) - } } \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt index 775b64f..19315e3 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt @@ -72,11 +72,7 @@ class DefaultRecipeLogic( isApprobationExpired(this), this.remark, this.company, - this.mixes.map { - with(mixLogic) { - it.toOutput() - } - }.toSet(), + this.mixes.map { mix(it) }.toSet(), this.groupsInformation, recipeImageLogic.getAllImages(this) .map { this.imageUrl(configService.getContent(ConfigurationType.INSTANCE_URL), it) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt index 23fd00c..024e651 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt @@ -2,13 +2,7 @@ package dev.fyloz.colorrecipesexplorer.model import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto -import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException -import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException -import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import javax.persistence.* -import javax.validation.constraints.Min - -const val SIMDUT_FILES_PATH = "pdf/simdut" @Entity @Table(name = "material") @@ -36,13 +30,6 @@ data class Material( } } -data class MaterialQuantityDto( - val material: Long, - - @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO) - val quantity: Float -) - // === DSL === fun material( @@ -71,10 +58,4 @@ fun material( @Deprecated("Temporary DSL for transition") fun materialDto( entity: Material -) = MaterialDto(entity.id!!, entity.name, entity.inventoryQuantity, entity.isMixType, materialTypeDto(entity.materialType!!)) - -fun materialQuantityDto( - materialId: Long, - quantity: Float, - op: MaterialQuantityDto.() -> Unit = {} -) = MaterialQuantityDto(materialId, quantity).apply(op) \ No newline at end of file +) = MaterialDto(entity.id!!, entity.name, entity.inventoryQuantity, entity.isMixType, materialTypeDto(entity.materialType!!)) \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt index 2670501..6931638 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt @@ -1,15 +1,11 @@ package dev.fyloz.colorrecipesexplorer.model -import com.fasterxml.jackson.annotation.JsonIgnore -import dev.fyloz.colorrecipesexplorer.Constants +import dev.fyloz.colorrecipesexplorer.dtos.MixDto import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import javax.persistence.* -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank - @Entity @Table(name = "mix") @@ -20,7 +16,6 @@ data class Mix( var location: String?, - @JsonIgnore @ManyToOne @JoinColumn(name = "recipe_id") val recipe: Recipe, @@ -31,53 +26,9 @@ data class Mix( @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true) @JoinColumn(name = "mix_id") - var mixMaterials: MutableSet, + var mixMaterials: Set, ) : ModelEntity -open class MixSaveDto( - @field:NotBlank - val name: String, - - val recipeId: Long, - - val materialTypeId: Long, - - val mixMaterials: Set? -) : EntityDto - -open class MixUpdateDto( - val id: Long, - - @field:NotBlank - val name: String?, - - val materialTypeId: Long?, - - var mixMaterials: Set? -) : EntityDto - -data class MixOutputDto( - val id: Long, - val location: String?, - val mixType: MixType, - val mixMaterials: Set -) - -data class MixDeductDto( - val id: Long, - - @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO) - val ratio: Float -) - -data class MixLocationDto( - val mixId: Long, - - val location: String? -) - -//fun Mix.toOutput() = - // ==== DSL ==== fun mix( id: Long? = null, @@ -88,59 +39,12 @@ fun mix( op: Mix.() -> Unit = {} ) = Mix(id, location, recipe, mixType, mixMaterials).apply(op) -fun mixSaveDto( - name: String = "name", - recipeId: Long = 0L, - materialTypeId: Long = 0L, - mixMaterials: Set? = setOf(), - op: MixSaveDto.() -> Unit = {} -) = MixSaveDto(name, recipeId, materialTypeId, mixMaterials).apply(op) +@Deprecated("Temporary DSL for transition") +fun mix( + dto: MixDto +) = Mix(dto.id, dto.location, dto.recipe, mixType(dto.mixType), dto.mixMaterials.map(::mixMaterial).toSet()) -fun mixUpdateDto( - id: Long = 0L, - name: String? = "name", - materialTypeId: Long? = 0L, - mixMaterials: Set? = setOf(), - op: MixUpdateDto.() -> Unit = {} -) = MixUpdateDto(id, name, materialTypeId, mixMaterials).apply(op) - -fun mixRatio( - id: Long = 0L, - ratio: Float = 1f, - op: MixDeductDto.() -> Unit = {} -) = MixDeductDto(id, ratio).apply(op) - -fun mixLocationDto( - mixId: Long = 0L, - location: String? = "location", - op: MixLocationDto.() -> Unit = {} -) = MixLocationDto(mixId, location).apply(op) - -// ==== Exceptions ==== -private const val MIX_NOT_FOUND_EXCEPTION_TITLE = "Mix not found" -private const val MIX_ALREADY_EXISTS_EXCEPTION_TITLE = "Mix already exists" -private const val MIX_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete mix" -private const val MIX_EXCEPTION_ERROR_CODE = "mix" - -fun mixIdNotFoundException(id: Long) = - NotFoundException( - MIX_EXCEPTION_ERROR_CODE, - MIX_NOT_FOUND_EXCEPTION_TITLE, - "A mix with the id $id could not be found", - id - ) - -fun mixIdAlreadyExistsException(id: Long) = - AlreadyExistsException( - MIX_EXCEPTION_ERROR_CODE, - MIX_ALREADY_EXISTS_EXCEPTION_TITLE, - "A mix with the id $id already exists", - id - ) - -fun cannotDeleteMixException(mix: Mix) = - CannotDeleteException( - MIX_EXCEPTION_ERROR_CODE, - MIX_CANNOT_DELETE_EXCEPTION_TITLE, - "Cannot delete the mix ${mix.mixType.name} because one or more mixes depends on it" - ) +@Deprecated("Temporary DSL for transition") +fun mix( + entity: Mix +) = MixDto(entity.id!!, entity.location, entity.recipe, mixTypeDto(entity.mixType), entity.mixMaterials.map(::mixMaterialDto).toSet()) \ 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 af8142c..3fc6873 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt @@ -2,6 +2,8 @@ package dev.fyloz.colorrecipesexplorer.model import com.fasterxml.jackson.annotation.JsonIgnore import dev.fyloz.colorrecipesexplorer.Constants +import dev.fyloz.colorrecipesexplorer.dtos.MixDto +import dev.fyloz.colorrecipesexplorer.dtos.MixLocationDto import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.model.account.Group @@ -150,7 +152,7 @@ data class RecipeOutputDto( val approbationExpired: Boolean?, val remark: String?, val company: Company, - val mixes: Set, + val mixes: Set, val groupsInformation: Set, var imagesUrls: Set ) : ModelEntity diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixRepository.kt index 51c39a9..a363199 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixRepository.kt @@ -7,21 +7,11 @@ import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query interface MixRepository : JpaRepository { - /** Finds all mixes with the given [mixType]. */ - fun findAllByMixType(mixType: MixType): Collection + /** Finds all mixes with the mix type with the given [mixTypeId]. */ + fun findAllByMixTypeId(mixTypeId: Long): Collection /** Updates the [location] of the [Mix] with the given [id]. */ @Modifying - @Query("UPDATE Mix m SET m.location = :location WHERE m.id = :id") + @Query("update Mix m set m.location = :location where m.id = :id") fun updateLocationById(id: Long, location: String?) - - @Query( - """ - select case when(count(mm.id) > 0) then false else true end - from Mix m - left join MixMaterial mm on m.mixType.material.id = mm.material.id - where m.id = :id - """ - ) - fun canBeDeleted(id: Long): Boolean } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixTypeRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixTypeRepository.kt index ac9e643..9e1c7f8 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixTypeRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixTypeRepository.kt @@ -1,6 +1,5 @@ package dev.fyloz.colorrecipesexplorer.repository -import dev.fyloz.colorrecipesexplorer.model.MaterialType import dev.fyloz.colorrecipesexplorer.model.MixType import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query @@ -24,4 +23,13 @@ interface MixTypeRepository : JpaRepository { """ ) fun isUsedByMixes(id: Long): Boolean + + /** Checks if the mix type with the given [id] is used by more than one mix. */ + @Query( + """ + select case when(count(m.id) > 1) then false else true end + from Mix m where m.mixType.id = :id + """ + ) + fun isShared(id: Long): Boolean } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/InventoryController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/InventoryController.kt index 47d0b96..5f5cbc5 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/InventoryController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/InventoryController.kt @@ -1,8 +1,8 @@ package dev.fyloz.colorrecipesexplorer.rest +import dev.fyloz.colorrecipesexplorer.dtos.MaterialQuantityDto +import dev.fyloz.colorrecipesexplorer.dtos.MixDeductDto import dev.fyloz.colorrecipesexplorer.logic.InventoryLogic -import dev.fyloz.colorrecipesexplorer.model.MaterialQuantityDto -import dev.fyloz.colorrecipesexplorer.model.MixDeductDto import org.springframework.context.annotation.Profile import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.PutMapping diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MixController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MixController.kt new file mode 100644 index 0000000..7392f80 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MixController.kt @@ -0,0 +1,42 @@ +package dev.fyloz.colorrecipesexplorer.rest + +import dev.fyloz.colorrecipesexplorer.Constants +import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditRecipes +import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewRecipes +import dev.fyloz.colorrecipesexplorer.dtos.MixDto +import dev.fyloz.colorrecipesexplorer.dtos.MixSaveDto +import dev.fyloz.colorrecipesexplorer.logic.MixLogic +import org.springframework.context.annotation.Profile +import org.springframework.web.bind.annotation.* +import javax.validation.Valid + +@RestController +@RequestMapping(Constants.ControllerPaths.MIX) +@Profile("!emergency") +@PreAuthorizeViewRecipes +class MixController(private val mixLogic: MixLogic) { + @GetMapping("{id}") + fun getById(@PathVariable id: Long) = + ok(mixLogic.getById(id)) + + @PostMapping + @PreAuthorizeEditRecipes + fun save(@Valid @RequestBody mix: MixSaveDto) = + created(Constants.ControllerPaths.MIX) { + mixLogic.save(mix) + } + + @PutMapping + @PreAuthorizeEditRecipes + fun update(@Valid @RequestBody mix: MixSaveDto) = + noContent { + mixLogic.update(mix) + } + + @DeleteMapping("{id}") + @PreAuthorizeEditRecipes + fun deleteById(@PathVariable id: Long) = + noContent { + mixLogic.deleteById(id) + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt index 90abb8a..875a879 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt @@ -16,7 +16,6 @@ import javax.validation.Valid private const val RECIPE_CONTROLLER_PATH = "api/recipe" -private const val MIX_CONTROLLER_PATH = "api/recipe/mix" @RestController @RequestMapping(RECIPE_CONTROLLER_PATH) @@ -83,34 +82,3 @@ class RecipeController( recipeImageLogic.delete(recipeLogic.getById(recipeId), name) } } - -@RestController -@RequestMapping(MIX_CONTROLLER_PATH) -@Profile("!emergency") -@PreAuthorizeViewRecipes -class MixController(private val mixLogic: MixLogic) { - @GetMapping("{id}") - fun getById(@PathVariable id: Long) = - ok(mixLogic.getByIdForOutput(id)) - - @PostMapping - @PreAuthorizeEditRecipes - fun save(@Valid @RequestBody mix: MixSaveDto) = - created(MIX_CONTROLLER_PATH) { - mixLogic.save(mix) - } - - @PutMapping - @PreAuthorizeEditRecipes - fun update(@Valid @RequestBody mix: MixUpdateDto) = - noContent { - mixLogic.update(mix) - } - - @DeleteMapping("{id}") - @PreAuthorizeEditRecipes - fun deleteById(@PathVariable id: Long) = - noContent { - mixLogic.deleteById(id) - } -} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt new file mode 100644 index 0000000..5905923 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt @@ -0,0 +1,42 @@ +package dev.fyloz.colorrecipesexplorer.service + +import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent +import dev.fyloz.colorrecipesexplorer.dtos.MixDto +import dev.fyloz.colorrecipesexplorer.model.Mix +import dev.fyloz.colorrecipesexplorer.repository.MixRepository + +interface MixService : Service { + /** Gets all mixes with the mix type with the given [mixTypeId]. */ + fun getAllByMixTypeId(mixTypeId: Long): Collection + + /** Updates the [location] of the mix with the given [id]. */ + fun updateLocationById(id: Long, location: String?) +} + +@ServiceComponent +class DefaultMixService( + repository: MixRepository, + private val mixTypeService: MixTypeService, + private val mixMaterialService: MixMaterialService +) : BaseService(repository), MixService { + override fun getAllByMixTypeId(mixTypeId: Long) = repository.findAllByMixTypeId(mixTypeId).map(::toDto) + override fun updateLocationById(id: Long, location: String?) = repository.updateLocationById(id, location) + + override fun toDto(entity: Mix) = + MixDto( + entity.id!!, + entity.location, + entity.recipe, + mixTypeService.toDto(entity.mixType), + entity.mixMaterials.map(mixMaterialService::toDto).toSet() + ) + + override fun toEntity(dto: MixDto) = + Mix( + dto.id, + dto.location, + dto.recipe, + mixTypeService.toEntity(dto.mixType), + dto.mixMaterials.map(mixMaterialService::toEntity).toSet() + ) +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeService.kt index 6d196f6..e942fc7 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeService.kt @@ -14,6 +14,9 @@ interface MixTypeService : Service { /** Checks if a mix depends on the mix type with the given [id]. */ fun isUsedByMixes(id: Long): Boolean + + /** Checks if the mix type with the given [id] is used by more than one mix. */ + fun isShared(id: Long): Boolean } @ServiceComponent @@ -25,8 +28,8 @@ class DefaultMixTypeService(repository: MixTypeRepository, val materialService: override fun getByNameAndMaterialType(name: String, materialTypeId: Long) = repository.findByNameAndMaterialType(name, materialTypeId)?.let(::toDto) - override fun isUsedByMixes(id: Long) = - repository.isUsedByMixes(id) + override fun isUsedByMixes(id: Long) = repository.isUsedByMixes(id) + override fun isShared(id: Long) = repository.isShared(id) override fun toDto(entity: MixType) = MixTypeDto(entity.id!!, entity.name, materialService.toDto(entity.material)) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt index 2d525d1..8c4d50b 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt @@ -1,6 +1,6 @@ package dev.fyloz.colorrecipesexplorer.utils -import dev.fyloz.colorrecipesexplorer.model.ModelEntity +import dev.fyloz.colorrecipesexplorer.dtos.EntityDto /** Returns a list containing the result of the given [transform] applied to each item of the [Iterable]. If the given [transform] throws, the [Throwable] will be passed to the given [throwableConsumer]. */ inline fun Iterable.mapMayThrow( @@ -19,26 +19,12 @@ inline fun Iterable.mapMayThrow( } } -/** Find duplicated keys in the given [Iterable], using keys obtained from the given [keySelector]. */ -inline fun Iterable.findDuplicated(keySelector: (T) -> K) = - this.groupBy(keySelector) - .filter { it.value.count() > 1 } - .map { it.key } - /** Find duplicated elements in the given [Iterable]. */ fun Iterable.findDuplicated() = this.groupBy { it } .filter { it.value.count() > 1 } .map { it.key } -/** Check if the given [Iterable] has gaps between each element, using keys obtained from the given [keySelector]. */ -inline fun Iterable.hasGaps(keySelector: (T) -> Int) = - this.map(keySelector) - .toIntArray() - .sorted() - .filterIndexed { index, it -> it != index + 1 } - .isNotEmpty() - /** Check if the given [Int] [Iterable] has gaps between each element. */ fun Iterable.hasGaps() = this.sorted() @@ -58,8 +44,20 @@ inline fun MutableCollection.excludeAll(predicate: (T) -> Boolean): Itera return matching } -/** Merge to [ModelEntity] [Iterable]s and prevent id duplication. */ -fun Iterable.merge(other: Iterable) = - this - .filter { model -> other.all { it.id != model.id } } +/** + * Merge two [EntityDto] [Iterable]s and prevent duplication of their ids. + * In case of collision, the items from the [other] iterable will be taken. + */ +@JvmName("mergeDto") +fun Iterable.merge(other: Iterable) = + this.merge(other) { it.id } + +/** + * Merge two [Iterable]s and prevent duplication of the keys determined by the given [keyMapper]. + * In case of collision, the items from the [other] iterable will be taken. + */ +fun Iterable.merge(other: Iterable, keyMapper: (T) -> K) = + this.associateBy { keyMapper(it) } + .filter { pair -> other.all { keyMapper(it) != pair.key } } + .map { it.value } .plus(other) diff --git a/src/main/resources/application-debug.properties b/src/main/resources/application-debug.properties new file mode 100644 index 0000000..72fc330 --- /dev/null +++ b/src/main/resources/application-debug.properties @@ -0,0 +1 @@ +spring.jpa.show-sql=true \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultInventoryLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultInventoryLogicTest.kt new file mode 100644 index 0000000..d5496d7 --- /dev/null +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultInventoryLogicTest.kt @@ -0,0 +1,247 @@ +package dev.fyloz.colorrecipesexplorer.logic + +import dev.fyloz.colorrecipesexplorer.dtos.* +import dev.fyloz.colorrecipesexplorer.model.Recipe +import dev.fyloz.colorrecipesexplorer.model.company +import io.mockk.* +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertEquals + +class DefaultInventoryLogicTest { + private val materialLogicMock = mockk() + private val mixLogicMock = mockk() + + private val inventoryLogic = spyk(DefaultInventoryLogic(materialLogicMock, mixLogicMock)) + + private val defaultInventoryQuantity = 5000f + private val materialType = MaterialTypeDto(1L, "Unit test material type", "UTMT", false) + private val material = MaterialDto(1L, "Unit test material", defaultInventoryQuantity, false, materialType) + + @AfterEach + internal fun afterEach() { + clearAllMocks() + } + + @Test + fun add_collection_normalBehavior_callsAddSingleForEachQuantity() { + // Arrange + val quantities = setOf( + MaterialQuantityDto(1L, 1000f), + MaterialQuantityDto(2L, 2000f) + ) + val expectedQuantities = quantities.map { it.copy(quantity = it.quantity + defaultInventoryQuantity) } + + every { inventoryLogic.add(any()) } answers { expectedQuantities[this.nArgs].quantity } + + // Act + inventoryLogic.add(quantities) + + // Assert + verify { + quantities.forEach { + inventoryLogic.add(it) + } + } + } + + @Test + fun add_collection_normalBehavior_returnsFromAddSingle() { + // Arrange + val quantities = setOf( + MaterialQuantityDto(1L, 1000f), + MaterialQuantityDto(2L, 2000f) + ) + val expectedQuantities = quantities.map { it.copy(quantity = it.quantity + defaultInventoryQuantity) } + + every { inventoryLogic.add(any()) } returnsMany (expectedQuantities.map { it.quantity }) + + // Act + val actualQuantities = inventoryLogic.add(quantities) + + // Assert + assertEquals(expectedQuantities, actualQuantities) + } + + @Test + fun add_single_normalBehavior_callsUpdateQuantityInMaterialLogic() { + // Arrange + every { materialLogicMock.getById(any()) } returns material + every { materialLogicMock.updateQuantity(any(), any()) } answers { + this.secondArg() + defaultInventoryQuantity + } + + val quantity = MaterialQuantityDto(1L, 1000f) + + // Act + inventoryLogic.add(quantity) + + // Assert + verify { + materialLogicMock.updateQuantity(material, quantity.quantity) + } + } + + @Test + fun deductMix_normalBehavior_callsDeductWithMixMaterials() { + // Arrange + val company = CompanyDto(1L, "Unit test company") + val recipe = Recipe( + 1L, + "Unit test recipe", + "Unit test recipe", + "FFFFFF", + 0xf, + null, + null, + "Remark", + company(company), + mutableListOf(), + setOf() + ) + val mixType = MixTypeDto(1L, "Unit test mix type", material) + val mixMaterial = MixMaterialDto(1L, material, 1000f, 1) + val mix = MixDto(1L, null, recipe, mixType, setOf(mixMaterial)) + + val dto = MixDeductDto(mix.id, 2f) + val expectedQuantities = listOf(MaterialQuantityDto(material.id, mixMaterial.quantity * dto.ratio)) + + every { mixLogicMock.getById(any()) } returns mix + every { inventoryLogic.deduct(any>()) } returns listOf() + + // Act + inventoryLogic.deductMix(dto) + + // Assert + verify { + inventoryLogic.deduct(expectedQuantities) + } + } + + @Test + fun deductMix_normalBehavior_returnsFromDeduct() { + // Arrange + val company = CompanyDto(1L, "Unit test company") + val recipe = Recipe( + 1L, + "Unit test recipe", + "Unit test recipe", + "FFFFFF", + 0xf, + null, + null, + "Remark", + company(company), + mutableListOf(), + setOf() + ) + val mixType = MixTypeDto(1L, "Unit test mix type", material) + val mixMaterial = MixMaterialDto(1L, material, 1000f, 1) + val mix = MixDto(1L, null, recipe, mixType, setOf(mixMaterial)) + + val dto = MixDeductDto(mix.id, 2f) + val expectedQuantities = listOf(MaterialQuantityDto(material.id, mixMaterial.quantity * dto.ratio)) + + every { mixLogicMock.getById(any()) } returns mix + every { inventoryLogic.deduct(any>()) } returns expectedQuantities + + // Act + val actualQuantities = inventoryLogic.deductMix(dto) + + // Assert + assertEquals(expectedQuantities, actualQuantities) + } + + @Test + fun deduct_collection_normalBehavior_callsDeductForEachQuantity() { + // Arrange + val quantities = setOf( + MaterialQuantityDto(1L, 1000f), + MaterialQuantityDto(2L, 2000f) + ) + + every { inventoryLogic.deduct(any()) } answers { + defaultInventoryQuantity - firstArg().quantity + } + + // Act + inventoryLogic.deduct(quantities) + + // Assert + verify { + quantities.forEach { + inventoryLogic.deduct(it) + } + } + } + + @Test + fun deduct_collection_normalBehavior_returnsFromDeduct() { + // Arrange + val quantities = setOf( + MaterialQuantityDto(1L, 1000f), + MaterialQuantityDto(2L, 2000f) + ) + + every { inventoryLogic.deduct(any()) } answers { + defaultInventoryQuantity - firstArg().quantity + } + + val expectedQuantities = + quantities.map { MaterialQuantityDto(it.materialId, defaultInventoryQuantity - it.quantity) } + + // Act + val actualQuantities = inventoryLogic.deduct(quantities) + + // Assert + assertEquals(expectedQuantities, actualQuantities) + } + + @Test + fun deduct_collection_notEnoughInventory_throwsMultiplesNotEnoughInventoryException() { + // Arrange + val quantities = setOf( + MaterialQuantityDto(1L, 1000f), + MaterialQuantityDto(2L, 2000f) + ) + + every { inventoryLogic.deduct(any()) } throws NotEnoughInventoryException(1000f, material) + + // Act + // Assert + assertThrows { inventoryLogic.deduct(quantities) } + } + + @Test + fun deduct_single_normalBehavior_callsUpdateQuantityInMaterialLogic() { + // Arrange + val quantity = MaterialQuantityDto(material.id, 1000f) + + every { materialLogicMock.getById(any()) } returns material + every { materialLogicMock.updateQuantity(any(), any()) } returns defaultInventoryQuantity - quantity.quantity + + // Act + inventoryLogic.deduct(quantity) + + // Assert + verify { + materialLogicMock.getById(quantity.materialId) + materialLogicMock.updateQuantity(material, -quantity.quantity) + } + confirmVerified(materialLogicMock) + } + + @Test + fun deduct_single_notEnoughInventory_throwsNotEnoughInventoryException() { + // Arrange + val quantity = MaterialQuantityDto(material.id, material.inventoryQuantity + 1000f) + + every { materialLogicMock.getById(any()) } returns material + every { materialLogicMock.updateQuantity(any(), any()) } returns defaultInventoryQuantity - quantity.quantity + + // Act + // Assert + assertThrows { inventoryLogic.deduct(quantity) } + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialLogicTest.kt index 20670b5..1238efe 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialLogicTest.kt @@ -1,12 +1,13 @@ package dev.fyloz.colorrecipesexplorer.logic -import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto -import dev.fyloz.colorrecipesexplorer.dtos.MaterialSaveDto -import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto +import dev.fyloz.colorrecipesexplorer.dtos.* import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic -import dev.fyloz.colorrecipesexplorer.model.* +import dev.fyloz.colorrecipesexplorer.model.Company +import dev.fyloz.colorrecipesexplorer.model.Material +import dev.fyloz.colorrecipesexplorer.model.Recipe +import dev.fyloz.colorrecipesexplorer.model.mix import dev.fyloz.colorrecipesexplorer.service.MaterialService import io.mockk.* import org.junit.jupiter.api.AfterEach @@ -50,10 +51,10 @@ class DefaultMaterialLogicTest { mutableListOf(), setOf() ) - private val mix = Mix( - 1L, "location", recipe, mixType = MixType(1L, "Unit test mix type", material(materialMixType)), mutableSetOf() + private val mix = MixDto( + 1L, "location", recipe, mixType = MixTypeDto(1L, "Unit test mix type", materialMixType), mutableSetOf() ) - private val mix2 = mix.copy(id = 2L, mixType = mix.mixType.copy(id = 2L, material = material(materialMixType2))) + private val mix2 = mix.copy(id = 2L, mixType = mix.mixType.copy(id = 2L, material = materialMixType2)) private val simdutFileMock = MockMultipartFile( "Unit test SIMDUT", @@ -62,7 +63,7 @@ class DefaultMaterialLogicTest { private val materialSaveDto = MaterialSaveDto(1L, "Unit test material", 1000f, materialType.id, simdutFileMock) init { - recipe.mixes.addAll(listOf(mix, mix2)) + recipe.mixes.addAll(listOf(mix(mix), mix(mix2))) } @AfterEach @@ -139,7 +140,7 @@ class DefaultMaterialLogicTest { every { mixLogicMock.getById(any()) } returns mix // Act - val materials = materialLogic.getAllForMixUpdate(mix.id!!) + val materials = materialLogic.getAllForMixUpdate(mix.id) // Assert assertContains(materials, material) @@ -152,7 +153,7 @@ class DefaultMaterialLogicTest { every { mixLogicMock.getById(any()) } returns mix // Act - val materials = materialLogic.getAllForMixUpdate(mix.id!!) + val materials = materialLogic.getAllForMixUpdate(mix.id) // Assert assertContains(materials, materialMixType2) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixLogicTest.kt new file mode 100644 index 0000000..1579f71 --- /dev/null +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixLogicTest.kt @@ -0,0 +1,180 @@ +package dev.fyloz.colorrecipesexplorer.logic + +import dev.fyloz.colorrecipesexplorer.dtos.* +import dev.fyloz.colorrecipesexplorer.model.Company +import dev.fyloz.colorrecipesexplorer.model.Recipe +import dev.fyloz.colorrecipesexplorer.service.MixService +import io.mockk.* +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test + +class DefaultMixLogicTest { + private val mixServiceMock = mockk() + private val recipeLogicMock = mockk() + private val materialTypeLogicMock = mockk() + private val mixTypeLogicMock = mockk() + private val mixMaterialLogicMock = mockk() + + private val mixLogic = spyk( + DefaultMixLogic( + mixServiceMock, + recipeLogicMock, + materialTypeLogicMock, + mixTypeLogicMock, + mixMaterialLogicMock + ) + ) + + private val company = Company(1L, "Unit test company") + private val recipe = Recipe( + 1L, + "Unit test recipe", + "Unit test recipe", + "FFFFFF", + 0xf, + null, + null, + "A remark", + company, + mutableListOf(), + setOf() + ) + private val materialType = MaterialTypeDto(1L, "Unit test material type", "UTMT", false) + private val mixType = + MixTypeDto(1L, "Unit test mix type", MaterialDto(1L, "Unit test mix type material", 1000f, true, materialType)) + private val mixMaterial = + MixMaterialDto(1L, MaterialDto(2L, "Unit test material", 1000f, false, materialType), 50f, 1) + private val mix = MixDto(recipe = recipe, mixType = mixType, mixMaterials = setOf(mixMaterial)) + + @AfterEach + internal fun afterEach() { + clearAllMocks() + } + + private fun setup_save_normalBehavior() { + every { recipeLogicMock.getById(any()) } returns recipe + every { materialTypeLogicMock.getById(any()) } returns materialType + every { mixTypeLogicMock.getOrCreateForNameAndMaterialType(any(), any()) } returns mixType + every { mixMaterialLogicMock.validateAndSaveAll(any()) } returns listOf(mixMaterial) + every { recipeLogicMock.addMix(any(), any()) } returns recipe + every { mixLogic.save(any()) } returnsArgument 0 + } + + private fun setup_update_normalBehavior() { + every { recipeLogicMock.getById(any()) } returns recipe + every { materialTypeLogicMock.getById(any()) } returns materialType + every { mixTypeLogicMock.updateOrCreateForNameAndMaterialType(any(), any(), any()) } returns mixType + every { mixMaterialLogicMock.validateAndSaveAll(any()) } returns listOf(mixMaterial) + every { mixLogic.getById(any()) } returns mix + every { mixLogic.update(any()) } returnsArgument 0 + } + + @Test + fun save_dto_normalBehavior_callsSave() { + // Arrange + setup_save_normalBehavior() + + val mixMaterialDto = + MixMaterialSaveDto(mixMaterial.id, mixMaterial.material.id, mixMaterial.quantity, mixMaterial.position) + val saveDto = MixSaveDto(0L, mixType.name, recipe.id!!, materialType.id, setOf(mixMaterialDto)) + + // Act + mixLogic.save(saveDto) + + // Assert + verify { + mixLogic.save(mix) + } + } + + @Test + fun save_dto_normalBehavior_callsValidateAndSaveAllInMixMaterialLogic() { + // Arrange + setup_save_normalBehavior() + + val mixMaterialDtos = + setOf( + MixMaterialSaveDto( + mixMaterial.id, + mixMaterial.material.id, + mixMaterial.quantity, + mixMaterial.position + ) + ) + val saveDto = MixSaveDto(0L, mixType.name, recipe.id!!, materialType.id, mixMaterialDtos) + + // Act + mixLogic.save(saveDto) + + // Assert + verify { + mixMaterialLogicMock.validateAndSaveAll(mixMaterialDtos) + } + confirmVerified(mixMaterialLogicMock) + } + + @Test + fun update_dto_normalBehavior_callsUpdate() { + // Arrange + setup_update_normalBehavior() + + val mixMaterialDto = + MixMaterialSaveDto(mixMaterial.id, mixMaterial.material.id, mixMaterial.quantity, mixMaterial.position) + val saveDto = MixSaveDto(mix.id, mixType.name, recipe.id!!, materialType.id, setOf(mixMaterialDto)) + + // Act + mixLogic.update(saveDto) + + // Assert + verify { + mixLogic.update(mix) + } + } + + @Test + fun update_dto_normalBehavior_callsValidateAndSaveAllInMixMaterialLogic() { + // Arrange + setup_update_normalBehavior() + + val mixMaterialDtos = setOf( + MixMaterialSaveDto( + mixMaterial.id, + mixMaterial.material.id, + mixMaterial.quantity, + mixMaterial.position + ) + ) + val saveDto = MixSaveDto(mix.id, mixType.name, recipe.id!!, materialType.id, mixMaterialDtos) + + // Act + mixLogic.update(saveDto) + + // Assert + verify { + mixMaterialLogicMock.validateAndSaveAll(mixMaterialDtos) + } + confirmVerified(mixMaterialLogicMock) + } + + @Test + fun updateLocations_normalBehavior_callsUpdateLocationByIdInServiceForEachUpdatedLocation() { + // Arrange + every { mixServiceMock.updateLocationById(any(), any()) } just runs + + val updatedLocations = listOf( + MixLocationDto(1L, "A location"), + MixLocationDto(2L, "Another location") + ) + + // Act + mixLogic.updateLocations(updatedLocations) + + // Assert + updatedLocations.forEach { + verify { + mixServiceMock.updateLocationById(it.mixId, it.location) + } + } + confirmVerified(mixServiceMock) + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixMaterialLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixMaterialLogicTest.kt index c27434c..f442552 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixMaterialLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixMaterialLogicTest.kt @@ -14,8 +14,9 @@ import org.junit.jupiter.api.assertThrows class DefaultMixMaterialLogicTest { private val mixMaterialServiceMock = mockk() + private val materialLogicMock = mockk() - private val mixMaterialLogic = DefaultMixMaterialLogic(mixMaterialServiceMock) + private val mixMaterialLogic = DefaultMixMaterialLogic(mixMaterialServiceMock, materialLogicMock) @AfterEach internal fun afterEach() { diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixTypeLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixTypeLogicTest.kt index 4c90086..cfe4b83 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixTypeLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixTypeLogicTest.kt @@ -9,6 +9,7 @@ import io.mockk.* import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import kotlin.math.exp import kotlin.test.assertEquals class DefaultMixTypeLogicTest { @@ -39,84 +40,105 @@ class DefaultMixTypeLogicTest { } @Test - fun getOrCreateForNameAndMaterialType_notFound_returnsFromSaveForNameAndMaterialType() { + fun getOrCreateForNameAndMaterialType_notFound_callsSave() { // Arrange every { mixTypeServiceMock.getByNameAndMaterialType(any(), any()) } returns null - every { mixTypeLogic.saveForNameAndMaterialType(any(), any()) } returns mixType + every { materialLogicMock.save(any()) } returns material + every { mixTypeLogic.save(any()) } returnsArgument 0 + + val expectedMixType = mixType.copy(id = 0L) + + // Act + mixTypeLogic.getOrCreateForNameAndMaterialType(mixType.name, materialType) + + // Assert + verify { + mixTypeLogic.save(expectedMixType) + } + } + + @Test + fun getOrCreateForNameAndMaterialType_notFound_returnsFromSave() { + // Arrange + every { mixTypeServiceMock.getByNameAndMaterialType(any(), any()) } returns null + every { materialLogicMock.save(any()) } returns material + every { mixTypeLogic.save(any()) } returnsArgument 0 + + val expectedMixType = mixType.copy(id = 0L) // Act val actualMixType = mixTypeLogic.getOrCreateForNameAndMaterialType(mixType.name, materialType) // Assert - assertEquals(mixType, actualMixType) + assertEquals(expectedMixType, actualMixType) } @Test - fun saveForNameAndMaterialType_normalBehavior_callsSavesInMaterialLogic() { + fun updateOrCreateForNameAndMaterialType_mixTypeShared_callsSave() { // Arrange - every { materialLogicMock.save(any()) } returnsArgument 0 + every { mixTypeServiceMock.isShared(any()) } returns true + every { materialLogicMock.save(any()) } returns material every { mixTypeLogic.save(any()) } returnsArgument 0 + val expectedMixType = mixType.copy(id = 0L, name = "${mixType.name} updated") + // Act - mixTypeLogic.saveForNameAndMaterialType(mixType.name, materialType) + mixTypeLogic.updateOrCreateForNameAndMaterialType(mixType, expectedMixType.name, materialType) // Assert verify { - materialLogicMock.save(match { it.name == mixType.name && it.materialType == materialType }) + mixTypeLogic.save(expectedMixType) } - confirmVerified(materialLogicMock) } @Test - fun saveForNameAndMaterialType_normalBehavior_callsSave() { + fun updateOrCreateForNameAndMaterialType_mixTypeShared_returnsFromSave() { // Arrange - every { materialLogicMock.save(any()) } returnsArgument 0 + every { mixTypeServiceMock.isShared(any()) } returns true + every { materialLogicMock.save(any()) } returns material every { mixTypeLogic.save(any()) } returnsArgument 0 + val expectedMixType = mixType.copy(id = 0L, name = "${mixType.name} updated") + // Act - mixTypeLogic.saveForNameAndMaterialType(mixType.name, materialType) + val actualMixType = mixTypeLogic.updateOrCreateForNameAndMaterialType(mixType, expectedMixType.name, materialType) + + // Assert + assertEquals(expectedMixType, actualMixType) + } + + @Test + fun updateOrCreateForNameAndMaterialType_mixTypeNotShared_callsUpdate() { + // Arrange + every { mixTypeServiceMock.isShared(any()) } returns false + every { materialLogicMock.update(any()) } returns material + every { mixTypeLogic.update(any()) } returnsArgument 0 + + val expectedMixType = mixType.copy(name = "${mixType.name} updated") + + // Act + mixTypeLogic.updateOrCreateForNameAndMaterialType(mixType, expectedMixType.name, materialType) // Assert verify { - mixTypeLogic.save(match { it.name == mixType.name && it.material.name == mixType.name && it.material.materialType == materialType }) + mixTypeLogic.update(expectedMixType) } } @Test - fun updateForNameAndMaterialType_normalBehavior_callsSavesInMaterialLogic() { + fun updateOrCreateForNameAndMaterialType_mixTypeNotShared_returnsFromUpdate() { // Arrange - val updatedName = mixType.name + " updated" - val updatedMaterialType = materialType.copy(id = 2L) - - every { materialLogicMock.update(any()) } returnsArgument 0 + every { mixTypeServiceMock.isShared(any()) } returns false + every { materialLogicMock.update(any()) } returns material every { mixTypeLogic.update(any()) } returnsArgument 0 - // Act - mixTypeLogic.updateForNameAndMaterialType(mixType, updatedName, updatedMaterialType) - - // Assert - verify { - materialLogicMock.update(match { it.name == updatedName && it.materialType == updatedMaterialType }) - } - confirmVerified(materialLogicMock) - } - - @Test - fun updateForNameAndMaterialType_normalBehavior_callsSave() { - // Arrange - val updatedName = mixType.name + " updated" - val updatedMaterialType = materialType.copy(id = 2L) - - every { materialLogicMock.update(any()) } returnsArgument 0 - every { mixTypeLogic.update(any()) } returnsArgument 0 + val expectedMixType = mixType.copy(name = "${mixType.name} updated") // Act - mixTypeLogic.updateForNameAndMaterialType(mixType, updatedName, updatedMaterialType) + val actualMixType = mixTypeLogic.updateOrCreateForNameAndMaterialType(mixType, expectedMixType.name, materialType) // Assert - verify { - mixTypeLogic.update(match { it.name == updatedName && it.material.name == updatedName && it.material.materialType == updatedMaterialType }) - } + assertEquals(expectedMixType, actualMixType) } @Test diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogicTest.kt deleted file mode 100644 index 96a416c..0000000 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogicTest.kt +++ /dev/null @@ -1,182 +0,0 @@ -package dev.fyloz.colorrecipesexplorer.logic - -import com.nhaarman.mockitokotlin2.* -import dev.fyloz.colorrecipesexplorer.model.* -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance -import org.junit.jupiter.api.assertThrows -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class InventoryLogicTest { - private val materialLogic: MaterialLogic = mock() - private val mixLogic: MixLogic = mock() - private val logic = spy(DefaultInventoryLogic(materialLogic, mixLogic)) - - @AfterEach - fun afterEach() { - reset(materialLogic, logic) - } - - // add() - - @Test - fun `add(materialQuantities) calls add() for each MaterialQuantityDto`() { - val materialQuantities = listOf( - materialQuantityDto(materialId = 1, quantity = 1234f), - materialQuantityDto(materialId = 2, quantity = 2345f), - materialQuantityDto(materialId = 3, quantity = 3456f), - materialQuantityDto(materialId = 4, quantity = 4567f) - ) - val storedQuantity = 2000f - - doAnswer { storedQuantity + (it.arguments[0] as MaterialQuantityDto).quantity }.whenever(logic) - .add(any()) - - val found = logic.add(materialQuantities) - - materialQuantities.forEach { - verify(logic).add(it) - assertTrue { found.any { updated -> updated.material == it.material && updated.quantity == storedQuantity + it.quantity } } - } - } - - @Test - fun `add(materialQuantity) updates material's quantity`() { - withGivenQuantities(0f, 1000f) { - val updatedQuantity = it + this.quantity - whenever(materialLogic.updateQuantity(any(), eq(this.quantity))).doReturn(updatedQuantity) - - val found = logic.add(this) - - verify(materialLogic).updateQuantity( - argThat { this.id == this@withGivenQuantities.material }, - eq(this.quantity) - ) - assertEquals(updatedQuantity, found) - } - } - - // deductMix() - - @Test - fun `deductMix() calls deduct() with a collection of MaterialQuantityDto with adjusted quantities`() { - val material = material(id = 0L, materialType = materialType(usePercentages = false)) - val materialPercents = material(id = 1L, materialType = materialType(usePercentages = true)) - val mixRatio = mixRatio(ratio = 1.5f) - val mix = mix( - id = mixRatio.id, - mixMaterials = mutableSetOf( - mixMaterial(id = 0L, material = material, quantity = 1000f, position = 0), - mixMaterial(id = 1L, material = materialPercents, quantity = 50f, position = 1) - ) - ) - val expectedQuantities = mapOf( - 0L to 1500f, - 1L to 750f - ) - - whenever(mixLogic.getById(mix.id!!)).doReturn(mix) - doAnswer { - (it.arguments[0] as Collection).map { materialQuantity -> - materialQuantityDto(materialId = materialQuantity.material, quantity = 0f) - } - }.whenever(logic).deduct(any>()) - - val found = logic.deductMix(mixRatio) - - verify(logic).deduct(argThat> { - this.all { it.quantity == expectedQuantities[it.material] } - }) - - assertEquals(expectedQuantities.size, found.size) - } - - // deduct() - - @Test - fun `deduct(materialQuantities) calls deduct() for each MaterialQuantityDto`() { - val materialQuantities = listOf( - materialQuantityDto(materialId = 1, quantity = 1234f), - materialQuantityDto(materialId = 2, quantity = 2345f), - materialQuantityDto(materialId = 3, quantity = 3456f), - materialQuantityDto(materialId = 4, quantity = 4567f) - ) - val storedQuantity = 5000f - - doAnswer { storedQuantity - (it.arguments[0] as MaterialQuantityDto).quantity }.whenever(logic) - .deduct(any()) - - val found = logic.deduct(materialQuantities) - - materialQuantities.forEach { - verify(logic).deduct(it) - assertTrue { found.any { updated -> updated.material == it.material && updated.quantity == storedQuantity - it.quantity } } - } - } - - @Test - fun `deduct(materialQuantities) throws MultiplesNotEnoughInventoryException when there is not enough inventory of a given material`() { - val materialQuantities = listOf( - materialQuantityDto(materialId = 1, quantity = 1234f), - materialQuantityDto(materialId = 2, quantity = 2345f), - materialQuantityDto(materialId = 3, quantity = 3456f), - materialQuantityDto(materialId = 4, quantity = 4567f) - ) - val inventoryQuantity = 3000f - - materialQuantities.forEach { - withGivenQuantities(inventoryQuantity, it) - } - - assertThrows { logic.deduct(materialQuantities) } - } - - @Test - fun `deduct(materialQuantity) updates material's quantity`() { - withGivenQuantities(5000f, 1000f) { - val updatedQuantity = it - this.quantity - whenever(materialLogic.updateQuantity(any(), eq(-this.quantity))).doReturn(updatedQuantity) - - val found = logic.deduct(this) - - verify(materialLogic).updateQuantity( - argThat { this.id == this@withGivenQuantities.material }, - eq(-this.quantity) - ) - assertEquals(updatedQuantity, found) - } - } - - @Test - fun `deduct(materialQuantity) throws NotEnoughInventoryException when there is not enough inventory of the given material`() { - withGivenQuantities(0f, 1000f) { - assertThrows { logic.deduct(this) } - } - } - - private fun withGivenQuantities( - stored: Float, - quantity: Float, - materialId: Long = 0L, - test: MaterialQuantityDto.(Float) -> Unit = {} - ) { - val materialQuantity = materialQuantityDto(materialId = materialId, quantity = quantity) - - withGivenQuantities(stored, materialQuantity, test) - } - - private fun withGivenQuantities( - stored: Float, - materialQuantity: MaterialQuantityDto, - test: MaterialQuantityDto.(Float) -> Unit = {} - ) { - val material = material(id = materialQuantity.material, inventoryQuantity = stored) - - whenever(materialLogic.getById(material.id!!)).doReturn(materialDto(material)) - - materialQuantity.test(stored) - } -} diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogicTest.kt deleted file mode 100644 index d76215b..0000000 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogicTest.kt +++ /dev/null @@ -1,245 +0,0 @@ -package dev.fyloz.colorrecipesexplorer.logic - -//@TestInstance(TestInstance.Lifecycle.PER_CLASS) -//class MixLogicTest : AbstractExternalModelServiceTest() { -// override val repository: MixRepository = mock() -// private val recipeService: RecipeLogic = mock() -// private val materialTypeService: MaterialTypeLogic = mock() -// private val mixMaterialService: MixMaterialLogic = mock() -// private val mixTypeService: MixTypeLogic = mock() -// override val logic: MixLogic = -// spy(DefaultMixLogic(repository, recipeService, materialTypeService, mixMaterialService, mixTypeService)) -// -// override val entity: Mix = mix(id = 0L, location = "location") -// override val anotherEntity: Mix = mix(id = 1L) -// override val entitySaveDto: MixSaveDto = spy(mixSaveDto(mixMaterials = setOf())) -// override val entityUpdateDto: MixUpdateDto = spy(mixUpdateDto(id = entity.id!!)) -// -// @AfterEach -// override fun afterEach() { -// super.afterEach() -// reset(recipeService, materialTypeService, mixMaterialService, mixTypeService) -// } -// -// // getAllByMixType() -// -//// @Test -//// fun `getAllByMixType() returns all mixes with the given mix type`() { -//// val mixType = mixType(id = 0L) -//// -//// whenever(repository.findAllByMixType(mixType)).doReturn(entityList) -//// -//// val found = logic.getAllByMixType(mixType) -//// -//// assertEquals(entityList, found) -//// } -//// -//// // save() -//// -//// @Test -//// override fun `save(dto) calls and returns save() with the created entity`() { -//// val recipe = recipe(id = entitySaveDto.recipeId) -//// val materialType = materialType(id = entitySaveDto.materialTypeId) -//// val material = material( -//// name = entitySaveDto.name, -//// inventoryQuantity = Float.MIN_VALUE, -//// isMixType = true, -//// materialType = materialType -//// ) -//// val mixType = mixType(name = entitySaveDto.name, material = material) -//// val mix = mix(recipe = recipe, mixType = mixType) -//// val mixWithId = mix(id = 0L, recipe = recipe, mixType = mixType) -//// val mixMaterials = setOf(mixMaterial(material = material(id = 1L), quantity = 1000f)) -//// -//// whenever(recipeService.getById(recipe.id!!)).doReturn(recipe) -//// whenever(materialTypeService.getById(materialType.id!!)).doReturn(materialTypeDto(materialType)) -//// whenever(mixMaterialService.create(entitySaveDto.mixMaterials!!)).doReturn(mixMaterials) -//// whenever( -//// mixTypeService.getOrCreateForNameAndMaterialType( -//// mixType.name, -//// mixType.material.materialType!! -//// ) -//// ).doReturn(mixType) -//// doReturn(true).whenever(logic).existsById(mixWithId.id!!) -//// doReturn(mixWithId).whenever(logic).save(any()) -//// -//// val found = logic.save(entitySaveDto) -//// -//// verify(logic).save(argThat { this.recipe == mix.recipe }) -//// verify(recipeService).addMix(recipe, mixWithId) -//// -//// // Verify if this method is called instead of the MixType's constructor, which does not check if the name is already taken by a material. -//// verify(mixTypeService).getOrCreateForNameAndMaterialType(mixType.name, mixType.material.materialType!!) -//// -//// assertEquals(mixWithId, found) -//// } -//// -//// // update() -//// -//// private fun mixUpdateDtoTest( -//// scope: MixUpdateDtoTestScope = MixUpdateDtoTestScope(), -//// sharedMixType: Boolean = false, -//// op: MixUpdateDtoTestScope.() -> Unit -//// ) { -//// with(scope) { -//// doReturn(true).whenever(logic).existsById(mix.id!!) -//// doReturn(mix).whenever(logic).getById(mix.id!!) -//// doReturn(sharedMixType).whenever(logic).mixTypeIsShared(mix.mixType) -//// doAnswer { it.arguments[0] }.whenever(logic).update(any()) -//// -//// if (mixUpdateDto.materialTypeId != null) { -//// whenever(materialTypeService.getById(materialType.id!!)).doReturn(materialTypeDto(materialType)) -//// } -//// -//// op() -//// } -//// } -//// -//// private fun mixUpdateDtoMixTypeTest(sharedMixType: Boolean = false, op: MixUpdateDtoTestScope.() -> Unit) { -//// with(MixUpdateDtoTestScope(mixUpdateDto = mixUpdateDto(id = 0L, name = "name", materialTypeId = 0L))) { -//// mixUpdateDtoTest(this, sharedMixType, op) -//// } -//// } -//// -//// @Test -//// override fun `update(dto) calls and returns update() with the created entity`() { -//// val mixUpdateDto = spy(mixUpdateDto(id = 0L, name = null, materialTypeId = null)) -//// -//// doReturn(entity).whenever(logic).getById(any()) -//// doReturn(entity).whenever(logic).update(entity) -//// -//// val found = logic.update(mixUpdateDto) -//// -//// verify(logic).update(entity) -//// -//// assertEquals(entity, found) -//// } -//// -//// @Test -//// fun `update(dto) calls MixTypeService saveForNameAndMaterialType() when mix type is shared`() { -//// mixUpdateDtoMixTypeTest(sharedMixType = true) { -//// whenever(mixTypeService.saveForNameAndMaterialType(mixUpdateDto.name!!, materialType)) -//// .doReturn(newMixType) -//// -//// val found = logic.update(mixUpdateDto) -//// -//// verify(mixTypeService).saveForNameAndMaterialType(mixUpdateDto.name!!, materialType) -//// -//// assertEquals(newMixType, found.mixType) -//// } -//// } -//// -//// @Test -//// fun `update(dto) calls MixTypeService updateForNameAndMaterialType() when mix type is not shared`() { -//// mixUpdateDtoMixTypeTest { -//// whenever(mixTypeService.updateForNameAndMaterialType(mixType, mixUpdateDto.name!!, materialType)) -//// .doReturn(newMixType) -//// -//// val found = logic.update(mixUpdateDto) -//// -//// verify(mixTypeService).updateForNameAndMaterialType(mixType, mixUpdateDto.name!!, materialType) -//// -//// assertEquals(newMixType, found.mixType) -//// } -//// } -//// -//// @Test -//// fun `update(dto) update, create and delete mix materials according to the given mix materials map`() { -//// mixUpdateDtoTest { -//// val mixMaterials = setOf( -//// mixMaterialDto(materialId = 0L, quantity = 100f, position = 0), -//// mixMaterialDto(materialId = 1L, quantity = 200f, position = 1), -//// mixMaterialDto(materialId = 2L, quantity = 300f, position = 2), -//// mixMaterialDto(materialId = 3L, quantity = 400f, position = 3), -//// ) -//// mixUpdateDto.mixMaterials = mixMaterials -//// -//// whenever(mixMaterialService.create(any>())).doAnswer { -//// (it.arguments[0] as Set).map { dto -> -//// mixMaterial( -//// material = material(id = dto.materialId), -//// quantity = dto.quantity, -//// position = dto.position -//// ) -//// }.toSet() -//// } -//// -//// val found = logic.update(mixUpdateDto) -//// -//// mixMaterials.forEach { -//// assertTrue { -//// found.mixMaterials.any { mixMaterial -> -//// mixMaterial.material.id == it.materialId && mixMaterial.quantity == it.quantity && mixMaterial.position == it.position -//// } -//// } -//// } -//// } -//// } -//// -//// // updateLocations() -//// -//// @Test -//// fun `updateLocations() calls updateLocation() for each given MixLocationDto`() { -//// val locations = setOf( -//// mixLocationDto(mixId = 0, location = "Loc 0"), -//// mixLocationDto(mixId = 1, location = "Loc 1"), -//// mixLocationDto(mixId = 2, location = "Loc 2"), -//// mixLocationDto(mixId = 3, location = "Loc 3") -//// ) -//// -//// logic.updateLocations(locations) -//// -//// locations.forEach { -//// verify(logic).updateLocation(it) -//// } -//// } -//// -//// // updateLocation() -//// -//// @Test -//// fun `updateLocation() updates the location of a mix in the repository according to the given MixLocationDto`() { -//// val locationDto = mixLocationDto(mixId = 0L, location = "Location") -//// -//// logic.updateLocation(locationDto) -//// -//// verify(repository).updateLocationById(locationDto.mixId, locationDto.location) -//// } -//// -//// // delete() -//// -//// override fun `delete() deletes in the repository`() { -//// whenCanBeDeleted { -//// super.`delete() deletes in the repository`() -//// } -//// } -//// -//// // deleteById() -//// -//// @Test -//// override fun `deleteById() deletes the entity with the given id in the repository`() { -//// whenCanBeDeleted { -//// super.`deleteById() deletes the entity with the given id in the repository`() -//// } -//// } -//// -//// private fun whenCanBeDeleted(id: Long = any(), test: () -> Unit) { -//// whenever(repository.canBeDeleted(id)).doReturn(true) -//// -//// test() -//// } -//} -// -//data class MixUpdateDtoTestScope( -// val mixType: MixType = mixType(name = "mix type"), -// val newMixType: MixType = mixType(name = "another mix type"), -// val materialType: MaterialType = materialType(id = 0L), -// val mix: Mix = mix(id = 0L, mixType = mixType), -// val mixUpdateDto: MixUpdateDto = spy( -// mixUpdateDto( -// id = 0L, -// name = null, -// materialTypeId = null, -// mixMaterials = setOf() -// ) -// ) -//) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogicTest.kt index 5be796c..53c34be 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogicTest.kt @@ -1,6 +1,7 @@ package dev.fyloz.colorrecipesexplorer.logic import com.nhaarman.mockitokotlin2.* +import dev.fyloz.colorrecipesexplorer.dtos.MixLocationDto import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic import dev.fyloz.colorrecipesexplorer.logic.files.CachedFile @@ -228,8 +229,8 @@ class RecipeLogicTest : fun `updatePublicData() update the location of a recipe mixes in the mix logic according to the RecipePublicDataDto`() { val publicData = recipePublicDataDto( mixesLocation = setOf( - mixLocationDto(mixId = 0L, location = "Loc 1"), - mixLocationDto(mixId = 1L, location = "Loc 2") + MixLocationDto(mixId = 0L, location = "Loc 1"), + MixLocationDto(mixId = 1L, location = "Loc 2") ) ) -- 2.40.1 From eb4bb6b52a5bf20d07d9e96c1ead4ad1f00c9392 Mon Sep 17 00:00:00 2001 From: FyloZ Date: Wed, 23 Mar 2022 23:40:48 -0400 Subject: [PATCH 09/11] #25 Migrate recipes to new logic --- .../logic/files/XlsService.java | 8 +- .../xlsx/XlsxExporter.java | 16 +- .../fyloz/colorrecipesexplorer/Constants.kt | 9 +- .../config/initializers/MixInitializer.kt | 4 +- .../config/initializers/RecipeInitializer.kt | 29 +- .../properties/MaterialTypeProperties.kt | 2 - .../fyloz/colorrecipesexplorer/dtos/MixDto.kt | 7 +- .../colorrecipesexplorer/dtos/RecipeDto.kt | 122 ++++++ .../fyloz/colorrecipesexplorer/logic/Logic.kt | 13 + .../logic/MaterialLogic.kt | 4 +- .../colorrecipesexplorer/logic/MixLogic.kt | 8 +- .../logic/MixMaterialLogic.kt | 4 +- .../colorrecipesexplorer/logic/RecipeLogic.kt | 335 +++++++--------- .../logic/RecipeStepLogic.kt | 10 +- .../colorrecipesexplorer/model/Company.kt | 20 +- .../colorrecipesexplorer/model/Material.kt | 33 +- .../model/MaterialType.kt | 38 +- .../fyloz/colorrecipesexplorer/model/Mix.kt | 34 +- .../colorrecipesexplorer/model/MixMaterial.kt | 22 +- .../colorrecipesexplorer/model/MixType.kt | 36 +- .../colorrecipesexplorer/model/Recipe.kt | 255 +----------- .../colorrecipesexplorer/model/RecipeStep.kt | 8 +- .../repository/MixRepository.kt | 2 +- .../repository/MixTypeRepository.kt | 9 +- .../repository/RecipeRepository.kt | 18 +- .../rest/RecipeController.kt | 79 ++-- .../service/MixService.kt | 8 +- .../service/RecipeService.kt | 86 ++++ .../colorrecipesexplorer/utils/Collections.kt | 2 +- .../utils/collections/LazyMapList.kt | 26 ++ .../logic/DefaultInventoryLogicTest.kt | 20 +- .../logic/DefaultMaterialLogicTest.kt | 30 +- .../logic/DefaultMixLogicTest.kt | 24 +- .../logic/DefaultRecipeImageLogicTest.kt | 101 +++++ .../logic/DefaultRecipeLogicTest.kt | 217 ++++++++++ .../logic/DefaultRecipeStepLogicTest.kt | 30 +- .../logic/RecipeLogicTest.kt | 371 ------------------ .../repository/MaterialRepositoryTest.kt | 29 -- .../repository/MixRepositoryTest.kt | 41 -- 39 files changed, 866 insertions(+), 1244 deletions(-) create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/RecipeDto.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/collections/LazyMapList.kt create mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeImageLogicTest.kt create mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeLogicTest.kt delete mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogicTest.kt delete mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/repository/MaterialRepositoryTest.kt delete mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixRepositoryTest.kt diff --git a/src/main/java/dev/fyloz/colorrecipesexplorer/logic/files/XlsService.java b/src/main/java/dev/fyloz/colorrecipesexplorer/logic/files/XlsService.java index f9a09e6..845f459 100644 --- a/src/main/java/dev/fyloz/colorrecipesexplorer/logic/files/XlsService.java +++ b/src/main/java/dev/fyloz/colorrecipesexplorer/logic/files/XlsService.java @@ -1,7 +1,7 @@ package dev.fyloz.colorrecipesexplorer.logic.files; +import dev.fyloz.colorrecipesexplorer.dtos.RecipeDto; import dev.fyloz.colorrecipesexplorer.logic.RecipeLogic; -import dev.fyloz.colorrecipesexplorer.model.Recipe; import dev.fyloz.colorrecipesexplorer.xlsx.XlsxExporter; import mu.KotlinLogging; import org.slf4j.Logger; @@ -32,7 +32,7 @@ public class XlsService { * @param recipe La recette * @return Le fichier XLS de la recette */ - public byte[] generate(Recipe recipe) { + public byte[] generate(RecipeDto recipe) { return new XlsxExporter(logger).generate(recipe); } @@ -55,10 +55,10 @@ public class XlsService { logger.info("Exportation de toutes les couleurs en XLS"); byte[] zipContent; - Collection recipes = recipeService.getAll(); + Collection recipes = recipeService.getAll(); try (ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(); ZipOutputStream zipOutput = new ZipOutputStream(byteOutput)) { - for (Recipe recipe : recipes) { + for (RecipeDto recipe : recipes) { byte[] recipeXLS = generate(recipe); zipOutput.putNextEntry(new ZipEntry(String.format("%s_%s.xlsx", recipe.getCompany().getName(), recipe.getName()))); zipOutput.write(recipeXLS, 0, recipeXLS.length); diff --git a/src/main/java/dev/fyloz/colorrecipesexplorer/xlsx/XlsxExporter.java b/src/main/java/dev/fyloz/colorrecipesexplorer/xlsx/XlsxExporter.java index 79b54e4..c2508e8 100644 --- a/src/main/java/dev/fyloz/colorrecipesexplorer/xlsx/XlsxExporter.java +++ b/src/main/java/dev/fyloz/colorrecipesexplorer/xlsx/XlsxExporter.java @@ -1,8 +1,8 @@ package dev.fyloz.colorrecipesexplorer.xlsx; -import dev.fyloz.colorrecipesexplorer.model.Mix; -import dev.fyloz.colorrecipesexplorer.model.MixMaterial; -import dev.fyloz.colorrecipesexplorer.model.Recipe; +import dev.fyloz.colorrecipesexplorer.dtos.MixDto; +import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto; +import dev.fyloz.colorrecipesexplorer.dtos.RecipeDto; import dev.fyloz.colorrecipesexplorer.xlsx.component.Document; import dev.fyloz.colorrecipesexplorer.xlsx.component.Sheet; import dev.fyloz.colorrecipesexplorer.xlsx.component.Table; @@ -23,7 +23,7 @@ public class XlsxExporter { this.logger = logger; } - public byte[] generate(Recipe recipe) { + public byte[] generate(RecipeDto recipe) { logger.info(String.format("Génération du XLS de la couleur %s (%s)", recipe.getName(), recipe.getId())); Document document = new Document(recipe.getName(), logger); @@ -44,7 +44,7 @@ public class XlsxExporter { return output; } - private void registerCells(Recipe recipe, Sheet sheet) { + private void registerCells(RecipeDto recipe, Sheet sheet) { // Header sheet.registerCell(new TitleCell(recipe.getName())); sheet.registerCell(new DescriptionCell(DescriptionCell.DescriptionCellType.NAME, "Bannière")); @@ -59,17 +59,17 @@ public class XlsxExporter { sheet.registerCell(new DescriptionCell(DescriptionCell.DescriptionCellType.VALUE_STR, recipe.getRemark())); // Mélanges - Collection recipeMixes = recipe.getMixes(); + Collection recipeMixes = recipe.getMixes(); if (recipeMixes.size() > 0) { sheet.registerCell(new SectionTitleCell("Recette")); - for (Mix mix : recipeMixes) { + for (MixDto mix : recipeMixes) { Table mixTable = new Table(4, mix.getMixMaterials().size() + 1, mix.getMixType().getName()); mixTable.setColumnName(0, "Quantité"); mixTable.setColumnName(2, "Unités"); int row = 0; - for (MixMaterial mixMaterial : mix.getMixMaterials()) { + for (MixMaterialDto mixMaterial : mix.getMixMaterials()) { mixTable.setRowName(row, mixMaterial.getMaterial().getName()); mixTable.setContent(new Position(1, row + 1), mixMaterial.getQuantity()); mixTable.setContent(new Position(3, row + 1), mixMaterial.getMaterial().getMaterialType().getUsePercentages() ? "%" : "mL"); diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt index 4cb51dc..2a8590c 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt @@ -6,12 +6,15 @@ object Constants { const val MATERIAL = "/api/material" const val MATERIAL_TYPE = "/api/materialtype" const val MIX = "/api/recipe/mix" + const val RECIPE = "/api/recipe" } object FilePaths { - const val PDF = "pdf" + private const val PDF = "pdf" + private const val IMAGES = "images" const val SIMDUT = "$PDF/simdut" + const val RECIPE_IMAGES = "$IMAGES/recipes" } object ValidationMessages { @@ -19,4 +22,8 @@ object Constants { 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" } + + object ValidationRegexes { + const val VALIDATION_COLOR_PATTERN = "^#([0-9a-f]{6})$" + } } \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MixInitializer.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MixInitializer.kt index bc48f94..71db2e0 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MixInitializer.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MixInitializer.kt @@ -34,7 +34,7 @@ class MixInitializer( private fun fixMixPositions(mix: MixDto) { val maxPosition = mix.mixMaterials.maxOf { it.position } - logger.warn("Mix ${mix.id} (${mix.mixType.name}, ${mix.recipe.name}) has invalid positions:") + logger.warn("Mix ${mix.id} (mix name: ${mix.mixType.name}, recipe id: ${mix.recipeId}) has invalid positions:") val invalidMixMaterials: Collection = with(mix.mixMaterials.filter { it.position == 0 }) { if (maxPosition == 0 && this.size > 1) { @@ -47,7 +47,7 @@ class MixInitializer( val fixedMixMaterials = increaseMixMaterialsPosition(invalidMixMaterials, maxPosition + 1) val updatedMixMaterials = mix.mixMaterials.merge(fixedMixMaterials) - with(mix.copy(mixMaterials = updatedMixMaterials.toMutableSet())) { + with(mix.copy(mixMaterials = updatedMixMaterials)) { mixLogic.update(this) } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/RecipeInitializer.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/RecipeInitializer.kt index 5890b57..5347e20 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/RecipeInitializer.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/RecipeInitializer.kt @@ -1,10 +1,10 @@ package dev.fyloz.colorrecipesexplorer.config.initializers import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase +import dev.fyloz.colorrecipesexplorer.dtos.RecipeDto +import dev.fyloz.colorrecipesexplorer.dtos.RecipeGroupInformationDto +import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto import dev.fyloz.colorrecipesexplorer.logic.RecipeLogic -import dev.fyloz.colorrecipesexplorer.model.Recipe -import dev.fyloz.colorrecipesexplorer.model.RecipeGroupInformation -import dev.fyloz.colorrecipesexplorer.model.RecipeStep import dev.fyloz.colorrecipesexplorer.utils.merge import mu.KotlinLogging import org.springframework.context.annotation.Configuration @@ -24,30 +24,29 @@ class RecipeInitializer( private fun fixAllPositions() { logger.debug("Validating recipes steps positions...") - recipeLogic.getAll() + recipeLogic.getAllWithMixesAndGroupsInformation() .forEach(this::fixRecipePositions) logger.debug("Recipes steps positions are valid!") } - private fun fixRecipePositions(recipe: Recipe) { + private fun fixRecipePositions(recipe: RecipeDto) { val fixedGroupInformation = recipe.groupsInformation - .filter { it.steps != null } - .filter { groupInfo -> groupInfo.steps!!.any { it.position == 0 } } + .filter { groupInfo -> groupInfo.steps.any { it.position == 0 } } .map { fixGroupInformationPositions(recipe, it) } val updatedGroupInformation = recipe.groupsInformation.merge(fixedGroupInformation) { it.id } - with(recipe.copy(groupsInformation = updatedGroupInformation.toMutableSet())) { + with(recipe.copy(groupsInformation = updatedGroupInformation)) { recipeLogic.update(this) } } private fun fixGroupInformationPositions( - recipe: Recipe, - groupInformation: RecipeGroupInformation - ): RecipeGroupInformation { - val steps = groupInformation.steps!! + recipe: RecipeDto, + groupInformation: RecipeGroupInformationDto + ): RecipeGroupInformationDto { + val steps = groupInformation.steps val maxPosition = steps.maxOf { it.position } logger.warn("Recipe ${recipe.id} (${recipe.name}) has invalid positions:") @@ -56,12 +55,12 @@ class RecipeInitializer( val fixedRecipeSteps = increaseRecipeStepsPosition(groupInformation, invalidRecipeSteps, maxPosition + 1) val updatedRecipeSteps = steps.merge(fixedRecipeSteps) { it.id } - return groupInformation.copy(steps = updatedRecipeSteps.toMutableSet()) + return groupInformation.copy(steps = updatedRecipeSteps) } private fun increaseRecipeStepsPosition( - groupInformation: RecipeGroupInformation, - recipeSteps: Iterable, + groupInformation: RecipeGroupInformationDto, + recipeSteps: Iterable, firstPosition: Int ) = recipeSteps diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/properties/MaterialTypeProperties.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/properties/MaterialTypeProperties.kt index 736cd91..343935a 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/properties/MaterialTypeProperties.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/properties/MaterialTypeProperties.kt @@ -1,8 +1,6 @@ package dev.fyloz.colorrecipesexplorer.config.properties import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto -import dev.fyloz.colorrecipesexplorer.model.MaterialType -import dev.fyloz.colorrecipesexplorer.model.materialType import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.stereotype.Component import org.springframework.util.Assert diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixDto.kt index 957a59f..3cc26dc 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixDto.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixDto.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.Recipe import javax.validation.constraints.Min import javax.validation.constraints.NotBlank @@ -12,11 +11,11 @@ data class MixDto( val location: String? = null, @JsonIgnore - val recipe: Recipe, // TODO change to dto + val recipeId: Long, val mixType: MixTypeDto, - val mixMaterials: Set + val mixMaterials: List ) : EntityDto data class MixSaveDto( @@ -29,7 +28,7 @@ data class MixSaveDto( val materialTypeId: Long, - val mixMaterials: Set + val mixMaterials: List ) data class MixDeductDto( diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/RecipeDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/RecipeDto.kt new file mode 100644 index 0000000..80c22c4 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/RecipeDto.kt @@ -0,0 +1,122 @@ +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 +import javax.validation.constraints.NotBlank +import javax.validation.constraints.Pattern + +data class RecipeDto( + override val id: Long = 0L, + + val name: String, + + val description: String, + + val color: String, + + val gloss: Byte, + + val sample: Int?, + + val approbationDate: LocalDate?, + + val approbationExpired: Boolean, + + val remark: String, + + val company: CompanyDto, + + val mixes: List, + + val groupsInformation: List +) : EntityDto { + val mixTypes: Collection + @JsonIgnore + get() = mixes.map { it.mixType } +} + +data class RecipeSaveDto( + @field:NotBlank + val name: String, + + @field:NotBlank + val description: String, + + @field:NotBlank + @field:Pattern(regexp = Constants.ValidationRegexes.VALIDATION_COLOR_PATTERN) + val color: String, + + @field:Min(0, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS) + @field:Max(100, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS) + val gloss: Byte, + + @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO) + val sample: Int?, + + val approbationDate: LocalDate?, + + val remark: String?, + + val companyId: Long +) + +data class RecipeUpdateDto( + val id: Long, + + @field:NotBlank + val name: String, + + @field:NotBlank + val description: String, + + @field:NotBlank + @field:Pattern(regexp = Constants.ValidationRegexes.VALIDATION_COLOR_PATTERN) + val color: String, + + @field:Min(0, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS) + @field:Max(100, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS) + val gloss: Byte, + + @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO) + val sample: Int?, + + val approbationDate: LocalDate?, + + val remark: String?, + + val steps: List +) + +data class RecipeGroupInformationDto( + override val id: Long = 0L, + + val group: Group, + + val note: String? = null, + + val steps: List = listOf() +) : EntityDto + +data class RecipeGroupStepsDto( + val groupId: Long, + + val steps: List +) + +data class RecipeGroupNoteDto( + val groupId: Long, + + val content: String? +) + +data class RecipePublicDataDto( + val recipeId: Long, + + val notes: List, + + val mixesLocation: List +) \ 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 1214c9e..997d089 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt @@ -5,6 +5,8 @@ import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.service.Service +import dev.fyloz.colorrecipesexplorer.utils.collections.LazyMapList +import org.springframework.transaction.annotation.Transactional /** * Represents the logic for a DTO type. @@ -92,6 +94,17 @@ 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/MaterialLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogic.kt index 7f3027f..9a9d974 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogic.kt @@ -56,9 +56,9 @@ class DefaultMaterialLogic( override fun getAllForMixUpdate(mixId: Long): Collection { val mix = mixLogic.getById(mixId) - val recipesMixTypes = mix.recipe.mixTypes + val recipe = recipeLogic.getById(mix.recipeId) - return getAll().filter { !it.isMixType || recipesMixTypes.any { mixType -> mixType.material.id == it.id } } + return getAll().filter { !it.isMixType || recipe.mixTypes.any { mixType -> mixType.material.id == it.id } } .filter { it.id != mix.mixType.material.id } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt index f719422..16c31d5 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt @@ -34,9 +34,9 @@ class DefaultMixLogic( val materialType = materialTypeLogic.getById(dto.materialTypeId) val mix = MixDto( - recipe = recipe, + recipeId = recipe.id, mixType = mixTypeLogic.getOrCreateForNameAndMaterialType(dto.name, materialType), - mixMaterials = mixMaterialLogic.validateAndSaveAll(dto.mixMaterials).toSet() + mixMaterials = mixMaterialLogic.validateAndSaveAll(dto.mixMaterials) ) return save(mix) @@ -50,9 +50,9 @@ class DefaultMixLogic( return update( MixDto( id = dto.id, - recipe = recipeLogic.getById(dto.recipeId), + recipeId = dto.recipeId, mixType = mixTypeLogic.updateOrCreateForNameAndMaterialType(mix.mixType, dto.name, materialType), - mixMaterials = mixMaterialLogic.validateAndSaveAll(dto.mixMaterials).toSet() + mixMaterials = mixMaterialLogic.validateAndSaveAll(dto.mixMaterials) ) ) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt index b6ddf38..1ff78fd 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt @@ -21,7 +21,7 @@ interface MixMaterialLogic : Logic { fun validateMixMaterials(mixMaterials: Set) /** Validates the given mix materials [dtos] and save them. */ - fun validateAndSaveAll(dtos: Collection): Collection + fun validateAndSaveAll(dtos: List): List } @LogicComponent @@ -43,7 +43,7 @@ class DefaultMixMaterialLogic(service: MixMaterialService, @Lazy private val mat } } - override fun validateAndSaveAll(dtos: Collection): Collection { + override fun validateAndSaveAll(dtos: List): List { val dtosWithMaterials = dtos.map { MixMaterialDto( id = it.id, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt index 19315e3..1daf5cd 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt @@ -1,256 +1,185 @@ package dev.fyloz.colorrecipesexplorer.logic -import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase -import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic +import dev.fyloz.colorrecipesexplorer.Constants +import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent +import dev.fyloz.colorrecipesexplorer.dtos.* +import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic -import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.model.account.Group -import dev.fyloz.colorrecipesexplorer.model.validation.or -import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository -import dev.fyloz.colorrecipesexplorer.utils.setAll -import org.springframework.context.annotation.Lazy -import org.springframework.stereotype.Service +import dev.fyloz.colorrecipesexplorer.model.Recipe +import dev.fyloz.colorrecipesexplorer.service.RecipeService +import dev.fyloz.colorrecipesexplorer.utils.collections.LazyMapList +import dev.fyloz.colorrecipesexplorer.utils.merge +import org.springframework.transaction.annotation.Transactional import org.springframework.web.multipart.MultipartFile -import java.time.LocalDate -import java.time.Period -import javax.transaction.Transactional -interface RecipeLogic : - ExternalModelService { - /** Checks if one or more recipes have the given [company]. */ - fun existsByCompany(company: Company): Boolean - - /** Checks if a recipe exists with the given [name] and [company]. */ - fun existsByNameAndCompany(name: String, company: Company): Boolean - - /** Checks if the approbation date of the given [recipe] is expired. */ - fun isApprobationExpired(recipe: Recipe): Boolean? +interface RecipeLogic : Logic { + /** Gets all recipes and load their mixes and groupsInformation, to prevent LazyInitializationExceptions */ + fun getAllWithMixesAndGroupsInformation(): Collection /** Gets all recipes with the given [name]. */ - fun getAllByName(name: String): Collection + fun getAllByName(name: String): Collection - /** Gets all recipes with the given [company]. */ - fun getAllByCompany(company: Company): Collection + /** Saves the given [dto]. */ + fun save(dto: RecipeSaveDto): RecipeDto + + /** Updates the given [dto]. */ + fun update(dto: RecipeUpdateDto): RecipeDto /** Updates the public data of a recipe with the given [publicDataDto]. */ fun updatePublicData(publicDataDto: RecipePublicDataDto) - - /** Adds the given [mix] to the given [recipe]. */ - fun addMix(recipe: Recipe, mix: Mix): Recipe - - /** Removes the given [mix] from its recipe. */ - fun removeMix(mix: Mix): Recipe } -@Service -@RequireDatabase +@LogicComponent class DefaultRecipeLogic( - recipeRepository: RecipeRepository, - val companyLogic: CompanyLogic, - val mixLogic: MixLogic, - val recipeStepLogic: RecipeStepLogic, - @Lazy val groupLogic: GroupLogic, - val recipeImageLogic: RecipeImageLogic, - val configService: ConfigurationLogic -) : - AbstractExternalModelService( - recipeRepository - ), - RecipeLogic { - override fun idNotFoundException(id: Long) = recipeIdNotFoundException(id) - override fun idAlreadyExistsException(id: Long) = recipeIdAlreadyExistsException(id) + service: RecipeService, + private val companyLogic: CompanyLogic, + private val recipeStepLogic: RecipeStepLogic, + private val mixLogic: MixLogic, + private val groupLogic: GroupLogic +) : BaseLogic(service, Recipe::class.simpleName!!), RecipeLogic { + @Transactional + override fun getAllWithMixesAndGroupsInformation() = + getAll().onEach { (it.mixes as LazyMapList<*, *>).initialize() } + .onEach { (it.groupsInformation as LazyMapList<*, *>).initialize() } - override fun Recipe.toOutput() = RecipeOutputDto( - this.id!!, - this.name, - this.description, - this.color, - this.gloss, - this.sample, - this.approbationDate, - isApprobationExpired(this), - this.remark, - this.company, - this.mixes.map { mix(it) }.toSet(), - this.groupsInformation, - recipeImageLogic.getAllImages(this) - .map { this.imageUrl(configService.getContent(ConfigurationType.INSTANCE_URL), it) } - .toSet() + override fun getAllByName(name: String) = service.getAllByName(name) + + override fun save(dto: RecipeSaveDto) = save( + RecipeDto( + name = dto.name, + description = dto.description, + color = dto.color, + gloss = dto.gloss, + sample = dto.sample, + approbationDate = dto.approbationDate, + approbationExpired = false, + remark = dto.remark ?: "", + company = companyLogic.getById(dto.companyId), + mixes = listOf(), + groupsInformation = listOf() + ) ) - override fun existsByCompany(company: Company): Boolean = repository.existsByCompany(company) - override fun existsByNameAndCompany(name: String, company: Company) = - repository.existsByNameAndCompany(name, company) + override fun save(dto: RecipeDto): RecipeDto { + throwIfNameAndCompanyAlreadyExists(dto.name, dto.company.id) - override fun isApprobationExpired(recipe: Recipe): Boolean? = - with(Period.parse(configService.getContent(ConfigurationType.RECIPE_APPROBATION_EXPIRATION))) { - recipe.approbationDate?.plus(this)?.isBefore(LocalDate.now()) - } - - override fun getAllByName(name: String) = repository.findAllByName(name) - override fun getAllByCompany(company: Company) = repository.findAllByCompany(company) - - override fun save(entity: RecipeSaveDto): Recipe { - val company = company(companyLogic.getById(entity.companyId)) - - if (existsByNameAndCompany(entity.name, company)) { - throw recipeNameAlreadyExistsForCompanyException(entity.name, company) - } - - return save(with(entity) { - recipe( - name = name, - description = description, - color = color, - gloss = gloss, - sample = sample, - approbationDate = approbationDate, - remark = remark ?: "", - company = company - ) - }) + return super.save(dto) } - @Transactional - override fun update(entity: RecipeUpdateDto): Recipe { - val persistedRecipe = getById(entity.id) - val name = entity.name - val company = persistedRecipe.company + override fun update(dto: RecipeUpdateDto): RecipeDto { + val recipe = getById(dto.id) - if (name != null && - name != persistedRecipe.name && - existsByNameAndCompany(name, company) - ) { - throw recipeNameAlreadyExistsForCompanyException(name, company) - } - - return update(with(entity) { - recipe( - id = id, - name = name or persistedRecipe.name, - description = description or persistedRecipe.description, - color = color or persistedRecipe.color, - gloss = gloss ?: persistedRecipe.gloss, - sample = sample ?: persistedRecipe.sample, - approbationDate = approbationDate ?: persistedRecipe.approbationDate, - remark = remark or persistedRecipe.remark, - company = company, - mixes = persistedRecipe.mixes, - groupsInformation = updateGroupsInformation(persistedRecipe, entity) + return update( + RecipeDto( + id = dto.id, + name = dto.name, + description = dto.description, + color = dto.color, + gloss = dto.gloss, + sample = dto.sample, + approbationDate = dto.approbationDate, + approbationExpired = false, + remark = dto.remark ?: "", + company = recipe.company, + mixes = recipe.mixes, + groupsInformation = updateGroupsInformationSteps(recipe, dto) ) - }) + ) } - private fun updateGroupsInformation(recipe: Recipe, updateDto: RecipeUpdateDto): Set { - val steps = updateDto.steps ?: return recipe.groupsInformation + override fun update(dto: RecipeDto): RecipeDto { + throwIfNameAndCompanyAlreadyExists(dto.name, dto.company.id, dto.id) - val updatedGroupsInformation = mutableSetOf() - steps.forEach { - with(recipe.groupInformationForGroup(it.groupId)) { - // Set steps for the existing RecipeGroupInformation or create a new one - val updatedGroupInformation = this?.apply { - if (this.steps != null) { - this.steps!!.setAll(it.steps) - } else { - this.steps = it.steps.toMutableSet() - } - } ?: recipeGroupInformation( - group = groupLogic.getById(it.groupId), - steps = it.steps.toMutableSet() - ) - - updatedGroupsInformation.add(updatedGroupInformation) - recipeStepLogic.validateGroupInformationSteps(updatedGroupInformation) - } - } - - return updatedGroupsInformation + return super.update(dto) } @Transactional override fun updatePublicData(publicDataDto: RecipePublicDataDto) { - if (publicDataDto.notes != null) { + // Update notes + if (publicDataDto.notes.isNotEmpty()) { val recipe = getById(publicDataDto.recipeId) - - fun noteForGroup(group: Group) = - publicDataDto.notes.firstOrNull { it.groupId == group.id }?.content - - // Notes - recipe.groupsInformation.map { - val updatedNote = noteForGroup(it.group) - it.apply { - note = updatedNote - } - } - - update(recipe) + update(recipe.copy(groupsInformation = updateGroupsInformationNotes(recipe, publicDataDto.notes))) } - if (publicDataDto.mixesLocation != null) { + // Update mixes locations + if (publicDataDto.mixesLocation.isNotEmpty()) { mixLogic.updateLocations(publicDataDto.mixesLocation) } } - override fun addMix(recipe: Recipe, mix: Mix) = - update(recipe.apply { mixes.add(mix) }) + private fun updateGroupsInformationSteps(recipe: RecipeDto, dto: RecipeUpdateDto): List { + val updatedGroupsInformation = dto.steps.map { updateGroupInformationSteps(recipe, it) } + return recipe.groupsInformation.merge(updatedGroupsInformation) + } - override fun removeMix(mix: Mix): Recipe = - update(mix.recipe.apply { mixes.remove(mix) }) + private fun updateGroupInformationSteps(recipe: RecipeDto, groupSteps: RecipeGroupStepsDto) = + getOrCreateGroupInformation(recipe, groupSteps.groupId).copy(steps = groupSteps.steps).also { + recipeStepLogic.validateGroupInformationSteps(it) + } + + private fun updateGroupsInformationNotes( + recipe: RecipeDto, notes: List + ): List { + val updatedGroupsInformation = notes.map { updateGroupInformationNote(recipe, it) } + return recipe.groupsInformation.merge(updatedGroupsInformation) + } + + private fun updateGroupInformationNote(recipe: RecipeDto, groupNote: RecipeGroupNoteDto) = + getOrCreateGroupInformation(recipe, groupNote.groupId).copy(note = groupNote.content) + + private fun getOrCreateGroupInformation(recipe: RecipeDto, groupId: Long) = + recipe.groupsInformation.firstOrNull { it.group.id == groupId } + ?: RecipeGroupInformationDto(group = groupLogic.getById(groupId)) + + private fun throwIfNameAndCompanyAlreadyExists(name: String, companyId: Long, id: Long? = null) { + if (service.existsByNameAndCompany(name, companyId, id)) { + throw AlreadyExistsException( + "$typeNameLowerCase-company", + "$typeName already exists", + "A recipe with the name '$name' already exists for the company with the id '$companyId'", + name, + NAME_IDENTIFIER_NAME, + mutableMapOf( + "companyId" to companyId + ) + ) + } + } } interface RecipeImageLogic { - /** Gets the name of every images associated to the recipe with the given [recipe]. */ - fun getAllImages(recipe: Recipe): Set + /** Gets the id of every image associated to the recipe with the given [recipeId]. */ + fun getAllImages(recipeId: Long): List - /** Saves the given [image] and associate it to the recipe with the given [recipe]. Returns the name of the saved image. */ - fun download(image: MultipartFile, recipe: Recipe): String + /** Saves the given [image] and associate it to the recipe with the given [recipeId]. Returns the id of the saved image. */ + fun download(image: MultipartFile, recipeId: Long): String - /** Deletes the image with the given [name] for the given [recipe]. */ - fun delete(recipe: Recipe, name: String) + /** Deletes the image with the given [path] for the given [recipeId]. */ + fun delete(recipeId: Long, path: String) } -const val RECIPE_IMAGE_ID_DELIMITER = "_" -const val RECIPE_IMAGE_EXTENSION = ".jpg" +@LogicComponent +class DefaultRecipeImageLogic(val fileLogic: WriteableFileLogic) : RecipeImageLogic { + override fun getAllImages(recipeId: Long) = + fileLogic.listDirectoryFiles(getRecipeImagesDirectory(recipeId)).map { it.name } -@Service -@RequireDatabase -class DefaultRecipeImageLogic( - val fileService: WriteableFileLogic -) : RecipeImageLogic { - override fun getAllImages(recipe: Recipe) = - fileService.listDirectoryFiles(recipe.imagesDirectoryPath) - .map { it.name } - .toSet() + override fun download(image: MultipartFile, recipeId: Long): String { + /** Gets the next id available for a new image for the given [recipeId]. */ + fun getNextAvailableId(): String = with(getAllImages(recipeId)) { + (if (isEmpty()) 0 else maxOf { it.toLong() } + 1L).toString() + } - override fun download(image: MultipartFile, recipe: Recipe): String { - /** Gets the next id available for a new image for the given [recipe]. */ - fun getNextAvailableId(): Long = - with(getAllImages(recipe)) { - if (isEmpty()) - 0 - else - maxOf { - it.split(RECIPE_IMAGE_ID_DELIMITER) - .last() - .replace(RECIPE_IMAGE_EXTENSION, "") - .toLong() - } + 1L - } - - return getImageFileName(recipe, getNextAvailableId()).also { - with(getImagePath(recipe, it)) { - fileService.writeToDirectory(image, this, recipe.imagesDirectoryPath, true) - } + return getNextAvailableId().also { + val imagePath = getImagePath(recipeId, it) + fileLogic.writeToDirectory(image, imagePath, getRecipeImagesDirectory(recipeId), true) } } - override fun delete(recipe: Recipe, name: String) = - fileService.deleteFromDirectory(getImagePath(recipe, name), recipe.imagesDirectoryPath) + override fun delete(recipeId: Long, path: String) = + fileLogic.deleteFromDirectory(path, getRecipeImagesDirectory(recipeId)) - private fun getImageFileName(recipe: Recipe, id: Long) = - "${recipe.name}$RECIPE_IMAGE_ID_DELIMITER$id" + private fun getImagePath(recipeId: Long, id: String) = "${getRecipeImagesDirectory(recipeId)}/$id" - private fun getImagePath(recipe: Recipe, name: String) = - "${recipe.imagesDirectoryPath}/$name$RECIPE_IMAGE_EXTENSION" + private fun getRecipeImagesDirectory(recipeId: Long) = "${Constants.FilePaths.RECIPE_IMAGES}/$recipeId" } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt index b7852fa..d0f8771 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt @@ -1,11 +1,11 @@ package dev.fyloz.colorrecipesexplorer.logic import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent +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.exception.RestException -import dev.fyloz.colorrecipesexplorer.model.RecipeGroupInformation import dev.fyloz.colorrecipesexplorer.model.RecipeStep import dev.fyloz.colorrecipesexplorer.model.account.Group import dev.fyloz.colorrecipesexplorer.service.RecipeStepService @@ -14,17 +14,15 @@ import org.springframework.http.HttpStatus interface RecipeStepLogic : Logic { /** Validates the steps of the given [groupInformation], according to the criteria of [PositionUtils.validate]. */ - fun validateGroupInformationSteps(groupInformation: RecipeGroupInformation) + fun validateGroupInformationSteps(groupInformation: RecipeGroupInformationDto) } @LogicComponent class DefaultRecipeStepLogic(recipeStepService: RecipeStepService) : BaseLogic(recipeStepService, RecipeStep::class.simpleName!!), RecipeStepLogic { - override fun validateGroupInformationSteps(groupInformation: RecipeGroupInformation) { - if (groupInformation.steps == null) return - + override fun validateGroupInformationSteps(groupInformation: RecipeGroupInformationDto) { try { - PositionUtils.validate(groupInformation.steps!!.map { it.position }.toList()) + PositionUtils.validate(groupInformation.steps.map { it.position }.toList()) } catch (ex: InvalidPositionsException) { throw InvalidGroupStepsPositionsException(groupInformation.group, ex) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt index b2e1158..f0d37a4 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt @@ -1,6 +1,5 @@ package dev.fyloz.colorrecipesexplorer.model -import dev.fyloz.colorrecipesexplorer.dtos.CompanyDto import javax.persistence.* @Entity @@ -12,21 +11,4 @@ data class Company( @Column(unique = true) val name: String -) : ModelEntity - -// ==== DSL ==== -fun company( - id: Long? = null, - name: String = "name", - op: Company.() -> Unit = {} -) = Company(id, name).apply(op) - -@Deprecated("Temporary DSL for transition") -fun company( - dto: CompanyDto -) = Company(dto.id, dto.name) - -@Deprecated("Temporary DSL for transition") -fun companyDto( - entity: Company -) = CompanyDto(entity.id!!, entity.name) \ No newline at end of file +) : ModelEntity \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt index 024e651..ac2eb93 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt @@ -1,7 +1,6 @@ package dev.fyloz.colorrecipesexplorer.model import dev.fyloz.colorrecipesexplorer.Constants -import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto import javax.persistence.* @Entity @@ -28,34 +27,4 @@ data class Material( fun getSimdutFilePath(name: String) = "${Constants.FilePaths.SIMDUT}/$name.pdf" } -} - -// === DSL === - -fun material( - id: Long? = null, - name: String = "name", - inventoryQuantity: Float = 0f, - isMixType: Boolean = false, - materialType: MaterialType? = materialType(), - op: Material.() -> Unit = {} -) = Material(id, name, inventoryQuantity, isMixType, materialType).apply(op) - -fun material( - material: Material, - id: Long? = null, - name: String? = null, -) = Material( - id ?: material.id, name - ?: material.name, material.inventoryQuantity, material.isMixType, material.materialType -) - -@Deprecated("Temporary DSL for transition") -fun material( - dto: MaterialDto -) = Material(dto.id, dto.name, dto.inventoryQuantity, dto.isMixType, materialType(dto.materialType)) - -@Deprecated("Temporary DSL for transition") -fun materialDto( - entity: Material -) = MaterialDto(entity.id!!, entity.name, entity.inventoryQuantity, entity.isMixType, materialTypeDto(entity.materialType!!)) \ No newline at end of file +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt index 279c9c4..9082e82 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt @@ -1,6 +1,5 @@ package dev.fyloz.colorrecipesexplorer.model -import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto import org.hibernate.annotations.ColumnDefault import javax.persistence.* @@ -24,39 +23,4 @@ data class MaterialType( @Column(name = "system_type") @ColumnDefault("false") val systemType: Boolean = false -) : ModelEntity - -// ==== DSL ==== -fun materialType( - id: Long? = null, - name: String = "name", - prefix: String = "PRE", - usePercentages: Boolean = false, - systemType: Boolean = false, - op: MaterialType.() -> Unit = {} -) = MaterialType(id, name, prefix, usePercentages, systemType).apply(op) - -fun materialType( - materialType: MaterialType, - newId: Long? = null, - newName: String? = null, - newSystemType: Boolean? = null -) = with(materialType) { - MaterialType( - newId ?: id, - newName ?: name, - prefix, - usePercentages, - newSystemType ?: systemType - ) -} - -@Deprecated("Temporary DSL for transition") -fun materialType( - dto: MaterialTypeDto -) = MaterialType(dto.id, dto.name, dto.prefix, dto.usePercentages, dto.systemType) - -@Deprecated("Temporary DSL for transition") -fun materialTypeDto( - entity: MaterialType -) = MaterialTypeDto(entity.id!!, entity.name, entity.prefix, entity.usePercentages, entity.systemType) \ No newline at end of file +) : ModelEntity \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt index 6931638..9656433 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt @@ -1,10 +1,5 @@ package dev.fyloz.colorrecipesexplorer.model -import dev.fyloz.colorrecipesexplorer.dtos.MixDto -import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto -import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException -import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException -import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import javax.persistence.* @Entity @@ -16,9 +11,8 @@ data class Mix( var location: String?, - @ManyToOne - @JoinColumn(name = "recipe_id") - val recipe: Recipe, + @Column(name = "recipe_id") + val recipeId: Long, @ManyToOne @JoinColumn(name = "mix_type_id") @@ -26,25 +20,5 @@ data class Mix( @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true) @JoinColumn(name = "mix_id") - var mixMaterials: Set, -) : ModelEntity - -// ==== DSL ==== -fun mix( - id: Long? = null, - location: String? = "location", - recipe: Recipe = recipe(), - mixType: MixType = mixType(), - mixMaterials: MutableSet = mutableSetOf(), - op: Mix.() -> Unit = {} -) = Mix(id, location, recipe, mixType, mixMaterials).apply(op) - -@Deprecated("Temporary DSL for transition") -fun mix( - dto: MixDto -) = Mix(dto.id, dto.location, dto.recipe, mixType(dto.mixType), dto.mixMaterials.map(::mixMaterial).toSet()) - -@Deprecated("Temporary DSL for transition") -fun mix( - entity: Mix -) = MixDto(entity.id!!, entity.location, entity.recipe, mixTypeDto(entity.mixType), entity.mixMaterials.map(::mixMaterialDto).toSet()) \ No newline at end of file + var mixMaterials: List, +) : ModelEntity \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt index 6ac7569..e243e14 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt @@ -1,6 +1,5 @@ package dev.fyloz.colorrecipesexplorer.model -import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto import javax.persistence.* @Entity @@ -17,23 +16,4 @@ data class MixMaterial( var quantity: Float, var position: Int -) : ModelEntity - -// ==== DSL ==== -fun mixMaterial( - id: Long? = null, - material: Material = material(), - quantity: Float = 0f, - position: Int = 0, - op: MixMaterial.() -> Unit = {} -) = MixMaterial(id, material, quantity, position).apply(op) - -@Deprecated("Temporary DSL for transition") -fun mixMaterialDto( - entity: MixMaterial -) = MixMaterialDto(entity.id!!, materialDto(entity.material), entity.quantity, entity.position) - -@Deprecated("Temporary DSL for transition") -fun mixMaterial( - dto: MixMaterialDto -) = MixMaterial(dto.id, material(dto.material), dto.quantity, dto.position) \ No newline at end of file +) : 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 8b25b67..7d471dc 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixType.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixType.kt @@ -1,11 +1,5 @@ package dev.fyloz.colorrecipesexplorer.model -import dev.fyloz.colorrecipesexplorer.dtos.MixTypeDto -import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException -import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException -import dev.fyloz.colorrecipesexplorer.exception.NotFoundException -import dev.fyloz.colorrecipesexplorer.exception.RestException -import org.springframework.http.HttpStatus import javax.persistence.* @Entity @@ -21,32 +15,4 @@ data class MixType( @OneToOne(cascade = [CascadeType.ALL]) @JoinColumn(name = "material_id") var material: Material -) : ModelEntity - -// ==== DSL ==== -fun mixType( - id: Long? = null, - name: String = "name", - material: Material = material(), - op: MixType.() -> Unit = {} -) = MixType(id, name, material).apply(op) - -fun mixType( - name: String = "name", - materialType: MaterialType = materialType(), - op: MixType.() -> Unit = {} -) = mixType( - id = null, - name, - material = material(name = name, inventoryQuantity = 0f, isMixType = true, materialType = materialType) -).apply(op) - -@Deprecated("Temporary DSL for transition") -fun mixTypeDto( - entity: MixType -) = MixTypeDto(entity.id!!, entity.name, materialDto(entity.material)) - -@Deprecated("Temporary DSL for transition") -fun mixType( - dto: MixTypeDto -) = MixType(dto.id, dto.name, material(dto.material)) \ No newline at end of file +) : ModelEntity \ 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 3fc6873..9692a56 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt @@ -1,25 +1,8 @@ package dev.fyloz.colorrecipesexplorer.model -import com.fasterxml.jackson.annotation.JsonIgnore -import dev.fyloz.colorrecipesexplorer.Constants -import dev.fyloz.colorrecipesexplorer.dtos.MixDto -import dev.fyloz.colorrecipesexplorer.dtos.MixLocationDto -import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException -import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.model.account.Group -import dev.fyloz.colorrecipesexplorer.model.account.group -import java.net.URLEncoder -import java.nio.charset.StandardCharsets import java.time.LocalDate import javax.persistence.* -import javax.validation.constraints.Max -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank -import javax.validation.constraints.Pattern - -private const val VALIDATION_COLOR_PATTERN = "^#([0-9a-f]{6})$" - -const val RECIPE_IMAGES_DIRECTORY = "images/recipes" @Entity @Table(name = "recipe") @@ -51,110 +34,12 @@ data class Recipe( @JoinColumn(name = "company_id") val company: Company, - @OneToMany(cascade = [CascadeType.ALL], mappedBy = "recipe") - val mixes: MutableList, + @OneToMany(cascade = [CascadeType.ALL], mappedBy = "recipeId") + val mixes: List, - @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true) + @OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true) @JoinColumn(name = "recipe_id") - val groupsInformation: Set -) : ModelEntity { - /** The mix types contained in this recipe. */ - val mixTypes: Collection - @JsonIgnore - get() = mixes.map { it.mixType } - - val imagesDirectoryPath - @JsonIgnore - @Transient - get() = "$RECIPE_IMAGES_DIRECTORY/$id" - - fun groupInformationForGroup(groupId: Long) = - groupsInformation.firstOrNull { it.group.id == groupId } - - fun imageUrl(deploymentUrl: String, name: String) = - "$deploymentUrl${Constants.ControllerPaths.FILE}?path=${ - URLEncoder.encode( - "${this.imagesDirectoryPath}/$name", - StandardCharsets.UTF_8 - ) - }" -} - -open class RecipeSaveDto( - @field:NotBlank - val name: String, - - @field:NotBlank - val description: String, - - @field:NotBlank - @field:Pattern(regexp = VALIDATION_COLOR_PATTERN) - val color: String, - - @field:Min(0, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS) - @field:Max(100, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS) - val gloss: Byte, - - @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO) - val sample: Int?, - - val approbationDate: LocalDate?, - - val remark: String?, - - val companyId: Long -) : EntityDto { - override fun toEntity(): Recipe = recipe( - name = name, - description = description, - sample = sample, - approbationDate = approbationDate, - remark = remark ?: "", - company = company(id = companyId) - ) -} - -open class RecipeUpdateDto( - val id: Long, - - @field:NotBlank - val name: String?, - - @field:NotBlank - val description: String?, - - @field:NotBlank - @field:Pattern(regexp = VALIDATION_COLOR_PATTERN) - val color: String?, - - @field:Min(0, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS) - @field:Max(100, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS) - val gloss: Byte?, - - @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO) - val sample: Int?, - - val approbationDate: LocalDate?, - - val remark: String?, - - val steps: Set? -) : EntityDto - -data class RecipeOutputDto( - override val id: Long, - val name: String, - val description: String, - val color: String, - val gloss: Byte, - val sample: Int?, - val approbationDate: LocalDate?, - val approbationExpired: Boolean?, - val remark: String?, - val company: Company, - val mixes: Set, - val groupsInformation: Set, - var imagesUrls: Set + val groupsInformation: List ) : ModelEntity @Entity @@ -172,133 +57,5 @@ data class RecipeGroupInformation( @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true) @JoinColumn(name = "recipe_group_information_id") - var steps: MutableSet? -) : ModelEntity - -data class RecipeStepsDto( - val groupId: Long, - - val steps: Set -) - -data class RecipePublicDataDto( - val recipeId: Long, - - val notes: Set?, - - val mixesLocation: Set? -) - -data class NoteDto( - val groupId: Long, - - val content: String? -) - -// ==== DSL ==== -fun recipe( - id: Long? = null, - name: String = "name", - description: String = "description", - color: String = "ffffff", - gloss: Byte = 0, - sample: Int? = -1, - approbationDate: LocalDate? = LocalDate.MIN, - remark: String = "remark", - company: Company = company(), - mixes: MutableList = mutableListOf(), - groupsInformation: Set = setOf(), - op: Recipe.() -> Unit = {} -) = Recipe( - id, - name, - description, - color, - gloss, - sample, - approbationDate, - remark, - company, - mixes, - groupsInformation -).apply(op) - -fun recipeSaveDto( - name: String = "name", - description: String = "description", - color: String = "ffffff", - gloss: Byte = 0, - sample: Int? = -1, - approbationDate: LocalDate? = LocalDate.MIN, - remark: String = "remark", - companyId: Long = 0L, - op: RecipeSaveDto.() -> Unit = {} -) = RecipeSaveDto(name, description, color, gloss, sample, approbationDate, remark, companyId).apply(op) - -fun recipeUpdateDto( - id: Long = 0L, - name: String? = "name", - description: String? = "description", - color: String? = "ffffff", - gloss: Byte? = 0, - sample: Int? = -1, - approbationDate: LocalDate? = LocalDate.MIN, - remark: String? = "remark", - steps: Set? = setOf(), - op: RecipeUpdateDto.() -> Unit = {} -) = RecipeUpdateDto(id, name, description, color, gloss, sample, approbationDate, remark, steps).apply(op) - -fun recipeGroupInformation( - id: Long? = null, - group: Group = group(), - note: String? = null, - steps: MutableSet? = mutableSetOf(), - op: RecipeGroupInformation.() -> Unit = {} -) = RecipeGroupInformation(id, group, note, steps).apply(op) - -fun recipePublicDataDto( - recipeId: Long = 0L, - notes: Set? = null, - mixesLocation: Set? = null, - op: RecipePublicDataDto.() -> Unit = {} -) = RecipePublicDataDto(recipeId, notes, mixesLocation).apply(op) - -fun noteDto( - groupId: Long = 0L, - content: String? = "note", - op: NoteDto.() -> Unit = {} -) = NoteDto(groupId, content).apply(op) - -// ==== Exceptions ==== -private const val RECIPE_NOT_FOUND_EXCEPTION_TITLE = "Recipe not found" -private const val RECIPE_ALREADY_EXISTS_EXCEPTION_TITLE = "Recipe already exists" -private const val RECIPE_EXCEPTION_ERROR_CODE = "recipe" - -fun recipeIdNotFoundException(id: Long) = - NotFoundException( - RECIPE_EXCEPTION_ERROR_CODE, - RECIPE_NOT_FOUND_EXCEPTION_TITLE, - "A recipe with the id $id could not be found", - id - ) - -fun recipeIdAlreadyExistsException(id: Long) = - AlreadyExistsException( - RECIPE_EXCEPTION_ERROR_CODE, - RECIPE_ALREADY_EXISTS_EXCEPTION_TITLE, - "A recipe with the id $id already exists", - id - ) - -fun recipeNameAlreadyExistsForCompanyException(name: String, company: Company) = - AlreadyExistsException( - "${RECIPE_EXCEPTION_ERROR_CODE}-company", - RECIPE_ALREADY_EXISTS_EXCEPTION_TITLE, - "A recipe with the name $name already exists for the company ${company.name}", - name, - "name", - mutableMapOf( - "company" to company.name, - "companyId" to company.id!! - ) - ) + var 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 ddc6eac..e71803b 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/RecipeStep.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/RecipeStep.kt @@ -1,6 +1,5 @@ package dev.fyloz.colorrecipesexplorer.model -import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto import javax.persistence.* @Entity @@ -13,9 +12,4 @@ data class RecipeStep( val position: Int, val message: String -) : ModelEntity - -@Deprecated("Temporary DSL for transition") -fun recipeStepDto( - entity: RecipeStep -) = RecipeStepDto(entity.id!!, entity.position, entity.message) \ No newline at end of file +) : ModelEntity \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixRepository.kt index a363199..cd3288b 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixRepository.kt @@ -12,6 +12,6 @@ interface MixRepository : JpaRepository { /** Updates the [location] of the [Mix] with the given [id]. */ @Modifying - @Query("update Mix m set m.location = :location where m.id = :id") + @Query("UPDATE Mix m SET m.location = :location WHERE m.id = :id") fun updateLocationById(id: Long, location: String?) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixTypeRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixTypeRepository.kt index 9e1c7f8..719262d 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixTypeRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixTypeRepository.kt @@ -8,11 +8,16 @@ import org.springframework.stereotype.Repository @Repository interface MixTypeRepository : JpaRepository { /** Checks if a mix type with the given [name], [materialTypeId] and a different [id] exists. */ - @Query("select case when(count(m) > 0) then true else false end from MixType m where m.name = :name and m.material.materialType.id = :materialTypeId and m.id <> :id") + @Query( + """ + SELECT CASE WHEN(COUNT(m) > 0) THEN TRUE ELSE FALSE END + FROM MixType m WHERE m.name = :name AND m.material.materialType.id = :materialTypeId AND m.id <> :id + """ + ) fun existsByNameAndMaterialType(name: String, materialTypeId: Long, id: Long): Boolean /** Finds the mix type with the given [name] and [materialTypeId]. */ - @Query("select m from MixType m where m.name = :name and m.material.materialType.id = :materialTypeId") + @Query("SELECT m FROM MixType m WHERE m.name = :name AND m.material.materialType.id = :materialTypeId") fun findByNameAndMaterialType(name: String, materialTypeId: Long): MixType? /** Checks if a mix depends on the mix type with the given [id]. */ diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/RecipeRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/RecipeRepository.kt index 251374d..2fa7ca5 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/RecipeRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/RecipeRepository.kt @@ -1,19 +1,19 @@ package dev.fyloz.colorrecipesexplorer.repository -import dev.fyloz.colorrecipesexplorer.model.Company import dev.fyloz.colorrecipesexplorer.model.Recipe import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query interface RecipeRepository : JpaRepository { - /** Checks if one or more recipes have the given [company]. */ - fun existsByCompany(company: Company): Boolean - - /** Checks if a recipe exists with the given [name] and [company]. */ - fun existsByNameAndCompany(name: String, company: Company): Boolean + /** Checks if a recipe with the given [name], [companyId] and a different [id] exists. */ + @Query( + """ + SELECT CASE WHEN(COUNT(r) > 0) THEN TRUE ELSE FALSE END + FROM Recipe r WHERE r.name = :name AND r.company.id = :companyId AND r.id <> :id + """ + ) + fun existsByNameAndCompanyAndIdNot(name: String, companyId: Long, id: Long): Boolean /** Gets all recipes with the given [name]. */ fun findAllByName(name: String): Collection - - /** Gets all recipes with the given [company]. */ - fun findAllByCompany(company: Company): Collection } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt index 875a879..44424f7 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt @@ -1,11 +1,14 @@ package dev.fyloz.colorrecipesexplorer.rest +import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditRecipes import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewRecipes -import dev.fyloz.colorrecipesexplorer.logic.MixLogic +import dev.fyloz.colorrecipesexplorer.dtos.RecipeDto +import dev.fyloz.colorrecipesexplorer.dtos.RecipePublicDataDto +import dev.fyloz.colorrecipesexplorer.dtos.RecipeSaveDto +import dev.fyloz.colorrecipesexplorer.dtos.RecipeUpdateDto import dev.fyloz.colorrecipesexplorer.logic.RecipeImageLogic import dev.fyloz.colorrecipesexplorer.logic.RecipeLogic -import dev.fyloz.colorrecipesexplorer.model.* import org.springframework.context.annotation.Profile import org.springframework.http.MediaType import org.springframework.http.ResponseEntity @@ -14,71 +17,63 @@ import org.springframework.web.bind.annotation.* import org.springframework.web.multipart.MultipartFile import javax.validation.Valid - -private const val RECIPE_CONTROLLER_PATH = "api/recipe" - @RestController -@RequestMapping(RECIPE_CONTROLLER_PATH) +@RequestMapping(Constants.ControllerPaths.RECIPE) @Profile("!emergency") @PreAuthorizeViewRecipes -class RecipeController( - private val recipeLogic: RecipeLogic, - private val recipeImageLogic: RecipeImageLogic -) { +class RecipeController(private val recipeLogic: RecipeLogic, private val recipeImageLogic: RecipeImageLogic) { @GetMapping - fun getAll(@RequestParam(required = false) name: String?) = - if (name == null) - ok(recipeLogic.getAllForOutput()) - else - ok(with(recipeLogic) { - getAllByName(name).map { it.toOutput() } - }) + fun getAll(@RequestParam(required = false) name: String?) = ok( + if (name == null) { + recipeLogic.getAll() + } else { + recipeLogic.getAllByName(name) + } + ) @GetMapping("{id}") - fun getById(@PathVariable id: Long) = - ok(recipeLogic.getByIdForOutput(id)) + fun getById(@PathVariable id: Long) = ok(recipeLogic.getById(id)) @PostMapping @PreAuthorizeEditRecipes fun save(@Valid @RequestBody recipe: RecipeSaveDto) = - created(RECIPE_CONTROLLER_PATH) { - with(recipeLogic) { - save(recipe).toOutput() - } + created(Constants.ControllerPaths.RECIPE) { + recipeLogic.save(recipe) } @PutMapping @PreAuthorizeEditRecipes - fun update(@Valid @RequestBody recipe: RecipeUpdateDto) = - noContent { - recipeLogic.update(recipe) - } + fun update(@Valid @RequestBody recipe: RecipeUpdateDto) = noContent { + recipeLogic.update(recipe) + } @PutMapping("public") @PreAuthorize("hasAuthority('EDIT_RECIPES_PUBLIC_DATA')") - fun updatePublicData(@Valid @RequestBody publicDataDto: RecipePublicDataDto) = - noContent { - recipeLogic.updatePublicData(publicDataDto) - } + fun updatePublicData(@Valid @RequestBody publicDataDto: RecipePublicDataDto) = noContent { + recipeLogic.updatePublicData(publicDataDto) + } @DeleteMapping("{id}") @PreAuthorizeEditRecipes - fun deleteById(@PathVariable id: Long) = - noContent { - recipeLogic.deleteById(id) - } + fun deleteById(@PathVariable id: Long) = noContent { + recipeLogic.deleteById(id) + } + + @GetMapping("{recipeId}/image") + fun getAllImages(@PathVariable recipeId: Long) = ok { + recipeImageLogic.getAllImages(recipeId) + } @PutMapping("{recipeId}/image", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) @PreAuthorizeEditRecipes - fun downloadImage(@PathVariable recipeId: Long, image: MultipartFile): ResponseEntity { - recipeImageLogic.download(image, recipeLogic.getById(recipeId)) + fun downloadImage(@PathVariable recipeId: Long, image: MultipartFile): ResponseEntity { + recipeImageLogic.download(image, recipeId) return getById(recipeId) } - @DeleteMapping("{recipeId}/image/{name}") + @DeleteMapping("{recipeId}/image/{path}") @PreAuthorizeEditRecipes - fun deleteImage(@PathVariable recipeId: Long, @PathVariable name: String) = - noContent { - recipeImageLogic.delete(recipeLogic.getById(recipeId), name) - } + fun deleteImage(@PathVariable recipeId: Long, @PathVariable path: String) = noContent { + recipeImageLogic.delete(recipeId, path) + } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt index 5905923..802dd98 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt @@ -26,17 +26,17 @@ class DefaultMixService( MixDto( entity.id!!, entity.location, - entity.recipe, + entity.recipeId, mixTypeService.toDto(entity.mixType), - entity.mixMaterials.map(mixMaterialService::toDto).toSet() + entity.mixMaterials.map(mixMaterialService::toDto) ) override fun toEntity(dto: MixDto) = Mix( dto.id, dto.location, - dto.recipe, + dto.recipeId, mixTypeService.toEntity(dto.mixType), - dto.mixMaterials.map(mixMaterialService::toEntity).toSet() + dto.mixMaterials.map(mixMaterialService::toEntity) ) } \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt new file mode 100644 index 0000000..de556e9 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt @@ -0,0 +1,86 @@ +package dev.fyloz.colorrecipesexplorer.service + +import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent +import dev.fyloz.colorrecipesexplorer.dtos.RecipeDto +import dev.fyloz.colorrecipesexplorer.dtos.RecipeGroupInformationDto +import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic +import dev.fyloz.colorrecipesexplorer.model.ConfigurationType +import dev.fyloz.colorrecipesexplorer.model.Recipe +import dev.fyloz.colorrecipesexplorer.model.RecipeGroupInformation +import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository +import dev.fyloz.colorrecipesexplorer.utils.collections.lazyMap +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.time.Period + +interface RecipeService : Service { + /** Checks if a recipe with the given [name], [companyId] and a different [id] exists. */ + fun existsByNameAndCompany(name: String, companyId: Long, id: Long?): Boolean + + /** Gets all recipes with the given [name]. */ + fun getAllByName(name: String): Collection +} + +@ServiceComponent +class DefaultRecipeService( + repository: RecipeRepository, + private val companyService: CompanyService, + private val mixService: MixService, + private val recipeStepService: RecipeStepService, + private val configLogic: ConfigurationLogic +) : + BaseService(repository), RecipeService { + override fun existsByNameAndCompany(name: String, companyId: Long, id: Long?) = + repository.existsByNameAndCompanyAndIdNot(name, companyId, id ?: 0L) + + override fun getAllByName(name: String) = + repository.findAllByName(name).map(::toDto) + + @Transactional + override fun toDto(entity: Recipe) = + RecipeDto( + entity.id!!, + entity.name, + entity.description, + entity.color, + entity.gloss, + entity.sample, + entity.approbationDate, + isApprobationExpired(entity) ?: false, + entity.remark, + companyService.toDto(entity.company), + entity.mixes.lazyMap(mixService::toDto), + entity.groupsInformation.lazyMap(::groupInformationToDto) + ) + + private fun groupInformationToDto(entity: RecipeGroupInformation) = + RecipeGroupInformationDto( + entity.id!!, + entity.group, + entity.note, + entity.steps?.lazyMap(recipeStepService::toDto) ?: listOf() + ) + + override fun toEntity(dto: RecipeDto) = + Recipe( + dto.id, + dto.name, + dto.description, + dto.color, + dto.gloss, + dto.sample, + dto.approbationDate, + dto.remark, + companyService.toEntity(dto.company), + dto.mixes.map(mixService::toEntity), + dto.groupsInformation.map(::groupInformationToEntity) + ) + + private fun groupInformationToEntity(dto: RecipeGroupInformationDto) = + RecipeGroupInformation(dto.id, dto.group, dto.note, dto.steps.map(recipeStepService::toEntity)) + + private fun isApprobationExpired(recipe: Recipe): Boolean? = + with(Period.parse(configLogic.getContent(ConfigurationType.RECIPE_APPROBATION_EXPIRATION))) { + recipe.approbationDate?.plus(this)?.isBefore(LocalDate.now()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt index 8c4d50b..5ce54ee 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt @@ -60,4 +60,4 @@ fun Iterable.merge(other: Iterable, keyMapper: (T) -> K) = this.associateBy { keyMapper(it) } .filter { pair -> other.all { keyMapper(it) != pair.key } } .map { it.value } - .plus(other) + .plus(other) \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/collections/LazyMapList.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/collections/LazyMapList.kt new file mode 100644 index 0000000..7863c65 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/collections/LazyMapList.kt @@ -0,0 +1,26 @@ +package dev.fyloz.colorrecipesexplorer.utils.collections + +class LazyMapList(private val sourceList: List, private val transform: (T) -> R) : List { + private val list by lazy { sourceList.map(transform) } + + fun initialize() { + // Call a property so the list is initialized + size + } + + override val size: Int + get() = list.size + + override fun contains(element: R) = list.contains(element) + override fun containsAll(elements: Collection) = list.containsAll(elements) + override fun get(index: Int) = list[index] + override fun indexOf(element: R) = list.indexOf(element) + override fun isEmpty() = list.isEmpty() + override fun iterator() = list.iterator() + override fun lastIndexOf(element: R) = list.lastIndexOf(element) + override fun listIterator() = list.listIterator() + override fun listIterator(index: Int) = list.listIterator(index) + override fun subList(fromIndex: Int, toIndex: Int) = list.subList(fromIndex, toIndex) +} + +fun List.lazyMap(transform: (T) -> R) = LazyMapList(this, transform) \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultInventoryLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultInventoryLogicTest.kt index d5496d7..c39a91b 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultInventoryLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultInventoryLogicTest.kt @@ -1,8 +1,6 @@ package dev.fyloz.colorrecipesexplorer.logic import dev.fyloz.colorrecipesexplorer.dtos.* -import dev.fyloz.colorrecipesexplorer.model.Recipe -import dev.fyloz.colorrecipesexplorer.model.company import io.mockk.* import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test @@ -87,7 +85,7 @@ class DefaultInventoryLogicTest { fun deductMix_normalBehavior_callsDeductWithMixMaterials() { // Arrange val company = CompanyDto(1L, "Unit test company") - val recipe = Recipe( + val recipe = RecipeDto( 1L, "Unit test recipe", "Unit test recipe", @@ -95,14 +93,15 @@ class DefaultInventoryLogicTest { 0xf, null, null, + false, "Remark", - company(company), + company, mutableListOf(), - setOf() + listOf() ) val mixType = MixTypeDto(1L, "Unit test mix type", material) val mixMaterial = MixMaterialDto(1L, material, 1000f, 1) - val mix = MixDto(1L, null, recipe, mixType, setOf(mixMaterial)) + val mix = MixDto(1L, null, recipe.id, mixType, listOf(mixMaterial)) val dto = MixDeductDto(mix.id, 2f) val expectedQuantities = listOf(MaterialQuantityDto(material.id, mixMaterial.quantity * dto.ratio)) @@ -123,7 +122,7 @@ class DefaultInventoryLogicTest { fun deductMix_normalBehavior_returnsFromDeduct() { // Arrange val company = CompanyDto(1L, "Unit test company") - val recipe = Recipe( + val recipe = RecipeDto( 1L, "Unit test recipe", "Unit test recipe", @@ -131,14 +130,15 @@ class DefaultInventoryLogicTest { 0xf, null, null, + false, "Remark", - company(company), + company, mutableListOf(), - setOf() + listOf() ) val mixType = MixTypeDto(1L, "Unit test mix type", material) val mixMaterial = MixMaterialDto(1L, material, 1000f, 1) - val mix = MixDto(1L, null, recipe, mixType, setOf(mixMaterial)) + val mix = MixDto(1L, null, recipe.id, mixType, listOf(mixMaterial)) val dto = MixDeductDto(mix.id, 2f) val expectedQuantities = listOf(MaterialQuantityDto(material.id, mixMaterial.quantity * dto.ratio)) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialLogicTest.kt index 1238efe..82e8a5d 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialLogicTest.kt @@ -4,10 +4,7 @@ import dev.fyloz.colorrecipesexplorer.dtos.* import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic -import dev.fyloz.colorrecipesexplorer.model.Company import dev.fyloz.colorrecipesexplorer.model.Material -import dev.fyloz.colorrecipesexplorer.model.Recipe -import dev.fyloz.colorrecipesexplorer.model.mix import dev.fyloz.colorrecipesexplorer.service.MaterialService import io.mockk.* import org.junit.jupiter.api.AfterEach @@ -37,8 +34,8 @@ class DefaultMaterialLogicTest { private val material = MaterialDto(1L, "Unit test material", 1000f, false, materialType) private val materialMixType = material.copy(id = 2L, isMixType = true) private val materialMixType2 = material.copy(id = 3L, isMixType = true) - private val company = Company(1L, "Unit test company") - private val recipe = Recipe( + private val company = CompanyDto(1L, "Unit test company") + private val recipe = RecipeDto( 1L, "Unit test recipe", "Unit test recipe", @@ -46,13 +43,14 @@ class DefaultMaterialLogicTest { 0, 123, null, + false, "A remark", company, mutableListOf(), - setOf() + listOf() ) private val mix = MixDto( - 1L, "location", recipe, mixType = MixTypeDto(1L, "Unit test mix type", materialMixType), mutableSetOf() + 1L, "location", recipe.id, mixType = MixTypeDto(1L, "Unit test mix type", materialMixType), listOf() ) private val mix2 = mix.copy(id = 2L, mixType = mix.mixType.copy(id = 2L, material = materialMixType2)) @@ -62,10 +60,6 @@ class DefaultMaterialLogicTest { ) // Put some content in the mock file, so it is not ignored private val materialSaveDto = MaterialSaveDto(1L, "Unit test material", 1000f, materialType.id, simdutFileMock) - init { - recipe.mixes.addAll(listOf(mix(mix), mix(mix2))) - } - @AfterEach internal fun afterEach() { clearAllMocks() @@ -114,7 +108,7 @@ class DefaultMaterialLogicTest { every { recipeLogicMock.getById(any()) } returns recipe // Act - val materials = materialLogic.getAllForMixCreation(recipe.id!!) + val materials = materialLogic.getAllForMixCreation(recipe.id) // Assert assertContains(materials, material) @@ -123,11 +117,13 @@ class DefaultMaterialLogicTest { @Test fun getAllForMixCreation_normalBehavior_returnsRecipeMixTypesMaterials() { // Arrange + val recipe = recipe.copy(mixes = listOf(mix, mix2)) + every { materialLogic.getAll() } returns listOf(material, materialMixType2) every { recipeLogicMock.getById(any()) } returns recipe // Act - val materials = materialLogic.getAllForMixCreation(recipe.id!!) + val materials = materialLogic.getAllForMixCreation(recipe.id) // Assert assertContains(materials, materialMixType2) @@ -137,6 +133,7 @@ class DefaultMaterialLogicTest { fun getAllForMixUpdate_normalBehavior_returnsNonMixTypeMaterials() { // Arrange every { materialLogic.getAll() } returns listOf(material, materialMixType, materialMixType2) + every { recipeLogicMock.getById(any()) } returns recipe every { mixLogicMock.getById(any()) } returns mix // Act @@ -149,7 +146,11 @@ class DefaultMaterialLogicTest { @Test fun getAllForMixUpdate_normalBehavior_returnsRecipeMixTypesMaterials() { // Arrange + val recipe = recipe.copy(mixes = listOf(mix, mix2)) + val mix = mix.copy(recipeId = recipe.id) + every { materialLogic.getAll() } returns listOf(material, materialMixType, materialMixType2) + every { recipeLogicMock.getById(any()) } returns recipe every { mixLogicMock.getById(any()) } returns mix // Act @@ -163,10 +164,11 @@ class DefaultMaterialLogicTest { fun getAllForMixUpdate_normalBehavior_excludesGivenMixTypeMaterial() { // Arrange every { materialLogic.getAll() } returns listOf(material, materialMixType, materialMixType2) + every { recipeLogicMock.getById(any()) } returns recipe every { mixLogicMock.getById(any()) } returns mix // Act - val materials = materialLogic.getAllForMixUpdate(mix.id!!) + val materials = materialLogic.getAllForMixUpdate(mix.id) // Assert assertFalse { materialMixType in materials } diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixLogicTest.kt index 1579f71..33204fe 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixLogicTest.kt @@ -1,8 +1,6 @@ package dev.fyloz.colorrecipesexplorer.logic import dev.fyloz.colorrecipesexplorer.dtos.* -import dev.fyloz.colorrecipesexplorer.model.Company -import dev.fyloz.colorrecipesexplorer.model.Recipe import dev.fyloz.colorrecipesexplorer.service.MixService import io.mockk.* import org.junit.jupiter.api.AfterEach @@ -25,8 +23,8 @@ class DefaultMixLogicTest { ) ) - private val company = Company(1L, "Unit test company") - private val recipe = Recipe( + private val company = CompanyDto(1L, "Unit test company") + private val recipe = RecipeDto( 1L, "Unit test recipe", "Unit test recipe", @@ -34,17 +32,18 @@ class DefaultMixLogicTest { 0xf, null, null, + false, "A remark", company, mutableListOf(), - setOf() + listOf() ) private val materialType = MaterialTypeDto(1L, "Unit test material type", "UTMT", false) private val mixType = MixTypeDto(1L, "Unit test mix type", MaterialDto(1L, "Unit test mix type material", 1000f, true, materialType)) private val mixMaterial = MixMaterialDto(1L, MaterialDto(2L, "Unit test material", 1000f, false, materialType), 50f, 1) - private val mix = MixDto(recipe = recipe, mixType = mixType, mixMaterials = setOf(mixMaterial)) + private val mix = MixDto(recipeId = recipe.id, mixType = mixType, mixMaterials = listOf(mixMaterial)) @AfterEach internal fun afterEach() { @@ -56,7 +55,6 @@ class DefaultMixLogicTest { every { materialTypeLogicMock.getById(any()) } returns materialType every { mixTypeLogicMock.getOrCreateForNameAndMaterialType(any(), any()) } returns mixType every { mixMaterialLogicMock.validateAndSaveAll(any()) } returns listOf(mixMaterial) - every { recipeLogicMock.addMix(any(), any()) } returns recipe every { mixLogic.save(any()) } returnsArgument 0 } @@ -76,7 +74,7 @@ class DefaultMixLogicTest { val mixMaterialDto = MixMaterialSaveDto(mixMaterial.id, mixMaterial.material.id, mixMaterial.quantity, mixMaterial.position) - val saveDto = MixSaveDto(0L, mixType.name, recipe.id!!, materialType.id, setOf(mixMaterialDto)) + val saveDto = MixSaveDto(0L, mixType.name, recipe.id, materialType.id, listOf(mixMaterialDto)) // Act mixLogic.save(saveDto) @@ -93,7 +91,7 @@ class DefaultMixLogicTest { setup_save_normalBehavior() val mixMaterialDtos = - setOf( + listOf( MixMaterialSaveDto( mixMaterial.id, mixMaterial.material.id, @@ -101,7 +99,7 @@ class DefaultMixLogicTest { mixMaterial.position ) ) - val saveDto = MixSaveDto(0L, mixType.name, recipe.id!!, materialType.id, mixMaterialDtos) + val saveDto = MixSaveDto(0L, mixType.name, recipe.id, materialType.id, mixMaterialDtos) // Act mixLogic.save(saveDto) @@ -120,7 +118,7 @@ class DefaultMixLogicTest { val mixMaterialDto = MixMaterialSaveDto(mixMaterial.id, mixMaterial.material.id, mixMaterial.quantity, mixMaterial.position) - val saveDto = MixSaveDto(mix.id, mixType.name, recipe.id!!, materialType.id, setOf(mixMaterialDto)) + val saveDto = MixSaveDto(mix.id, mixType.name, recipe.id, materialType.id, listOf(mixMaterialDto)) // Act mixLogic.update(saveDto) @@ -136,7 +134,7 @@ class DefaultMixLogicTest { // Arrange setup_update_normalBehavior() - val mixMaterialDtos = setOf( + val mixMaterialDtos = listOf( MixMaterialSaveDto( mixMaterial.id, mixMaterial.material.id, @@ -144,7 +142,7 @@ class DefaultMixLogicTest { mixMaterial.position ) ) - val saveDto = MixSaveDto(mix.id, mixType.name, recipe.id!!, materialType.id, mixMaterialDtos) + val saveDto = MixSaveDto(mix.id, mixType.name, recipe.id, materialType.id, mixMaterialDtos) // Act mixLogic.update(saveDto) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeImageLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeImageLogicTest.kt new file mode 100644 index 0000000..7ab069c --- /dev/null +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeImageLogicTest.kt @@ -0,0 +1,101 @@ +package dev.fyloz.colorrecipesexplorer.logic + +import dev.fyloz.colorrecipesexplorer.Constants +import dev.fyloz.colorrecipesexplorer.logic.files.CachedFile +import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic +import dev.fyloz.colorrecipesexplorer.utils.FilePath +import io.mockk.* +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.springframework.mock.web.MockMultipartFile +import kotlin.test.assertEquals + +class DefaultRecipeImageLogicTest { + private val fileLogicMock = mockk() + + private val recipeImageLogic = spyk(DefaultRecipeImageLogic(fileLogicMock)) + + private val recipeId = 1L + + @AfterEach + internal fun afterEach() { + clearAllMocks() + } + + @Test + fun getAllImages_normalBehavior_returnsAllRecipeImages() { + // Arrange + val filePath = FilePath("${Constants.FilePaths.RECIPE_IMAGES}/$recipeId") + val files = listOf( + CachedFile("0", filePath, true), + CachedFile("1", filePath, true) + ) + val expectedImages = files.map { it.name } + + every { fileLogicMock.listDirectoryFiles(any()) } returns files + + // Act + val actualImages = recipeImageLogic.getAllImages(recipeId) + + // Assert + assertEquals(expectedImages, actualImages) + } + + @Test + fun download_normalBehavior_callsWriteToDirectoryInFileLogic() { + // Arrange + val previousImageId = 0L + + every { recipeImageLogic.getAllImages(recipeId) } returns listOf(previousImageId.toString()) + every { fileLogicMock.writeToDirectory(any(), any(), any(), any()) } just runs + + val file = MockMultipartFile("Unit test name", byteArrayOf()) + + val expectedFilePath = "${Constants.FilePaths.RECIPE_IMAGES}/$recipeId" + val expectedImageId = previousImageId + 1 + + // Act + recipeImageLogic.download(file, recipeId) + + // Assert + verify { + fileLogicMock.writeToDirectory(file, "$expectedFilePath/$expectedImageId", expectedFilePath, true) + } + } + + @Test + fun download_normalBehavior_returnsImageId() { + // Arrange + val previousImageId = 0L + + every { recipeImageLogic.getAllImages(recipeId) } returns listOf(previousImageId.toString()) + every { fileLogicMock.writeToDirectory(any(), any(), any(), any()) } just runs + + val file = MockMultipartFile("Unit test name", byteArrayOf()) + + val expectedImageId = previousImageId + 1 + + // Act + val downloadedImageId = recipeImageLogic.download(file, recipeId) + + // Assert + assertEquals(expectedImageId.toString(), downloadedImageId) + } + + @Test + fun delete_normalBehavior_callsDeleteFromDirectoryInFileLogic() { + // Arrange + every { fileLogicMock.deleteFromDirectory(any(), any()) } just runs + + val recipeImagesDirectoryPath = "${Constants.FilePaths.RECIPE_IMAGES}/$recipeId" + val imagePath = "$recipeImagesDirectoryPath/1" + + // Act + recipeImageLogic.delete(recipeId, imagePath) + + // Assert + verify { + fileLogicMock.deleteFromDirectory(imagePath, recipeImagesDirectoryPath) + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeLogicTest.kt new file mode 100644 index 0000000..cdc0ae8 --- /dev/null +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeLogicTest.kt @@ -0,0 +1,217 @@ +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 +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertContains +import kotlin.test.assertEquals + +class DefaultRecipeLogicTest { + private val recipeServiceMock = mockk() + private val companyLogicMock = mockk() + private val recipeStepLogicMock = mockk() + private val mixLogicMock = mockk() + private val groupLogicMock = mockk() + + private val recipeLogic = + 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 recipe = RecipeDto( + 1L, + "Unit test recipe", + "Unit test recipe", + "FFFFFF", + 0xf, + null, + null, + false, + "Remark", + company, + listOf(), + listOf() + ) + + @AfterEach + internal fun afterEach() { + clearAllMocks() + } + + @Test + fun getAllByName_normalBehavior_returnsFromService() { + // Arrange + val expectedRecipes = listOf(recipe) + + every { recipeServiceMock.getAllByName(any()) } returns expectedRecipes + + // Act + val actualRecipes = recipeLogic.getAllByName(recipe.name) + + // Assert + assertEquals(actualRecipes, expectedRecipes) + } + + @Test + fun save_dto_normalBehavior_returnsFromSave() { + // Arrange + every { recipeServiceMock.existsByNameAndCompany(any(), any(), any()) } returns false + every { companyLogicMock.getById(any()) } returns company + every { recipeLogic.save(any()) } returns recipe + + val dto = RecipeSaveDto( + recipe.name, + recipe.description, + recipe.color, + recipe.gloss, + recipe.sample, + recipe.approbationDate, + recipe.remark, + company.id + ) + + // Act + val savedRecipe = recipeLogic.save(dto) + + // Assert + assertEquals(recipe, savedRecipe) + } + + @Test + fun save_nameAndCompanyExists_throwsAlreadyExistsException() { + // Arrange + every { recipeServiceMock.existsByNameAndCompany(any(), any(), any()) } returns true + + // Act + // Assert + assertThrows { recipeLogic.save(recipe) } + } + + @Test + fun update_dto_normalBehavior_returnsFromSave() { + // Arrange + every { recipeServiceMock.existsByNameAndCompany(any(), any(), any()) } returns false + every { recipeServiceMock.getById(any()) } returns recipe + every { companyLogicMock.getById(any()) } returns company + every { recipeLogic.update(any()) } returns recipe + + val dto = RecipeUpdateDto( + recipe.id, + recipe.name, + recipe.description, + recipe.color, + recipe.gloss, + recipe.sample, + recipe.approbationDate, + recipe.remark, + listOf() + ) + + // Act + val updatedRecipe = recipeLogic.update(dto) + + // Assert + assertEquals(recipe, updatedRecipe) + } + + @Test + fun update_nameAndCompanyExists_throwsAlreadyExistsException() { + // Arrange + every { recipeServiceMock.existsByNameAndCompany(any(), any(), any()) } returns true + + // Act + // Assert + assertThrows { recipeLogic.update(recipe) } + } + + @Test + fun updatePublicData_normalBehavior_callsUpdate() { + // Arrange + every { recipeLogic.getById(any()) } returns recipe + every { recipeLogic.update(any()) } returnsArgument 0 + every { groupLogicMock.getById(any()) } returns group + + val groupNote = RecipeGroupNoteDto(1L, "Unit test note") + val dto = RecipePublicDataDto(recipe.id, listOf(groupNote), listOf()) + + // Act + recipeLogic.updatePublicData(dto) + + // Assert + verify { + recipeLogic.update(any()) + } + } + + @Test + fun updatePublicData_normalBehavior_updatesRecipeGroupsInformation() { + // Arrange + var updatedRecipe = recipe + + every { recipeLogic.getById(any()) } returns recipe + every { recipeLogic.update(any()) } answers { firstArg().also { updatedRecipe = it } } + every { groupLogicMock.getById(any()) } returns group + + val expectedGroupInformation = RecipeGroupInformationDto(0L, group, "Unit test note", listOf()) + + val groupNote = RecipeGroupNoteDto(group.id!!, expectedGroupInformation.note) + val dto = RecipePublicDataDto(recipe.id, listOf(groupNote), listOf()) + + // Act + recipeLogic.updatePublicData(dto) + + // Assert + assertContains(updatedRecipe.groupsInformation, expectedGroupInformation) + } + + @Test + fun updatePublicData_emptyNotes_doesNothing() { + // Arrange + val dto = RecipePublicDataDto(recipe.id, listOf(), listOf()) + + // Act + recipeLogic.updatePublicData(dto) + + // Assert + verify(exactly = 0) { + recipeLogic.update(any()) + } + } + + @Test + fun updatePublicData_normalBehavior_callsUpdateLocationsInMixLogic() { + // Arrange + every { mixLogicMock.updateLocations(any()) } just runs + + val mixesLocation = listOf(MixLocationDto(group.id!!, "location")) + val dto = RecipePublicDataDto(recipe.id, listOf(), mixesLocation) + + // Act + recipeLogic.updatePublicData(dto) + + // Assert + verify { + mixLogicMock.updateLocations(mixesLocation) + } + } + + @Test + fun updatePublicData_emptyMixesLocation_doesNothing() { + // Arrange + val dto = RecipePublicDataDto(recipe.id, listOf(), listOf()) + + // Act + recipeLogic.updatePublicData(dto) + + // Assert + verify(exactly = 0) { + mixLogicMock.updateLocations(any()) + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeStepLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeStepLogicTest.kt index c9defbc..d24ef2f 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeStepLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeStepLogicTest.kt @@ -1,9 +1,9 @@ package dev.fyloz.colorrecipesexplorer.logic +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.RecipeGroupInformation -import dev.fyloz.colorrecipesexplorer.model.RecipeStep import dev.fyloz.colorrecipesexplorer.model.account.Group import dev.fyloz.colorrecipesexplorer.service.RecipeStepService import dev.fyloz.colorrecipesexplorer.utils.PositionUtils @@ -29,8 +29,8 @@ class DefaultRecipeStepLogicTest { every { PositionUtils.validate(any()) } just runs val group = Group(1L, "Unit test group") - val steps = mutableSetOf(RecipeStep(1L, 1, "A message")) - val groupInfo = RecipeGroupInformation(1L, group, "A note", steps) + val steps = listOf(RecipeStepDto(1L, 1, "A message")) + val groupInfo = RecipeGroupInformationDto(1L, group, "A note", steps) // Act recipeStepLogic.validateGroupInformationSteps(groupInfo) @@ -41,24 +41,6 @@ class DefaultRecipeStepLogicTest { } } - @Test - fun validateGroupInformationSteps_stepSetIsNull_doesNothing() { - // Arrange - mockkObject(PositionUtils) - every { PositionUtils.validate(any()) } just runs - - val group = Group(1L, "Unit test group") - val groupInfo = RecipeGroupInformation(1L, group, "A note", null) - - // Act - recipeStepLogic.validateGroupInformationSteps(groupInfo) - - // Assert - verify(exactly = 0) { - PositionUtils.validate(any()) - } - } - @Test fun validateGroupInformationSteps_invalidSteps_throwsInvalidGroupStepsPositionsException() { // Arrange @@ -68,8 +50,8 @@ class DefaultRecipeStepLogicTest { every { PositionUtils.validate(any()) } throws InvalidPositionsException(errors) val group = Group(1L, "Unit test group") - val steps = mutableSetOf(RecipeStep(1L, 1, "A message")) - val groupInfo = RecipeGroupInformation(1L, group, "A note", steps) + val steps = listOf(RecipeStepDto(1L, 1, "A message")) + val groupInfo = RecipeGroupInformationDto(1L, group, "A note", steps) // Act // Assert diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogicTest.kt deleted file mode 100644 index 53c34be..0000000 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogicTest.kt +++ /dev/null @@ -1,371 +0,0 @@ -package dev.fyloz.colorrecipesexplorer.logic - -import com.nhaarman.mockitokotlin2.* -import dev.fyloz.colorrecipesexplorer.dtos.MixLocationDto -import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException -import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic -import dev.fyloz.colorrecipesexplorer.logic.files.CachedFile -import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic -import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic -import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.model.account.group -import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository -import dev.fyloz.colorrecipesexplorer.utils.FilePath -import io.mockk.* -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance -import org.junit.jupiter.api.assertThrows -import org.springframework.mock.web.MockMultipartFile -import org.springframework.web.multipart.MultipartFile -import java.time.LocalDate -import java.time.Period -import kotlin.test.* - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class RecipeLogicTest : - AbstractExternalModelServiceTest() { - override val repository: RecipeRepository = mock() - private val companyLogic: CompanyLogic = mock() - private val mixService: MixLogic = mock() - private val groupService: GroupLogic = mock() - private val recipeStepService: RecipeStepLogic = mock() - private val configService: ConfigurationLogic = mock() - override val logic: RecipeLogic = - spy( - DefaultRecipeLogic( - repository, - companyLogic, - mixService, - recipeStepService, - groupService, - mock(), - configService - ) - ) - - private val company: Company = company(id = 0L) - override val entity: Recipe = recipe(id = 0L, name = "recipe", company = company) - override val anotherEntity: Recipe = recipe(id = 1L, name = "another recipe", company = company) - override val entitySaveDto: RecipeSaveDto = spy(recipeSaveDto(name = entity.name, companyId = entity.company.id!!)) - override val entityUpdateDto: RecipeUpdateDto = spy(recipeUpdateDto(id = entity.id!!, name = entity.name)) - - @AfterEach - override fun afterEach() { - reset(companyLogic, mixService) - super.afterEach() - } - - // existsByCompany() - - @Test - fun `existsByCompany() returns true when at least one recipe exists for the given company`() { - whenever(repository.existsByCompany(company)).doReturn(true) - - val found = logic.existsByCompany(company) - - assertTrue(found) - } - - @Test - fun `existsByCompany() returns false when no recipe exists for the given company`() { - whenever(repository.existsByCompany(company)).doReturn(false) - - val found = logic.existsByCompany(company) - - assertFalse(found) - } - - // existsByNameAndCompany() - - @Test - fun `existsByNameAndCompany() returns if a recipe exists for the given name and company in the repository`() { - setOf(true, false).forEach { - whenever(repository.existsByNameAndCompany(entity.name, company)).doReturn(it) - - val exists = logic.existsByNameAndCompany(entity.name, company) - - assertEquals(it, exists) - } - } - - // isApprobationExpired() - - @Test - fun `isApprobationExpired() returns false when the approbation date of the given recipe is within the configured period`() { - val period = Period.ofMonths(4) - val recipe = recipe(approbationDate = LocalDate.now()) - - whenever(configService.getContent(ConfigurationType.RECIPE_APPROBATION_EXPIRATION)).doReturn(period.toString()) - - val approbationExpired = logic.isApprobationExpired(recipe) - - assertNotNull(approbationExpired) - assertFalse(approbationExpired) - } - - @Test - fun `isApprobationExpired() returns true when the approbation date of the given recipe is outside the configured period`() { - val period = Period.ofMonths(4) - val recipe = recipe(approbationDate = LocalDate.now().minus(period).minusMonths(1)) - - whenever(configService.getContent(ConfigurationType.RECIPE_APPROBATION_EXPIRATION)).doReturn(period.toString()) - - val approbationExpired = logic.isApprobationExpired(recipe) - - assertNotNull(approbationExpired) - assertTrue(approbationExpired) - } - - @Test - fun `isApprobationExpired() returns null when the given recipe as no approbation date`() { - val period = Period.ofMonths(4) - val recipe = recipe(approbationDate = null) - - whenever(configService.getContent(ConfigurationType.RECIPE_APPROBATION_EXPIRATION)).doReturn(period.toString()) - - val approbationExpired = logic.isApprobationExpired(recipe) - - assertNull(approbationExpired) - } - - // getAllByName() - - @Test - fun `getAllByName() returns the recipes with the given name`() { - val recipes = listOf(entity, anotherEntity) - - whenever(repository.findAllByName(entity.name)).doReturn(recipes) - - val found = logic.getAllByName(entity.name) - - assertEquals(recipes, found) - } - - // getAllByCompany() - - @Test - fun `getAllByCompany() returns the recipes with the given company`() { - val companies = listOf(entity, anotherEntity) - whenever(repository.findAllByCompany(company)).doReturn(companies) - - val found = logic.getAllByCompany(company) - - assertEquals(companies, found) - } - - // save() - - @Test - override fun `save(dto) calls and returns save() with the created entity`() { - whenever(companyLogic.getById(company.id!!)).doReturn(companyDto(company)) - doReturn(false).whenever(logic).existsByNameAndCompany(entity.name, company) - withBaseSaveDtoTest(entity, entitySaveDto, logic, { argThat { this.id == null && this.color == color } }) - } - - @Test - fun `save(dto) throw AlreadyExistsException when a recipe with the given name and company exists in the repository`() { - whenever(companyLogic.getById(company.id!!)).doReturn(companyDto(company)) - doReturn(true).whenever(logic).existsByNameAndCompany(entity.name, company) - - with(assertThrows { logic.save(entitySaveDto) }) { - this.assertErrorCode("company-name") - } - } - - // update() - - @Test - override fun `update(dto) calls and returns update() with the created entity`() { - doReturn(false).whenever(logic).existsByNameAndCompany(entity.name, company) - withBaseUpdateDtoTest(entity, entityUpdateDto, logic, { any() }) - } - - @Test - fun `update(dto) throws AlreadyExistsException when a recipe exists for the given name and company`() { - val name = "another recipe" - - doReturn(entity).whenever(logic).getById(entity.id!!) - doReturn(true).whenever(logic).existsByNameAndCompany(name, company) - doReturn(name).whenever(entityUpdateDto).name - - with(assertThrows { logic.update(entityUpdateDto) }) { - this.assertErrorCode("company-name") - } - } - - // updatePublicData() - - @Test - fun `updatePublicData() updates the notes of a recipe groups information according to the RecipePublicDataDto`() { - val recipe = recipe( - id = 0L, groupsInformation = setOf( - recipeGroupInformation(id = 0L, group = group(id = 1L), note = "Old note"), - recipeGroupInformation(id = 1L, group = group(id = 2L), note = "Another note"), - recipeGroupInformation(id = 2L, group = group(id = 3L), note = "Up to date note") - ) - ) - val notes = setOf( - noteDto(groupId = 1, content = "Note 1"), - noteDto(groupId = 2, content = null) - ) - val publicData = recipePublicDataDto(recipeId = recipe.id!!, notes = notes) - - doReturn(recipe).whenever(logic).getById(recipe.id!!) - doAnswer { it.arguments[0] }.whenever(logic).update(any()) - - logic.updatePublicData(publicData) - - verify(logic).update(argThat { - assertTrue { this.groupsInformation.first { it.group.id == 1L }.note == notes.first { it.groupId == 1L }.content } - assertTrue { this.groupsInformation.first { it.group.id == 2L }.note == notes.first { it.groupId == 2L }.content } - assertTrue { this.groupsInformation.any { it.group.id == 3L } && this.groupsInformation.first { it.group.id == 3L }.note == null } - true - }) - verify(mixService, times(0)).updateLocations(any()) - } - - @Test - fun `updatePublicData() update the location of a recipe mixes in the mix logic according to the RecipePublicDataDto`() { - val publicData = recipePublicDataDto( - mixesLocation = setOf( - MixLocationDto(mixId = 0L, location = "Loc 1"), - MixLocationDto(mixId = 1L, location = "Loc 2") - ) - ) - - logic.updatePublicData(publicData) - - verify(mixService).updateLocations(publicData.mixesLocation!!) - verify(logic, times(0)).update(any()) - } - - // addMix() - - @Test - fun `addMix() adds the given mix to the given recipe and updates it`() { - val mix = mix(id = 0L) - val recipe = recipe(id = 0L, mixes = mutableListOf()) - - doAnswer { it.arguments[0] }.whenever(logic).update(any()) - - val found = logic.addMix(recipe, mix) - - verify(logic).update(any()) - - assertEquals(recipe.id, found.id) - assertTrue(found.mixes.contains(mix)) - } - - // removeMix() - - @Test - fun `removeMix() removes the given mix from its recipe and updates it`() { - val recipe = recipe(id = 0L, mixes = mutableListOf()) - val mix = mix(id = 0L, recipe = recipe) - recipe.mixes.add(mix) - - doAnswer { it.arguments[0] }.whenever(logic).update(any()) - - val found = logic.removeMix(mix) - - verify(logic).update(any()) - - assertEquals(recipe.id, found.id) - assertFalse(found.mixes.contains(mix)) - } -} - -private class RecipeImageServiceTestContext { - val fileService = mockk { - every { write(any(), any(), any()) } just Runs - every { delete(any()) } just Runs - } - val recipeImageService = spyk(DefaultRecipeImageLogic(fileService)) - val recipe = spyk(recipe()) - val recipeImagesIds = setOf(1L, 10L, 21L) - val recipeImagesNames = recipeImagesIds.map { it.imageName }.toSet() - val recipeImagesFiles = recipeImagesNames.map { CachedFile(it, FilePath(it), true) } - - val Long.imageName - get() = "${recipe.name}$RECIPE_IMAGE_ID_DELIMITER$this" - - val String.imagePath - get() = "${recipe.imagesDirectoryPath}/$this$RECIPE_IMAGE_EXTENSION" -} - -class RecipeImageLogicTest { - @AfterEach - internal fun afterEach() { - clearAllMocks() - } - - private fun test(test: RecipeImageServiceTestContext.() -> Unit) { - RecipeImageServiceTestContext().test() - } - - // getAllImages() - - @Test - fun `getAllImages() returns a Set containing the name of every files in the recipe's directory`() { - test { - every { fileService.listDirectoryFiles(any()) } returns recipeImagesFiles - - val foundImagesNames = recipeImageService.getAllImages(recipe) - - assertEquals(recipeImagesNames, foundImagesNames) - } - } - - @Test - fun `getAllImages() returns an empty Set when the recipe's directory does not exists`() { - test { - every { fileService.listDirectoryFiles(any()) } returns emptySet() - - assertTrue { - recipeImageService.getAllImages(recipe).isEmpty() - } - } - } - - // download() - - @Test - fun `download() writes the given image to the FileService and returns its name`() { - test { - val mockImage = MockMultipartFile("image.jpg", byteArrayOf(*"Random data".encodeToByteArray())) - val expectedImageId = recipeImagesIds.maxOrNull()!! + 1L - val expectedImageName = expectedImageId.imageName - val expectedImagePath = expectedImageName.imagePath - - every { fileService.listDirectoryFiles(any()) } returns recipeImagesFiles - every { fileService.writeToDirectory(any(), any(), any(), any()) } just runs - - val foundImageName = recipeImageService.download(mockImage, recipe) - - assertEquals(expectedImageName, foundImageName) - - verify { - fileService.writeToDirectory(mockImage, expectedImagePath, any(), true) - } - } - } - - // delete() - - @Test - fun `delete() deletes the image with the given name in the FileService`() { - test { - val imageName = recipeImagesIds.first().imageName - val imagePath = imageName.imagePath - - every { fileService.deleteFromDirectory(any(), any()) } just runs - - recipeImageService.delete(recipe, imageName) - - verify { - fileService.deleteFromDirectory(imagePath, any()) - } - } - } -} diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/repository/MaterialRepositoryTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/repository/MaterialRepositoryTest.kt deleted file mode 100644 index fa622f0..0000000 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/repository/MaterialRepositoryTest.kt +++ /dev/null @@ -1,29 +0,0 @@ -package dev.fyloz.colorrecipesexplorer.repository - -import dev.fyloz.colorrecipesexplorer.model.material -import org.junit.jupiter.api.Test -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager -import kotlin.test.assertEquals - -@DataJpaTest(excludeAutoConfiguration = [LiquibaseAutoConfiguration::class]) -class MaterialRepositoryTest @Autowired constructor( - private val materialRepository: MaterialRepository, - private val entityManager: TestEntityManager -) { - // updateInventoryQuantityById() - - @Test - fun `updateInventoryQuantityById() updates the quantity of the material with the given identifier`() { - var material = entityManager.persist(material(inventoryQuantity = 1000f, materialType = null)) - val updatedQuantity = 1235f - - materialRepository.updateInventoryQuantityById(material.id!!, updatedQuantity) - - material = entityManager.refresh(material) - - assertEquals(updatedQuantity, material.inventoryQuantity) - } -} diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixRepositoryTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixRepositoryTest.kt deleted file mode 100644 index e87c425..0000000 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixRepositoryTest.kt +++ /dev/null @@ -1,41 +0,0 @@ -package dev.fyloz.colorrecipesexplorer.repository - -import dev.fyloz.colorrecipesexplorer.model.* -import org.junit.jupiter.api.Test -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager -import kotlin.test.assertEquals - -@DataJpaTest(excludeAutoConfiguration = [LiquibaseAutoConfiguration::class]) -class MixRepositoryTest @Autowired constructor( - private val mixRepository: MixRepository, - private val entityManager: TestEntityManager -) { - // updateLocationById() - - @Test - fun `updateLocationById() updates the location of the mix with the given identifier`() { - withMixLocation(null) { mix -> - val updatedLocation = "new location" - - mixRepository.updateLocationById(mix.id!!, updatedLocation) - - val updated = entityManager.refresh(mix) - - assertEquals(updatedLocation, updated.location) - } - } - - private fun withMixLocation(location: String?, test: (Mix) -> Unit) { - val materialType = entityManager.persist(materialType()) - val mixType = entityManager.persist(mixType(materialType = materialType)) - - val company = entityManager.persist(company()) - val recipe = entityManager.persist(recipe(company = company)) - - val mix = mix(id = null, location = location, recipe = recipe, mixType = mixType) - test(entityManager.persist(mix)) - } -} -- 2.40.1 From 129fc4dcb91b228ec25a8ef6c7575e35c2fe7943 Mon Sep 17 00:00:00 2001 From: FyloZ Date: Fri, 15 Apr 2022 14:03:41 -0400 Subject: [PATCH 10/11] #25 Separate mix materials and mix types --- build.gradle.kts | 8 +- gradle/wrapper/gradle-wrapper.properties | 2 +- .../xlsx/XlsxExporter.java | 12 +- .../fyloz/colorrecipesexplorer/Constants.kt | 15 ++ .../DatabaseVersioning.kt | 2 +- .../config/initializers/MixInitializer.kt | 43 ++-- .../colorrecipesexplorer/dtos/MaterialDto.kt | 2 +- .../fyloz/colorrecipesexplorer/dtos/MixDto.kt | 38 ++- .../dtos/MixMaterialDto.kt | 42 ++- .../colorrecipesexplorer/dtos/MixTypeDto.kt | 9 +- .../dtos/TouchUpKitDto.kt | 52 ++++ .../logic/CompanyLogic.kt | 4 +- .../logic/InventoryLogic.kt | 4 +- .../logic/MaterialLogic.kt | 43 ++-- .../logic/MaterialTypeLogic.kt | 4 +- .../colorrecipesexplorer/logic/MixLogic.kt | 21 +- ...ixMaterialLogic.kt => MixQuantityLogic.kt} | 50 ++-- .../logic/MixTypeLogic.kt | 28 +- .../colorrecipesexplorer/logic/RecipeLogic.kt | 19 +- .../logic/RecipeStepLogic.kt | 4 +- .../logic/TouchUpKitLogic.kt | 127 +++------ .../logic/jobs/TouchUpKitRemover.kt | 4 +- .../logic/users/UserLogic.kt | 5 +- .../fyloz/colorrecipesexplorer/model/Mix.kt | 4 +- .../colorrecipesexplorer/model/MixMixType.kt | 19 ++ .../colorrecipesexplorer/model/MixType.kt | 7 +- .../model/touchupkit/TouchUpKit.kt | 199 +-------------- .../model/validation/NullOrNotBlank.kt | 45 ---- .../model/validation/NullOrSize.kt | 46 ---- .../repository/MixMaterialRepository.kt | 1 + .../repository/MixMixTypeRepository.kt | 34 +++ .../repository/MixTypeRepository.kt | 9 +- .../repository/TouchUpKitRepository.kt | 10 +- .../rest/CompanyController.kt | 7 +- .../rest/MaterialController.kt | 8 +- .../rest/RecipeController.kt | 20 +- .../colorrecipesexplorer/rest/RestUtils.kt | 9 + .../rest/TouchUpKitController.kt | 25 +- .../service/MaterialService.kt | 21 +- .../service/MixMixTypeService.kt | 34 +++ .../service/MixService.kt | 18 +- .../service/MixTypeService.kt | 21 +- .../service/TouchUpKitService.kt | 69 +++++ .../logic/DefaultInventoryLogicTest.kt | 8 +- .../logic/DefaultMaterialLogicTest.kt | 36 +-- .../logic/DefaultMixLogicTest.kt | 35 +-- ...Test.kt => DefaultMixQuantityLogicTest.kt} | 15 +- .../logic/DefaultMixTypeLogicTest.kt | 33 ++- .../logic/DefaultRecipeImageLogicTest.kt | 5 +- .../logic/DefaultTouchUpKitLogicTest.kt | 146 +++++++++++ .../logic/TouchUpKitLogicTest.kt | 240 +++++++++--------- 51 files changed, 895 insertions(+), 767 deletions(-) create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/TouchUpKitDto.kt rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/{MixMaterialLogic.kt => MixQuantityLogic.kt} (59%) create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMixType.kt delete mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/validation/NullOrNotBlank.kt delete mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/validation/NullOrSize.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixMixTypeRepository.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMixTypeService.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/TouchUpKitService.kt rename src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/{DefaultMixMaterialLogicTest.kt => DefaultMixQuantityLogicTest.kt} (83%) create mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultTouchUpKitLogicTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 230d484..1325653 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,17 +2,17 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile group = "dev.fyloz.colorrecipesexplorer" -val kotlinVersion = "1.6.0" +val kotlinVersion = "1.6.20" val springBootVersion = "2.6.1" plugins { // Outer scope variables can't be accessed in the plugins section, so we have to redefine them here - val kotlinVersion = "1.6.0" + val kotlinVersion = "1.6.20" val springBootVersion = "2.6.1" id("java") id("org.jetbrains.kotlin.jvm") version kotlinVersion - id("org.jetbrains.dokka") version "1.4.32" + id("org.jetbrains.dokka") version "1.6.10" id("org.springframework.boot") version springBootVersion id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion @@ -30,7 +30,7 @@ dependencies { implementation(platform("org.jetbrains.kotlin:kotlin-bom:${kotlinVersion}")) implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") - implementation("dev.fyloz.colorrecipesexplorer:database-manager:5.2.1") + implementation("dev.fyloz.colorrecipesexplorer:database-manager:6.2") implementation("dev.fyloz:memorycache:1.0") implementation("io.github.microutils:kotlin-logging-jvm:2.1.21") implementation("io.jsonwebtoken:jjwt-api:0.11.2") diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ffed3a2..aa991fc 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/java/dev/fyloz/colorrecipesexplorer/xlsx/XlsxExporter.java b/src/main/java/dev/fyloz/colorrecipesexplorer/xlsx/XlsxExporter.java index c2508e8..9955c37 100644 --- a/src/main/java/dev/fyloz/colorrecipesexplorer/xlsx/XlsxExporter.java +++ b/src/main/java/dev/fyloz/colorrecipesexplorer/xlsx/XlsxExporter.java @@ -1,7 +1,7 @@ package dev.fyloz.colorrecipesexplorer.xlsx; import dev.fyloz.colorrecipesexplorer.dtos.MixDto; -import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto; +import dev.fyloz.colorrecipesexplorer.dtos.MixQuantityOutputDto; import dev.fyloz.colorrecipesexplorer.dtos.RecipeDto; import dev.fyloz.colorrecipesexplorer.xlsx.component.Document; import dev.fyloz.colorrecipesexplorer.xlsx.component.Sheet; @@ -64,15 +64,15 @@ public class XlsxExporter { sheet.registerCell(new SectionTitleCell("Recette")); for (MixDto mix : recipeMixes) { - Table mixTable = new Table(4, mix.getMixMaterials().size() + 1, mix.getMixType().getName()); + Table mixTable = new Table(4, mix.getMixQuantities().getAll().size() + 1, mix.getMixType().getName()); mixTable.setColumnName(0, "Quantité"); mixTable.setColumnName(2, "Unités"); int row = 0; - for (MixMaterialDto mixMaterial : mix.getMixMaterials()) { - mixTable.setRowName(row, mixMaterial.getMaterial().getName()); - mixTable.setContent(new Position(1, row + 1), mixMaterial.getQuantity()); - mixTable.setContent(new Position(3, row + 1), mixMaterial.getMaterial().getMaterialType().getUsePercentages() ? "%" : "mL"); + for (MixQuantityOutputDto mixQuantity : mix.getMixQuantitiesOutput()) { + mixTable.setRowName(row, mixQuantity.getMaterial().getName()); + mixTable.setContent(new Position(1, row + 1), mixQuantity.getQuantity()); + mixTable.setContent(new Position(3, row + 1), mixQuantity.getMaterial().getMaterialType().getUsePercentages() ? "%" : "mL"); row++; } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt index 2a8590c..4c754aa 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt @@ -2,11 +2,13 @@ package dev.fyloz.colorrecipesexplorer object Constants { object ControllerPaths { + const val COMPANY = "/api/company" const val FILE = "/api/file" 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" } object FilePaths { @@ -14,9 +16,22 @@ object Constants { private const val IMAGES = "images" const val SIMDUT = "$PDF/simdut" + const val TOUCH_UP_KITS = "$PDF/touchupkits" const val RECIPE_IMAGES = "$IMAGES/recipes" } + object ModelNames { + const val COMPANY = "Company" + const val MATERIAL = "Material" + const val MATERIAL_TYPE = "MaterialType" + const val MIX = "Mix" + const val MIX_MATERIAL = "MixMaterial" + const val MIX_TYPE = "MixType" + const val RECIPE = "Recipe" + const val RECIPE_STEP = "RecipeStep" + const val TOUCH_UP_KIT = "TouchUpKit" + } + 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" diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/DatabaseVersioning.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/DatabaseVersioning.kt index aacabee..6ca8e8f 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/DatabaseVersioning.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/DatabaseVersioning.kt @@ -15,7 +15,7 @@ import org.springframework.core.env.ConfigurableEnvironment import javax.sql.DataSource import org.springframework.context.annotation.Configuration as SpringConfiguration -const val SUPPORTED_DATABASE_VERSION = 5 +const val SUPPORTED_DATABASE_VERSION = 6 const val ENV_VAR_ENABLE_DATABASE_UPDATE_NAME = "CRE_ENABLE_DB_UPDATE" val DATABASE_NAME_REGEX = Regex("(\\w+)$") diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MixInitializer.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MixInitializer.kt index 71db2e0..8b7a8d0 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MixInitializer.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MixInitializer.kt @@ -1,8 +1,7 @@ package dev.fyloz.colorrecipesexplorer.config.initializers import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase -import dev.fyloz.colorrecipesexplorer.dtos.MixDto -import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto +import dev.fyloz.colorrecipesexplorer.dtos.* import dev.fyloz.colorrecipesexplorer.logic.MixLogic import dev.fyloz.colorrecipesexplorer.utils.merge import mu.KotlinLogging @@ -25,18 +24,19 @@ class MixInitializer( logger.debug("Validating mix materials positions...") mixLogic.getAll() - .filter { mix -> mix.mixMaterials.any { it.position == 0 } } + .filter { it.mixQuantities.all.any { mq -> mq.position == 0 } } .forEach(this::fixMixPositions) logger.debug("Mix materials positions are valid!") } private fun fixMixPositions(mix: MixDto) { - val maxPosition = mix.mixMaterials.maxOf { it.position } + val mixQuantities = mix.mixQuantitiesOutput + val maxPosition = mixQuantities.maxOf { it.position } logger.warn("Mix ${mix.id} (mix name: ${mix.mixType.name}, recipe id: ${mix.recipeId}) has invalid positions:") - val invalidMixMaterials: Collection = with(mix.mixMaterials.filter { it.position == 0 }) { + val invalidMixQuantities: Collection = with(mixQuantities.filter { it.position == 0 }) { if (maxPosition == 0 && this.size > 1) { orderMixMaterials(this) } else { @@ -44,28 +44,37 @@ class MixInitializer( } } - val fixedMixMaterials = increaseMixMaterialsPosition(invalidMixMaterials, maxPosition + 1) - val updatedMixMaterials = mix.mixMaterials.merge(fixedMixMaterials) + val fixedMixQuantities = increaseMixMaterialsPosition(invalidMixQuantities, maxPosition + 1) + val updatedMixQuantities = + mixQuantities.map { MixQuantitySaveDto(it.id, it.material.id, it.quantity, it.position, it.isMixType) } + .merge(fixedMixQuantities) - with(mix.copy(mixMaterials = updatedMixMaterials)) { - mixLogic.update(this) - } + val updatedMix = MixSaveDto(mix.id, mix.mixType.name, mix.recipeId, mix.mixType.materialType.id, updatedMixQuantities) + mixLogic.update(updatedMix) } - private fun increaseMixMaterialsPosition(mixMaterials: Iterable, firstPosition: Int) = - mixMaterials - .mapIndexed { index, mixMaterial -> mixMaterial.copy(position = firstPosition + index) } + private fun increaseMixMaterialsPosition(mixQuantities: Iterable, firstPosition: Int) = + mixQuantities + .mapIndexed { index, mixQuantity -> + MixQuantitySaveDto( + mixQuantity.id, + mixQuantity.material.id, + mixQuantity.quantity, + firstPosition + index, + mixQuantity.isMixType + ) + } .onEach { - logger.info("\tPosition of material ${it.material.id} (${it.material.name}) has been set to ${it.position}") + logger.info("\tPosition of material ${it.id} (mixType: ${it.isMixType}) has been set to ${it.position}") } - private fun orderMixMaterials(mixMaterials: Collection) = - LinkedList(mixMaterials).apply { + private fun orderMixMaterials(mixQuantities: Collection) = + LinkedList(mixQuantities).apply { while (this.peek().material.materialType.usePercentages) { // The first mix material can't use percents, so move it to the end of the queue val pop = this.pop() this.add(pop) - logger.debug("\tMaterial ${pop.material.id} (${pop.material.name}) uses percents, moving to the end of the queue") + logger.debug("\tMaterial ${pop.id} (mixType: ${pop.isMixType}) uses percents, moving to the end of the queue") } } } \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt index f45aa91..f9ceba7 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt @@ -16,7 +16,7 @@ data class MaterialDto( val materialType: MaterialTypeDto, - val simdutUrl: String? = null + val hasSimdut: Boolean = false ) : EntityDto data class MaterialSaveDto( diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixDto.kt index 3cc26dc..186380a 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixDto.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixDto.kt @@ -1,6 +1,7 @@ package dev.fyloz.colorrecipesexplorer.dtos import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonProperty import dev.fyloz.colorrecipesexplorer.Constants import javax.validation.constraints.Min import javax.validation.constraints.NotBlank @@ -15,8 +16,39 @@ data class MixDto( val mixType: MixTypeDto, - val mixMaterials: List -) : EntityDto + @JsonIgnore + val mixQuantities: MixQuantitiesDto, +) : EntityDto { + @Suppress("unused") + @get:JsonProperty("mixQuantities") + val mixQuantitiesOutput by lazy { + mixQuantities.materials.map { + MixQuantityOutputDto(it.id, it.material, it.quantity, it.position, false) + } + mixQuantities.mixTypes.map { + MixQuantityOutputDto(it.id, it.mixType.asMaterial(), it.quantity, it.position, true) + } + } +} + +data class MixQuantitiesDto( + val materials: List = listOf(), + + val mixTypes: List = listOf() +) { + val all get() = materials + mixTypes +} + +data class MixQuantityOutputDto( + val id: Long, + + val material: MaterialDto, + + val quantity: Float, + + val position: Int, + + val isMixType: Boolean +) data class MixSaveDto( val id: Long = 0L, @@ -28,7 +60,7 @@ data class MixSaveDto( val materialTypeId: Long, - val mixMaterials: List + val mixQuantities: List ) data class MixDeductDto( diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixMaterialDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixMaterialDto.kt index 7ba94ce..a2ce9c4 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixMaterialDto.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixMaterialDto.kt @@ -3,17 +3,47 @@ package dev.fyloz.colorrecipesexplorer.dtos import dev.fyloz.colorrecipesexplorer.Constants import javax.validation.constraints.Min +sealed interface MixQuantityDto : EntityDto { + val quantity: Float + val position: Int + + val materialType: MaterialTypeDto + val name: String +} + data class MixMaterialDto( override val id: Long = 0L, val material: MaterialDto, - val quantity: Float, + override val quantity: Float, - val position: Int -) : EntityDto + override val position: Int +) : MixQuantityDto { + override val materialType: MaterialTypeDto + get() = material.materialType -data class MixMaterialSaveDto( + override val name: String + get() = material.name +} + +data class MixMixTypeDto( + override val id: Long, + + val mixType: MixTypeDto, + + override val quantity: Float, + + override val position: Int +) : MixQuantityDto { + override val materialType: MaterialTypeDto + get() = mixType.materialType + + override val name: String + get() = mixType.name +} + +data class MixQuantitySaveDto( override val id: Long = 0L, val materialId: Long, @@ -21,5 +51,7 @@ data class MixMaterialSaveDto( @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO) val quantity: Float, - val position: Int + val position: Int, + + val isMixType: Boolean ) : EntityDto \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixTypeDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixTypeDto.kt index 3d26a6c..29022d6 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixTypeDto.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixTypeDto.kt @@ -5,5 +5,10 @@ data class MixTypeDto( val name: String, - val material: MaterialDto -) : EntityDto + val materialType: MaterialTypeDto, + + val material: MaterialDto? = null +) : EntityDto { + fun asMaterial() = + MaterialDto(id, name, 0f, true, materialType) +} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/TouchUpKitDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/TouchUpKitDto.kt new file mode 100644 index 0000000..b597c91 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/TouchUpKitDto.kt @@ -0,0 +1,52 @@ +package dev.fyloz.colorrecipesexplorer.dtos + +import dev.fyloz.colorrecipesexplorer.Constants +import java.time.LocalDate +import javax.validation.constraints.Min +import javax.validation.constraints.NotBlank +import javax.validation.constraints.NotEmpty + +data class TouchUpKitDto( + override val id: Long = 0L, + + @field:NotBlank + val project: String, + + @field:NotBlank + val buggy: String, + + @field:NotBlank + val company: String, + + @field:Min(1, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ONE) + val quantity: Int, + + val shippingDate: LocalDate, + + val completionDate: LocalDate?, + + val completed: Boolean = false, + + val expired: Boolean = false, + + @field:NotEmpty + val finish: List, + + @field:NotEmpty + val material: List, + + @field:NotEmpty + val content: List +) : EntityDto + +data class TouchUpKitProductDto( + override val id: Long = 0L, + + val name: String, + + val description: String?, + + val quantity: Float, + + val ready: Boolean +) : EntityDto \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogic.kt index ea323ad..2b7539a 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/CompanyLogic.kt @@ -1,15 +1,15 @@ package dev.fyloz.colorrecipesexplorer.logic +import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent import dev.fyloz.colorrecipesexplorer.dtos.CompanyDto -import dev.fyloz.colorrecipesexplorer.model.Company import dev.fyloz.colorrecipesexplorer.service.CompanyService interface CompanyLogic : Logic @LogicComponent class DefaultCompanyLogic(service: CompanyService) : - BaseLogic(service, Company::class.simpleName!!), CompanyLogic { + BaseLogic(service, Constants.ModelNames.COMPANY), CompanyLogic { override fun save(dto: CompanyDto): CompanyDto { throwIfNameAlreadyExists(dto.name) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogic.kt index d938cde..d4db8a1 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogic.kt @@ -48,8 +48,10 @@ class DefaultInventoryLogic( @Transactional override fun deductMix(mixRatio: MixDeductDto): Collection { val mix = mixLogic.getById(mixRatio.id) + val mixMaterials = mix.mixQuantities.materials - return deduct(getMaterialsWithAdjustedQuantities(mix.mixMaterials, mixRatio)) + if (mixMaterials.isEmpty()) return listOf() + return deduct(getMaterialsWithAdjustedQuantities(mixMaterials, mixRatio)) } @Transactional diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogic.kt index 9a9d974..926af5c 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogic.kt @@ -1,8 +1,10 @@ package dev.fyloz.colorrecipesexplorer.logic +import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto import dev.fyloz.colorrecipesexplorer.dtos.MaterialSaveDto +import dev.fyloz.colorrecipesexplorer.dtos.MixTypeDto import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic import dev.fyloz.colorrecipesexplorer.model.Material import dev.fyloz.colorrecipesexplorer.service.MaterialService @@ -11,21 +13,18 @@ interface MaterialLogic : Logic { /** Checks if a material with the given [name] exists. */ fun existsByName(name: String): Boolean - /** Gets all materials that are not a mix type. */ - fun getAllNotMixType(): Collection + /** + * Returns every material available in the context of the recipe with the given [recipeId]. + * The materials included contains every non mix type material, and the materials generated for the recipe mix types. + */ + fun getAllForRecipe(recipeId: Long): Collection /** - * Gets all materials available for the creation of a mix for the recipe with the given [recipeId], - * including normal materials and materials from mix types included in the said recipe. + * Returns every material available in the context of the mix with the given [mixId]. + * The materials included contains every non mix type material, and the materials generated for + * the mix's recipe mix types, excluding the mix's mix type. */ - fun getAllForMixCreation(recipeId: Long): Collection - - /** - * Gets all materials available for updating the mix with the given [mixId], - * including normal materials and materials from mix types included in the mix recipe - * and excluding the material of the mix type of the said mix. - */ - fun getAllForMixUpdate(mixId: Long): Collection + fun getAllForMix(mixId: Long): Collection /** Saves the given [dto]. */ fun save(dto: MaterialSaveDto): MaterialDto @@ -44,24 +43,26 @@ class DefaultMaterialLogic( val mixLogic: MixLogic, val materialTypeLogic: MaterialTypeLogic, val fileLogic: WriteableFileLogic -) : BaseLogic(service, Material::class.simpleName!!), MaterialLogic { +) : BaseLogic(service, Constants.ModelNames.MATERIAL), MaterialLogic { override fun existsByName(name: String) = service.existsByName(name, null) - override fun getAllNotMixType() = service.getAllNotMixType() - override fun getAllForMixCreation(recipeId: Long): Collection { - val recipesMixTypes = recipeLogic.getById(recipeId).mixTypes + override fun getAllForRecipe(recipeId: Long): Collection { + val recipe = recipeLogic.getById(recipeId) - return getAll().filter { !it.isMixType || recipesMixTypes.any { mixType -> mixType.material.id == it.id } } + return getAllWithMixTypesMaterials(recipe.mixTypes) } - override fun getAllForMixUpdate(mixId: Long): Collection { + override fun getAllForMix(mixId: Long): Collection { val mix = mixLogic.getById(mixId) val recipe = recipeLogic.getById(mix.recipeId) - return getAll().filter { !it.isMixType || recipe.mixTypes.any { mixType -> mixType.material.id == it.id } } - .filter { it.id != mix.mixType.material.id } + val availableMixTypes = recipe.mixTypes.filter { it != mix.mixType } + return getAllWithMixTypesMaterials(availableMixTypes) } + private fun getAllWithMixTypesMaterials(mixTypes: Collection) = + getAll() + mixTypes.map { it.asMaterial() } + override fun save(dto: MaterialSaveDto) = save(saveDtoToDto(dto, false)).also { saveSimdutFile(dto, false) } override fun save(dto: MaterialDto): MaterialDto { throwIfNameAlreadyExists(dto.name) @@ -101,7 +102,7 @@ class DefaultMaterialLogic( val isMixType = !updating || getById(saveDto.id).isMixType val materialType = materialTypeLogic.getById(saveDto.materialTypeId) - return MaterialDto(saveDto.id, saveDto.name, saveDto.inventoryQuantity, isMixType, materialType, null) + return MaterialDto(saveDto.id, saveDto.name, saveDto.inventoryQuantity, isMixType, materialType) } private fun saveSimdutFile(dto: MaterialSaveDto, updating: Boolean) { diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogic.kt index e4d5c46..49b202f 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialTypeLogic.kt @@ -1,9 +1,9 @@ package dev.fyloz.colorrecipesexplorer.logic +import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto import dev.fyloz.colorrecipesexplorer.exception.CannotUpdateException -import dev.fyloz.colorrecipesexplorer.model.MaterialType import dev.fyloz.colorrecipesexplorer.service.MaterialTypeService interface MaterialTypeLogic : Logic { @@ -19,7 +19,7 @@ interface MaterialTypeLogic : Logic { @LogicComponent class DefaultMaterialTypeLogic(service: MaterialTypeService) : - BaseLogic(service, MaterialType::class.simpleName!!), MaterialTypeLogic { + BaseLogic(service, Constants.ModelNames.MATERIAL_TYPE), MaterialTypeLogic { override fun getAll(systemType: Boolean) = service.getAll(systemType) override fun getByName(name: String) = service.getByName(name) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt index 16c31d5..64da2e3 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt @@ -1,10 +1,10 @@ package dev.fyloz.colorrecipesexplorer.logic +import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent import dev.fyloz.colorrecipesexplorer.dtos.MixDto import dev.fyloz.colorrecipesexplorer.dtos.MixLocationDto import dev.fyloz.colorrecipesexplorer.dtos.MixSaveDto -import dev.fyloz.colorrecipesexplorer.model.Mix import dev.fyloz.colorrecipesexplorer.service.MixService import org.springframework.context.annotation.Lazy import org.springframework.transaction.annotation.Transactional @@ -26,8 +26,8 @@ class DefaultMixLogic( @Lazy private val recipeLogic: RecipeLogic, @Lazy private val materialTypeLogic: MaterialTypeLogic, private val mixTypeLogic: MixTypeLogic, - private val mixMaterialLogic: MixMaterialLogic -) : BaseLogic(service, Mix::class.simpleName!!), MixLogic { + private val mixQuantityLogic: MixQuantityLogic +) : BaseLogic(service, Constants.ModelNames.MIX), MixLogic { @Transactional override fun save(dto: MixSaveDto): MixDto { val recipe = recipeLogic.getById(dto.recipeId) @@ -36,7 +36,7 @@ class DefaultMixLogic( val mix = MixDto( recipeId = recipe.id, mixType = mixTypeLogic.getOrCreateForNameAndMaterialType(dto.name, materialType), - mixMaterials = mixMaterialLogic.validateAndSaveAll(dto.mixMaterials) + mixQuantities = mixQuantityLogic.validateAndPrepareForMix(dto.mixQuantities) ) return save(mix) @@ -47,12 +47,19 @@ class DefaultMixLogic( val materialType = materialTypeLogic.getById(dto.materialTypeId) val mix = getById(dto.id) + // Update the mix type if it has been changed + val mixType = if (mix.mixType.name != dto.name || mix.mixType.materialType.id != dto.materialTypeId) { + mixTypeLogic.updateOrCreateForNameAndMaterialType(mix.mixType, dto.name, materialType) + } else { + mix.mixType + } + return update( MixDto( id = dto.id, - recipeId = dto.recipeId, - mixType = mixTypeLogic.updateOrCreateForNameAndMaterialType(mix.mixType, dto.name, materialType), - mixMaterials = mixMaterialLogic.validateAndSaveAll(dto.mixMaterials) + recipeId = mix.recipeId, + mixType = mixType, + mixQuantities = mixQuantityLogic.validateAndPrepareForMix(dto.mixQuantities) ) ) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixQuantityLogic.kt similarity index 59% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixQuantityLogic.kt index 1ff78fd..a6ddae8 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixQuantityLogic.kt @@ -1,33 +1,32 @@ package dev.fyloz.colorrecipesexplorer.logic import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent -import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto -import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialSaveDto +import dev.fyloz.colorrecipesexplorer.dtos.* import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException import dev.fyloz.colorrecipesexplorer.exception.RestException -import dev.fyloz.colorrecipesexplorer.model.MixMaterial -import dev.fyloz.colorrecipesexplorer.service.MixMaterialService import dev.fyloz.colorrecipesexplorer.utils.PositionUtils import org.springframework.context.annotation.Lazy import org.springframework.http.HttpStatus -interface MixMaterialLogic : Logic { +interface MixQuantityLogic { /** * Validates if the given [mixMaterials]. To be valid, the position of each mix material must be greater or equals to 1 and unique in the set. * There must also be no gap between the positions. Also, the quantity of the first mix material in the set must not be expressed in percentages. * If any of those criteria are not met, an [InvalidGroupStepsPositionsException] will be thrown. */ - fun validateMixMaterials(mixMaterials: Set) + fun validateMixQuantities(mixMaterials: List) - /** Validates the given mix materials [dtos] and save them. */ - fun validateAndSaveAll(dtos: List): List + /** Validates the given mix quantities [dtos] and put them in [MixQuantitiesDto] to be consumed by a mix. */ + fun validateAndPrepareForMix(dtos: List): MixQuantitiesDto } @LogicComponent -class DefaultMixMaterialLogic(service: MixMaterialService, @Lazy private val materialLogic: MaterialLogic) : - BaseLogic(service, MixMaterial::class.simpleName!!), MixMaterialLogic { - override fun validateMixMaterials(mixMaterials: Set) { +class DefaultMixQuantityLogic( + @Lazy private val materialLogic: MaterialLogic, + private val mixTypeLogic: MixTypeLogic +) : MixQuantityLogic { + override fun validateMixQuantities(mixMaterials: List) { if (mixMaterials.isEmpty()) return val sortedMixMaterials = mixMaterials.sortedBy { it.position } @@ -38,24 +37,35 @@ class DefaultMixMaterialLogic(service: MixMaterialService, @Lazy private val mat throw InvalidMixMaterialsPositionsException(ex.errors) } - if (sortedMixMaterials[0].material.materialType.usePercentages) { - throw InvalidFirstMixMaterialException(sortedMixMaterials[0]) + val firstMixMaterial = sortedMixMaterials[0] + if (firstMixMaterial is MixMaterialDto) { + if (firstMixMaterial.material.materialType.usePercentages) { + throw InvalidFirstMixMaterialException(sortedMixMaterials[0]) + } } } - override fun validateAndSaveAll(dtos: List): List { - val dtosWithMaterials = dtos.map { + override fun validateAndPrepareForMix(dtos: List): MixQuantitiesDto { + val mixMixTypes = dtos.filter { it.isMixType }.map { + MixMixTypeDto( + id = it.id, + mixType = mixTypeLogic.getById(it.materialId), + quantity = it.quantity, + position = it.position + ) + } + + val mixMaterials = dtos.filter { !it.isMixType }.map { MixMaterialDto( id = it.id, material = materialLogic.getById(it.materialId), quantity = it.quantity, position = it.position ) - }.toSet() + } - validateMixMaterials(dtosWithMaterials) - - return dtosWithMaterials.map(::save) + validateMixQuantities(mixMixTypes + mixMaterials) + return MixQuantitiesDto(mixMaterials, mixMixTypes) } } @@ -73,7 +83,7 @@ class InvalidMixMaterialsPositionsException( ) class InvalidFirstMixMaterialException( - val mixMaterial: MixMaterialDto + val mixMaterial: MixQuantityDto ) : RestException( "invalid-mixmaterial-first", "Invalid first mix material", diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt index 3226392..5d49677 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt @@ -1,12 +1,10 @@ package dev.fyloz.colorrecipesexplorer.logic +import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent -import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto import dev.fyloz.colorrecipesexplorer.dtos.MixTypeDto -import dev.fyloz.colorrecipesexplorer.model.MixType import dev.fyloz.colorrecipesexplorer.service.MixTypeService -import org.springframework.context.annotation.Lazy import org.springframework.transaction.annotation.Transactional interface MixTypeLogic : Logic { @@ -22,11 +20,7 @@ interface MixTypeLogic : Logic { } @LogicComponent -class DefaultMixTypeLogic( - service: MixTypeService, - @Lazy private val materialLogic: MaterialLogic -) : - BaseLogic(service, MixType::class.simpleName!!), MixTypeLogic { +class DefaultMixTypeLogic(service: MixTypeService) : BaseLogic(service, Constants.ModelNames.MIX_TYPE), MixTypeLogic { @Transactional override fun getOrCreateForNameAndMaterialType(name: String, materialType: MaterialTypeDto) = service.getByNameAndMaterialType(name, materialType.id) ?: saveForNameAndMaterialType(name, materialType) @@ -35,7 +29,9 @@ class DefaultMixTypeLogic( mixType: MixTypeDto, name: String, materialType: MaterialTypeDto - ) = if (service.isShared(mixType.id)) { + ) = if (service.existsByNameAndMaterialType(name, materialType.id, mixType.id)) { + service.getByNameAndMaterialType(name, materialType.id)!! + } else if (service.isShared(mixType.id)) { saveForNameAndMaterialType(name, materialType) } else { updateForNameAndMaterialType(mixType, name, materialType) @@ -50,16 +46,7 @@ class DefaultMixTypeLogic( } private fun saveForNameAndMaterialType(name: String, materialType: MaterialTypeDto): MixTypeDto { - val material = materialLogic.save( - MaterialDto( - name = name, - inventoryQuantity = Float.MIN_VALUE, - isMixType = true, - materialType = materialType - ) - ) - - return save(MixTypeDto(name = name, material = material)) + return save(MixTypeDto(name = name, materialType = materialType)) } private fun updateForNameAndMaterialType( @@ -67,7 +54,6 @@ class DefaultMixTypeLogic( name: String, materialType: MaterialTypeDto ): MixTypeDto { - val material = materialLogic.update(mixType.material.copy(name = name, materialType = materialType)) - return update(mixType.copy(name = name, material = material)) + return update(mixType.copy(name = name, materialType = materialType, material = mixType.material)) } } \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt index 1daf5cd..2687249 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt @@ -6,7 +6,6 @@ import dev.fyloz.colorrecipesexplorer.dtos.* import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic -import dev.fyloz.colorrecipesexplorer.model.Recipe import dev.fyloz.colorrecipesexplorer.service.RecipeService import dev.fyloz.colorrecipesexplorer.utils.collections.LazyMapList import dev.fyloz.colorrecipesexplorer.utils.merge @@ -37,7 +36,7 @@ class DefaultRecipeLogic( private val recipeStepLogic: RecipeStepLogic, private val mixLogic: MixLogic, private val groupLogic: GroupLogic -) : BaseLogic(service, Recipe::class.simpleName!!), RecipeLogic { +) : BaseLogic(service, Constants.ModelNames.RECIPE), RecipeLogic { @Transactional override fun getAllWithMixesAndGroupsInformation() = getAll().onEach { (it.mixes as LazyMapList<*, *>).initialize() } @@ -155,8 +154,8 @@ interface RecipeImageLogic { /** Saves the given [image] and associate it to the recipe with the given [recipeId]. Returns the id of the saved image. */ fun download(image: MultipartFile, recipeId: Long): String - /** Deletes the image with the given [path] for the given [recipeId]. */ - fun delete(recipeId: Long, path: String) + /** Deletes the image with the given [id] for the given [recipeId]. */ + fun delete(recipeId: Long, id: String) } @LogicComponent @@ -167,7 +166,13 @@ class DefaultRecipeImageLogic(val fileLogic: WriteableFileLogic) : RecipeImageLo override fun download(image: MultipartFile, recipeId: Long): String { /** Gets the next id available for a new image for the given [recipeId]. */ fun getNextAvailableId(): String = with(getAllImages(recipeId)) { - (if (isEmpty()) 0 else maxOf { it.toLong() } + 1L).toString() + val currentIds = mapNotNull { it.toLongOrNull() } + if (currentIds.isEmpty()) { + return 0.toString() + } + + val nextId = currentIds.maxOf { it } + 1L + return nextId.toString() } return getNextAvailableId().also { @@ -176,8 +181,8 @@ class DefaultRecipeImageLogic(val fileLogic: WriteableFileLogic) : RecipeImageLo } } - override fun delete(recipeId: Long, path: String) = - fileLogic.deleteFromDirectory(path, getRecipeImagesDirectory(recipeId)) + override fun delete(recipeId: Long, id: String) = + fileLogic.deleteFromDirectory(getImagePath(recipeId, id), getRecipeImagesDirectory(recipeId)) private fun getImagePath(recipeId: Long, id: String) = "${getRecipeImagesDirectory(recipeId)}/$id" diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt index d0f8771..7bbf75c 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt @@ -1,12 +1,12 @@ package dev.fyloz.colorrecipesexplorer.logic +import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent 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.exception.RestException -import dev.fyloz.colorrecipesexplorer.model.RecipeStep import dev.fyloz.colorrecipesexplorer.model.account.Group import dev.fyloz.colorrecipesexplorer.service.RecipeStepService import dev.fyloz.colorrecipesexplorer.utils.PositionUtils @@ -19,7 +19,7 @@ interface RecipeStepLogic : Logic { @LogicComponent class DefaultRecipeStepLogic(recipeStepService: RecipeStepService) : - BaseLogic(recipeStepService, RecipeStep::class.simpleName!!), RecipeStepLogic { + BaseLogic(recipeStepService, Constants.ModelNames.RECIPE_STEP), RecipeStepLogic { override fun validateGroupInformationSteps(groupInformation: RecipeGroupInformationDto) { try { PositionUtils.validate(groupInformation.steps.map { it.position }.toList()) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/TouchUpKitLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/TouchUpKitLogic.kt index 7ae0f8f..2b4fed7 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/TouchUpKitLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/TouchUpKitLogic.kt @@ -1,33 +1,21 @@ package dev.fyloz.colorrecipesexplorer.logic -import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase +import dev.fyloz.colorrecipesexplorer.Constants +import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent +import dev.fyloz.colorrecipesexplorer.dtos.TouchUpKitDto 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.touchupkit.* -import dev.fyloz.colorrecipesexplorer.repository.TouchUpKitRepository -import dev.fyloz.colorrecipesexplorer.rest.TOUCH_UP_KIT_CONTROLLER_PATH +import dev.fyloz.colorrecipesexplorer.service.TouchUpKitService import dev.fyloz.colorrecipesexplorer.utils.* import org.springframework.core.io.ByteArrayResource import org.springframework.core.io.Resource -import org.springframework.stereotype.Service import java.time.LocalDate -import java.time.Period - -private const val TOUCH_UP_KIT_FILES_PATH = "pdf/touchupkits" - -const val TOUCH_UP_TEXT_FR = "KIT DE RETOUCHE" -const val TOUCH_UP_TEXT_EN = "TOUCH UP KIT" - -interface TouchUpKitLogic : - ExternalModelService { - fun isExpired(touchUpKit: TouchUpKit): Boolean +interface TouchUpKitLogic : Logic { + /** Sets the touch up kit with the given [id] as complete. */ fun complete(id: Long) - /** Generates and returns a [PdfDocument] for the given [job]. */ - fun generateJobPdf(job: String): PdfDocument - /** * Generates and returns a [PdfDocument] for the given [job] as a [ByteArrayResource]. * @@ -36,68 +24,37 @@ interface TouchUpKitLogic : */ fun generateJobPdfResource(job: String): Resource - /** Writes the given [document] to the [FileService] if TOUCH_UP_KIT_CACHE_PDF is enabled. */ - fun String.cachePdfDocument(document: PdfDocument) + /** Generates and returns a [PdfDocument] for the given [job]. */ + fun generateJobPdf(job: String): PdfDocument + + /** Writes the given [pdf] to the disk if TOUCH_UP_KIT_CACHE_PDF is enabled. */ + fun cacheJobPdf(job: String, pdf: PdfDocument) } -@Service -@RequireDatabase +@LogicComponent class DefaultTouchUpKitLogic( - private val fileService: WriteableFileLogic, - private val configService: ConfigurationLogic, - touchUpKitRepository: TouchUpKitRepository -) : AbstractExternalModelService( - touchUpKitRepository -), TouchUpKitLogic { + service: TouchUpKitService, + private val fileLogic: WriteableFileLogic, + private val configLogic: ConfigurationLogic +) : BaseLogic(service, Constants.ModelNames.TOUCH_UP_KIT), TouchUpKitLogic { private val cacheGeneratedFiles by lazy { - configService.getContent(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) == true.toString() + configLogic.getContent(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) == true.toString() } - override fun idNotFoundException(id: Long) = touchUpKitIdNotFoundException(id) - override fun idAlreadyExistsException(id: Long) = touchUpKitIdAlreadyExistsException(id) + override fun complete(id: Long) = service.updateCompletionDateById(id, LocalDate.now()) - override fun TouchUpKit.toOutput() = TouchUpKitOutputDto( - this.id!!, - this.project, - this.buggy, - this.company, - this.quantity, - this.shippingDate, - this.completionDate != null, - this.completionDate, - isExpired(this), - this.finish, - this.material, - this.content, - this.pdfUrl() - ) - - override fun update(entity: TouchUpKitUpdateDto): TouchUpKit { - val persistedKit by lazy { getById(entity.id) } - - return super.update(with(entity) { - touchUpKit( - id = id, - project = project ?: persistedKit.project, - buggy = buggy ?: persistedKit.buggy, - company = company ?: persistedKit.company, - quantity = quantity ?: persistedKit.quantity, - shippingDate = shippingDate ?: persistedKit.shippingDate, - completionDate = completionDate ?: persistedKit.completionDate, - finish = finish ?: persistedKit.finish, - material = material ?: persistedKit.material, - content = content?.map { touchUpKitProduct(it) }?.toSet() ?: persistedKit.content - ) - }) - } - - override fun isExpired(touchUpKit: TouchUpKit) = - with(Period.parse(configService.getContent(ConfigurationType.TOUCH_UP_KIT_EXPIRATION))) { - touchUpKit.completed && touchUpKit.completionDate!!.plus(this) < LocalDate.now() + override fun generateJobPdfResource(job: String): Resource { + if (cacheGeneratedFiles) { + val pdfPath = jobPdfPath(job) + if (fileLogic.exists(pdfPath)) { + return fileLogic.read(pdfPath) + } } - override fun complete(id: Long) { - update(touchUpKitUpdateDto(id = id, completionDate = LocalDate.now())) + val pdf = generateJobPdf(job) + cacheJobPdf(job, pdf) + + return pdf.toByteArrayResource() } override fun generateJobPdf(job: String) = pdf { @@ -122,29 +79,17 @@ class DefaultTouchUpKitLogic( } } - override fun generateJobPdfResource(job: String): Resource { - if (cacheGeneratedFiles) { - with(job.pdfDocumentPath()) { - if (fileService.exists(this)) { - return fileService.read(this) - } - } - } - - return generateJobPdf(job).apply { - job.cachePdfDocument(this) - }.toByteArrayResource() - } - - override fun String.cachePdfDocument(document: PdfDocument) { + override fun cacheJobPdf(job: String, pdf: PdfDocument) { if (!cacheGeneratedFiles) return - fileService.write(document.toByteArrayResource(), this.pdfDocumentPath(), true) + fileLogic.write(pdf.toByteArrayResource(), jobPdfPath(job), true) } - private fun String.pdfDocumentPath() = - "$TOUCH_UP_KIT_FILES_PATH/$this.pdf" + private fun jobPdfPath(job: String) = + "${Constants.FilePaths.TOUCH_UP_KITS}/$job.pdf" - private fun TouchUpKit.pdfUrl() = - "${configService.getContent(ConfigurationType.INSTANCE_URL)}$TOUCH_UP_KIT_CONTROLLER_PATH/pdf?job=$project" + companion object { + const val TOUCH_UP_TEXT_FR = "KIT DE RETOUCHE" + const val TOUCH_UP_TEXT_EN = "TOUCH UP KIT" + } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/jobs/TouchUpKitRemover.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/jobs/TouchUpKitRemover.kt index d14be17..df75b16 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/jobs/TouchUpKitRemover.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/jobs/TouchUpKitRemover.kt @@ -20,10 +20,10 @@ class TouchUpKitRemover( } private fun removeExpiredKits() { - with(touchUpKitLogic.getAll().filter(touchUpKitLogic::isExpired)) { + with(touchUpKitLogic.getAll().filter { it.expired }) { this.forEach { logger.debug("Removed expired touch up kit ${it.id} (${it.project} ${it.buggy})") - touchUpKitLogic.delete(it) + touchUpKitLogic.deleteById(it.id) } logger.info("Removed ${this.size} expired touch up kits") } 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 1de1f80..ef92174 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/UserLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/users/UserLogic.kt @@ -4,7 +4,6 @@ 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.model.validation.or import dev.fyloz.colorrecipesexplorer.repository.UserRepository import org.springframework.context.annotation.Lazy import org.springframework.context.annotation.Profile @@ -136,8 +135,8 @@ class DefaultUserLogic( return update(with(entity) { User( id = id, - firstName = firstName or persistedUser.firstName, - lastName = lastName or persistedUser.lastName, + firstName = firstName ?: persistedUser.firstName, + lastName = lastName ?: persistedUser.lastName, password = persistedUser.password, isDefaultGroupUser = false, isSystemUser = false, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt index 9656433..8fa3a19 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt @@ -16,9 +16,9 @@ data class Mix( @ManyToOne @JoinColumn(name = "mix_type_id") - var mixType: MixType, + val mixType: MixType, @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true) @JoinColumn(name = "mix_id") - var mixMaterials: List, + val mixMaterials: List ) : ModelEntity \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMixType.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMixType.kt new file mode 100644 index 0000000..e39a27c --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMixType.kt @@ -0,0 +1,19 @@ +package dev.fyloz.colorrecipesexplorer.model + +import javax.persistence.* + +@Entity +@Table(name = "mix_mix_type") +data class MixMixType( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + override val id: Long, + + @ManyToOne + @JoinColumn(name = "mix_type_id") + val mixType: MixType, + + val quantity: Float, + + 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 7d471dc..13dd296 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixType.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixType.kt @@ -9,10 +9,13 @@ data class MixType( @GeneratedValue(strategy = GenerationType.IDENTITY) override val id: Long?, - @Column(unique = true) val name: String, + @ManyToOne + @JoinColumn(name = "material_type_id") + val materialType: MaterialType, + @OneToOne(cascade = [CascadeType.ALL]) @JoinColumn(name = "material_id") - var material: Material + val material: Material? ) : 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 623e92d..1f7ada4 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt @@ -1,17 +1,8 @@ package dev.fyloz.colorrecipesexplorer.model.touchupkit -import dev.fyloz.colorrecipesexplorer.Constants -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 java.time.LocalDate import javax.persistence.* -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank -import javax.validation.constraints.NotEmpty - -const val TOUCH_UP_KIT_DELIMITER = ';' @Entity @Table(name = "touch_up_kit") @@ -35,24 +26,15 @@ data class TouchUpKit( val completionDate: LocalDate?, @Column(name = "finish") - private val finishConcatenated: String, + val finish: String, @Column(name = "material") - private val materialConcatenated: String, + val material: String, @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true) @JoinColumn(name = "touch_up_kit_id") - val content: Set -) : ModelEntity { - val finish - get() = finishConcatenated.split(TOUCH_UP_KIT_DELIMITER) - - val material - get() = materialConcatenated.split(TOUCH_UP_KIT_DELIMITER) - - val completed - get() = completionDate != null -} + val content: List +) : ModelEntity @Entity @Table(name = "touch_up_kit_product") @@ -68,175 +50,4 @@ data class TouchUpKitProduct( val quantity: Float, val ready: Boolean -) : ModelEntity - -data class TouchUpKitSaveDto( - @field:NotBlank - val project: String, - - @field:NotBlank - val buggy: String, - - @field:NotBlank - val company: String, - - @field:Min(1, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ONE) - val quantity: Int, - - val shippingDate: LocalDate, - - @field:NotEmpty - val finish: List, - - @field:NotEmpty - val material: List, - - @field:NotEmpty - val content: Set -) : EntityDto { - override fun toEntity() = touchUpKit(this) -} - -data class TouchUpKitUpdateDto( - val id: Long, - - @field:NotBlank - val project: String?, - - @field:NotBlank - val buggy: String?, - - @field:NotBlank - val company: String?, - - @field:Min(1, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ONE) - val quantity: Int?, - - val shippingDate: LocalDate?, - - val completionDate: LocalDate?, - - @field:NotEmpty - val finish: List?, - - @field:NotEmpty - val material: List?, - - @field:NotEmpty - val content: Set? -) : EntityDto - -data class TouchUpKitOutputDto( - override val id: Long, - val project: String, - val buggy: String, - val company: String, - val quantity: Int, - val shippingDate: LocalDate, - val completed: Boolean, - val completionDate: LocalDate?, - val expired: Boolean, - val finish: List, - val material: List, - val content: Set, - val pdfUrl: String -) : ModelEntity - -data class TouchUpKitProductDto( - val name: String, - val description: String?, - val quantity: Float, - val ready: Boolean -) - -// ==== DSL ==== -fun touchUpKit( - id: Long? = null, - project: String = "project", - buggy: String = "buggy", - company: String = "company", - quantity: Int = 1, - shippingDate: LocalDate = LocalDate.now(), - completionDate: LocalDate? = null, - finish: List, - material: List, - content: Set, - op: TouchUpKit.() -> Unit = {} -) = TouchUpKit( - id, - project, - buggy, - company, - quantity, - shippingDate, - completionDate, - finish.reduce { acc, f -> "$acc$TOUCH_UP_KIT_DELIMITER$f" }, - material.reduce { acc, f -> "$acc$TOUCH_UP_KIT_DELIMITER$f" }, - content -).apply(op) - -fun touchUpKit(touchUpKitSaveDto: TouchUpKitSaveDto) = - with(touchUpKitSaveDto) { - touchUpKit( - project = project, - buggy = buggy, - company = company, - quantity = quantity, - shippingDate = shippingDate, - finish = finish, - material = material, - content = content.map { touchUpKitProduct(it) }.toSet() - ) - } - -fun touchUpKitProduct( - id: Long? = null, - name: String = "product", - description: String? = "description", - quantity: Float = 1f, - ready: Boolean = false, - op: TouchUpKitProduct.() -> Unit = {} -) = TouchUpKitProduct(id, name, description, quantity, ready) - .apply(op) - -fun touchUpKitUpdateDto( - id: Long = 0L, - project: String? = null, - buggy: String? = null, - company: String? = null, - quantity: Int? = null, - shippingDate: LocalDate? = null, - completionDate: LocalDate? = null, - finish: List? = null, - material: List? = null, - content: Set? = null -) = TouchUpKitUpdateDto(id, project, buggy, company, quantity, shippingDate, completionDate, finish, material, content) - -fun touchUpKitProduct(touchUpKitProductDto: TouchUpKitProductDto) = - touchUpKitProduct( - name = touchUpKitProductDto.name, - description = touchUpKitProductDto.description, - quantity = touchUpKitProductDto.quantity, - ready = touchUpKitProductDto.ready - ) - -// ==== Exceptions ==== -private const val TOUCH_UP_KIT_NOT_FOUND_EXCEPTION_TITLE = "Touch up kit not found" -private const val TOUCH_UP_KIT_ALREADY_EXISTS_EXCEPTION_TITLE = "Touch up kit already exists" -private const val TOUCH_UP_KIT_EXCEPTION_ERROR_CODE = "touchupkit" - -fun touchUpKitIdNotFoundException(id: Long) = - NotFoundException( - TOUCH_UP_KIT_EXCEPTION_ERROR_CODE, - TOUCH_UP_KIT_NOT_FOUND_EXCEPTION_TITLE, - "A touch up kit with the id $id could not be found", - id - ) - -fun touchUpKitIdAlreadyExistsException(id: Long) = - AlreadyExistsException( - TOUCH_UP_KIT_EXCEPTION_ERROR_CODE, - TOUCH_UP_KIT_ALREADY_EXISTS_EXCEPTION_TITLE, - "A touch up kit with the id $id already exists", - id - ) +) : ModelEntity \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/validation/NullOrNotBlank.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/validation/NullOrNotBlank.kt deleted file mode 100644 index 9a381b9..0000000 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/validation/NullOrNotBlank.kt +++ /dev/null @@ -1,45 +0,0 @@ -package dev.fyloz.colorrecipesexplorer.model.validation - -import javax.validation.Constraint -import javax.validation.ConstraintValidator -import javax.validation.ConstraintValidatorContext -import javax.validation.Payload -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.contract -import kotlin.reflect.KClass - -private const val MESSAGE = "must be null or not blank" - -@Target(AnnotationTarget.FIELD) -@MustBeDocumented -@Constraint(validatedBy = [NullOrNotBlankValidator::class]) -annotation class NullOrNotBlank( - val message: String = MESSAGE, - val groups: Array> = [], - @Suppress("unused") val payload: Array> = [] -) - -class NullOrNotBlankValidator : ConstraintValidator { - var message = MESSAGE - - override fun initialize(constraintAnnotation: NullOrNotBlank) { - message = constraintAnnotation.message - } - - override fun isValid(value: String?, context: ConstraintValidatorContext): Boolean { - return value.isNullOrNotBlank().apply { - if (!this) context.buildConstraintViolationWithTemplate(message) - } - } -} - -fun String?.isNullOrNotBlank(): Boolean = this == null || isNotBlank() - -/** Checks if the given string [value] is not null and not blank. */ -@ExperimentalContracts -fun isNotNullAndNotBlank(value: String?): Boolean { - contract { returns(true) implies (value != null) } - return value != null && value.isNotBlank() -} - -infix fun String?.or(alternative: String): String = if (isNotNullAndNotBlank(this)) this else alternative diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/validation/NullOrSize.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/validation/NullOrSize.kt deleted file mode 100644 index e6c4208..0000000 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/validation/NullOrSize.kt +++ /dev/null @@ -1,46 +0,0 @@ -package dev.fyloz.colorrecipesexplorer.model.validation - -import javax.validation.Constraint -import javax.validation.ConstraintValidator -import javax.validation.ConstraintValidatorContext -import javax.validation.Payload -import kotlin.reflect.KClass - -private const val MIN_SIZE = Long.MIN_VALUE -private const val MAX_SIZE = Long.MAX_VALUE -private const val MESSAGE = "must be null or have a correct length" - -@Target(AnnotationTarget.FIELD) -@MustBeDocumented -@Constraint(validatedBy = [NullOrSizeValidator::class]) -annotation class NullOrSize( - val min: Long = MIN_SIZE, - val max: Long = MAX_SIZE, - val message: String = MESSAGE, - val groups: Array> = [], - @Suppress("unused") val payload: Array> = [] -) - -class NullOrSizeValidator : ConstraintValidator { - var min = MIN_SIZE - var max = MAX_SIZE - var message = MESSAGE - - override fun initialize(constraintAnnotation: NullOrSize) { - min = constraintAnnotation.min - max = constraintAnnotation.max - message = constraintAnnotation.message - } - - override fun isValid(value: Any?, context: ConstraintValidatorContext): Boolean { - if (value == null) return true - return when (value) { - is Number -> value.toLong() in min..max - is String -> value.length in min..max - is Collection<*> -> value.size in min..max - else -> throw IllegalStateException("Cannot use @NullOrSize on type ${value::class}") - }.apply { - if (!this) context.buildConstraintViolationWithTemplate(message) - } - } -} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixMaterialRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixMaterialRepository.kt index ae20f67..ff2597f 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixMaterialRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixMaterialRepository.kt @@ -2,6 +2,7 @@ package dev.fyloz.colorrecipesexplorer.repository import dev.fyloz.colorrecipesexplorer.model.MixMaterial import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository @Repository diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixMixTypeRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixMixTypeRepository.kt new file mode 100644 index 0000000..63aaf9c --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixMixTypeRepository.kt @@ -0,0 +1,34 @@ +package dev.fyloz.colorrecipesexplorer.repository + +import dev.fyloz.colorrecipesexplorer.model.MixMixType +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Modifying +import org.springframework.data.jpa.repository.Query +import org.springframework.stereotype.Repository + +@Repository +interface MixMixTypeRepository : JpaRepository { + @Query( + nativeQuery = true, value = """ + SELECT * FROM mix_mix_type mmt WHERE mmt.mix_id = :mixId + """ + ) + fun findAllByMixId(mixId: Long): List + + @Modifying + @Query( + nativeQuery = true, value = """ + INSERT INTO mix_mix_type (id, mix_type_id, mix_id, quantity, position) + VALUES (:id, :mixTypeId, :mixId, :quantity, :position) + """ + ) + fun saveForMixId(id: Long?, mixTypeId: Long, mixId: Long, quantity: Float, position: Int) + + @Modifying + @Query( + nativeQuery = true, value = """ + DELETE FROM mix_mix_type mmt WHERE mmt.mix_id = :mixId + """ + ) + fun deleteAllByMixId(mixId: Long) +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixTypeRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixTypeRepository.kt index 719262d..e0d11d0 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixTypeRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixTypeRepository.kt @@ -10,11 +10,12 @@ interface MixTypeRepository : JpaRepository { /** Checks if a mix type with the given [name], [materialTypeId] and a different [id] exists. */ @Query( """ - SELECT CASE WHEN(COUNT(m) > 0) THEN TRUE ELSE FALSE END - FROM MixType m WHERE m.name = :name AND m.material.materialType.id = :materialTypeId AND m.id <> :id - """ + SELECT CASE WHEN(COUNT(mt.id)) > 1 THEN TRUE ELSE FALSE END + FROM MixType mt + WHERE mt.name = :name AND mt.materialType.id = :materialTypeId AND mt.id <> :id + """ ) - fun existsByNameAndMaterialType(name: String, materialTypeId: Long, id: Long): Boolean + fun existsByNameAndMaterialTypeAndIdNot(name: String, materialTypeId: Long, id: Long): Boolean /** Finds the mix type with the given [name] and [materialTypeId]. */ @Query("SELECT m FROM MixType m WHERE m.name = :name AND m.material.materialType.id = :materialTypeId") diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/TouchUpKitRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/TouchUpKitRepository.kt index 0819613..e1ef51c 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/TouchUpKitRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/TouchUpKitRepository.kt @@ -2,5 +2,13 @@ package dev.fyloz.colorrecipesexplorer.repository import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKit import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Modifying +import org.springframework.data.jpa.repository.Query +import java.time.LocalDate -interface TouchUpKitRepository : JpaRepository +interface TouchUpKitRepository : JpaRepository { + /** Updates the [completionDate] of the touch up kit with the given [id]. */ + @Modifying + @Query("UPDATE TouchUpKit t SET t.completionDate = :completionDate WHERE t.id = :id") + fun updateCompletionDateById(id: Long, completionDate: LocalDate) +} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt index e2a00f7..bbc7659 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt @@ -1,5 +1,6 @@ package dev.fyloz.colorrecipesexplorer.rest +import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewCatalog import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase import dev.fyloz.colorrecipesexplorer.dtos.CompanyDto @@ -8,10 +9,8 @@ import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.* import javax.validation.Valid -private const val COMPANY_CONTROLLER_PATH = "api/company" - @RestController -@RequestMapping(COMPANY_CONTROLLER_PATH) +@RequestMapping(Constants.ControllerPaths.COMPANY) @RequireDatabase @PreAuthorizeViewCatalog class CompanyController(private val companyLogic: CompanyLogic) { @@ -26,7 +25,7 @@ class CompanyController(private val companyLogic: CompanyLogic) { @PostMapping @PreAuthorize("hasAuthority('EDIT_COMPANIES')") fun save(@Valid @RequestBody company: CompanyDto) = - created(COMPANY_CONTROLLER_PATH) { + created(Constants.ControllerPaths.COMPANY) { companyLogic.save(company) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt index 5674227..882ab25 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt @@ -23,10 +23,6 @@ class MaterialController( fun getAll() = ok(materialLogic.getAll()) - @GetMapping("notmixtype") - fun getAllNotMixType() = - ok(materialLogic.getAllNotMixType()) - @GetMapping("{id}") fun getById(@PathVariable id: Long) = ok(materialLogic.getById(id)) @@ -54,10 +50,10 @@ class MaterialController( @GetMapping("mix/create/{recipeId}") fun getAllForMixCreation(@PathVariable recipeId: Long) = - ok(materialLogic.getAllForMixCreation(recipeId)) + ok(materialLogic.getAllForRecipe(recipeId)) @GetMapping("mix/update/{mixId}") fun getAllForMixUpdate(@PathVariable mixId: Long) = - ok(materialLogic.getAllForMixUpdate(mixId)) + ok(materialLogic.getAllForMix(mixId)) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt index 44424f7..b1c67b2 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt @@ -11,7 +11,6 @@ import dev.fyloz.colorrecipesexplorer.logic.RecipeImageLogic import dev.fyloz.colorrecipesexplorer.logic.RecipeLogic import org.springframework.context.annotation.Profile import org.springframework.http.MediaType -import org.springframework.http.ResponseEntity import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.* import org.springframework.web.multipart.MultipartFile @@ -60,20 +59,19 @@ class RecipeController(private val recipeLogic: RecipeLogic, private val recipeI } @GetMapping("{recipeId}/image") - fun getAllImages(@PathVariable recipeId: Long) = ok { - recipeImageLogic.getAllImages(recipeId) - } + fun getAllImages(@PathVariable recipeId: Long) = + ok(recipeImageLogic.getAllImages(recipeId)) @PutMapping("{recipeId}/image", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) @PreAuthorizeEditRecipes - fun downloadImage(@PathVariable recipeId: Long, image: MultipartFile): ResponseEntity { - recipeImageLogic.download(image, recipeId) - return getById(recipeId) - } + fun downloadImage(@PathVariable recipeId: Long, image: MultipartFile) = + fileCreated("images/recipes/$recipeId") { + recipeImageLogic.download(image, recipeId) + } - @DeleteMapping("{recipeId}/image/{path}") + @DeleteMapping("{recipeId}/image/{id}") @PreAuthorizeEditRecipes - fun deleteImage(@PathVariable recipeId: Long, @PathVariable path: String) = noContent { - recipeImageLogic.delete(recipeId, path) + fun deleteImage(@PathVariable recipeId: Long, @PathVariable id: String) = noContent { + recipeImageLogic.delete(recipeId, id) } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt index 2c1e2f5..d50ce29 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt @@ -1,5 +1,6 @@ package dev.fyloz.colorrecipesexplorer.rest +import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties import dev.fyloz.colorrecipesexplorer.dtos.EntityDto import dev.fyloz.colorrecipesexplorer.model.ModelEntity @@ -35,6 +36,14 @@ fun okFile(file: Resource, mediaType: String? = null): ResponseEntity .contentType(MediaType.parseMediaType(mediaType ?: DEFAULT_MEDIA_TYPE)) .body(file) +/** Creates a HTTP CREATED [ResponseEntity] for the file created by the given [producer]. */ +fun fileCreated(basePath: String, producer: () -> String): ResponseEntity { + val fileName = producer() + val path = "${Constants.ControllerPaths.FILE}?path=$basePath/$fileName" + + return ResponseEntity.created(URI.create(path)).body(fileName) +} + /** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */ fun created(controllerPath: String, body: T): ResponseEntity = created(controllerPath, body, body.id!!) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/TouchUpKitController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/TouchUpKitController.kt index 4ef42ef..d52e958 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/TouchUpKitController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/TouchUpKitController.kt @@ -1,9 +1,8 @@ package dev.fyloz.colorrecipesexplorer.rest +import dev.fyloz.colorrecipesexplorer.Constants +import dev.fyloz.colorrecipesexplorer.dtos.TouchUpKitDto import dev.fyloz.colorrecipesexplorer.logic.TouchUpKitLogic -import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKitOutputDto -import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKitSaveDto -import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKitUpdateDto import org.springframework.context.annotation.Profile import org.springframework.core.io.Resource import org.springframework.http.MediaType @@ -12,35 +11,29 @@ import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.* import javax.validation.Valid -const val TOUCH_UP_KIT_CONTROLLER_PATH = "/api/touchupkit" - @RestController -@RequestMapping(TOUCH_UP_KIT_CONTROLLER_PATH) +@RequestMapping(Constants.ControllerPaths.TOUCH_UP_KIT) @Profile("!emergency") @PreAuthorize("hasAuthority('VIEW_TOUCH_UP_KITS')") class TouchUpKitController( private val touchUpKitLogic: TouchUpKitLogic ) { @GetMapping - fun getAll() = - ok(touchUpKitLogic.getAllForOutput()) + fun getAll() = ok(touchUpKitLogic.getAll()) @GetMapping("{id}") - fun getById(@PathVariable id: Long) = - ok(touchUpKitLogic.getByIdForOutput(id)) + fun getById(@PathVariable id: Long) = ok(touchUpKitLogic.getById(id)) @PostMapping @PreAuthorize("hasAuthority('EDIT_TOUCH_UP_KITS')") - fun save(@Valid @RequestBody touchUpKit: TouchUpKitSaveDto) = - created(TOUCH_UP_KIT_CONTROLLER_PATH) { - with(touchUpKitLogic) { - save(touchUpKit).toOutput() - } + fun save(@Valid @RequestBody touchUpKit: TouchUpKitDto) = + created(Constants.ControllerPaths.TOUCH_UP_KIT) { + touchUpKitLogic.save(touchUpKit) } @PutMapping @PreAuthorize("hasAuthority('EDIT_TOUCH_UP_KITS')") - fun update(@Valid @RequestBody touchUpKit: TouchUpKitUpdateDto) = noContent { + fun update(@Valid @RequestBody touchUpKit: TouchUpKitDto) = noContent { touchUpKitLogic.update(touchUpKit) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt index 8930bf9..1e2c6bc 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt @@ -7,16 +7,11 @@ import dev.fyloz.colorrecipesexplorer.logic.files.FileLogic import dev.fyloz.colorrecipesexplorer.model.Material import dev.fyloz.colorrecipesexplorer.repository.MaterialRepository import org.springframework.beans.factory.annotation.Qualifier -import java.net.URLEncoder -import java.nio.charset.StandardCharsets interface MaterialService : Service { /** Checks if a material with the given [name] and a different [id] exists. */ fun existsByName(name: String, id: Long?): Boolean - /** Gets all non mix type materials. */ - fun getAllNotMixType(): Collection - /** Updates the [inventoryQuantity] of the [Material] with the given [id]. */ fun updateInventoryQuantityById(id: Long, inventoryQuantity: Float) @@ -32,7 +27,7 @@ class DefaultMaterialService( ) : BaseService(repository), MaterialService { override fun existsByName(name: String, id: Long?) = repository.existsByNameAndIdNot(name, id ?: 0) - override fun getAllNotMixType() = repository.findAllByIsMixTypeIsFalse().map(::toDto) + override fun getAll() = repository.findAllByIsMixTypeIsFalse().map(::toDto) override fun updateInventoryQuantityById(id: Long, inventoryQuantity: Float) = repository.updateInventoryQuantityById(id, inventoryQuantity) @@ -45,20 +40,12 @@ class DefaultMaterialService( entity.inventoryQuantity, entity.isMixType, materialTypeService.toDto(entity.materialType!!), - getSimdutUrl(entity) + hasSimdut(entity) ) override fun toEntity(dto: MaterialDto) = Material(dto.id, dto.name, dto.inventoryQuantity, dto.isMixType, materialTypeService.toEntity(dto.materialType)) - private fun getSimdutUrl(material: Material): String? { - val filePath = "${Constants.FilePaths.SIMDUT}/${material.name}.pdf" - - if (!fileLogic.exists(filePath)) { - return null - } - - val encodedPath = URLEncoder.encode(filePath, StandardCharsets.UTF_8) - return "${Constants.ControllerPaths.FILE}?path=$encodedPath" - } + private fun hasSimdut(material: Material) = + fileLogic.exists("${Constants.FilePaths.SIMDUT}/${material.name}.pdf") } \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMixTypeService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMixTypeService.kt new file mode 100644 index 0000000..8f1f0c4 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMixTypeService.kt @@ -0,0 +1,34 @@ +package dev.fyloz.colorrecipesexplorer.service + +import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent +import dev.fyloz.colorrecipesexplorer.dtos.MixMixTypeDto +import dev.fyloz.colorrecipesexplorer.model.MixMixType +import dev.fyloz.colorrecipesexplorer.repository.MixMixTypeRepository + +interface MixMixTypeService : Service { + fun getAllByMixId(mixId: Long): List + + fun saveAllForMixId(mixMixTypes: List, mixId: Long): List +} + +@ServiceComponent +class DefaultMixMixTypeService(repository: MixMixTypeRepository, private val mixTypeService: MixTypeService) : + BaseService(repository), MixMixTypeService { + override fun getAllByMixId(mixId: Long) = repository.findAllByMixId(mixId).map(::toDto) + + override fun saveAllForMixId(mixMixTypes: List, mixId: Long): List { + repository.deleteAllByMixId(mixId) + + mixMixTypes.forEach { saveForMixId(it, mixId) } + return getAllByMixId(mixId) + } + + fun saveForMixId(mixMixType: MixMixTypeDto, mixId: Long) = + repository.saveForMixId(mixMixType.id, mixMixType.mixType.id, mixId, mixMixType.quantity, mixMixType.position) + + override fun toDto(entity: MixMixType) = + MixMixTypeDto(entity.id, mixTypeService.toDto(entity.mixType), entity.quantity, entity.position) + + override fun toEntity(dto: MixMixTypeDto) = + MixMixType(dto.id, mixTypeService.toEntity(dto.mixType), dto.quantity, dto.position) +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt index 802dd98..96b914e 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt @@ -2,6 +2,7 @@ package dev.fyloz.colorrecipesexplorer.service import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent import dev.fyloz.colorrecipesexplorer.dtos.MixDto +import dev.fyloz.colorrecipesexplorer.dtos.MixQuantitiesDto import dev.fyloz.colorrecipesexplorer.model.Mix import dev.fyloz.colorrecipesexplorer.repository.MixRepository @@ -17,18 +18,29 @@ interface MixService : Service { class DefaultMixService( repository: MixRepository, private val mixTypeService: MixTypeService, - private val mixMaterialService: MixMaterialService + private val mixMaterialService: MixMaterialService, + private val mixMixTypeService: MixMixTypeService ) : BaseService(repository), MixService { override fun getAllByMixTypeId(mixTypeId: Long) = repository.findAllByMixTypeId(mixTypeId).map(::toDto) override fun updateLocationById(id: Long, location: String?) = repository.updateLocationById(id, location) + override fun save(dto: MixDto): MixDto { + val savedMix = super.save(dto) + val savedMixMixTypes = mixMixTypeService.saveAllForMixId(dto.mixQuantities.mixTypes, savedMix.id) + + return savedMix.copy(mixQuantities = savedMix.mixQuantities.copy(mixTypes = savedMixMixTypes)) + } + override fun toDto(entity: Mix) = MixDto( entity.id!!, entity.location, entity.recipeId, mixTypeService.toDto(entity.mixType), - entity.mixMaterials.map(mixMaterialService::toDto) + MixQuantitiesDto( + entity.mixMaterials.filter { !it.material.isMixType }.map(mixMaterialService::toDto), + mixMixTypeService.getAllByMixId(entity.id) + ) ) override fun toEntity(dto: MixDto) = @@ -37,6 +49,6 @@ class DefaultMixService( dto.location, dto.recipeId, mixTypeService.toEntity(dto.mixType), - dto.mixMaterials.map(mixMaterialService::toEntity) + dto.mixQuantities.materials.map(mixMaterialService::toEntity) ) } \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeService.kt index e942fc7..eaa7393 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeService.kt @@ -20,10 +20,14 @@ interface MixTypeService : Service { } @ServiceComponent -class DefaultMixTypeService(repository: MixTypeRepository, val materialService: MaterialService) : +class DefaultMixTypeService( + repository: MixTypeRepository, + val materialService: MaterialService, + val materialTypeService: MaterialTypeService +) : BaseService(repository), MixTypeService { override fun existsByNameAndMaterialType(name: String, materialTypeId: Long, id: Long?) = - repository.existsByNameAndMaterialType(name, materialTypeId, id ?: 0L) + repository.existsByNameAndMaterialTypeAndIdNot(name, materialTypeId, id ?: 0L) override fun getByNameAndMaterialType(name: String, materialTypeId: Long) = repository.findByNameAndMaterialType(name, materialTypeId)?.let(::toDto) @@ -32,8 +36,17 @@ class DefaultMixTypeService(repository: MixTypeRepository, val materialService: override fun isShared(id: Long) = repository.isShared(id) override fun toDto(entity: MixType) = - MixTypeDto(entity.id!!, entity.name, materialService.toDto(entity.material)) + MixTypeDto( + entity.id!!, + entity.name, + materialTypeService.toDto(entity.materialType), + if (entity.material != null) materialService.toDto(entity.material) else null + ) override fun toEntity(dto: MixTypeDto) = - MixType(dto.id, dto.name, materialService.toEntity(dto.material)) + MixType( + dto.id, dto.name, + materialTypeService.toEntity(dto.materialType), + if (dto.material != null) materialService.toEntity(dto.material) else null + ) } \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/TouchUpKitService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/TouchUpKitService.kt new file mode 100644 index 0000000..b45d954 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/TouchUpKitService.kt @@ -0,0 +1,69 @@ +package dev.fyloz.colorrecipesexplorer.service + +import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent +import dev.fyloz.colorrecipesexplorer.dtos.TouchUpKitDto +import dev.fyloz.colorrecipesexplorer.dtos.TouchUpKitProductDto +import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic +import dev.fyloz.colorrecipesexplorer.model.ConfigurationType +import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKit +import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKitProduct +import dev.fyloz.colorrecipesexplorer.repository.TouchUpKitRepository +import java.time.LocalDate +import java.time.Period + +interface TouchUpKitService : Service { + /** Updates the [completionDate] of the touch up kit with the given [id]. */ + fun updateCompletionDateById(id: Long, completionDate: LocalDate) +} + +@ServiceComponent +class DefaultTouchUpKitService(repository: TouchUpKitRepository, private val configurationLogic: ConfigurationLogic) : + BaseService(repository), TouchUpKitService { + override fun updateCompletionDateById(id: Long, completionDate: LocalDate) = + repository.updateCompletionDateById(id, completionDate) + + override fun toDto(entity: TouchUpKit) = + TouchUpKitDto( + entity.id!!, + entity.project, + entity.buggy, + entity.company, + entity.quantity, + entity.shippingDate, + entity.completionDate, + entity.completionDate != null, + isExpired(entity), + entity.finish.split(LIST_DELIMITER), + entity.material.split(LIST_DELIMITER), + entity.content.map(::touchUpKitProductToDto) + ) + + private fun touchUpKitProductToDto(entity: TouchUpKitProduct) = + TouchUpKitProductDto(entity.id!!, entity.name, entity.description, entity.quantity, entity.ready) + + override fun toEntity(dto: TouchUpKitDto) = + TouchUpKit( + dto.id, + dto.project, + dto.buggy, + dto.company, + dto.quantity, + dto.shippingDate, + dto.completionDate, + dto.finish.joinToString(LIST_DELIMITER), + dto.material.joinToString(LIST_DELIMITER), + dto.content.map(::touchUpKitProductToEntity) + ) + + private fun touchUpKitProductToEntity(dto: TouchUpKitProductDto) = + TouchUpKitProduct(dto.id, dto.name, dto.description, dto.quantity, dto.ready) + + private fun isExpired(touchUpKit: TouchUpKit) = + with(Period.parse(configurationLogic.getContent(ConfigurationType.TOUCH_UP_KIT_EXPIRATION))) { + touchUpKit.completionDate != null && touchUpKit.completionDate.plus(this) < LocalDate.now() + } + + companion object { + private const val LIST_DELIMITER = ";" + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultInventoryLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultInventoryLogicTest.kt index c39a91b..0a82d67 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultInventoryLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultInventoryLogicTest.kt @@ -99,9 +99,9 @@ class DefaultInventoryLogicTest { mutableListOf(), listOf() ) - val mixType = MixTypeDto(1L, "Unit test mix type", material) + val mixType = MixTypeDto(1L, "Unit test mix type", materialType) val mixMaterial = MixMaterialDto(1L, material, 1000f, 1) - val mix = MixDto(1L, null, recipe.id, mixType, listOf(mixMaterial)) + val mix = MixDto(1L, null, recipe.id, mixType, MixQuantitiesDto(listOf(mixMaterial), listOf())) val dto = MixDeductDto(mix.id, 2f) val expectedQuantities = listOf(MaterialQuantityDto(material.id, mixMaterial.quantity * dto.ratio)) @@ -136,9 +136,9 @@ class DefaultInventoryLogicTest { mutableListOf(), listOf() ) - val mixType = MixTypeDto(1L, "Unit test mix type", material) + val mixType = MixTypeDto(1L, "Unit test mix type", materialType) val mixMaterial = MixMaterialDto(1L, material, 1000f, 1) - val mix = MixDto(1L, null, recipe.id, mixType, listOf(mixMaterial)) + val mix = MixDto(1L, null, recipe.id, mixType, MixQuantitiesDto(listOf(mixMaterial), listOf())) val dto = MixDeductDto(mix.id, 2f) val expectedQuantities = listOf(MaterialQuantityDto(material.id, mixMaterial.quantity * dto.ratio)) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialLogicTest.kt index 82e8a5d..f7a929f 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialLogicTest.kt @@ -50,7 +50,7 @@ class DefaultMaterialLogicTest { listOf() ) private val mix = MixDto( - 1L, "location", recipe.id, mixType = MixTypeDto(1L, "Unit test mix type", materialMixType), listOf() + 1L, "location", recipe.id, mixType = MixTypeDto(1L, "Unit test mix type", materialType), MixQuantitiesDto() ) private val mix2 = mix.copy(id = 2L, mixType = mix.mixType.copy(id = 2L, material = materialMixType2)) @@ -89,18 +89,6 @@ class DefaultMaterialLogicTest { assertFalse(exists) } - @Test - fun getAllNotMixType_normalBehavior_returnsMaterialsFromService() { - // Arrange - every { materialServiceMock.getAllNotMixType() } returns listOf(material) - - // Act - val materials = materialLogic.getAllNotMixType() - - // Assert - assertContains(materials, material) - } - @Test fun getAllForMixCreation_normalBehavior_returnsNonMixTypeMaterials() { // Arrange @@ -108,7 +96,7 @@ class DefaultMaterialLogicTest { every { recipeLogicMock.getById(any()) } returns recipe // Act - val materials = materialLogic.getAllForMixCreation(recipe.id) + val materials = materialLogic.getAllForRecipe(recipe.id) // Assert assertContains(materials, material) @@ -123,7 +111,7 @@ class DefaultMaterialLogicTest { every { recipeLogicMock.getById(any()) } returns recipe // Act - val materials = materialLogic.getAllForMixCreation(recipe.id) + val materials = materialLogic.getAllForRecipe(recipe.id) // Assert assertContains(materials, materialMixType2) @@ -137,7 +125,7 @@ class DefaultMaterialLogicTest { every { mixLogicMock.getById(any()) } returns mix // Act - val materials = materialLogic.getAllForMixUpdate(mix.id) + val materials = materialLogic.getAllForMix(mix.id) // Assert assertContains(materials, material) @@ -154,26 +142,12 @@ class DefaultMaterialLogicTest { every { mixLogicMock.getById(any()) } returns mix // Act - val materials = materialLogic.getAllForMixUpdate(mix.id) + val materials = materialLogic.getAllForMix(mix.id) // Assert assertContains(materials, materialMixType2) } - @Test - fun getAllForMixUpdate_normalBehavior_excludesGivenMixTypeMaterial() { - // Arrange - every { materialLogic.getAll() } returns listOf(material, materialMixType, materialMixType2) - every { recipeLogicMock.getById(any()) } returns recipe - every { mixLogicMock.getById(any()) } returns mix - - // Act - val materials = materialLogic.getAllForMixUpdate(mix.id) - - // Assert - assertFalse { materialMixType in materials } - } - @Test fun save_materialSaveDto_normalBehavior_callsSave() { // Arrange diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixLogicTest.kt index 33204fe..93adba9 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixLogicTest.kt @@ -11,7 +11,7 @@ class DefaultMixLogicTest { private val recipeLogicMock = mockk() private val materialTypeLogicMock = mockk() private val mixTypeLogicMock = mockk() - private val mixMaterialLogicMock = mockk() + private val mixQuantityLogicMock = mockk() private val mixLogic = spyk( DefaultMixLogic( @@ -19,7 +19,7 @@ class DefaultMixLogicTest { recipeLogicMock, materialTypeLogicMock, mixTypeLogicMock, - mixMaterialLogicMock + mixQuantityLogicMock ) ) @@ -40,10 +40,11 @@ class DefaultMixLogicTest { ) private val materialType = MaterialTypeDto(1L, "Unit test material type", "UTMT", false) private val mixType = - MixTypeDto(1L, "Unit test mix type", MaterialDto(1L, "Unit test mix type material", 1000f, true, materialType)) + MixTypeDto(1L, "Unit test mix type", materialType) private val mixMaterial = MixMaterialDto(1L, MaterialDto(2L, "Unit test material", 1000f, false, materialType), 50f, 1) - private val mix = MixDto(recipeId = recipe.id, mixType = mixType, mixMaterials = listOf(mixMaterial)) + private val mixQuantities = MixQuantitiesDto(listOf(mixMaterial)) + private val mix = MixDto(recipeId = recipe.id, mixType = mixType, mixQuantities = mixQuantities) @AfterEach internal fun afterEach() { @@ -54,7 +55,7 @@ class DefaultMixLogicTest { every { recipeLogicMock.getById(any()) } returns recipe every { materialTypeLogicMock.getById(any()) } returns materialType every { mixTypeLogicMock.getOrCreateForNameAndMaterialType(any(), any()) } returns mixType - every { mixMaterialLogicMock.validateAndSaveAll(any()) } returns listOf(mixMaterial) + every { mixQuantityLogicMock.validateAndPrepareForMix(any()) } returns mixQuantities every { mixLogic.save(any()) } returnsArgument 0 } @@ -62,7 +63,7 @@ class DefaultMixLogicTest { every { recipeLogicMock.getById(any()) } returns recipe every { materialTypeLogicMock.getById(any()) } returns materialType every { mixTypeLogicMock.updateOrCreateForNameAndMaterialType(any(), any(), any()) } returns mixType - every { mixMaterialLogicMock.validateAndSaveAll(any()) } returns listOf(mixMaterial) + every { mixQuantityLogicMock.validateAndPrepareForMix(any()) } returns mixQuantities every { mixLogic.getById(any()) } returns mix every { mixLogic.update(any()) } returnsArgument 0 } @@ -73,7 +74,7 @@ class DefaultMixLogicTest { setup_save_normalBehavior() val mixMaterialDto = - MixMaterialSaveDto(mixMaterial.id, mixMaterial.material.id, mixMaterial.quantity, mixMaterial.position) + MixQuantitySaveDto(mixMaterial.id, mixMaterial.material.id, mixMaterial.quantity, mixMaterial.position, false) val saveDto = MixSaveDto(0L, mixType.name, recipe.id, materialType.id, listOf(mixMaterialDto)) // Act @@ -92,11 +93,12 @@ class DefaultMixLogicTest { val mixMaterialDtos = listOf( - MixMaterialSaveDto( + MixQuantitySaveDto( mixMaterial.id, mixMaterial.material.id, mixMaterial.quantity, - mixMaterial.position + mixMaterial.position, + false ) ) val saveDto = MixSaveDto(0L, mixType.name, recipe.id, materialType.id, mixMaterialDtos) @@ -106,9 +108,9 @@ class DefaultMixLogicTest { // Assert verify { - mixMaterialLogicMock.validateAndSaveAll(mixMaterialDtos) + mixQuantityLogicMock.validateAndPrepareForMix(mixMaterialDtos) } - confirmVerified(mixMaterialLogicMock) + confirmVerified(mixQuantityLogicMock) } @Test @@ -117,7 +119,7 @@ class DefaultMixLogicTest { setup_update_normalBehavior() val mixMaterialDto = - MixMaterialSaveDto(mixMaterial.id, mixMaterial.material.id, mixMaterial.quantity, mixMaterial.position) + MixQuantitySaveDto(mixMaterial.id, mixMaterial.material.id, mixMaterial.quantity, mixMaterial.position, false) val saveDto = MixSaveDto(mix.id, mixType.name, recipe.id, materialType.id, listOf(mixMaterialDto)) // Act @@ -135,11 +137,12 @@ class DefaultMixLogicTest { setup_update_normalBehavior() val mixMaterialDtos = listOf( - MixMaterialSaveDto( + MixQuantitySaveDto( mixMaterial.id, mixMaterial.material.id, mixMaterial.quantity, - mixMaterial.position + mixMaterial.position, + false ) ) val saveDto = MixSaveDto(mix.id, mixType.name, recipe.id, materialType.id, mixMaterialDtos) @@ -149,9 +152,9 @@ class DefaultMixLogicTest { // Assert verify { - mixMaterialLogicMock.validateAndSaveAll(mixMaterialDtos) + mixQuantityLogicMock.validateAndPrepareForMix(mixMaterialDtos) } - confirmVerified(mixMaterialLogicMock) + confirmVerified(mixQuantityLogicMock) } @Test diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixMaterialLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixQuantityLogicTest.kt similarity index 83% rename from src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixMaterialLogicTest.kt rename to src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixQuantityLogicTest.kt index f442552..39374aa 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixMaterialLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixQuantityLogicTest.kt @@ -5,18 +5,17 @@ import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException -import dev.fyloz.colorrecipesexplorer.service.MixMaterialService import dev.fyloz.colorrecipesexplorer.utils.PositionUtils import io.mockk.* import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows -class DefaultMixMaterialLogicTest { - private val mixMaterialServiceMock = mockk() +class DefaultMixQuantityLogicTest { private val materialLogicMock = mockk() + private val mixTypeLogicMock = mockk() - private val mixMaterialLogic = DefaultMixMaterialLogic(mixMaterialServiceMock, materialLogicMock) + private val mixMaterialLogic = DefaultMixQuantityLogic(materialLogicMock, mixTypeLogicMock) @AfterEach internal fun afterEach() { @@ -35,7 +34,7 @@ class DefaultMixMaterialLogicTest { // Act // Assert - mixMaterialLogic.validateMixMaterials(setOf(mixMaterial)) + mixMaterialLogic.validateMixQuantities(listOf(mixMaterial)) } @Test @@ -43,7 +42,7 @@ class DefaultMixMaterialLogicTest { // Arrange // Act // Assert - mixMaterialLogic.validateMixMaterials(setOf()) + mixMaterialLogic.validateMixQuantities(listOf()) } @Test @@ -58,7 +57,7 @@ class DefaultMixMaterialLogicTest { // Act // Assert - assertThrows { mixMaterialLogic.validateMixMaterials(setOf(mixMaterial)) } + assertThrows { mixMaterialLogic.validateMixQuantities(listOf(mixMaterial)) } } @Test @@ -75,6 +74,6 @@ class DefaultMixMaterialLogicTest { // Act // Assert - assertThrows { mixMaterialLogic.validateMixMaterials(setOf(mixMaterial)) } + assertThrows { mixMaterialLogic.validateMixQuantities(listOf(mixMaterial)) } } } \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixTypeLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixTypeLogicTest.kt index cfe4b83..f5a41c6 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixTypeLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixTypeLogicTest.kt @@ -1,6 +1,5 @@ package dev.fyloz.colorrecipesexplorer.logic -import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto import dev.fyloz.colorrecipesexplorer.dtos.MixTypeDto import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException @@ -9,18 +8,15 @@ import io.mockk.* import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows -import kotlin.math.exp import kotlin.test.assertEquals class DefaultMixTypeLogicTest { private val mixTypeServiceMock = mockk() - private val materialLogicMock = mockk() - private val mixTypeLogic = spyk(DefaultMixTypeLogic(mixTypeServiceMock, materialLogicMock)) + private val mixTypeLogic = spyk(DefaultMixTypeLogic(mixTypeServiceMock)) private val materialType = MaterialTypeDto(1L, "Unit test material type", "UTMT", false) - private val material = MaterialDto(1L, "Unit test material", 1000f, true, materialType) - private val mixType = MixTypeDto(id = 1L, name = "Unit test mix type", material) + private val mixType = MixTypeDto(id = 1L, name = "Unit test mix type", materialType) @AfterEach fun afterEach() { @@ -43,7 +39,6 @@ class DefaultMixTypeLogicTest { fun getOrCreateForNameAndMaterialType_notFound_callsSave() { // Arrange every { mixTypeServiceMock.getByNameAndMaterialType(any(), any()) } returns null - every { materialLogicMock.save(any()) } returns material every { mixTypeLogic.save(any()) } returnsArgument 0 val expectedMixType = mixType.copy(id = 0L) @@ -61,7 +56,6 @@ class DefaultMixTypeLogicTest { fun getOrCreateForNameAndMaterialType_notFound_returnsFromSave() { // Arrange every { mixTypeServiceMock.getByNameAndMaterialType(any(), any()) } returns null - every { materialLogicMock.save(any()) } returns material every { mixTypeLogic.save(any()) } returnsArgument 0 val expectedMixType = mixType.copy(id = 0L) @@ -73,11 +67,26 @@ class DefaultMixTypeLogicTest { assertEquals(expectedMixType, actualMixType) } + @Test + fun updateOrCreateForNameAndMaterialType_alreadyExists_returnsFromgetByNameAndMaterialType() { + // Arrange + every { mixTypeServiceMock.existsByNameAndMaterialType(any(), any(), any()) } returns true + every { mixTypeServiceMock.isShared(any()) } returns true + every { mixTypeServiceMock.getByNameAndMaterialType(any(), any()) } returns mixType + every { mixTypeLogic.save(any()) } returnsArgument 0 + + // Act + val actualMixType = mixTypeLogic.updateOrCreateForNameAndMaterialType(mixType, mixType.name, materialType) + + // Assert + assertEquals(mixType, actualMixType) + } + @Test fun updateOrCreateForNameAndMaterialType_mixTypeShared_callsSave() { // Arrange + every { mixTypeServiceMock.existsByNameAndMaterialType(any(), any(), any()) } returns false every { mixTypeServiceMock.isShared(any()) } returns true - every { materialLogicMock.save(any()) } returns material every { mixTypeLogic.save(any()) } returnsArgument 0 val expectedMixType = mixType.copy(id = 0L, name = "${mixType.name} updated") @@ -94,8 +103,8 @@ class DefaultMixTypeLogicTest { @Test fun updateOrCreateForNameAndMaterialType_mixTypeShared_returnsFromSave() { // Arrange + every { mixTypeServiceMock.existsByNameAndMaterialType(any(), any(), any()) } returns false every { mixTypeServiceMock.isShared(any()) } returns true - every { materialLogicMock.save(any()) } returns material every { mixTypeLogic.save(any()) } returnsArgument 0 val expectedMixType = mixType.copy(id = 0L, name = "${mixType.name} updated") @@ -110,8 +119,8 @@ class DefaultMixTypeLogicTest { @Test fun updateOrCreateForNameAndMaterialType_mixTypeNotShared_callsUpdate() { // Arrange + every { mixTypeServiceMock.existsByNameAndMaterialType(any(), any(), any()) } returns false every { mixTypeServiceMock.isShared(any()) } returns false - every { materialLogicMock.update(any()) } returns material every { mixTypeLogic.update(any()) } returnsArgument 0 val expectedMixType = mixType.copy(name = "${mixType.name} updated") @@ -128,8 +137,8 @@ class DefaultMixTypeLogicTest { @Test fun updateOrCreateForNameAndMaterialType_mixTypeNotShared_returnsFromUpdate() { // Arrange + every { mixTypeServiceMock.existsByNameAndMaterialType(any(), any(), any()) } returns false every { mixTypeServiceMock.isShared(any()) } returns false - every { materialLogicMock.update(any()) } returns material every { mixTypeLogic.update(any()) } returnsArgument 0 val expectedMixType = mixType.copy(name = "${mixType.name} updated") diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeImageLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeImageLogicTest.kt index 7ab069c..50ac6f8 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeImageLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeImageLogicTest.kt @@ -88,10 +88,11 @@ class DefaultRecipeImageLogicTest { every { fileLogicMock.deleteFromDirectory(any(), any()) } just runs val recipeImagesDirectoryPath = "${Constants.FilePaths.RECIPE_IMAGES}/$recipeId" - val imagePath = "$recipeImagesDirectoryPath/1" + val imageId = 1.toString() + val imagePath = "$recipeImagesDirectoryPath/$imageId" // Act - recipeImageLogic.delete(recipeId, imagePath) + recipeImageLogic.delete(recipeId, imageId) // Assert verify { diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultTouchUpKitLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultTouchUpKitLogicTest.kt new file mode 100644 index 0000000..0e3ce09 --- /dev/null +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultTouchUpKitLogicTest.kt @@ -0,0 +1,146 @@ +package dev.fyloz.colorrecipesexplorer.logic + +import dev.fyloz.colorrecipesexplorer.Constants +import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic +import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic +import dev.fyloz.colorrecipesexplorer.model.ConfigurationType +import dev.fyloz.colorrecipesexplorer.service.TouchUpKitService +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 + +class DefaultTouchUpKitLogicTest { + private val touchUpKitServiceMock = mockk() + private val fileLogicMock = mockk() + private val configLogicMock = mockk() + + private val touchUpKitLogic = spyk(DefaultTouchUpKitLogic(touchUpKitServiceMock, fileLogicMock, configLogicMock)) + + private val pdfMockData = mockk() + private val pdfMock = mockk { + mockkStatic(PdfDocument::toByteArrayResource) + mockkStatic(PdfDocument::toByteArrayResource) + every { toByteArrayResource() } returns pdfMockData + } + + @AfterEach + internal fun afterEach() { + clearAllMocks() + } + + @Test + fun complete_normalBehavior_callsUpdateCompletionDateByIdInService() { + // Arrange + every { touchUpKitServiceMock.updateCompletionDateById(any(), any()) } just runs + + val touchUpKitId = 1L + + // Act + touchUpKitLogic.complete(touchUpKitId) + + // Assert + verify { + touchUpKitServiceMock.updateCompletionDateById(touchUpKitId, any()) + } + } + + @Test + fun generateJobPdfResource_normalBehavior_returnsGeneratedPdf() { + // Arrange + every { touchUpKitLogic.generateJobPdf(any()) } returns pdfMock + every { configLogicMock.getContent(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns false.toString() + + // Act + val generatedPdfData = touchUpKitLogic.generateJobPdfResource("Unit test job") + + // Assert + assertEquals(pdfMockData, generatedPdfData) + } + + @Test + fun generateJobPdfResource_normalBehavior_callsCacheJobPdf() { + // Arrange + every { touchUpKitLogic.generateJobPdf(any()) } returns pdfMock + every { configLogicMock.getContent(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns false.toString() + + val job = "Unit test job" + + // Act + touchUpKitLogic.generateJobPdfResource(job) + + // Assert + verify { + touchUpKitLogic.cacheJobPdf(job, pdfMock) + } + } + + @Test + fun generateJobPdfResource_cacheEnabledAndPdfExists_returnsCachedJobPdf() { + // Arrange + every { configLogicMock.getContent(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns true.toString() + every { fileLogicMock.exists(any()) } returns true + every { fileLogicMock.read(any()) } returns pdfMockData + + // Act + val pdfData = touchUpKitLogic.generateJobPdfResource("Unit test job") + + // Assert + assertEquals(pdfMockData, pdfData) + } + + @Test + fun generateJobPdfResource_cacheEnabledAndPdfNotExists_generatesPdf() { + // Arrange + every { touchUpKitLogic.generateJobPdf(any()) } returns pdfMock + every { touchUpKitLogic.cacheJobPdf(any(), any()) } just runs + every { configLogicMock.getContent(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns true.toString() + every { fileLogicMock.exists(any()) } returns false + + // Act + touchUpKitLogic.generateJobPdfResource("Unit test job") + + // Assert + verify { + touchUpKitLogic.cacheJobPdf(any(), any()) + } + } + + @Test + fun cacheJobPdf_normalBehavior_callsWriteInFileLogic() { + // Arrange + every { configLogicMock.getContent(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns true.toString() + every { fileLogicMock.write(any(), any(), any()) } just runs + + val job = "Unit test job" + val pdfPath = "${Constants.FilePaths.TOUCH_UP_KITS}/$job.pdf" + + // Act + touchUpKitLogic.cacheJobPdf(job, pdfMock) + + // Assert + verify { + fileLogicMock.write(pdfMockData, pdfPath, true) + } + confirmVerified(fileLogicMock) + } + + @Test + fun cacheJobPdf_cacheDisabled_doNothing() { + // Arrange + every { configLogicMock.getContent(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns false.toString() + every { fileLogicMock.write(any(), any(), any()) } just runs + + // Act + touchUpKitLogic.cacheJobPdf("Unit test job", pdfMock) + + // Assert + verify(exactly = 0) { + fileLogicMock.write(any(), any(), any()) + } + confirmVerified(fileLogicMock) + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/TouchUpKitLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/TouchUpKitLogicTest.kt index 2bf5256..0bc8ffe 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/TouchUpKitLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/TouchUpKitLogicTest.kt @@ -13,126 +13,126 @@ 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 - } -} +//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() - } +// 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() +// } } -- 2.40.1 From d0965d75a007c034ce2f8d246fb290b878cbef6d Mon Sep 17 00:00:00 2001 From: FyloZ Date: Wed, 20 Apr 2022 22:17:38 -0400 Subject: [PATCH 11/11] #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 } -- 2.40.1