Merge branch 'features' into 'master'

Changement du nom interne "employé" version "utilisateur", ajout du support complet pour les kits de retouche

See merge request color-recipes-explorer/backend!30
This commit is contained in:
William Nolin 2021-05-20 17:42:33 +00:00
commit d7e335779b
48 changed files with 1263 additions and 904 deletions

View File

@ -2,17 +2,23 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
group = "dev.fyloz.colorrecipesexplorer" group = "dev.fyloz.colorrecipesexplorer"
val kotlinVersion = "1.5.0"
val springBootVersion = "2.3.4.RELEASE"
plugins { plugins {
// Outer scope variables can't be accessed in the plugins section, so we have to redefine them here
val kotlinVersion = "1.5.0"
val springBootVersion = "2.3.4.RELEASE"
id("java") id("java")
id("org.jetbrains.kotlin.jvm") version "1.4.30" id("org.jetbrains.kotlin.jvm") version kotlinVersion
id("org.jetbrains.dokka") version "1.4.20" id("org.jetbrains.dokka") version "1.4.32"
id("org.springframework.boot") version "2.3.4.RELEASE" id("org.springframework.boot") version springBootVersion
id("org.jetbrains.kotlin.plugin.spring") version "1.4.30" id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion
id("org.jetbrains.kotlin.plugin.jpa") version "1.4.30" id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion
} }
repositories { repositories {
jcenter()
mavenCentral() mavenCentral()
maven { maven {
@ -21,33 +27,32 @@ repositories {
} }
dependencies { dependencies {
implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.4.10")) implementation(platform("org.jetbrains.kotlin:kotlin-bom:${kotlinVersion}"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.10") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlinVersion}")
implementation("org.jetbrains.kotlin:kotlin-reflect:1.4.10") implementation("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.11.3") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.11.3")
implementation("javax.xml.bind:jaxb-api:2.3.0") implementation("javax.xml.bind:jaxb-api:2.3.0")
implementation("io.jsonwebtoken:jjwt:0.9.1") implementation("io.jsonwebtoken:jjwt:0.9.1")
implementation("org.apache.poi:poi-ooxml:4.1.0") implementation("org.apache.poi:poi-ooxml:4.1.0")
implementation("org.apache.pdfbox:pdfbox:2.0.4") implementation("org.apache.pdfbox:pdfbox:2.0.4")
implementation("dev.fyloz.colorrecipesexplorer:database-manager:1.2.0") implementation("dev.fyloz.colorrecipesexplorer:database-manager:5.1")
implementation("org.springframework.boot:spring-boot-starter-data-jpa:2.3.4.RELEASE") implementation("org.springframework.boot:spring-boot-starter-data-jpa:${springBootVersion}")
implementation("org.springframework.boot:spring-boot-starter-jdbc:2.3.4.RELEASE") implementation("org.springframework.boot:spring-boot-starter-jdbc:${springBootVersion}")
implementation("org.springframework.boot:spring-boot-starter-web:2.3.4.RELEASE") implementation("org.springframework.boot:spring-boot-starter-web:${springBootVersion}")
implementation("org.springframework.boot:spring-boot-starter-validation:2.3.4.RELEASE") implementation("org.springframework.boot:spring-boot-starter-validation:${springBootVersion}")
implementation("org.springframework.boot:spring-boot-starter-security:2.3.4.RELEASE") implementation("org.springframework.boot:spring-boot-starter-security:${springBootVersion}")
implementation("org.springframework.boot:spring-boot-configuration-processor:2.3.4.RELEASE") implementation("org.springframework.boot:spring-boot-configuration-processor:${springBootVersion}")
implementation("org.springframework.boot:spring-boot-devtools:2.3.4.RELEASE") implementation("org.springframework.boot:spring-boot-devtools:${springBootVersion}")
testImplementation("org.springframework:spring-test:5.1.6.RELEASE") testImplementation("org.springframework:spring-test:5.1.6.RELEASE")
testImplementation("org.mockito:mockito-inline:3.6.0") testImplementation("org.mockito:mockito-inline:3.6.0")
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0") testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.3.2") testImplementation("org.junit.jupiter:junit-jupiter-api:5.3.2")
testImplementation("io.mockk:mockk:1.10.6") testImplementation("io.mockk:mockk:1.10.6")
testImplementation("org.springframework.boot:spring-boot-starter-test:2.3.4.RELEASE") testImplementation("org.springframework.boot:spring-boot-starter-test:${springBootVersion}")
testImplementation("org.springframework.boot:spring-boot-test-autoconfigure:2.3.4.RELEASE") testImplementation("org.springframework.boot:spring-boot-test-autoconfigure:${springBootVersion}")
testImplementation("org.jetbrains.kotlin:kotlin-test:1.4.10") testImplementation("org.jetbrains.kotlin:kotlin-test:${kotlinVersion}")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:1.4.10")
runtimeOnly("com.h2database:h2:1.4.199") runtimeOnly("com.h2database:h2:1.4.199")
runtimeOnly("mysql:mysql-connector-java:8.0.22") runtimeOnly("mysql:mysql-connector-java:8.0.22")
@ -90,8 +95,8 @@ tasks.withType<KotlinCompile>().all {
jvmTarget = JavaVersion.VERSION_11.toString() jvmTarget = JavaVersion.VERSION_11.toString()
useIR = true useIR = true
freeCompilerArgs = listOf( freeCompilerArgs = listOf(
"-Xopt-in=kotlin.contracts.ExperimentalContracts", "-Xopt-in=kotlin.contracts.ExperimentalContracts",
"-Xinline-classes" "-Xinline-classes"
) )
} }
} }

View File

@ -11,7 +11,7 @@ import org.springframework.context.annotation.Configuration
import org.springframework.core.env.Environment import org.springframework.core.env.Environment
import javax.sql.DataSource import javax.sql.DataSource
const val SUPPORTED_DATABASE_VERSION = 4 const val SUPPORTED_DATABASE_VERSION = 5
const val ENV_VAR_ENABLE_DATABASE_UPDATE_NAME = "CRE_ENABLE_DB_UPDATE" const val ENV_VAR_ENABLE_DATABASE_UPDATE_NAME = "CRE_ENABLE_DB_UPDATE"
val DATABASE_NAME_REGEX = Regex("(\\w+)$") val DATABASE_NAME_REGEX = Regex("(\\w+)$")

View File

@ -2,13 +2,13 @@ package dev.fyloz.colorrecipesexplorer.config
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.Employee import dev.fyloz.colorrecipesexplorer.model.account.UserLoginRequest
import dev.fyloz.colorrecipesexplorer.model.EmployeeLoginRequest import dev.fyloz.colorrecipesexplorer.model.account.Permission
import dev.fyloz.colorrecipesexplorer.model.EmployeePermission import dev.fyloz.colorrecipesexplorer.model.account.User
import dev.fyloz.colorrecipesexplorer.service.EmployeeService import dev.fyloz.colorrecipesexplorer.service.UserService
import dev.fyloz.colorrecipesexplorer.service.EmployeeServiceImpl import dev.fyloz.colorrecipesexplorer.service.UserServiceImpl
import dev.fyloz.colorrecipesexplorer.service.EmployeeUserDetailsService import dev.fyloz.colorrecipesexplorer.service.CreUserDetailsService
import dev.fyloz.colorrecipesexplorer.service.EmployeeUserDetailsServiceImpl import dev.fyloz.colorrecipesexplorer.service.CreUserDetailsServiceImpl
import io.jsonwebtoken.ExpiredJwtException import io.jsonwebtoken.ExpiredJwtException
import io.jsonwebtoken.Jwts import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.SignatureAlgorithm
@ -31,7 +31,7 @@ import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.core.Authentication import org.springframework.security.core.Authentication
import org.springframework.security.core.AuthenticationException import org.springframework.security.core.AuthenticationException
import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.userdetails.User import org.springframework.security.core.userdetails.User as SpringUser
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.web.AuthenticationEntryPoint import org.springframework.security.web.AuthenticationEntryPoint
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
@ -52,11 +52,11 @@ import javax.servlet.http.HttpServletResponse
@EnableGlobalMethodSecurity(prePostEnabled = true) @EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableConfigurationProperties(SecurityConfigurationProperties::class) @EnableConfigurationProperties(SecurityConfigurationProperties::class)
class WebSecurityConfig( class WebSecurityConfig(
val securityConfigurationProperties: SecurityConfigurationProperties, val securityConfigurationProperties: SecurityConfigurationProperties,
@Lazy val userDetailsService: EmployeeUserDetailsServiceImpl, @Lazy val userDetailsService: CreUserDetailsServiceImpl,
@Lazy val employeeService: EmployeeServiceImpl, @Lazy val userService: UserServiceImpl,
val environment: Environment, val environment: Environment,
val logger: Logger val logger: Logger
) : WebSecurityConfigurerAdapter() { ) : WebSecurityConfigurerAdapter() {
var debugMode = false var debugMode = false
@ -95,15 +95,15 @@ class WebSecurityConfig(
credentials: SecurityConfigurationProperties.SystemUserCredentials?, credentials: SecurityConfigurationProperties.SystemUserCredentials?,
firstName: String, firstName: String,
lastName: String, lastName: String,
permissions: List<EmployeePermission> permissions: List<Permission>
) { ) {
Assert.notNull(credentials, "No root user has been defined.") Assert.notNull(credentials, "No root user has been defined.")
credentials!! credentials!!
Assert.notNull(credentials.id, "The root user has no identifier defined.") Assert.notNull(credentials.id, "The root user has no identifier defined.")
Assert.notNull(credentials.password, "The root user has no password defined.") Assert.notNull(credentials.password, "The root user has no password defined.")
if (!employeeService.existsById(credentials.id!!)) { if (!userService.existsById(credentials.id!!)) {
employeeService.save( userService.save(
Employee( User(
id = credentials.id!!, id = credentials.id!!,
firstName = firstName, firstName = firstName,
lastName = lastName, lastName = lastName,
@ -115,7 +115,7 @@ class WebSecurityConfig(
} }
} }
createUser(securityConfigurationProperties.root, "Root", "User", listOf(EmployeePermission.ADMIN)) createUser(securityConfigurationProperties.root, "Root", "User", listOf(Permission.ADMIN))
debugMode = "debug" in environment.activeProfiles debugMode = "debug" in environment.activeProfiles
if (debugMode) logger.warn("Debug mode is enabled, security will be disabled!") if (debugMode) logger.warn("Debug mode is enabled, security will be disabled!")
} }
@ -128,7 +128,7 @@ class WebSecurityConfig(
.addFilter( .addFilter(
JwtAuthenticationFilter( JwtAuthenticationFilter(
authenticationManager(), authenticationManager(),
employeeService, userService,
securityConfigurationProperties securityConfigurationProperties
) )
) )
@ -145,7 +145,7 @@ class WebSecurityConfig(
http.authorizeRequests() http.authorizeRequests()
.antMatchers("/api/login").permitAll() .antMatchers("/api/login").permitAll()
.antMatchers("/api/logout").authenticated() .antMatchers("/api/logout").authenticated()
.antMatchers("/api/employee/current").authenticated() .antMatchers("/api/user/current").authenticated()
.anyRequest().authenticated() .anyRequest().authenticated()
} else { } else {
http http
@ -171,9 +171,9 @@ const val defaultGroupCookieName = "Default-Group"
val blacklistedJwtTokens = mutableListOf<String>() val blacklistedJwtTokens = mutableListOf<String>()
class JwtAuthenticationFilter( class JwtAuthenticationFilter(
private val authManager: AuthenticationManager, private val authManager: AuthenticationManager,
private val employeeService: EmployeeService, private val userService: UserService,
private val securityConfigurationProperties: SecurityConfigurationProperties private val securityConfigurationProperties: SecurityConfigurationProperties
) : UsernamePasswordAuthenticationFilter() { ) : UsernamePasswordAuthenticationFilter() {
private var debugMode = false private var debugMode = false
@ -183,7 +183,7 @@ class JwtAuthenticationFilter(
} }
override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication { override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication {
val loginRequest = jacksonObjectMapper().readValue(request.inputStream, EmployeeLoginRequest::class.java) val loginRequest = jacksonObjectMapper().readValue(request.inputStream, UserLoginRequest::class.java)
return authManager.authenticate(UsernamePasswordAuthenticationToken(loginRequest.id, loginRequest.password)) return authManager.authenticate(UsernamePasswordAuthenticationToken(loginRequest.id, loginRequest.password))
} }
@ -197,12 +197,12 @@ class JwtAuthenticationFilter(
val jwtDuration = securityConfigurationProperties.jwtDuration val jwtDuration = securityConfigurationProperties.jwtDuration
Assert.notNull(jwtSecret, "No JWT secret has been defined.") Assert.notNull(jwtSecret, "No JWT secret has been defined.")
Assert.notNull(jwtDuration, "No JWT duration has been defined.") Assert.notNull(jwtDuration, "No JWT duration has been defined.")
val employeeId = (authResult.principal as User).username val userId = (authResult.principal as SpringUser).username
employeeService.updateLastLoginTime(employeeId.toLong()) userService.updateLastLoginTime(userId.toLong())
val expirationMs = System.currentTimeMillis() + jwtDuration!! val expirationMs = System.currentTimeMillis() + jwtDuration!!
val expirationDate = Date(expirationMs) val expirationDate = Date(expirationMs)
val token = Jwts.builder() val token = Jwts.builder()
.setSubject(employeeId) .setSubject(userId)
.setExpiration(expirationDate) .setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret!!.toByteArray()) .signWith(SignatureAlgorithm.HS512, jwtSecret!!.toByteArray())
.compact() .compact()
@ -220,9 +220,9 @@ class JwtAuthenticationFilter(
} }
class JwtAuthorizationFilter( class JwtAuthorizationFilter(
private val userDetailsService: EmployeeUserDetailsService, private val userDetailsService: CreUserDetailsService,
private val securityConfigurationProperties: SecurityConfigurationProperties, private val securityConfigurationProperties: SecurityConfigurationProperties,
authenticationManager: AuthenticationManager authenticationManager: AuthenticationManager
) : BasicAuthenticationFilter(authenticationManager) { ) : BasicAuthenticationFilter(authenticationManager) {
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) { override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
fun tryLoginFromBearer(): Boolean { fun tryLoginFromBearer(): Boolean {
@ -259,20 +259,20 @@ class JwtAuthorizationFilter(
val jwtSecret = securityConfigurationProperties.jwtSecret val jwtSecret = securityConfigurationProperties.jwtSecret
Assert.notNull(jwtSecret, "No JWT secret has been defined.") Assert.notNull(jwtSecret, "No JWT secret has been defined.")
return try { return try {
val employeeId = Jwts.parser() val userId = Jwts.parser()
.setSigningKey(jwtSecret!!.toByteArray()) .setSigningKey(jwtSecret!!.toByteArray())
.parseClaimsJws(token.replace("Bearer", "")) .parseClaimsJws(token.replace("Bearer", ""))
.body .body
.subject .subject
if (employeeId != null) getAuthenticationToken(employeeId) else null if (userId != null) getAuthenticationToken(userId) else null
} catch (_: ExpiredJwtException) { } catch (_: ExpiredJwtException) {
null null
} }
} }
private fun getAuthenticationToken(employeeId: String): UsernamePasswordAuthenticationToken? = try { private fun getAuthenticationToken(userId: String): UsernamePasswordAuthenticationToken? = try {
val employeeDetails = userDetailsService.loadUserByEmployeeId(employeeId.toLong(), false) val userDetails = userDetailsService.loadUserById(userId.toLong(), false)
UsernamePasswordAuthenticationToken(employeeDetails.username, null, employeeDetails.authorities) UsernamePasswordAuthenticationToken(userDetails.username, null, userDetails.authorities)
} catch (_: NotFoundException) { } catch (_: NotFoundException) {
null null
} }

View File

@ -14,12 +14,6 @@ annotation class PreAuthorizeViewRecipes
@PreAuthorize("hasAuthority('EDIT_RECIPES')") @PreAuthorize("hasAuthority('EDIT_RECIPES')")
annotation class PreAuthorizeEditRecipes annotation class PreAuthorizeEditRecipes
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@PreAuthorize("hasAuthority('REMOVE_RECIPES')")
annotation class PreAuthorizeRemoveRecipes
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) @Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented @MustBeDocumented
@ -37,9 +31,3 @@ annotation class PreAuthorizeViewUsers
@MustBeDocumented @MustBeDocumented
@PreAuthorize("hasAuthority('EDIT_USERS')") @PreAuthorize("hasAuthority('EDIT_USERS')")
annotation class PreAuthorizeEditUsers annotation class PreAuthorizeEditUsers
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@PreAuthorize("hasAuthority('REMOVE_USERS')")
annotation class PreAuthorizeRemoveUsers

View File

@ -12,63 +12,62 @@ import org.springframework.web.context.request.WebRequest
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
abstract class RestException( abstract class RestException(
val errorCode: String, val errorCode: String,
val title: String, val title: String,
val status: HttpStatus, val status: HttpStatus,
val details: String, val details: String,
val extensions: Map<String, Any> = mapOf() val extensions: Map<String, Any> = mapOf()
) : RuntimeException(details) { ) : RuntimeException(details) {
fun buildExceptionBody() = mapOf( fun buildExceptionBody() = mapOf(
"type" to errorCode, "type" to errorCode,
"title" to title, "title" to title,
"status" to status.value(), "status" to status.value(),
"detail" to details, "detail" to details,
*extensions.map { it.key to it.value }.toTypedArray() *extensions.map { it.key to it.value }.toTypedArray()
) )
} }
class NotFoundException( class NotFoundException(
errorCode: String, errorCode: String,
title: String, title: String,
details: String, details: String,
identifierValue: Any, identifierValue: Any,
identifierName: String = "id" identifierName: String = "id"
) : RestException( ) : RestException(
errorCode = "notfound-$errorCode-$identifierName", errorCode = "notfound-$errorCode-$identifierName",
title = title, title = title,
status = HttpStatus.NOT_FOUND, status = HttpStatus.NOT_FOUND,
details = details, details = details,
extensions = mapOf( extensions = mapOf(
identifierName to identifierValue identifierName to identifierValue
) )
) )
class AlreadyExistsException( class AlreadyExistsException(
errorCode: String, errorCode: String,
title: String, title: String,
details: String, details: String,
identifierValue: Any, identifierValue: Any,
identifierName: String = "id" identifierName: String = "id",
extensions: MutableMap<String, Any> = mutableMapOf()
) : RestException( ) : RestException(
errorCode = "exists-$errorCode-$identifierName", errorCode = "exists-$errorCode-$identifierName",
title = title, title = title,
status = HttpStatus.CONFLICT, status = HttpStatus.CONFLICT,
details = details, details = details,
extensions = mapOf( extensions = extensions.apply { this[identifierName] = identifierValue }.toMap()
identifierName to identifierValue
)
) )
class CannotDeleteException( class CannotDeleteException(
errorCode: String, errorCode: String,
title: String, title: String,
details: String details: String
) : RestException( ) : RestException(
errorCode = "cannotdelete-$errorCode", errorCode = "cannotdelete-$errorCode",
title = title, title = title,
status = HttpStatus.CONFLICT, status = HttpStatus.CONFLICT,
details = details details = details
) )
@ControllerAdvice @ControllerAdvice
@ -79,19 +78,19 @@ class RestResponseEntityExceptionHandler : ResponseEntityExceptionHandler() {
finalBody["instance"] = (request as ServletWebRequest).request.requestURI finalBody["instance"] = (request as ServletWebRequest).request.requestURI
return handleExceptionInternal( return handleExceptionInternal(
exception, exception,
finalBody, finalBody,
HttpHeaders(), HttpHeaders(),
exception.status, exception.status,
request request
) )
} }
override fun handleMethodArgumentNotValid( override fun handleMethodArgumentNotValid(
ex: MethodArgumentNotValidException, ex: MethodArgumentNotValidException,
headers: HttpHeaders, headers: HttpHeaders,
status: HttpStatus, status: HttpStatus,
request: WebRequest request: WebRequest
): ResponseEntity<Any> { ): ResponseEntity<Any> {
val errors = hashMapOf<String, String>() val errors = hashMapOf<String, String>()
ex.bindingResult.allErrors.forEach { ex.bindingResult.allErrors.forEach {

View File

@ -8,9 +8,6 @@ import javax.persistence.*
import javax.validation.constraints.NotBlank import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull import javax.validation.constraints.NotNull
private const val COMPANY_ID_NULL_MESSAGE = "Un identifiant est requis"
private const val COMPANY_NAME_NULL_MESSAGE = "Un nom est requis"
@Entity @Entity
@Table(name = "company") @Table(name = "company")
data class Company( data class Company(
@ -20,11 +17,15 @@ data class Company(
@Column(unique = true) @Column(unique = true)
override val name: String override val name: String
) : NamedModel ) : NamedModel {
override fun toString(): String {
return name
}
}
open class CompanySaveDto( open class CompanySaveDto(
@field:NotBlank(message = COMPANY_NAME_NULL_MESSAGE) @field:NotBlank
val name: String val name: String
) : EntityDto<Company> { ) : EntityDto<Company> {
override fun toEntity(): Company = Company(null, name) override fun toEntity(): Company = Company(null, name)
@ -32,10 +33,9 @@ open class CompanySaveDto(
open class CompanyUpdateDto( open class CompanyUpdateDto(
@field:NotNull(message = COMPANY_ID_NULL_MESSAGE)
val id: Long, val id: Long,
@field:NullOrNotBlank(message = COMPANY_NAME_NULL_MESSAGE) @field:NotBlank
val name: String? val name: String?
) : EntityDto<Company> { ) : EntityDto<Company> {
override fun toEntity(): Company = Company(id, name ?: "") override fun toEntity(): Company = Company(id, name ?: "")

View File

@ -0,0 +1,20 @@
package dev.fyloz.colorrecipesexplorer.model
import java.time.LocalDateTime
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.Id
import javax.persistence.Table
@Entity
@Table(name = "configuration")
data class Configuration(
@Id
@Column(name = "config_key")
val key: String,
val content: String,
@Column(name = "last_updated")
val lastUpdated: LocalDateTime
)

View File

@ -1,193 +0,0 @@
package dev.fyloz.colorrecipesexplorer.model
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank
import org.hibernate.annotations.Fetch
import org.hibernate.annotations.FetchMode
import org.springframework.security.core.GrantedAuthority
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.NotNull
import javax.validation.constraints.Size
private const val EMPLOYEE_ID_NULL_MESSAGE = "Un numéro d'employé est requis"
private const val EMPLOYEE_LAST_NAME_EMPTY_MESSAGE = "Un nom est requis"
private const val EMPLOYEE_FIRST_NAME_EMPTY_MESSAGE = "Un prénom est requis"
private const val EMPLOYEE_PASSWORD_EMPTY_MESSAGE = "Un mot de passe est requis"
private const val EMPLOYEE_PASSWORD_TOO_SHORT_MESSAGE = "Le mot de passe doit contenir au moins 8 caractères"
@Entity
@Table(name = "employee")
data class Employee(
@Id
override val id: Long,
@Column(name = "first_name")
val firstName: String = "",
@Column(name = "last_name")
val lastName: String = "",
val password: String = "",
@Column(name = "default_group_user")
val isDefaultGroupUser: Boolean = false,
@Column(name = "system_user")
val isSystemUser: Boolean = false,
@ManyToOne
@JoinColumn(name = "group_id")
@Fetch(FetchMode.SELECT)
var group: EmployeeGroup? = null,
@Enumerated(EnumType.STRING)
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "employee_permission", joinColumns = [JoinColumn(name = "employee_id")])
@Column(name = "permission")
@Fetch(FetchMode.SUBSELECT)
val permissions: MutableSet<EmployeePermission> = mutableSetOf(),
@Column(name = "last_login_time")
var lastLoginTime: LocalDateTime? = null
) : Model {
val flatPermissions: Set<EmployeePermission>
get() = permissions
.flatMap { it.flat() }
.filter { !it.deprecated }
.toMutableSet()
.apply {
if (group != null) this.addAll(group!!.flatPermissions)
}
val authorities: Set<GrantedAuthority>
get() = flatPermissions.map { it.toAuthority() }.toMutableSet()
}
/** DTO for creating employees. Allows a [password] a [groupId]. */
open class EmployeeSaveDto(
@field:NotNull(message = EMPLOYEE_ID_NULL_MESSAGE)
val id: Long,
@field:NotBlank(message = EMPLOYEE_FIRST_NAME_EMPTY_MESSAGE)
val firstName: String,
@field:NotBlank(message = EMPLOYEE_LAST_NAME_EMPTY_MESSAGE)
val lastName: String,
@field:NotBlank(message = EMPLOYEE_PASSWORD_EMPTY_MESSAGE)
@field:Size(min = 8, message = EMPLOYEE_PASSWORD_TOO_SHORT_MESSAGE)
val password: String,
val groupId: Long?,
@Enumerated(EnumType.STRING)
val permissions: MutableSet<EmployeePermission> = mutableSetOf()
) : EntityDto<Employee>
open class EmployeeUpdateDto(
@field:NotNull(message = EMPLOYEE_ID_NULL_MESSAGE)
val id: Long,
@field:NullOrNotBlank(message = EMPLOYEE_FIRST_NAME_EMPTY_MESSAGE)
val firstName: String?,
@field:NullOrNotBlank(message = EMPLOYEE_LAST_NAME_EMPTY_MESSAGE)
val lastName: String?,
val groupId: Long?,
@Enumerated(EnumType.STRING)
val permissions: Set<EmployeePermission>?
) : EntityDto<Employee>
data class EmployeeOutputDto(
override val id: Long,
val firstName: String,
val lastName: String,
val group: EmployeeGroup?,
val permissions: Set<EmployeePermission>,
val explicitPermissions: Set<EmployeePermission>,
val lastLoginTime: LocalDateTime?
) : Model
data class EmployeeLoginRequest(val id: Long, val password: String)
// ==== DSL ====
fun employee(
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
password: String = passwordEncoder.encode("password"),
isDefaultGroupUser: Boolean = false,
isSystemUser: Boolean = false,
group: EmployeeGroup? = null,
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
lastLoginTime: LocalDateTime? = null,
op: Employee.() -> Unit = {}
) = Employee(
id,
firstName,
lastName,
password,
isDefaultGroupUser,
isSystemUser,
group,
permissions,
lastLoginTime
).apply(op)
fun employeeSaveDto(
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
password: String = passwordEncoder.encode("password"),
groupId: Long? = null,
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeSaveDto.() -> Unit = {}
) = EmployeeSaveDto(id, firstName, lastName, password, groupId, permissions).apply(op)
fun employeeUpdateDto(
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
groupId: Long? = null,
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeUpdateDto.() -> Unit = {}
) = EmployeeUpdateDto(id, firstName, lastName, groupId, permissions).apply(op)
// ==== Exceptions ====
private const val EMPLOYEE_NOT_FOUND_EXCEPTION_TITLE = "Employee not found"
private const val EMPLOYEE_ALREADY_EXISTS_EXCEPTION_TITLE = "Employee already exists"
private const val EMPLOYEE_EXCEPTION_ERROR_CODE = "employee"
fun employeeIdNotFoundException(id: Long) =
NotFoundException(
EMPLOYEE_EXCEPTION_ERROR_CODE,
EMPLOYEE_NOT_FOUND_EXCEPTION_TITLE,
"An employee with the id $id could not be found",
id
)
fun employeeIdAlreadyExistsException(id: Long) =
AlreadyExistsException(
EMPLOYEE_EXCEPTION_ERROR_CODE,
EMPLOYEE_ALREADY_EXISTS_EXCEPTION_TITLE,
"An employee with the id $id already exists",
id
)
fun employeeFullNameAlreadyExistsException(firstName: String, lastName: String) =
AlreadyExistsException(
EMPLOYEE_EXCEPTION_ERROR_CODE,
EMPLOYEE_ALREADY_EXISTS_EXCEPTION_TITLE,
"An employee with the name '$firstName $lastName' already exists",
"$firstName $lastName",
"fullName"
)

View File

@ -1,141 +0,0 @@
package dev.fyloz.colorrecipesexplorer.model
import com.fasterxml.jackson.annotation.JsonProperty
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.exception.RestException
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.NotNull
import javax.validation.constraints.Size
private const val GROUP_ID_NULL_MESSAGE = "Un identifiant est requis"
private const val GROUP_NAME_NULL_MESSAGE = "Un nom est requis"
private const val GROUP_PERMISSIONS_EMPTY_MESSAGE = "Au moins une permission est requise"
@Entity
@Table(name = "employee_group")
data class EmployeeGroup(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override var id: Long? = null,
@Column(unique = true)
override 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<EmployeePermission> = mutableSetOf(),
) : NamedModel {
val flatPermissions: Set<EmployeePermission>
get() = this.permissions
.flatMap { it.flat() }
.filter { !it.deprecated }
.toSet()
}
open class EmployeeGroupSaveDto(
@field:NotBlank(message = GROUP_NAME_NULL_MESSAGE)
@field:Size(min = 3)
val name: String,
@field:Size(min = 1, message = GROUP_PERMISSIONS_EMPTY_MESSAGE)
val permissions: MutableSet<EmployeePermission>
) : EntityDto<EmployeeGroup> {
override fun toEntity(): EmployeeGroup =
EmployeeGroup(null, name, permissions)
}
open class EmployeeGroupUpdateDto(
@field:NotNull(message = GROUP_ID_NULL_MESSAGE)
val id: Long,
@field:NotBlank(message = GROUP_NAME_NULL_MESSAGE)
@field:Size(min = 3)
val name: String,
@field:Size(min = 1, message = GROUP_PERMISSIONS_EMPTY_MESSAGE)
val permissions: MutableSet<EmployeePermission>
) : EntityDto<EmployeeGroup> {
override fun toEntity(): EmployeeGroup =
EmployeeGroup(id, name, permissions)
}
data class EmployeeGroupOutputDto(
override val id: Long,
val name: String,
val permissions: Set<EmployeePermission>,
val explicitPermissions: Set<EmployeePermission>
): Model
fun employeeGroup(
id: Long? = null,
name: String = "name",
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeGroup.() -> Unit = {}
) = EmployeeGroup(id, name, permissions).apply(op)
fun employeeGroupSaveDto(
name: String = "name",
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeGroupSaveDto.() -> Unit = {}
) = EmployeeGroupSaveDto(name, permissions).apply(op)
fun employeeGroupUpdateDto(
id: Long = 0L,
name: String = "name",
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeGroupUpdateDto.() -> Unit = {}
) = EmployeeGroupUpdateDto(id, name, permissions).apply(op)
// ==== Exceptions ====
private const val EMPLOYEE_NOT_FOUND_EXCEPTION_TITLE = "Employee group not found"
private const val EMPLOYEE_ALREADY_EXISTS_EXCEPTION_TITLE = "Employee group already exists"
private const val EMPLOYEE_EXCEPTION_ERROR_CODE = "employeegroup"
class NoDefaultGroupException : RestException(
"nodefaultgroup",
"No default group",
HttpStatus.NOT_FOUND,
"No default group cookie is defined in the current request"
)
fun employeeGroupIdNotFoundException(id: Long) =
NotFoundException(
EMPLOYEE_EXCEPTION_ERROR_CODE,
EMPLOYEE_NOT_FOUND_EXCEPTION_TITLE,
"An employee group with the id $id could not be found",
id
)
fun employeeGroupNameNotFoundException(name: String) =
NotFoundException(
EMPLOYEE_EXCEPTION_ERROR_CODE,
EMPLOYEE_NOT_FOUND_EXCEPTION_TITLE,
"An employee group with the name $name could not be found",
name,
"name"
)
fun employeeGroupIdAlreadyExistsException(id: Long) =
AlreadyExistsException(
EMPLOYEE_EXCEPTION_ERROR_CODE,
EMPLOYEE_ALREADY_EXISTS_EXCEPTION_TITLE,
"An employee group with the id $id already exists",
id,
)
fun employeeGroupNameAlreadyExistsException(name: String) =
AlreadyExistsException(
EMPLOYEE_EXCEPTION_ERROR_CODE,
EMPLOYEE_ALREADY_EXISTS_EXCEPTION_TITLE,
"An employee group with the name $name already exists",
name,
"name"
)

View File

@ -4,27 +4,11 @@ import com.fasterxml.jackson.annotation.JsonIgnore
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrSize
import dev.fyloz.colorrecipesexplorer.rest.CRE_PROPERTIES
import dev.fyloz.colorrecipesexplorer.rest.files.FILE_CONTROLLER_PATH
import org.springframework.web.multipart.MultipartFile import org.springframework.web.multipart.MultipartFile
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import javax.persistence.* import javax.persistence.*
import javax.validation.constraints.Min import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull import javax.validation.constraints.Size
private const val MATERIAL_ID_NULL_MESSAGE = "Un identifiant est requis"
private const val MATERIAL_NAME_NULL_MESSAGE = "Un nom est requis"
private const val MATERIAL_INVENTORY_QUANTITY_NULL_MESSAGE = "Une quantité est requise"
private const val MATERIAL_INVENTORY_QUANTITY_NEGATIVE_MESSAGE = "La quantité doit être supérieure ou égale à 0"
private const val MATERIAL_TYPE_NULL_MESSAGE = "Un type de produit est requis"
private const val MATERIAL_QUANTITY_MATERIAL_NULL_MESSAGE = "Un produit est requis"
private const val MATERIAL_QUANTITY_QUANTITY_NULL_MESSAGE = "Une quantité est requises"
private const val MATERIAL_QUANTITY_QUANTITY_NEGATIVE_MESSAGE = "La quantité doit être supérieure ou égale à 0"
const val SIMDUT_FILES_PATH = "pdf/simdut" const val SIMDUT_FILES_PATH = "pdf/simdut"
@ -52,32 +36,27 @@ data class Material(
@JsonIgnore @JsonIgnore
@Transient @Transient
get() = "$SIMDUT_FILES_PATH/$name.pdf" get() = "$SIMDUT_FILES_PATH/$name.pdf"
} }
open class MaterialSaveDto( open class MaterialSaveDto(
@field:NotBlank(message = MATERIAL_NAME_NULL_MESSAGE) @field:NotBlank
val name: String, val name: String,
@field:NotNull(message = MATERIAL_INVENTORY_QUANTITY_NULL_MESSAGE) @field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
@field:Min(value = 0, message = MATERIAL_INVENTORY_QUANTITY_NEGATIVE_MESSAGE)
val inventoryQuantity: Float, val inventoryQuantity: Float,
@field:NotNull(message = MATERIAL_TYPE_NULL_MESSAGE)
val materialTypeId: Long, val materialTypeId: Long,
val simdutFile: MultipartFile? = null val simdutFile: MultipartFile? = null
) : EntityDto<Material> ) : EntityDto<Material>
open class MaterialUpdateDto( open class MaterialUpdateDto(
@field:NotNull(message = MATERIAL_ID_NULL_MESSAGE)
val id: Long, val id: Long,
@field:NullOrNotBlank(message = MATERIAL_NAME_NULL_MESSAGE) @field:NotBlank
val name: String?, val name: String?,
@field:NullOrSize(min = 0, message = MATERIAL_INVENTORY_QUANTITY_NEGATIVE_MESSAGE) @field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
val inventoryQuantity: Float?, val inventoryQuantity: Float?,
val materialTypeId: Long?, val materialTypeId: Long?,
@ -95,11 +74,9 @@ data class MaterialOutputDto(
) : Model ) : Model
data class MaterialQuantityDto( data class MaterialQuantityDto(
@field:NotNull(message = MATERIAL_QUANTITY_MATERIAL_NULL_MESSAGE)
val material: Long, val material: Long,
@field:NotNull(message = MATERIAL_QUANTITY_QUANTITY_NULL_MESSAGE) @field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
@field:Min(value = 0, message = MATERIAL_QUANTITY_QUANTITY_NEGATIVE_MESSAGE)
val quantity: Float val quantity: Float
) )
@ -147,7 +124,7 @@ fun materialQuantityDto(
) = MaterialQuantityDto(materialId, quantity).apply(op) ) = MaterialQuantityDto(materialId, quantity).apply(op)
// ==== Exceptions ==== // ==== Exceptions ====
private const private const
val MATERIAL_NOT_FOUND_EXCEPTION_TITLE = "Material not found" val MATERIAL_NOT_FOUND_EXCEPTION_TITLE = "Material not found"
private const val MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE = "Material already exists" 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_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete material"

View File

@ -11,10 +11,7 @@ import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull import javax.validation.constraints.NotNull
import javax.validation.constraints.Size import javax.validation.constraints.Size
private const val MATERIAL_TYPE_ID_NULL_MESSAGE = "Un identifiant est requis" private const val VALIDATION_PREFIX_SIZE = "Must contains exactly 3 characters"
private const val MATERIAL_TYPE_NAME_NULL_MESSAGE = "Un nom est requis"
private const val MATERIAL_TYPE_PREFIX_NULL_MESSAGE = "Un préfixe est requis"
private const val MATERIAL_TYPE_PREFIX_SIZE_MESSAGE = "Le préfixe doit faire exactement 3 caractères"
@Entity @Entity
@Table(name = "material_type") @Table(name = "material_type")
@ -39,11 +36,11 @@ data class MaterialType(
) : NamedModel ) : NamedModel
open class MaterialTypeSaveDto( open class MaterialTypeSaveDto(
@field:NotBlank(message = MATERIAL_TYPE_NAME_NULL_MESSAGE) @field:NotBlank
val name: String, val name: String,
@field:NotBlank(message = MATERIAL_TYPE_PREFIX_NULL_MESSAGE) @field:NotBlank
@field:Size(min = 3, max = 3, message = MATERIAL_TYPE_PREFIX_SIZE_MESSAGE) @field:Size(min = 3, max = 3, message = VALIDATION_PREFIX_SIZE)
val prefix: String, val prefix: String,
val usePercentages: Boolean = false val usePercentages: Boolean = false
@ -53,13 +50,12 @@ open class MaterialTypeSaveDto(
} }
open class MaterialTypeUpdateDto( open class MaterialTypeUpdateDto(
@field:NotNull(message = MATERIAL_TYPE_ID_NULL_MESSAGE)
val id: Long, val id: Long,
@field:NullOrNotBlank(message = MATERIAL_TYPE_NAME_NULL_MESSAGE) @field:NotBlank
val name: String?, val name: String?,
@field:NullOrSize(min = 3, max = 3, message = MATERIAL_TYPE_PREFIX_NULL_MESSAGE) @field:Size(min = 3, max = 3, message = VALIDATION_PREFIX_SIZE)
val prefix: String? val prefix: String?
) : EntityDto<MaterialType> { ) : EntityDto<MaterialType> {
override fun toEntity(): MaterialType = override fun toEntity(): MaterialType =

View File

@ -4,20 +4,10 @@ import com.fasterxml.jackson.annotation.JsonIgnore
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank
import javax.persistence.* import javax.persistence.*
import javax.validation.constraints.Min import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull
private const val MIX_ID_NULL_MESSAGE = "Un identifiant est requis"
private const val MIX_NAME_NULL_MESSAGE = "Un nom est requis"
private const val MIX_RECIPE_NULL_MESSAGE = "Un recette est requise"
private const val MIX_MATERIAL_TYPE_NULL_MESSAGE = "Un type de produit est requis"
private const val MIX_DEDUCT_MIX_ID_NULL_MESSAGE = "Un identifiant de mélange est requis"
private const val MIX_DEDUCT_RATIO_NULL_MESSAGE = "Un ratio est requis"
private const val MIX_DEDUCT_RATION_NEGATIVE_MESSAGE = "Le ratio doit être égal ou supérieur à 0"
@Entity @Entity
@Table(name = "mix") @Table(name = "mix")
@ -43,33 +33,26 @@ data class Mix(
) : Model ) : Model
open class MixSaveDto( open class MixSaveDto(
@field:NotBlank(message = MIX_NAME_NULL_MESSAGE) @field:NotBlank
val name: String, val name: String,
@field:NotNull(message = MIX_RECIPE_NULL_MESSAGE)
val recipeId: Long, val recipeId: Long,
@field:NotNull(message = MIX_MATERIAL_TYPE_NULL_MESSAGE)
val materialTypeId: Long, val materialTypeId: Long,
val mixMaterials: Set<MixMaterialDto>? val mixMaterials: Set<MixMaterialDto>?
) : EntityDto<Mix> { ) : EntityDto<Mix>
override fun toEntity(): Mix = throw UnsupportedOperationException()
}
open class MixUpdateDto( open class MixUpdateDto(
@field:NotNull(message = MIX_ID_NULL_MESSAGE)
val id: Long, val id: Long,
@field:NullOrNotBlank(message = MIX_NAME_NULL_MESSAGE) @field:NotBlank
val name: String?, val name: String?,
val materialTypeId: Long?, val materialTypeId: Long?,
var mixMaterials: Set<MixMaterialDto>? var mixMaterials: Set<MixMaterialDto>?
) : EntityDto<Mix> { ) : EntityDto<Mix>
override fun toEntity(): Mix = throw UnsupportedOperationException()
}
data class MixOutputDto( data class MixOutputDto(
val id: Long, val id: Long,
@ -79,16 +62,13 @@ data class MixOutputDto(
) )
data class MixDeductDto( data class MixDeductDto(
@field:NotNull(message = MIX_DEDUCT_MIX_ID_NULL_MESSAGE)
val id: Long, val id: Long,
@field:NotNull(message = MIX_DEDUCT_RATIO_NULL_MESSAGE) @field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
@field:Min(value = 0, message = MIX_DEDUCT_RATION_NEGATIVE_MESSAGE)
val ratio: Float val ratio: Float
) )
data class MixLocationDto( data class MixLocationDto(
@field:NotNull(message = MIX_DEDUCT_MIX_ID_NULL_MESSAGE)
val mixId: Long, val mixId: Long,
val location: String? val location: String?

View File

@ -6,10 +6,6 @@ import javax.persistence.*
import javax.validation.constraints.Min import javax.validation.constraints.Min
import javax.validation.constraints.NotNull import javax.validation.constraints.NotNull
private const val MIX_MATERIAL_DTO_MATERIAL_ID_NULL_MESSAGE = "Un identifiant de produit est requis"
private const val MIX_MATERIAL_DTO_QUANTITY_NULL_MESSAGE = "Une quantité est requise"
private const val MIX_MATERIAL_DTO_QUANTITY_NEGATIVE_MESSAGE = "La quantité ne peut pas être négative"
@Entity @Entity
@Table(name = "mix_material") @Table(name = "mix_material")
data class MixMaterial( data class MixMaterial(
@ -26,6 +22,15 @@ data class MixMaterial(
var position: Int var position: Int
) : Model ) : Model
data class MixMaterialDto(
val materialId: Long,
@field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
val quantity: Float,
val position: Int
)
data class MixMaterialOutputDto( data class MixMaterialOutputDto(
val id: Long, val id: Long,
val material: MaterialOutputDto, val material: MaterialOutputDto,
@ -33,17 +38,6 @@ data class MixMaterialOutputDto(
val position: Int val position: Int
) )
data class MixMaterialDto(
@field:NotNull(message = MIX_MATERIAL_DTO_MATERIAL_ID_NULL_MESSAGE)
val materialId: Long,
@field:NotNull(message = MIX_MATERIAL_DTO_QUANTITY_NULL_MESSAGE)
@field:Min(value = 0, message = MIX_MATERIAL_DTO_QUANTITY_NEGATIVE_MESSAGE)
val quantity: Float,
val position: Int
)
// ==== DSL ==== // ==== DSL ====
fun mixMaterial( fun mixMaterial(
id: Long? = null, id: Long? = null,

View File

@ -15,3 +15,8 @@ interface EntityDto<out E> {
throw UnsupportedOperationException() 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"

View File

@ -3,8 +3,8 @@ package dev.fyloz.colorrecipesexplorer.model
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank import dev.fyloz.colorrecipesexplorer.model.account.Group
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrSize import dev.fyloz.colorrecipesexplorer.model.account.group
import dev.fyloz.colorrecipesexplorer.rest.CRE_PROPERTIES import dev.fyloz.colorrecipesexplorer.rest.CRE_PROPERTIES
import dev.fyloz.colorrecipesexplorer.rest.files.FILE_CONTROLLER_PATH import dev.fyloz.colorrecipesexplorer.rest.files.FILE_CONTROLLER_PATH
import java.net.URLEncoder import java.net.URLEncoder
@ -13,19 +13,7 @@ import java.time.LocalDate
import javax.persistence.* import javax.persistence.*
import javax.validation.constraints.* import javax.validation.constraints.*
private const val RECIPE_ID_NULL_MESSAGE = "Un identifiant est requis" private const val VALIDATION_COLOR_PATTERN = "^#([0-9a-f]{6})$"
private const val RECIPE_NAME_NULL_MESSAGE = "Un nom est requis"
private const val RECIPE_DESCRIPTION_NULL_MESSAGE = "Une description est requise"
private const val RECIPE_COLOR_NULL_MESSAGE = "Une couleur est requise"
private const val RECIPE_GLOSS_NULL_MESSAGE = "Le lustre de la couleur est requis"
private const val RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE = "Le lustre doit être entre 0 et 100"
private const val RECIPE_SAMPLE_TOO_SMALL_MESSAGE = "Le numéro d'échantillon doit être supérieur ou égal à 0"
private const val RECIPE_COMPANY_NULL_MESSAGE = "Une bannière est requise"
private const val RECIPE_STEPS_DTO_GROUP_ID_NULL_MESSAGE = "Un identifiant de groupe est requis"
private const val RECIPE_STEPS_DTO_MESSAGES_NULL_MESSAGE = "Des messages sont requis"
private const val NOTE_GROUP_ID_NULL_MESSAGE = "Un identifiant de groupe est requis"
const val RECIPE_IMAGES_DIRECTORY = "images/recipes" const val RECIPE_IMAGES_DIRECTORY = "images/recipes"
@ -89,30 +77,28 @@ data class Recipe(
} }
open class RecipeSaveDto( open class RecipeSaveDto(
@field:NotBlank(message = RECIPE_NAME_NULL_MESSAGE) @field:NotBlank
val name: String, val name: String,
@field:NotBlank(message = RECIPE_DESCRIPTION_NULL_MESSAGE) @field:NotBlank
val description: String, val description: String,
@field:NotBlank(message = RECIPE_COLOR_NULL_MESSAGE) @field:NotBlank
@field:Pattern(regexp = "^#([0-9a-f]{6})$") @field:Pattern(regexp = VALIDATION_COLOR_PATTERN)
val color: String, val color: String,
@field:NotNull(message = RECIPE_GLOSS_NULL_MESSAGE) @field:Min(0, message = VALIDATION_RANGE_PERCENTS)
@field:Min(value = 0, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE) @field:Max(100, message = VALIDATION_RANGE_PERCENTS)
@field:Max(value = 100, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE)
val gloss: Byte, val gloss: Byte,
@field:Min(value = 0, message = RECIPE_SAMPLE_TOO_SMALL_MESSAGE) @field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
val sample: Int?, val sample: Int?,
val approbationDate: LocalDate?, val approbationDate: LocalDate?,
val remark: String?, val remark: String?,
@field:Min(value = 0, message = RECIPE_COMPANY_NULL_MESSAGE) val companyId: Long
val companyId: Long = -1L,
) : EntityDto<Recipe> { ) : EntityDto<Recipe> {
override fun toEntity(): Recipe = recipe( override fun toEntity(): Recipe = recipe(
name = name, name = name,
@ -125,24 +111,23 @@ open class RecipeSaveDto(
} }
open class RecipeUpdateDto( open class RecipeUpdateDto(
@field:NotNull(message = RECIPE_ID_NULL_MESSAGE)
val id: Long, val id: Long,
@field:NullOrNotBlank(message = RECIPE_NAME_NULL_MESSAGE) @field:NotBlank
val name: String?, val name: String?,
@field:NullOrNotBlank(message = RECIPE_DESCRIPTION_NULL_MESSAGE) @field:NotBlank
val description: String?, val description: String?,
@field:NullOrNotBlank(message = RECIPE_COLOR_NULL_MESSAGE) @field:NotBlank
@field:Pattern(regexp = "^#([0-9a-f]{6})$") @field:Pattern(regexp = VALIDATION_COLOR_PATTERN)
val color: String?, val color: String?,
@field:Min(value = 0, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE) @field:Min(0, message = VALIDATION_RANGE_PERCENTS)
@field:Max(value = 100, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE) @field:Max(100, message = VALIDATION_RANGE_PERCENTS)
val gloss: Byte?, val gloss: Byte?,
@field:NullOrSize(min = 0, message = RECIPE_SAMPLE_TOO_SMALL_MESSAGE) @field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
val sample: Int?, val sample: Int?,
val approbationDate: LocalDate?, val approbationDate: LocalDate?,
@ -176,7 +161,7 @@ data class RecipeGroupInformation(
@ManyToOne @ManyToOne
@JoinColumn(name = "group_id") @JoinColumn(name = "group_id")
val group: EmployeeGroup, val group: Group,
var note: String?, var note: String?,
@ -186,15 +171,12 @@ data class RecipeGroupInformation(
) )
data class RecipeStepsDto( data class RecipeStepsDto(
@field:NotNull(message = RECIPE_STEPS_DTO_GROUP_ID_NULL_MESSAGE)
val groupId: Long, val groupId: Long,
@field:NotNull(message = RECIPE_STEPS_DTO_MESSAGES_NULL_MESSAGE)
val steps: Set<RecipeStep> val steps: Set<RecipeStep>
) )
data class RecipePublicDataDto( data class RecipePublicDataDto(
@field:NotNull(message = RECIPE_ID_NULL_MESSAGE)
val recipeId: Long, val recipeId: Long,
val notes: Set<NoteDto>?, val notes: Set<NoteDto>?,
@ -203,7 +185,6 @@ data class RecipePublicDataDto(
) )
data class NoteDto( data class NoteDto(
@field:NotNull(message = NOTE_GROUP_ID_NULL_MESSAGE)
val groupId: Long, val groupId: Long,
val content: String? val content: String?
@ -264,7 +245,7 @@ fun recipeUpdateDto(
fun recipeGroupInformation( fun recipeGroupInformation(
id: Long? = null, id: Long? = null,
group: EmployeeGroup = employeeGroup(), group: Group = group(),
note: String? = null, note: String? = null,
steps: MutableSet<RecipeStep>? = mutableSetOf(), steps: MutableSet<RecipeStep>? = mutableSetOf(),
op: RecipeGroupInformation.() -> Unit = {} op: RecipeGroupInformation.() -> Unit = {}
@ -303,3 +284,16 @@ fun recipeIdAlreadyExistsException(id: Long) =
"A recipe with the id $id already exists", "A recipe with the id $id already exists",
id 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!!
)
)

View File

@ -0,0 +1,135 @@
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 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")
data class Group(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override var id: Long? = null,
@Column(unique = true)
override 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<Permission> = mutableSetOf(),
) : NamedModel {
val flatPermissions: Set<Permission>
get() = this.permissions
.flatMap { it.flat() }
.filter { !it.deprecated }
.toSet()
}
open class GroupSaveDto(
@field:NotBlank
val name: String,
@field:NotEmpty
val permissions: MutableSet<Permission>
) : EntityDto<Group> {
override fun toEntity(): Group =
Group(null, name, permissions)
}
open class GroupUpdateDto(
val id: Long,
@field:NotBlank
val name: String,
@field:NotEmpty
val permissions: MutableSet<Permission>
) : EntityDto<Group> {
override fun toEntity(): Group =
Group(id, name, permissions)
}
data class GroupOutputDto(
override val id: Long,
val name: String,
val permissions: Set<Permission>,
val explicitPermissions: Set<Permission>
): Model
fun group(
id: Long? = null,
name: String = "name",
permissions: MutableSet<Permission> = mutableSetOf(),
op: Group.() -> Unit = {}
) = Group(id, name, permissions).apply(op)
fun groupSaveDto(
name: String = "name",
permissions: MutableSet<Permission> = mutableSetOf(),
op: GroupSaveDto.() -> Unit = {}
) = GroupSaveDto(name, permissions).apply(op)
fun groupUpdateDto(
id: Long = 0L,
name: String = "name",
permissions: MutableSet<Permission> = mutableSetOf(),
op: GroupUpdateDto.() -> Unit = {}
) = GroupUpdateDto(id, name, permissions).apply(op)
// ==== Exceptions ====
private const val GROUP_NOT_FOUND_EXCEPTION_TITLE = "Group not found"
private const val GROUP_ALREADY_EXISTS_EXCEPTION_TITLE = "Group already exists"
private const val GROUP_EXCEPTION_ERROR_CODE = "group"
class NoDefaultGroupException : RestException(
"nodefaultgroup",
"No default group",
HttpStatus.NOT_FOUND,
"No default group cookie is defined in the current request"
)
fun groupIdNotFoundException(id: Long) =
NotFoundException(
GROUP_EXCEPTION_ERROR_CODE,
GROUP_NOT_FOUND_EXCEPTION_TITLE,
"A group with the id $id could not be found",
id
)
fun groupNameNotFoundException(name: String) =
NotFoundException(
GROUP_EXCEPTION_ERROR_CODE,
GROUP_NOT_FOUND_EXCEPTION_TITLE,
"A group with the name $name could not be found",
name,
"name"
)
fun groupIdAlreadyExistsException(id: Long) =
AlreadyExistsException(
GROUP_EXCEPTION_ERROR_CODE,
GROUP_ALREADY_EXISTS_EXCEPTION_TITLE,
"A group with the id $id already exists",
id,
)
fun groupNameAlreadyExistsException(name: String) =
AlreadyExistsException(
GROUP_EXCEPTION_ERROR_CODE,
GROUP_ALREADY_EXISTS_EXCEPTION_TITLE,
"A group with the name $name already exists",
name,
"name"
)

View File

@ -1,22 +1,19 @@
package dev.fyloz.colorrecipesexplorer.model package dev.fyloz.colorrecipesexplorer.model.account
import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority
enum class EmployeePermission( enum class Permission(
val impliedPermissions: List<EmployeePermission> = listOf(), val impliedPermissions: List<Permission> = listOf(),
val deprecated: Boolean = false val deprecated: Boolean = false
) { ) {
READ_FILE, READ_FILE,
WRITE_FILE(listOf(READ_FILE)), WRITE_FILE(listOf(READ_FILE)),
REMOVE_FILE(listOf(WRITE_FILE)),
VIEW_RECIPES(listOf(READ_FILE)), VIEW_RECIPES(listOf(READ_FILE)),
VIEW_CATALOG(listOf(READ_FILE)), VIEW_CATALOG(listOf(READ_FILE)),
VIEW_USERS, VIEW_USERS,
PRINT_MIXES(listOf(VIEW_RECIPES)),
EDIT_RECIPES_PUBLIC_DATA(listOf(VIEW_RECIPES)), EDIT_RECIPES_PUBLIC_DATA(listOf(VIEW_RECIPES)),
EDIT_RECIPES(listOf(EDIT_RECIPES_PUBLIC_DATA, WRITE_FILE)), EDIT_RECIPES(listOf(EDIT_RECIPES_PUBLIC_DATA, WRITE_FILE)),
EDIT_MATERIALS(listOf(VIEW_CATALOG, WRITE_FILE)), EDIT_MATERIALS(listOf(VIEW_CATALOG, WRITE_FILE)),
@ -25,29 +22,24 @@ enum class EmployeePermission(
EDIT_USERS(listOf(VIEW_USERS)), EDIT_USERS(listOf(VIEW_USERS)),
EDIT_CATALOG(listOf(EDIT_MATERIALS, EDIT_MATERIAL_TYPES, EDIT_COMPANIES)), EDIT_CATALOG(listOf(EDIT_MATERIALS, EDIT_MATERIAL_TYPES, EDIT_COMPANIES)),
REMOVE_RECIPES(listOf(EDIT_RECIPES, REMOVE_FILE)), VIEW_TOUCH_UP_KITS,
REMOVE_MATERIALS(listOf(EDIT_MATERIALS, REMOVE_FILE)), EDIT_TOUCH_UP_KITS(listOf(VIEW_TOUCH_UP_KITS)),
REMOVE_MATERIAL_TYPES(listOf(EDIT_MATERIAL_TYPES)),
REMOVE_COMPANIES(listOf(EDIT_COMPANIES)),
REMOVE_USERS(listOf(EDIT_USERS)),
REMOVE_CATALOG(listOf(REMOVE_MATERIALS, REMOVE_MATERIAL_TYPES, REMOVE_COMPANIES)),
PRINT_MIXES(listOf(VIEW_RECIPES)),
ADD_TO_INVENTORY(listOf(VIEW_CATALOG)), ADD_TO_INVENTORY(listOf(VIEW_CATALOG)),
DEDUCT_FROM_INVENTORY(listOf(VIEW_RECIPES)), DEDUCT_FROM_INVENTORY(listOf(VIEW_RECIPES)),
GENERATE_TOUCH_UP_KIT,
ADMIN( ADMIN(
listOf( listOf(
EDIT_RECIPES,
EDIT_CATALOG, EDIT_CATALOG,
EDIT_USERS,
REMOVE_RECIPES, EDIT_TOUCH_UP_KITS,
REMOVE_USERS,
REMOVE_CATALOG,
PRINT_MIXES, PRINT_MIXES,
ADD_TO_INVENTORY, ADD_TO_INVENTORY,
DEDUCT_FROM_INVENTORY, DEDUCT_FROM_INVENTORY,
GENERATE_TOUCH_UP_KIT
) )
), ),
@ -69,6 +61,16 @@ enum class EmployeePermission(
EDIT_EMPLOYEE_PASSWORD(listOf(EDIT_USERS), true), EDIT_EMPLOYEE_PASSWORD(listOf(EDIT_USERS), true),
EDIT_EMPLOYEE_GROUP(listOf(EDIT_USERS), true), EDIT_EMPLOYEE_GROUP(listOf(EDIT_USERS), true),
REMOVE_FILE(listOf(WRITE_FILE), true),
GENERATE_TOUCH_UP_KIT(listOf(VIEW_TOUCH_UP_KITS), true),
REMOVE_RECIPES(listOf(EDIT_RECIPES, REMOVE_FILE), true),
REMOVE_MATERIALS(listOf(EDIT_MATERIALS, REMOVE_FILE), true),
REMOVE_MATERIAL_TYPES(listOf(EDIT_MATERIAL_TYPES), true),
REMOVE_COMPANIES(listOf(EDIT_COMPANIES), true),
REMOVE_USERS(listOf(EDIT_USERS), true),
REMOVE_CATALOG(listOf(REMOVE_MATERIALS, REMOVE_MATERIAL_TYPES, REMOVE_COMPANIES), true),
REMOVE_RECIPE(listOf(REMOVE_RECIPES), true), REMOVE_RECIPE(listOf(REMOVE_RECIPES), true),
REMOVE_MATERIAL(listOf(REMOVE_MATERIALS), true), REMOVE_MATERIAL(listOf(REMOVE_MATERIALS), true),
REMOVE_MATERIAL_TYPE(listOf(REMOVE_MATERIAL_TYPES), true), REMOVE_MATERIAL_TYPE(listOf(REMOVE_MATERIAL_TYPES), true),
@ -80,12 +82,12 @@ enum class EmployeePermission(
SET_BROWSER_DEFAULT_GROUP(listOf(VIEW_USERS), true), SET_BROWSER_DEFAULT_GROUP(listOf(VIEW_USERS), true),
; ;
operator fun contains(permission: EmployeePermission): Boolean { operator fun contains(permission: Permission): Boolean {
return permission == this || impliedPermissions.any { permission in it } return permission == this || impliedPermissions.any { permission in it }
} }
} }
fun EmployeePermission.flat(): Iterable<EmployeePermission> { fun Permission.flat(): Iterable<Permission> {
return mutableSetOf(this).apply { return mutableSetOf(this).apply {
impliedPermissions.forEach { impliedPermissions.forEach {
addAll(it.flat()) addAll(it.flat())
@ -93,7 +95,7 @@ fun EmployeePermission.flat(): Iterable<EmployeePermission> {
} }
} }
/** Converts the given [EmployeePermission] to a [GrantedAuthority]. */ /** Converts the given [Permission] to a [GrantedAuthority]. */
fun EmployeePermission.toAuthority(): GrantedAuthority { fun Permission.toAuthority(): GrantedAuthority {
return SimpleGrantedAuthority(name) return SimpleGrantedAuthority(name)
} }

View File

@ -0,0 +1,186 @@
package dev.fyloz.colorrecipesexplorer.model.account
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 org.hibernate.annotations.Fetch
import org.hibernate.annotations.FetchMode
import org.springframework.security.core.GrantedAuthority
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")
data class User(
@Id
override val id: Long,
@Column(name = "first_name")
val firstName: String = "",
@Column(name = "last_name")
val lastName: String = "",
val password: String = "",
@Column(name = "default_group_user")
val isDefaultGroupUser: Boolean = false,
@Column(name = "system_user")
val isSystemUser: Boolean = false,
@ManyToOne
@JoinColumn(name = "group_id")
@Fetch(FetchMode.SELECT)
var group: Group? = null,
@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<Permission> = mutableSetOf(),
@Column(name = "last_login_time")
var lastLoginTime: LocalDateTime? = null
) : Model {
val flatPermissions: Set<Permission>
get() = permissions
.flatMap { it.flat() }
.filter { !it.deprecated }
.toMutableSet()
.apply {
if (group != null) this.addAll(group!!.flatPermissions)
}
val authorities: Set<GrantedAuthority>
get() = flatPermissions.map { it.toAuthority() }.toMutableSet()
}
open class UserSaveDto(
val id: Long,
@field:NotBlank
val firstName: String,
@field:NotBlank
val lastName: String,
@field:NotBlank
@field:Size(min = 8, message = VALIDATION_PASSWORD_LENGTH)
val password: String,
val groupId: Long?,
@Enumerated(EnumType.STRING)
val permissions: MutableSet<Permission> = mutableSetOf()
) : EntityDto<User>
open class UserUpdateDto(
val id: Long,
@field:NotBlank
val firstName: String?,
@field:NotBlank
val lastName: String?,
val groupId: Long?,
@Enumerated(EnumType.STRING)
val permissions: Set<Permission>?
) : EntityDto<User>
data class UserOutputDto(
override val id: Long,
val firstName: String,
val lastName: String,
val group: Group?,
val permissions: Set<Permission>,
val explicitPermissions: Set<Permission>,
val lastLoginTime: LocalDateTime?
) : Model
data class UserLoginRequest(val id: Long, val password: String)
// ==== DSL ====
fun user(
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
password: String = passwordEncoder.encode("password"),
isDefaultGroupUser: Boolean = false,
isSystemUser: Boolean = false,
group: Group? = null,
permissions: MutableSet<Permission> = mutableSetOf(),
lastLoginTime: LocalDateTime? = null,
op: User.() -> Unit = {}
) = User(
id,
firstName,
lastName,
password,
isDefaultGroupUser,
isSystemUser,
group,
permissions,
lastLoginTime
).apply(op)
fun userSaveDto(
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
password: String = passwordEncoder.encode("password"),
groupId: Long? = null,
permissions: MutableSet<Permission> = mutableSetOf(),
op: UserSaveDto.() -> Unit = {}
) = UserSaveDto(id, firstName, lastName, password, groupId, permissions).apply(op)
fun userUpdateDto(
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
groupId: Long? = null,
permissions: MutableSet<Permission> = mutableSetOf(),
op: UserUpdateDto.() -> Unit = {}
) = UserUpdateDto(id, firstName, lastName, groupId, permissions).apply(op)
// ==== Exceptions ====
private const val USER_NOT_FOUND_EXCEPTION_TITLE = "User not found"
private const val USER_ALREADY_EXISTS_EXCEPTION_TITLE = "User already exists"
private const val USER_EXCEPTION_ERROR_CODE = "user"
fun userIdNotFoundException(id: Long) =
NotFoundException(
USER_EXCEPTION_ERROR_CODE,
USER_NOT_FOUND_EXCEPTION_TITLE,
"An user with the id $id could not be found",
id
)
fun userIdAlreadyExistsException(id: Long) =
AlreadyExistsException(
USER_EXCEPTION_ERROR_CODE,
USER_ALREADY_EXISTS_EXCEPTION_TITLE,
"An user with the id $id already exists",
id
)
fun userFullNameAlreadyExistsException(firstName: String, lastName: String) =
AlreadyExistsException(
USER_EXCEPTION_ERROR_CODE,
USER_ALREADY_EXISTS_EXCEPTION_TITLE,
"An user with the name '$firstName $lastName' already exists",
"$firstName $lastName",
"fullName"
)

View File

@ -0,0 +1,211 @@
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.VALIDATION_SIZE_GE_ONE
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")
data class TouchUpKit(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
val project: String,
val buggy: String,
val company: String,
val quantity: Int,
@Column(name = "shipping_date")
val shippingDate: LocalDate,
@Column(name = "finish")
private val finishConcatenated: String,
@Column(name = "material")
private val materialConcatenated: String,
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
@JoinColumn(name = "touch_up_kit_id")
val content: Set<TouchUpKitProduct>
) : Model {
val finish
get() = finishConcatenated.split(TOUCH_UP_KIT_DELIMITER)
val material
get() = materialConcatenated.split(TOUCH_UP_KIT_DELIMITER)
}
@Entity
@Table(name = "touch_up_kit_product")
data class TouchUpKitProduct(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
val name: String,
val description: String?,
val quantity: Float
) : Model
data class TouchUpKitSaveDto(
@field:NotBlank
val project: String,
@field:NotBlank
val buggy: String,
@field:NotBlank
val company: String,
@field:Min(1, message = VALIDATION_SIZE_GE_ONE)
val quantity: Int,
val shippingDate: LocalDate,
@field:NotEmpty
val finish: List<String>,
@field:NotEmpty
val material: List<String>,
@field:NotEmpty
val content: Set<TouchUpKitProductDto>
) : EntityDto<TouchUpKit> {
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 = VALIDATION_SIZE_GE_ONE)
val quantity: Int?,
val shippingDate: LocalDate?,
@field:NotEmpty
val finish: List<String>?,
@field:NotEmpty
val material: List<String>?,
@field:NotEmpty
val content: Set<TouchUpKitProductDto>?
) : EntityDto<TouchUpKit>
data class TouchUpKitOutputDto(
override val id: Long,
val project: String,
val buggy: String,
val company: String,
val quantity: Int,
val shippingDate: LocalDate,
val finish: List<String>,
val material: List<String>,
val content: Set<TouchUpKitProduct>,
val pdfUrl: String
) : Model
data class TouchUpKitProductDto(
val name: String,
val description: String?,
val quantity: Float
)
// ==== DSL ====
fun touchUpKit(
id: Long? = null,
project: String = "project",
buggy: String = "buggy",
company: String = "company",
quantity: Int = 1,
shippingDate: LocalDate = LocalDate.now(),
finish: List<String>,
material: List<String>,
content: Set<TouchUpKitProduct>,
op: TouchUpKit.() -> Unit = {}
) = TouchUpKit(
id,
project,
buggy,
company,
quantity,
shippingDate,
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,
op: TouchUpKitProduct.() -> Unit = {}
) = TouchUpKitProduct(id, name, description, quantity)
.apply(op)
fun touchUpKitProduct(touchUpKitProductDto: TouchUpKitProductDto) =
touchUpKitProduct(
name = touchUpKitProductDto.name,
description = touchUpKitProductDto.description,
quantity = touchUpKitProductDto.quantity
)
// ==== 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
)

View File

@ -1,20 +1,20 @@
package dev.fyloz.colorrecipesexplorer.repository package dev.fyloz.colorrecipesexplorer.repository
import dev.fyloz.colorrecipesexplorer.model.Employee import dev.fyloz.colorrecipesexplorer.model.account.Group
import dev.fyloz.colorrecipesexplorer.model.EmployeeGroup import dev.fyloz.colorrecipesexplorer.model.account.User
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
@Repository @Repository
interface EmployeeRepository : JpaRepository<Employee, Long> { interface UserRepository : JpaRepository<User, Long> {
fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean
fun findByFirstNameAndLastName(firstName: String, lastName: String): Employee? fun findByFirstNameAndLastName(firstName: String, lastName: String): User?
fun findAllByGroup(group: EmployeeGroup): Collection<Employee> fun findAllByGroup(group: Group): Collection<User>
fun findByIsDefaultGroupUserIsTrueAndGroupIs(group: EmployeeGroup): Employee fun findByIsDefaultGroupUserIsTrueAndGroupIs(group: Group): User
} }
@Repository @Repository
interface EmployeeGroupRepository : NamedJpaRepository<EmployeeGroup> interface GroupRepository : NamedJpaRepository<Group>

View File

@ -8,6 +8,12 @@ interface RecipeRepository : JpaRepository<Recipe, Long> {
/** Checks if one or more recipes have the given [company]. */ /** Checks if one or more recipes have the given [company]. */
fun existsByCompany(company: Company): Boolean fun existsByCompany(company: Company): Boolean
/** Checks if a recipe exists with the given [name] and [company]. */
fun existsByNameAndCompany(name: String, company: Company): Boolean
/** Gets all recipes with the given [name]. */
fun findAllByName(name: String): Collection<Recipe>
/** Gets all recipes with the given [company]. */ /** Gets all recipes with the given [company]. */
fun findAllByCompany(company: Company): Collection<Recipe> fun findAllByCompany(company: Company): Collection<Recipe>
} }

View File

@ -0,0 +1,6 @@
package dev.fyloz.colorrecipesexplorer.repository
import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKit
import org.springframework.data.jpa.repository.JpaRepository
interface TouchUpKitRepository : JpaRepository<TouchUpKit, Long>

View File

@ -1,11 +1,10 @@
package dev.fyloz.colorrecipesexplorer.rest package dev.fyloz.colorrecipesexplorer.rest
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditUsers import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditUsers
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeRemoveUsers
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewUsers import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewUsers
import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.model.account.*
import dev.fyloz.colorrecipesexplorer.service.EmployeeGroupService import dev.fyloz.colorrecipesexplorer.service.UserService
import dev.fyloz.colorrecipesexplorer.service.EmployeeService import dev.fyloz.colorrecipesexplorer.service.GroupService
import org.springframework.http.MediaType import org.springframework.http.MediaType
import org.springframework.security.access.prepost.PreAuthorize import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
@ -14,29 +13,29 @@ import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse import javax.servlet.http.HttpServletResponse
import javax.validation.Valid import javax.validation.Valid
private const val EMPLOYEE_CONTROLLER_PATH = "api/employee" private const val USER_CONTROLLER_PATH = "api/user"
private const val EMPLOYEE_GROUP_CONTROLLER_PATH = "api/employee/group" private const val GROUP_CONTROLLER_PATH = "api/user/group"
@RestController @RestController
@RequestMapping(EMPLOYEE_CONTROLLER_PATH) @RequestMapping(USER_CONTROLLER_PATH)
class EmployeeController(private val employeeService: EmployeeService) { class UserController(private val userService: UserService) {
@GetMapping @GetMapping
@PreAuthorizeViewUsers @PreAuthorizeViewUsers
fun getAll() = fun getAll() =
ok(employeeService.getAllForOutput()) ok(userService.getAllForOutput())
@GetMapping("{id}") @GetMapping("{id}")
@PreAuthorizeViewUsers @PreAuthorizeViewUsers
fun getById(@PathVariable id: Long) = fun getById(@PathVariable id: Long) =
ok(employeeService.getByIdForOutput(id)) ok(userService.getByIdForOutput(id))
@GetMapping("current") @GetMapping("current")
fun getCurrent(loggedInEmployee: Principal?) = fun getCurrent(loggedInUser: Principal?) =
if (loggedInEmployee != null) if (loggedInUser != null)
ok( ok(
with(employeeService) { with(userService) {
getById( getById(
loggedInEmployee.name.toLong(), loggedInUser.name.toLong(),
ignoreDefaultGroupUsers = false, ignoreDefaultGroupUsers = false,
ignoreSystemUsers = false ignoreSystemUsers = false
).toOutput() ).toOutput()
@ -47,56 +46,56 @@ class EmployeeController(private val employeeService: EmployeeService) {
@PostMapping @PostMapping
@PreAuthorizeEditUsers @PreAuthorizeEditUsers
fun save(@Valid @RequestBody employee: EmployeeSaveDto) = fun save(@Valid @RequestBody user: UserSaveDto) =
created<EmployeeOutputDto>(EMPLOYEE_CONTROLLER_PATH) { created<UserOutputDto>(USER_CONTROLLER_PATH) {
with(employeeService) { with(userService) {
save(employee).toOutput() save(user).toOutput()
} }
} }
@PutMapping @PutMapping
@PreAuthorizeEditUsers @PreAuthorizeEditUsers
fun update(@Valid @RequestBody employee: EmployeeUpdateDto) = fun update(@Valid @RequestBody user: UserUpdateDto) =
noContent { noContent {
employeeService.update(employee) userService.update(user)
} }
@PutMapping("{id}/password", consumes = [MediaType.TEXT_PLAIN_VALUE]) @PutMapping("{id}/password", consumes = [MediaType.TEXT_PLAIN_VALUE])
@PreAuthorizeEditUsers @PreAuthorizeEditUsers
fun updatePassword(@PathVariable id: Long, @RequestBody password: String) = fun updatePassword(@PathVariable id: Long, @RequestBody password: String) =
noContent { noContent {
employeeService.updatePassword(id, password) userService.updatePassword(id, password)
} }
@PutMapping("{employeeId}/permissions/{permission}") @PutMapping("{userId}/permissions/{permission}")
@PreAuthorizeEditUsers @PreAuthorizeEditUsers
fun addPermission( fun addPermission(
@PathVariable employeeId: Long, @PathVariable userId: Long,
@PathVariable permission: EmployeePermission @PathVariable permission: Permission
) = noContent { ) = noContent {
employeeService.addPermission(employeeId, permission) userService.addPermission(userId, permission)
} }
@DeleteMapping("{employeeId}/permissions/{permission}") @DeleteMapping("{userId}/permissions/{permission}")
@PreAuthorizeEditUsers @PreAuthorizeEditUsers
fun removePermission( fun removePermission(
@PathVariable employeeId: Long, @PathVariable userId: Long,
@PathVariable permission: EmployeePermission @PathVariable permission: Permission
) = noContent { ) = noContent {
employeeService.removePermission(employeeId, permission) userService.removePermission(userId, permission)
} }
@DeleteMapping("{id}") @DeleteMapping("{id}")
@PreAuthorizeRemoveUsers @PreAuthorizeEditUsers
fun deleteById(@PathVariable id: Long) = fun deleteById(@PathVariable id: Long) =
employeeService.deleteById(id) userService.deleteById(id)
} }
@RestController @RestController
@RequestMapping(EMPLOYEE_GROUP_CONTROLLER_PATH) @RequestMapping(GROUP_CONTROLLER_PATH)
class GroupsController( class GroupsController(
private val groupService: EmployeeGroupService, private val groupService: GroupService,
private val employeeService: EmployeeService private val userService: UserService
) { ) {
@GetMapping @GetMapping
@PreAuthorize("hasAnyAuthority('VIEW_RECIPES', 'VIEW_USERS')") @PreAuthorize("hasAnyAuthority('VIEW_RECIPES', 'VIEW_USERS')")
@ -108,11 +107,11 @@ class GroupsController(
fun getById(@PathVariable id: Long) = fun getById(@PathVariable id: Long) =
ok(groupService.getByIdForOutput(id)) ok(groupService.getByIdForOutput(id))
@GetMapping("{id}/employees") @GetMapping("{id}/users")
@PreAuthorizeViewUsers @PreAuthorizeViewUsers
fun getEmployeesForGroup(@PathVariable id: Long) = fun getUsersForGroup(@PathVariable id: Long) =
ok(with(employeeService) { ok(with(userService) {
groupService.getEmployeesForGroup(id) groupService.getUsersForGroup(id)
.map { it.toOutput() } .map { it.toOutput() }
}) })
@ -132,8 +131,8 @@ class GroupsController(
@PostMapping @PostMapping
@PreAuthorizeEditUsers @PreAuthorizeEditUsers
fun save(@Valid @RequestBody group: EmployeeGroupSaveDto) = fun save(@Valid @RequestBody group: GroupSaveDto) =
created<EmployeeGroupOutputDto>(EMPLOYEE_GROUP_CONTROLLER_PATH) { created<GroupOutputDto>(GROUP_CONTROLLER_PATH) {
with(groupService) { with(groupService) {
save(group).toOutput() save(group).toOutput()
} }
@ -141,13 +140,13 @@ class GroupsController(
@PutMapping @PutMapping
@PreAuthorizeEditUsers @PreAuthorizeEditUsers
fun update(@Valid @RequestBody group: EmployeeGroupUpdateDto) = fun update(@Valid @RequestBody group: GroupUpdateDto) =
noContent { noContent {
groupService.update(group) groupService.update(group)
} }
@DeleteMapping("{id}") @DeleteMapping("{id}")
@PreAuthorizeRemoveUsers @PreAuthorizeEditUsers
fun deleteById(@PathVariable id: Long) = fun deleteById(@PathVariable id: Long) =
noContent { noContent {
groupService.deleteById(id) groupService.deleteById(id)
@ -156,10 +155,10 @@ class GroupsController(
@RestController @RestController
@RequestMapping("api") @RequestMapping("api")
class LogoutController(private val employeeService: EmployeeService) { class LogoutController(private val userService: UserService) {
@GetMapping("logout") @GetMapping("logout")
fun logout(request: HttpServletRequest) = fun logout(request: HttpServletRequest) =
ok<Void> { ok<Void> {
employeeService.logout(request) userService.logout(request)
} }
} }

View File

@ -38,7 +38,7 @@ class CompanyController(private val companyService: CompanyService) {
} }
@DeleteMapping("{id}") @DeleteMapping("{id}")
@PreAuthorize("hasAuthority('REMOVE_COMPANIES')") @PreAuthorize("hasAuthority('EDIT_COMPANIES')")
fun deleteById(@PathVariable id: Long) = fun deleteById(@PathVariable id: Long) =
noContent { noContent {
companyService.deleteById(id) companyService.deleteById(id)

View File

@ -64,7 +64,7 @@ class MaterialController(
} }
@DeleteMapping("{id}") @DeleteMapping("{id}")
@PreAuthorize("hasAuthority('REMOVE_MATERIALS')") @PreAuthorize("hasAuthority('EDIT_MATERIALS')")
fun deleteById(@PathVariable id: Long) = fun deleteById(@PathVariable id: Long) =
noContent { noContent {
materialService.deleteById(id) materialService.deleteById(id)

View File

@ -38,7 +38,7 @@ class MaterialTypeController(private val materialTypeService: MaterialTypeServic
} }
@DeleteMapping("{id}") @DeleteMapping("{id}")
@PreAuthorize("hasAuthority('REMOVE_MATERIAL_TYPES')") @PreAuthorize("hasAuthority('EDIT_MATERIAL_TYPES')")
fun deleteById(@PathVariable id: Long) = fun deleteById(@PathVariable id: Long) =
noContent { noContent {
materialTypeService.deleteById(id) materialTypeService.deleteById(id)

View File

@ -1,11 +1,8 @@
package dev.fyloz.colorrecipesexplorer.rest package dev.fyloz.colorrecipesexplorer.rest
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditRecipes import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditRecipes
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeRemoveRecipes
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewRecipes import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewRecipes
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.rest.files.FILE_CONTROLLER_PATH
import dev.fyloz.colorrecipesexplorer.service.MixService import dev.fyloz.colorrecipesexplorer.service.MixService
import dev.fyloz.colorrecipesexplorer.service.RecipeImageService import dev.fyloz.colorrecipesexplorer.service.RecipeImageService
import dev.fyloz.colorrecipesexplorer.service.RecipeService import dev.fyloz.colorrecipesexplorer.service.RecipeService
@ -14,8 +11,6 @@ import org.springframework.http.ResponseEntity
import org.springframework.security.access.prepost.PreAuthorize import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile import org.springframework.web.multipart.MultipartFile
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import javax.validation.Valid import javax.validation.Valid
@ -30,8 +25,13 @@ class RecipeController(
private val recipeImageService: RecipeImageService private val recipeImageService: RecipeImageService
) { ) {
@GetMapping @GetMapping
fun getAll() = fun getAll(@RequestParam(required = false) name: String?) =
ok(recipeService.getAllForOutput()) if (name == null)
ok(recipeService.getAllForOutput())
else
ok(with(recipeService) {
getAllByName(name).map { it.toOutput() }
})
@GetMapping("{id}") @GetMapping("{id}")
fun getById(@PathVariable id: Long) = fun getById(@PathVariable id: Long) =
@ -61,7 +61,7 @@ class RecipeController(
} }
@DeleteMapping("{id}") @DeleteMapping("{id}")
@PreAuthorizeRemoveRecipes @PreAuthorizeEditRecipes
fun deleteById(@PathVariable id: Long) = fun deleteById(@PathVariable id: Long) =
noContent { noContent {
recipeService.deleteById(id) recipeService.deleteById(id)
@ -105,7 +105,7 @@ class MixController(private val mixService: MixService) {
} }
@DeleteMapping("{id}") @DeleteMapping("{id}")
@PreAuthorizeRemoveRecipes @PreAuthorizeEditRecipes
fun deleteById(@PathVariable id: Long) = fun deleteById(@PathVariable id: Long) =
noContent { noContent {
mixService.deleteById(id) mixService.deleteById(id)

View File

@ -0,0 +1,63 @@
package dev.fyloz.colorrecipesexplorer.rest
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.touchupkit.TouchUpKitService
import org.springframework.core.io.ByteArrayResource
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
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)
@PreAuthorize("hasAuthority('VIEW_TOUCH_UP_KITS')")
class TouchUpKitController(
private val touchUpKitService: TouchUpKitService
) {
@GetMapping
fun getAll() =
ok(touchUpKitService.getAllForOutput())
@GetMapping("{id}")
fun getById(@PathVariable id: Long) =
ok(touchUpKitService.getByIdForOutput(id))
@PostMapping
@PreAuthorize("hasAuthority('EDIT_TOUCH_UP_KITS')")
fun save(@Valid @RequestBody touchUpKit: TouchUpKitSaveDto) =
created<TouchUpKitOutputDto>(TOUCH_UP_KIT_CONTROLLER_PATH) {
with(touchUpKitService) {
save(touchUpKit).toOutput()
}
}
@PutMapping
@PreAuthorize("hasAuthority('EDIT_TOUCH_UP_KITS')")
fun update(@Valid @RequestBody touchUpKit: TouchUpKitUpdateDto) =
noContent {
touchUpKitService.update(touchUpKit)
}
@DeleteMapping("{id}")
@PreAuthorize("hasAuthority('EDIT_TOUCH_UP_KITS')")
fun deleteById(@PathVariable id: Long) =
noContent {
touchUpKitService.deleteById(id)
}
@GetMapping("pdf")
fun getJobPdf(@RequestParam project: String): ResponseEntity<ByteArrayResource> {
with(touchUpKitService.generateJobPdfResource(project)) {
return ResponseEntity.ok()
.header("Content-Disposition", "filename=TouchUpKit_$project.pdf")
.contentLength(this.contentLength())
.contentType(MediaType.APPLICATION_PDF)
.body(this)
}
}
}

View File

@ -46,7 +46,7 @@ class FileController(
} }
@DeleteMapping @DeleteMapping
@PreAuthorize("hasAnyAuthority('REMOVE_FILE')") @PreAuthorize("hasAnyAuthority('WRITE_FILE')")
fun delete(@RequestParam path: String): ResponseEntity<Void> { fun delete(@RequestParam path: String): ResponseEntity<Void> {
return noContent { return noContent {
fileService.delete(path) fileService.delete(path)

View File

@ -1,26 +0,0 @@
package dev.fyloz.colorrecipesexplorer.rest.files
import dev.fyloz.colorrecipesexplorer.service.files.TouchUpKitService
import org.springframework.core.io.ByteArrayResource
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/api/touchup")
@PreAuthorize("hasAuthority('GENERATE_TOUCH_UP_KIT')")
class TouchUpKitController(
private val touchUpKitService: TouchUpKitService
) {
@GetMapping
fun getJobPdf(@RequestParam job: String): ResponseEntity<ByteArrayResource> {
with(touchUpKitService.generateJobPdfResource(job)) {
return ResponseEntity.ok()
.header("Content-Disposition", "filename=TouchUpKit_$job.pdf")
.contentLength(this.contentLength())
.contentType(MediaType.APPLICATION_PDF)
.body(this)
}
}
}

View File

@ -3,12 +3,11 @@ package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.config.blacklistedJwtTokens import dev.fyloz.colorrecipesexplorer.config.blacklistedJwtTokens
import dev.fyloz.colorrecipesexplorer.config.defaultGroupCookieName import dev.fyloz.colorrecipesexplorer.config.defaultGroupCookieName
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.model.account.*
import dev.fyloz.colorrecipesexplorer.model.validation.or import dev.fyloz.colorrecipesexplorer.model.validation.or
import dev.fyloz.colorrecipesexplorer.repository.EmployeeGroupRepository import dev.fyloz.colorrecipesexplorer.repository.UserRepository
import dev.fyloz.colorrecipesexplorer.repository.EmployeeRepository import dev.fyloz.colorrecipesexplorer.repository.GroupRepository
import org.springframework.context.annotation.Lazy import org.springframework.context.annotation.Lazy
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.security.core.userdetails.UsernameNotFoundException
@ -19,73 +18,74 @@ import java.time.LocalDateTime
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse import javax.servlet.http.HttpServletResponse
import javax.transaction.Transactional import javax.transaction.Transactional
import org.springframework.security.core.userdetails.User as SpringUser
interface EmployeeService : interface UserService :
ExternalModelService<Employee, EmployeeSaveDto, EmployeeUpdateDto, EmployeeOutputDto, EmployeeRepository> { ExternalModelService<User, UserSaveDto, UserUpdateDto, UserOutputDto, UserRepository> {
/** Check if an [Employee] with the given [firstName] and [lastName] exists. */ /** Check if an [User] with the given [firstName] and [lastName] exists. */
fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean
/** Gets the employee with the given [id]. */ /** Gets the user with the given [id]. */
fun getById(id: Long, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): Employee fun getById(id: Long, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): User
/** Gets all employees which have the given [group]. */ /** Gets all users which have the given [group]. */
fun getByGroup(group: EmployeeGroup): Collection<Employee> fun getByGroup(group: Group): Collection<User>
/** Gets the default user of the given [group]. */ /** Gets the default user of the given [group]. */
fun getDefaultGroupEmployee(group: EmployeeGroup): Employee fun getDefaultGroupUser(group: Group): User
/** Save a default group employee for the given [group]. */ /** Save a default group user for the given [group]. */
fun saveDefaultGroupEmployee(group: EmployeeGroup) fun saveDefaultGroupUser(group: Group)
/** Updates de given [entity]. **/ /** Updates de given [entity]. **/
fun update(entity: Employee, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): Employee fun update(entity: User, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): User
/** Updates the last login time of the employee with the given [employeeId]. */ /** Updates the last login time of the user with the given [userId]. */
fun updateLastLoginTime(employeeId: Long, time: LocalDateTime = LocalDateTime.now()): Employee fun updateLastLoginTime(userId: Long, time: LocalDateTime = LocalDateTime.now()): User
/** Updates the password of the employee with the given [id]. */ /** Updates the password of the user with the given [id]. */
fun updatePassword(id: Long, password: String): Employee fun updatePassword(id: Long, password: String): User
/** Adds the given [permission] to the employee with the given [employeeId]. */ /** Adds the given [permission] to the user with the given [userId]. */
fun addPermission(employeeId: Long, permission: EmployeePermission): Employee fun addPermission(userId: Long, permission: Permission): User
/** Removes the given [permission] from the employee with the given [employeeId]. */ /** Removes the given [permission] from the user with the given [userId]. */
fun removePermission(employeeId: Long, permission: EmployeePermission): Employee fun removePermission(userId: Long, permission: Permission): User
/** Logout an user. Add the authorization token of the given [request] to the blacklisted tokens. */ /** Logout an user. Add the authorization token of the given [request] to the blacklisted tokens. */
fun logout(request: HttpServletRequest) fun logout(request: HttpServletRequest)
} }
interface EmployeeGroupService : interface GroupService :
ExternalNamedModelService<EmployeeGroup, EmployeeGroupSaveDto, EmployeeGroupUpdateDto, EmployeeGroupOutputDto, EmployeeGroupRepository> { ExternalNamedModelService<Group, GroupSaveDto, GroupUpdateDto, GroupOutputDto, GroupRepository> {
/** Gets all the employees of the group with the given [id]. */ /** Gets all the users of the group with the given [id]. */
fun getEmployeesForGroup(id: Long): Collection<Employee> fun getUsersForGroup(id: Long): Collection<User>
/** Gets the default group from a cookie in the given HTTP [request]. */ /** Gets the default group from a cookie in the given HTTP [request]. */
fun getRequestDefaultGroup(request: HttpServletRequest): EmployeeGroup fun getRequestDefaultGroup(request: HttpServletRequest): Group
/** Sets the default group cookie for the given HTTP [response]. */ /** Sets the default group cookie for the given HTTP [response]. */
fun setResponseDefaultGroup(groupId: Long, response: HttpServletResponse) fun setResponseDefaultGroup(groupId: Long, response: HttpServletResponse)
} }
interface EmployeeUserDetailsService : UserDetailsService { interface CreUserDetailsService : UserDetailsService {
/** Loads an [User] for the given [employeeId]. */ /** Loads an [User] for the given [id]. */
fun loadUserByEmployeeId(employeeId: Long, ignoreDefaultGroupUsers: Boolean = false): UserDetails fun loadUserById(id: Long, ignoreDefaultGroupUsers: Boolean = false): UserDetails
} }
@Service @Service
class EmployeeServiceImpl( class UserServiceImpl(
employeeRepository: EmployeeRepository, userRepository: UserRepository,
@Lazy val groupService: EmployeeGroupService, @Lazy val groupService: GroupService,
@Lazy val passwordEncoder: PasswordEncoder, @Lazy val passwordEncoder: PasswordEncoder,
) : AbstractExternalModelService<Employee, EmployeeSaveDto, EmployeeUpdateDto, EmployeeOutputDto, EmployeeRepository>( ) : AbstractExternalModelService<User, UserSaveDto, UserUpdateDto, UserOutputDto, UserRepository>(
employeeRepository userRepository
), ),
EmployeeService { UserService {
override fun idNotFoundException(id: Long) = employeeIdNotFoundException(id) override fun idNotFoundException(id: Long) = userIdNotFoundException(id)
override fun idAlreadyExistsException(id: Long) = employeeIdAlreadyExistsException(id) override fun idAlreadyExistsException(id: Long) = userIdAlreadyExistsException(id)
override fun Employee.toOutput() = EmployeeOutputDto( override fun User.toOutput() = UserOutputDto(
this.id, this.id,
this.firstName, this.firstName,
this.lastName, this.lastName,
@ -98,29 +98,29 @@ class EmployeeServiceImpl(
override fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean = override fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean =
repository.existsByFirstNameAndLastName(firstName, lastName) repository.existsByFirstNameAndLastName(firstName, lastName)
override fun getAll(): Collection<Employee> = override fun getAll(): Collection<User> =
super.getAll().filter { !it.isSystemUser && !it.isDefaultGroupUser } super.getAll().filter { !it.isSystemUser && !it.isDefaultGroupUser }
override fun getById(id: Long): Employee = override fun getById(id: Long): User =
getById(id, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true) getById(id, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)
override fun getById(id: Long, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): Employee = override fun getById(id: Long, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): User =
super.getById(id).apply { super.getById(id).apply {
if (ignoreSystemUsers && isSystemUser || ignoreDefaultGroupUsers && isDefaultGroupUser) if (ignoreSystemUsers && isSystemUser || ignoreDefaultGroupUsers && isDefaultGroupUser)
throw idNotFoundException(id) throw idNotFoundException(id)
} }
override fun getByGroup(group: EmployeeGroup): Collection<Employee> = override fun getByGroup(group: Group): Collection<User> =
repository.findAllByGroup(group).filter { repository.findAllByGroup(group).filter {
!it.isSystemUser && !it.isDefaultGroupUser !it.isSystemUser && !it.isDefaultGroupUser
} }
override fun getDefaultGroupEmployee(group: EmployeeGroup): Employee = override fun getDefaultGroupUser(group: Group): User =
repository.findByIsDefaultGroupUserIsTrueAndGroupIs(group) repository.findByIsDefaultGroupUserIsTrueAndGroupIs(group)
override fun save(entity: EmployeeSaveDto): Employee = override fun save(entity: UserSaveDto): User =
save(with(entity) { save(with(entity) {
Employee( User(
id, id,
firstName, firstName,
lastName, lastName,
@ -132,20 +132,20 @@ class EmployeeServiceImpl(
) )
}) })
override fun save(entity: Employee): Employee { override fun save(entity: User): User {
if (existsById(entity.id)) if (existsById(entity.id))
throw employeeIdAlreadyExistsException(entity.id) throw userIdAlreadyExistsException(entity.id)
if (existsByFirstNameAndLastName(entity.firstName, entity.lastName)) if (existsByFirstNameAndLastName(entity.firstName, entity.lastName))
throw employeeFullNameAlreadyExistsException(entity.firstName, entity.lastName) throw userFullNameAlreadyExistsException(entity.firstName, entity.lastName)
return super<AbstractExternalModelService>.save(entity) return super<AbstractExternalModelService>.save(entity)
} }
override fun saveDefaultGroupEmployee(group: EmployeeGroup) { override fun saveDefaultGroupUser(group: Group) {
save( save(
employee( user(
id = 1000000L + group.id!!, id = 1000000L + group.id!!,
firstName = group.name, firstName = group.name,
lastName = "EmployeeModel", lastName = "User",
password = passwordEncoder.encode(group.name), password = passwordEncoder.encode(group.name),
group = group, group = group,
isDefaultGroupUser = true isDefaultGroupUser = true
@ -153,49 +153,49 @@ class EmployeeServiceImpl(
) )
} }
override fun updateLastLoginTime(employeeId: Long, time: LocalDateTime): Employee { override fun updateLastLoginTime(userId: Long, time: LocalDateTime): User {
val employee = getById(employeeId, ignoreDefaultGroupUsers = true, ignoreSystemUsers = false) val user = getById(userId, ignoreDefaultGroupUsers = true, ignoreSystemUsers = false)
employee.lastLoginTime = time user.lastLoginTime = time
return update( return update(
employee, user,
ignoreDefaultGroupUsers = true, ignoreDefaultGroupUsers = true,
ignoreSystemUsers = false ignoreSystemUsers = false
) )
} }
override fun update(entity: EmployeeUpdateDto): Employee { override fun update(entity: UserUpdateDto): User {
val persistedEmployee by lazy { getById(entity.id) } val persistedUser by lazy { getById(entity.id) }
return update(with(entity) { return update(with(entity) {
Employee( User(
id = id, id = id,
firstName = firstName or persistedEmployee.firstName, firstName = firstName or persistedUser.firstName,
lastName = lastName or persistedEmployee.lastName, lastName = lastName or persistedUser.lastName,
password = persistedEmployee.password, password = persistedUser.password,
isDefaultGroupUser = false, isDefaultGroupUser = false,
isSystemUser = false, isSystemUser = false,
group = if (entity.groupId != null) groupService.getById(entity.groupId) else persistedEmployee.group, group = if (entity.groupId != null) groupService.getById(entity.groupId) else persistedUser.group,
permissions = permissions?.toMutableSet() ?: persistedEmployee.permissions, permissions = permissions?.toMutableSet() ?: persistedUser.permissions,
lastLoginTime = persistedEmployee.lastLoginTime lastLoginTime = persistedUser.lastLoginTime
) )
}) })
} }
override fun update(entity: Employee): Employee = override fun update(entity: User): User =
update(entity, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true) update(entity, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)
override fun update(entity: Employee, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): Employee { override fun update(entity: User, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): User {
with(repository.findByFirstNameAndLastName(entity.firstName, entity.lastName)) { with(repository.findByFirstNameAndLastName(entity.firstName, entity.lastName)) {
if (this != null && id != entity.id) if (this != null && id != entity.id)
throw employeeFullNameAlreadyExistsException(entity.firstName, entity.lastName) throw userFullNameAlreadyExistsException(entity.firstName, entity.lastName)
} }
return super<AbstractExternalModelService>.update(entity) return super.update(entity)
} }
override fun updatePassword(id: Long, password: String): Employee { override fun updatePassword(id: Long, password: String): User {
val persistedEmployee = getById(id, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true) val persistedUser = getById(id, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)
return super<AbstractExternalModelService>.update(with(persistedEmployee) { return super.update(with(persistedUser) {
Employee( User(
id, id,
firstName, firstName,
lastName, lastName,
@ -209,11 +209,11 @@ class EmployeeServiceImpl(
}) })
} }
override fun addPermission(employeeId: Long, permission: EmployeePermission): Employee = override fun addPermission(userId: Long, permission: Permission): User =
super<AbstractExternalModelService>.update(getById(employeeId).apply { permissions += permission }) super.update(getById(userId).apply { permissions += permission })
override fun removePermission(employeeId: Long, permission: EmployeePermission): Employee = override fun removePermission(userId: Long, permission: Permission): User =
super<AbstractExternalModelService>.update(getById(employeeId).apply { permissions -= permission }) super.update(getById(userId).apply { permissions -= permission })
override fun logout(request: HttpServletRequest) { override fun logout(request: HttpServletRequest) {
val authorizationCookie = WebUtils.getCookie(request, "Authorization") val authorizationCookie = WebUtils.getCookie(request, "Authorization")
@ -229,19 +229,19 @@ class EmployeeServiceImpl(
const val defaultGroupCookieMaxAge = 10 * 365 * 24 * 60 * 60 // 10 ans const val defaultGroupCookieMaxAge = 10 * 365 * 24 * 60 * 60 // 10 ans
@Service @Service
class EmployeeGroupServiceImpl( class GroupServiceImpl(
private val employeeService: EmployeeService, private val userService: UserService,
employeeGroupRepository: EmployeeGroupRepository groupRepository: GroupRepository
) : AbstractExternalNamedModelService<EmployeeGroup, EmployeeGroupSaveDto, EmployeeGroupUpdateDto, EmployeeGroupOutputDto, EmployeeGroupRepository>( ) : AbstractExternalNamedModelService<Group, GroupSaveDto, GroupUpdateDto, GroupOutputDto, GroupRepository>(
employeeGroupRepository groupRepository
), ),
EmployeeGroupService { GroupService {
override fun idNotFoundException(id: Long) = employeeGroupIdNotFoundException(id) override fun idNotFoundException(id: Long) = groupIdNotFoundException(id)
override fun idAlreadyExistsException(id: Long) = employeeGroupIdAlreadyExistsException(id) override fun idAlreadyExistsException(id: Long) = groupIdAlreadyExistsException(id)
override fun nameNotFoundException(name: String) = employeeGroupNameNotFoundException(name) override fun nameNotFoundException(name: String) = groupNameNotFoundException(name)
override fun nameAlreadyExistsException(name: String) = employeeGroupNameAlreadyExistsException(name) override fun nameAlreadyExistsException(name: String) = groupNameAlreadyExistsException(name)
override fun EmployeeGroup.toOutput() = EmployeeGroupOutputDto( override fun Group.toOutput() = GroupOutputDto(
this.id!!, this.id!!,
this.name, this.name,
this.permissions, this.permissions,
@ -249,20 +249,20 @@ class EmployeeGroupServiceImpl(
) )
override fun existsByName(name: String): Boolean = repository.existsByName(name) override fun existsByName(name: String): Boolean = repository.existsByName(name)
override fun getEmployeesForGroup(id: Long): Collection<Employee> = override fun getUsersForGroup(id: Long): Collection<User> =
employeeService.getByGroup(getById(id)) userService.getByGroup(getById(id))
@Transactional @Transactional
override fun save(entity: EmployeeGroup): EmployeeGroup { override fun save(entity: Group): Group {
return super<AbstractExternalNamedModelService>.save(entity).apply { return super<AbstractExternalNamedModelService>.save(entity).apply {
employeeService.saveDefaultGroupEmployee(this) userService.saveDefaultGroupUser(this)
} }
} }
override fun update(entity: EmployeeGroupUpdateDto): EmployeeGroup { override fun update(entity: GroupUpdateDto): Group {
val persistedGroup by lazy { getById(entity.id) } val persistedGroup by lazy { getById(entity.id) }
return update(with(entity) { return update(with(entity) {
EmployeeGroup( Group(
entity.id, entity.id,
if (name.isNotBlank()) entity.name else persistedGroup.name, if (name.isNotBlank()) entity.name else persistedGroup.name,
if (permissions.isNotEmpty()) entity.permissions else persistedGroup.permissions if (permissions.isNotEmpty()) entity.permissions else persistedGroup.permissions
@ -271,15 +271,15 @@ class EmployeeGroupServiceImpl(
} }
@Transactional @Transactional
override fun delete(entity: EmployeeGroup) { override fun delete(entity: Group) {
employeeService.delete(employeeService.getDefaultGroupEmployee(entity)) userService.delete(userService.getDefaultGroupUser(entity))
super.delete(entity) super.delete(entity)
} }
override fun getRequestDefaultGroup(request: HttpServletRequest): EmployeeGroup { override fun getRequestDefaultGroup(request: HttpServletRequest): Group {
val defaultGroupCookie = WebUtils.getCookie(request, defaultGroupCookieName) val defaultGroupCookie = WebUtils.getCookie(request, defaultGroupCookieName)
?: throw NoDefaultGroupException() ?: throw NoDefaultGroupException()
val defaultGroupUser = employeeService.getById( val defaultGroupUser = userService.getById(
defaultGroupCookie.value.toLong(), defaultGroupCookie.value.toLong(),
ignoreDefaultGroupUsers = false, ignoreDefaultGroupUsers = false,
ignoreSystemUsers = true ignoreSystemUsers = true
@ -289,7 +289,7 @@ class EmployeeGroupServiceImpl(
override fun setResponseDefaultGroup(groupId: Long, response: HttpServletResponse) { override fun setResponseDefaultGroup(groupId: Long, response: HttpServletResponse) {
val group = getById(groupId) val group = getById(groupId)
val defaultGroupUser = employeeService.getDefaultGroupEmployee(group) val defaultGroupUser = userService.getDefaultGroupUser(group)
response.addHeader( response.addHeader(
"Set-Cookie", "Set-Cookie",
"$defaultGroupCookieName=${defaultGroupUser.id}; Max-Age=${defaultGroupCookieMaxAge}; Path=/api; HttpOnly; Secure; SameSite=strict" "$defaultGroupCookieName=${defaultGroupUser.id}; Max-Age=${defaultGroupCookieMaxAge}; Path=/api; HttpOnly; Secure; SameSite=strict"
@ -298,13 +298,13 @@ class EmployeeGroupServiceImpl(
} }
@Service @Service
class EmployeeUserDetailsServiceImpl( class CreUserDetailsServiceImpl(
private val employeeService: EmployeeService private val userService: UserService
) : ) :
EmployeeUserDetailsService { CreUserDetailsService {
override fun loadUserByUsername(username: String): UserDetails { override fun loadUserByUsername(username: String): UserDetails {
try { try {
return loadUserByEmployeeId(username.toLong(), true) return loadUserById(username.toLong(), true)
} catch (ex: NotFoundException) { } catch (ex: NotFoundException) {
throw UsernameNotFoundException(username) throw UsernameNotFoundException(username)
} catch (ex: NotFoundException) { } catch (ex: NotFoundException) {
@ -312,12 +312,12 @@ class EmployeeUserDetailsServiceImpl(
} }
} }
override fun loadUserByEmployeeId(employeeId: Long, ignoreDefaultGroupUsers: Boolean): UserDetails { override fun loadUserById(id: Long, ignoreDefaultGroupUsers: Boolean): UserDetails {
val employee = employeeService.getById( val user = userService.getById(
employeeId, id,
ignoreDefaultGroupUsers = ignoreDefaultGroupUsers, ignoreDefaultGroupUsers = ignoreDefaultGroupUsers,
ignoreSystemUsers = false ignoreSystemUsers = false
) )
return User(employee.id.toString(), employee.password, employee.authorities) return SpringUser(user.id.toString(), user.password, user.authorities)
} }
} }

View File

@ -136,7 +136,7 @@ class MaterialServiceImpl(
override fun delete(entity: Material) { override fun delete(entity: Material) {
if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteMaterialException(entity) if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteMaterialException(entity)
fileService.delete(entity.simdutFilePath) if (fileService.exists(entity.simdutFilePath)) fileService.delete(entity.simdutFilePath)
super.delete(entity) super.delete(entity)
} }
} }

View File

@ -1,6 +1,7 @@
package dev.fyloz.colorrecipesexplorer.service package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.model.account.Group
import dev.fyloz.colorrecipesexplorer.model.validation.or import dev.fyloz.colorrecipesexplorer.model.validation.or
import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository
import dev.fyloz.colorrecipesexplorer.service.files.FileService import dev.fyloz.colorrecipesexplorer.service.files.FileService
@ -16,6 +17,12 @@ interface RecipeService :
/** Checks if one or more recipes have the given [company]. */ /** Checks if one or more recipes have the given [company]. */
fun existsByCompany(company: Company): Boolean fun existsByCompany(company: Company): Boolean
/** Checks if a recipe exists with the given [name] and [company]. */
fun existsByNameAndCompany(name: String, company: Company): Boolean
/** Gets all recipes with the given [name]. */
fun getAllByName(name: String): Collection<Recipe>
/** Gets all recipes with the given [company]. */ /** Gets all recipes with the given [company]. */
fun getAllByCompany(company: Company): Collection<Recipe> fun getAllByCompany(company: Company): Collection<Recipe>
@ -35,7 +42,7 @@ class RecipeServiceImpl(
val companyService: CompanyService, val companyService: CompanyService,
val mixService: MixService, val mixService: MixService,
val recipeStepService: RecipeStepService, val recipeStepService: RecipeStepService,
@Lazy val groupService: EmployeeGroupService, @Lazy val groupService: GroupService,
val recipeImageService: RecipeImageService val recipeImageService: RecipeImageService
) : ) :
AbstractExternalModelService<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeOutputDto, RecipeRepository>( AbstractExternalModelService<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeOutputDto, RecipeRepository>(
@ -67,10 +74,19 @@ class RecipeServiceImpl(
) )
override fun existsByCompany(company: Company): Boolean = repository.existsByCompany(company) override fun existsByCompany(company: Company): Boolean = repository.existsByCompany(company)
override fun getAllByCompany(company: Company): Collection<Recipe> = repository.findAllByCompany(company) override fun existsByNameAndCompany(name: String, company: Company) =
repository.existsByNameAndCompany(name, company)
override fun getAllByName(name: String) = repository.findAllByName(name)
override fun getAllByCompany(company: Company) = repository.findAllByCompany(company)
override fun save(entity: RecipeSaveDto): Recipe { override fun save(entity: RecipeSaveDto): Recipe {
// TODO checks if name is unique in the scope of the [company] val company = companyService.getById(entity.companyId)
if (existsByNameAndCompany(entity.name, company)) {
throw recipeNameAlreadyExistsForCompanyException(entity.name, company)
}
return save(with(entity) { return save(with(entity) {
recipe( recipe(
name = name, name = name,
@ -80,14 +96,23 @@ class RecipeServiceImpl(
sample = sample, sample = sample,
approbationDate = approbationDate, approbationDate = approbationDate,
remark = remark ?: "", remark = remark ?: "",
company = companyService.getById(companyId) company = company
) )
}) })
} }
@Transactional @Transactional
override fun update(entity: RecipeUpdateDto): Recipe { override fun update(entity: RecipeUpdateDto): Recipe {
val persistedRecipe by lazy { getById(entity.id) } val persistedRecipe = getById(entity.id)
val name = entity.name
val company = persistedRecipe.company
if (name != null &&
name != persistedRecipe.name &&
existsByNameAndCompany(name, company)
) {
throw recipeNameAlreadyExistsForCompanyException(name, company)
}
return update(with(entity) { return update(with(entity) {
recipe( recipe(
@ -99,7 +124,7 @@ class RecipeServiceImpl(
sample = sample ?: persistedRecipe.sample, sample = sample ?: persistedRecipe.sample,
approbationDate = approbationDate ?: persistedRecipe.approbationDate, approbationDate = approbationDate ?: persistedRecipe.approbationDate,
remark = remark or persistedRecipe.remark, remark = remark or persistedRecipe.remark,
company = persistedRecipe.company, company = company,
mixes = persistedRecipe.mixes, mixes = persistedRecipe.mixes,
groupsInformation = updateGroupsInformation(persistedRecipe, entity) groupsInformation = updateGroupsInformation(persistedRecipe, entity)
) )
@ -137,7 +162,7 @@ class RecipeServiceImpl(
if (publicDataDto.notes != null) { if (publicDataDto.notes != null) {
val recipe = getById(publicDataDto.recipeId) val recipe = getById(publicDataDto.recipeId)
fun noteForGroup(group: EmployeeGroup) = fun noteForGroup(group: Group) =
publicDataDto.notes.firstOrNull { it.groupId == group.id }?.content publicDataDto.notes.firstOrNull { it.groupId == group.id }?.content
// Notes // Notes

View File

@ -2,6 +2,7 @@ package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.exception.RestException import dev.fyloz.colorrecipesexplorer.exception.RestException
import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.model.account.Group
import dev.fyloz.colorrecipesexplorer.repository.RecipeStepRepository import dev.fyloz.colorrecipesexplorer.repository.RecipeStepRepository
import dev.fyloz.colorrecipesexplorer.utils.findDuplicated import dev.fyloz.colorrecipesexplorer.utils.findDuplicated
import dev.fyloz.colorrecipesexplorer.utils.hasGaps import dev.fyloz.colorrecipesexplorer.utils.hasGaps
@ -81,8 +82,8 @@ class InvalidStepsPositionsException(
) )
class InvalidGroupStepsPositionsException( class InvalidGroupStepsPositionsException(
val group: EmployeeGroup, val group: Group,
val exception: InvalidStepsPositionsException val exception: InvalidStepsPositionsException
) : RestException( ) : RestException(
"invalid-groupinformation-recipestep-position", "invalid-groupinformation-recipestep-position",
"Invalid steps positions", "Invalid steps positions",

View File

@ -1,6 +1,12 @@
package dev.fyloz.colorrecipesexplorer.service.files package dev.fyloz.colorrecipesexplorer.service.touchupkit
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
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.AbstractExternalModelService
import dev.fyloz.colorrecipesexplorer.service.ExternalModelService
import dev.fyloz.colorrecipesexplorer.service.files.FileService
import dev.fyloz.colorrecipesexplorer.utils.* import dev.fyloz.colorrecipesexplorer.utils.*
import org.springframework.core.io.ByteArrayResource import org.springframework.core.io.ByteArrayResource
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@ -10,7 +16,8 @@ private const val TOUCH_UP_KIT_FILES_PATH = "pdf/touchupkits"
const val TOUCH_UP_TEXT_FR = "KIT DE RETOUCHE" const val TOUCH_UP_TEXT_FR = "KIT DE RETOUCHE"
const val TOUCH_UP_TEXT_EN = "TOUCH UP KIT" const val TOUCH_UP_TEXT_EN = "TOUCH UP KIT"
interface TouchUpKitService { interface TouchUpKitService :
ExternalModelService<TouchUpKit, TouchUpKitSaveDto, TouchUpKitUpdateDto, TouchUpKitOutputDto, TouchUpKitRepository> {
/** Generates and returns a [PdfDocument] for the given [job]. */ /** Generates and returns a [PdfDocument] for the given [job]. */
fun generateJobPdf(job: String): PdfDocument fun generateJobPdf(job: String): PdfDocument
@ -29,8 +36,45 @@ interface TouchUpKitService {
@Service @Service
class TouchUpKitServiceImpl( class TouchUpKitServiceImpl(
private val fileService: FileService, private val fileService: FileService,
private val creProperties: CreProperties touchUpKitRepository: TouchUpKitRepository,
) : TouchUpKitService { private val creProperties: CreProperties,
) : AbstractExternalModelService<TouchUpKit, TouchUpKitSaveDto, TouchUpKitUpdateDto, TouchUpKitOutputDto, TouchUpKitRepository>(
touchUpKitRepository
), TouchUpKitService {
override fun idNotFoundException(id: Long) = touchUpKitIdNotFoundException(id)
override fun idAlreadyExistsException(id: Long) = touchUpKitIdAlreadyExistsException(id)
override fun TouchUpKit.toOutput() = TouchUpKitOutputDto(
this.id!!,
this.project,
this.buggy,
this.company,
this.quantity,
this.shippingDate,
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,
finish = finish ?: persistedKit.finish,
material = material ?: persistedKit.material,
content = content?.map { touchUpKitProduct(it) }?.toSet() ?: persistedKit.content
)
})
}
override fun generateJobPdf(job: String) = pdf { override fun generateJobPdf(job: String) = pdf {
container { container {
centeredVertically = true centeredVertically = true
@ -75,4 +119,7 @@ class TouchUpKitServiceImpl(
private fun String.pdfDocumentPath() = private fun String.pdfDocumentPath() =
"$TOUCH_UP_KIT_FILES_PATH/$this.pdf" "$TOUCH_UP_KIT_FILES_PATH/$this.pdf"
private fun TouchUpKit.pdfUrl() =
"${creProperties.deploymentUrl}$TOUCH_UP_KIT_CONTROLLER_PATH/pdf?job=$project"
} }

View File

@ -21,7 +21,7 @@ entities.material-types.baseName=Base
databaseupdater.username=root databaseupdater.username=root
databaseupdater.password=pass databaseupdater.password=pass
# DEBUG # DEBUG
spring.jpa.show-sql=true spring.jpa.show-sql=false
# Do not modify # Do not modify
spring.messages.fallback-to-system-locale=true spring.messages.fallback-to-system-locale=true
spring.servlet.multipart.max-file-size=10MB spring.servlet.multipart.max-file-size=10MB
@ -30,4 +30,6 @@ spring.jpa.open-in-view=true
server.http2.enabled=true server.http2.enabled=true
server.error.whitelabel.enabled=false server.error.whitelabel.enabled=false
spring.h2.console.enabled=false spring.h2.console.enabled=false
spring.jackson.deserialization.fail-on-null-for-primitives=true
spring.jackson.default-property-inclusion=non_null
spring.profiles.active=@spring.profiles.active@ spring.profiles.active=@spring.profiles.active@

View File

@ -1 +0,0 @@
junit.jupiter.testinstance.lifecycle.default=per_class

View File

@ -4,15 +4,11 @@ import com.nhaarman.mockitokotlin2.*
import dev.fyloz.colorrecipesexplorer.config.defaultGroupCookieName import dev.fyloz.colorrecipesexplorer.config.defaultGroupCookieName
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.model.account.*
import dev.fyloz.colorrecipesexplorer.repository.EmployeeGroupRepository import dev.fyloz.colorrecipesexplorer.repository.UserRepository
import dev.fyloz.colorrecipesexplorer.repository.EmployeeRepository import dev.fyloz.colorrecipesexplorer.repository.GroupRepository
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.*
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.springframework.mock.web.MockHttpServletResponse import org.springframework.mock.web.MockHttpServletResponse
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import java.util.* import java.util.*
@ -22,24 +18,26 @@ import kotlin.test.assertEquals
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertNotNull import kotlin.test.assertNotNull
import kotlin.test.assertTrue import kotlin.test.assertTrue
import org.springframework.security.core.userdetails.User as SpringUser
class EmployeeServiceTest : @TestInstance(TestInstance.Lifecycle.PER_CLASS)
AbstractExternalModelServiceTest<Employee, EmployeeSaveDto, EmployeeUpdateDto, EmployeeService, EmployeeRepository>() { class UserServiceTest :
AbstractExternalModelServiceTest<User, UserSaveDto, UserUpdateDto, UserService, UserRepository>() {
private val passwordEncoder = BCryptPasswordEncoder() private val passwordEncoder = BCryptPasswordEncoder()
override val entity: Employee = employee(passwordEncoder, id = 0L) override val entity: User = user(passwordEncoder, id = 0L)
override val anotherEntity: Employee = employee(passwordEncoder, id = 1L) override val anotherEntity: User = user(passwordEncoder, id = 1L)
private val entityDefaultGroupUser = employee(passwordEncoder, id = 2L, isDefaultGroupUser = true) private val entityDefaultGroupUser = user(passwordEncoder, id = 2L, isDefaultGroupUser = true)
private val entitySystemUser = employee(passwordEncoder, id = 3L, isSystemUser = true) private val entitySystemUser = user(passwordEncoder, id = 3L, isSystemUser = true)
private val group = employeeGroup(id = 0L) private val group = group(id = 0L)
override val entitySaveDto: EmployeeSaveDto = spy(employeeSaveDto(passwordEncoder, id = 0L)) override val entitySaveDto: UserSaveDto = spy(userSaveDto(passwordEncoder, id = 0L))
override val entityUpdateDto: EmployeeUpdateDto = spy(employeeUpdateDto(id = 0L)) override val entityUpdateDto: UserUpdateDto = spy(userUpdateDto(id = 0L))
override val repository: EmployeeRepository = mock() override val repository: UserRepository = mock()
private val employeeGroupService: EmployeeGroupService = mock() private val groupService: GroupService = mock()
override val service: EmployeeService = spy(EmployeeServiceImpl(repository, employeeGroupService, passwordEncoder)) override val service: UserService = spy(UserServiceImpl(repository, groupService, passwordEncoder))
private val entitySaveDtoEmployee = Employee( private val entitySaveDtoUser = User(
entitySaveDto.id, entitySaveDto.id,
entitySaveDto.firstName, entitySaveDto.firstName,
entitySaveDto.lastName, entitySaveDto.lastName,
@ -52,14 +50,14 @@ class EmployeeServiceTest :
@AfterEach @AfterEach
override fun afterEach() { override fun afterEach() {
reset(employeeGroupService) reset(groupService)
super.afterEach() super.afterEach()
} }
// existsByFirstNameAndLastName() // existsByFirstNameAndLastName()
@Test @Test
fun `existsByFirstNameAndLastName() returns true when an employee with the given first name and last name exists`() { 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) whenever(repository.existsByFirstNameAndLastName(entity.firstName, entity.lastName)).doReturn(true)
val found = service.existsByFirstNameAndLastName(entity.firstName, entity.lastName) val found = service.existsByFirstNameAndLastName(entity.firstName, entity.lastName)
@ -68,7 +66,7 @@ class EmployeeServiceTest :
} }
@Test @Test
fun `existsByFirstNameAndLastName() returns false when no employee with the given first name and last name exists`() { 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) whenever(repository.existsByFirstNameAndLastName(entity.firstName, entity.lastName)).doReturn(false)
val found = service.existsByFirstNameAndLastName(entity.firstName, entity.lastName) val found = service.existsByFirstNameAndLastName(entity.firstName, entity.lastName)
@ -79,7 +77,7 @@ class EmployeeServiceTest :
// getById() // getById()
@Test @Test
fun `getById() throws NotFoundException when the corresponding employee is a default group user`() { fun `getById() throws NotFoundException when the corresponding user is a default group user`() {
whenever(repository.findById(entityDefaultGroupUser.id)).doReturn(Optional.of(entityDefaultGroupUser)) whenever(repository.findById(entityDefaultGroupUser.id)).doReturn(Optional.of(entityDefaultGroupUser))
assertThrows<NotFoundException> { assertThrows<NotFoundException> {
@ -92,7 +90,7 @@ class EmployeeServiceTest :
} }
@Test @Test
fun `getById() throws NotFoundException when the corresponding employee is a system user`() { fun `getById() throws NotFoundException when the corresponding user is a system user`() {
whenever(repository.findById(entitySystemUser.id)).doReturn(Optional.of(entitySystemUser)) whenever(repository.findById(entitySystemUser.id)).doReturn(Optional.of(entitySystemUser))
assertThrows<NotFoundException> { assertThrows<NotFoundException> {
@ -107,7 +105,7 @@ class EmployeeServiceTest :
// getByGroup() // getByGroup()
@Test @Test
fun `getByGroup() returns all the employees with the given group from the repository`() { fun `getByGroup() returns all the users with the given group from the repository`() {
whenever(repository.findAllByGroup(group)).doReturn(entityList) whenever(repository.findAllByGroup(group)).doReturn(entityList)
val found = service.getByGroup(group) val found = service.getByGroup(group)
@ -117,7 +115,7 @@ class EmployeeServiceTest :
} }
@Test @Test
fun `getByGroup() returns an empty list when there is no employee with the given group in the repository`() { fun `getByGroup() returns an empty list when there is no user with the given group in the repository`() {
whenever(repository.findAllByGroup(group)).doReturn(listOf()) whenever(repository.findAllByGroup(group)).doReturn(listOf())
val found = service.getByGroup(group) val found = service.getByGroup(group)
@ -128,10 +126,10 @@ class EmployeeServiceTest :
// getDefaultGroupUser() // getDefaultGroupUser()
@Test @Test
fun `getDefaultGroupUser() returns the default employee of the given group from the repository`() { fun `getDefaultGroupUser() returns the default user of the given group from the repository`() {
whenever(repository.findByIsDefaultGroupUserIsTrueAndGroupIs(group)).doReturn(entityDefaultGroupUser) whenever(repository.findByIsDefaultGroupUserIsTrueAndGroupIs(group)).doReturn(entityDefaultGroupUser)
val found = service.getDefaultGroupEmployee(group) val found = service.getDefaultGroupUser(group)
assertEquals(entityDefaultGroupUser, found) assertEquals(entityDefaultGroupUser, found)
} }
@ -166,13 +164,13 @@ class EmployeeServiceTest :
} }
@Test @Test
fun `save(dto) calls and returns save() with the created employee`() { fun `save(dto) calls and returns save() with the created user`() {
doReturn(entitySaveDtoEmployee).whenever(service).save(any<Employee>()) doReturn(entitySaveDtoUser).whenever(service).save(any<User>())
val found = service.save(entitySaveDto) val found = service.save(entitySaveDto)
verify(service).save(argThat<Employee> { this.id == entity.id && this.firstName == entity.firstName && this.lastName == entity.lastName }) verify(service).save(argThat<User> { this.id == entity.id && this.firstName == entity.firstName && this.lastName == entity.lastName })
assertEquals(entitySaveDtoEmployee, found) assertEquals(entitySaveDtoUser, found)
} }
// update() // update()
@ -182,7 +180,7 @@ class EmployeeServiceTest :
withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() }) withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() })
@Test @Test
fun `update() throws AlreadyExistsException when a different employee with the given first name and last name exists`() { 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( whenever(repository.findByFirstNameAndLastName(entity.firstName, entity.lastName)).doReturn(
entityDefaultGroupUser entityDefaultGroupUser
) )
@ -198,47 +196,48 @@ class EmployeeServiceTest :
} }
} }
class EmployeeGroupServiceTest : @TestInstance(TestInstance.Lifecycle.PER_CLASS)
AbstractExternalNamedModelServiceTest<EmployeeGroup, EmployeeGroupSaveDto, EmployeeGroupUpdateDto, EmployeeGroupService, EmployeeGroupRepository>() { class GroupServiceTest :
private val employeeService: EmployeeService = mock() AbstractExternalNamedModelServiceTest<Group, GroupSaveDto, GroupUpdateDto, GroupService, GroupRepository>() {
override val repository: EmployeeGroupRepository = mock() private val userService: UserService = mock()
override val service: EmployeeGroupServiceImpl = spy(EmployeeGroupServiceImpl(employeeService, repository)) override val repository: GroupRepository = mock()
override val service: GroupServiceImpl = spy(GroupServiceImpl(userService, repository))
override val entity: EmployeeGroup = employeeGroup(id = 0L, name = "group") override val entity: Group = group(id = 0L, name = "group")
override val anotherEntity: EmployeeGroup = employeeGroup(id = 1L, name = "another group") override val anotherEntity: Group = group(id = 1L, name = "another group")
override val entitySaveDto: EmployeeGroupSaveDto = spy(employeeGroupSaveDto(name = "group")) override val entitySaveDto: GroupSaveDto = spy(groupSaveDto(name = "group"))
override val entityUpdateDto: EmployeeGroupUpdateDto = spy(employeeGroupUpdateDto(id = 0L, name = "group")) override val entityUpdateDto: GroupUpdateDto = spy(groupUpdateDto(id = 0L, name = "group"))
override val entityWithEntityName: EmployeeGroup = employeeGroup(id = 2L, name = entity.name) override val entityWithEntityName: Group = group(id = 2L, name = entity.name)
private val groupEmployeeId = 1000000L + entity.id!! private val groupUserId = 1000000L + entity.id!!
private val groupEmployee = employee(BCryptPasswordEncoder(), id = groupEmployeeId, group = entity) private val groupUser = user(BCryptPasswordEncoder(), id = groupUserId, group = entity)
@BeforeEach @BeforeEach
override fun afterEach() { override fun afterEach() {
reset(employeeService) reset(userService)
super.afterEach() super.afterEach()
} }
// getEmployeesForGroup() // getUsersForGroup()
@Test @Test
fun `getEmployeesForGroup() returns all employees in the given group`() { fun `getUsersForGroup() returns all users in the given group`() {
val group = employeeGroup(id = 1L) val group = group(id = 1L)
doReturn(group).whenever(service).getById(group.id!!) doReturn(group).whenever(service).getById(group.id!!)
whenever(employeeService.getByGroup(group)).doReturn(listOf(groupEmployee)) whenever(userService.getByGroup(group)).doReturn(listOf(groupUser))
val found = service.getEmployeesForGroup(group.id!!) val found = service.getUsersForGroup(group.id!!)
assertTrue(found.contains(groupEmployee)) assertTrue(found.contains(groupUser))
assertTrue(found.size == 1) assertTrue(found.size == 1)
} }
@Test @Test
fun `getEmployeesForGroup() returns empty collection when the given group contains any employee`() { fun `getUsersForGroup() returns empty collection when the given group contains any user`() {
doReturn(entity).whenever(service).getById(entity.id!!) doReturn(entity).whenever(service).getById(entity.id!!)
val found = service.getEmployeesForGroup(entity.id!!) val found = service.getUsersForGroup(entity.id!!)
assertTrue(found.isEmpty()) assertTrue(found.isEmpty())
} }
@ -247,11 +246,11 @@ class EmployeeGroupServiceTest :
@Test @Test
fun `getRequestDefaultGroup() returns the group contained in the cookie of the HTTP request`() { fun `getRequestDefaultGroup() returns the group contained in the cookie of the HTTP request`() {
val cookies: Array<Cookie> = arrayOf(Cookie(defaultGroupCookieName, groupEmployeeId.toString())) val cookies: Array<Cookie> = arrayOf(Cookie(defaultGroupCookieName, groupUserId.toString()))
val request: HttpServletRequest = mock() val request: HttpServletRequest = mock()
whenever(request.cookies).doReturn(cookies) whenever(request.cookies).doReturn(cookies)
whenever(employeeService.getById(eq(groupEmployeeId), any(), any())).doReturn(groupEmployee) whenever(userService.getById(eq(groupUserId), any(), any())).doReturn(groupUser)
val found = service.getRequestDefaultGroup(request) val found = service.getRequestDefaultGroup(request)
@ -273,7 +272,7 @@ class EmployeeGroupServiceTest :
fun `setResponseDefaultGroup() the default group cookie has been added to the given HTTP response with the given group id`() { fun `setResponseDefaultGroup() the default group cookie has been added to the given HTTP response with the given group id`() {
val response = MockHttpServletResponse() val response = MockHttpServletResponse()
whenever(employeeService.getDefaultGroupEmployee(entity)).doReturn(groupEmployee) whenever(userService.getDefaultGroupUser(entity)).doReturn(groupUser)
doReturn(entity).whenever(service).getById(entity.id!!) doReturn(entity).whenever(service).getById(entity.id!!)
service.setResponseDefaultGroup(entity.id!!, response) service.setResponseDefaultGroup(entity.id!!, response)
@ -281,7 +280,7 @@ class EmployeeGroupServiceTest :
assertNotNull(found) assertNotNull(found)
assertEquals(defaultGroupCookieName, found.name) assertEquals(defaultGroupCookieName, found.name)
assertEquals(groupEmployeeId.toString(), found.value) assertEquals(groupUserId.toString(), found.value)
assertEquals(defaultGroupCookieMaxAge, found.maxAge) assertEquals(defaultGroupCookieMaxAge, found.maxAge)
assertTrue(found.isHttpOnly) assertTrue(found.isHttpOnly)
assertTrue(found.secure) assertTrue(found.secure)
@ -301,48 +300,49 @@ class EmployeeGroupServiceTest :
withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() }) withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() })
} }
class EmployeeUserDetailsServiceTest { @TestInstance(TestInstance.Lifecycle.PER_CLASS)
private val employeeService: EmployeeService = mock() class UserUserDetailsServiceTest {
private val service = spy(EmployeeUserDetailsServiceImpl(employeeService)) private val userService: UserService = mock()
private val service = spy(CreUserDetailsServiceImpl(userService))
private val employee = employee(id = 0L) private val user = user(id = 0L)
@BeforeEach @BeforeEach
fun beforeEach() { fun beforeEach() {
reset(employeeService, service) reset(userService, service)
} }
// loadUserByUsername() // loadUserByUsername()
@Test @Test
fun `loadUserByUsername() calls loadUserByEmployeeId() with the given username as an id`() { fun `loadUserByUsername() calls loadUserByUserId() with the given username as an id`() {
whenever(employeeService.getById(eq(employee.id), any(), any())).doReturn(employee) whenever(userService.getById(eq(user.id), any(), any())).doReturn(user)
doReturn(User(employee.id.toString(), employee.password, listOf())).whenever(service) doReturn(SpringUser(user.id.toString(), user.password, listOf())).whenever(service)
.loadUserByEmployeeId(employee.id) .loadUserById(user.id)
service.loadUserByUsername(employee.id.toString()) service.loadUserByUsername(user.id.toString())
verify(service).loadUserByEmployeeId(eq(employee.id), any()) verify(service).loadUserById(eq(user.id), any())
} }
@Test @Test
fun `loadUserByUsername() throws UsernameNotFoundException when no employee with the given id exists`() { fun `loadUserByUsername() throws UsernameNotFoundException when no user with the given id exists`() {
whenever(employeeService.getById(eq(employee.id), any(), any())).doThrow( whenever(userService.getById(eq(user.id), any(), any())).doThrow(
employeeIdNotFoundException(employee.id) userIdNotFoundException(user.id)
) )
assertThrows<UsernameNotFoundException> { service.loadUserByUsername(employee.id.toString()) } assertThrows<UsernameNotFoundException> { service.loadUserByUsername(user.id.toString()) }
} }
// loadUserByEmployeeId // loadUserByUserId
@Test @Test
fun `loadUserByEmployeeId() returns an User corresponding to the employee with the given id`() { fun `loadUserByUserId() returns an User corresponding to the user with the given id`() {
whenever(employeeService.getById(eq(employee.id), any(), any())).doReturn(employee) whenever(userService.getById(eq(user.id), any(), any())).doReturn(user)
val found = service.loadUserByEmployeeId(employee.id) val found = service.loadUserById(user.id)
assertEquals(employee.id, found.username.toLong()) assertEquals(user.id, found.username.toLong())
assertEquals(employee.password, found.password) assertEquals(user.password, found.password)
} }
} }

View File

@ -5,9 +5,11 @@ import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.CompanyRepository import dev.fyloz.colorrecipesexplorer.repository.CompanyRepository
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class CompanyServiceTest : class CompanyServiceTest :
AbstractExternalNamedModelServiceTest<Company, CompanySaveDto, CompanyUpdateDto, CompanyService, CompanyRepository>() { AbstractExternalNamedModelServiceTest<Company, CompanySaveDto, CompanyUpdateDto, CompanyService, CompanyRepository>() {
private val recipeService: RecipeService = mock() private val recipeService: RecipeService = mock()

View File

@ -4,10 +4,12 @@ import com.nhaarman.mockitokotlin2.*
import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.model.*
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class InventoryServiceTest { class InventoryServiceTest {
private val materialService: MaterialService = mock() private val materialService: MaterialService = mock()
private val mixService: MixService = mock() private val mixService: MixService = mock()

View File

@ -7,12 +7,14 @@ import dev.fyloz.colorrecipesexplorer.repository.MaterialRepository
import dev.fyloz.colorrecipesexplorer.service.files.FileService import dev.fyloz.colorrecipesexplorer.service.files.FileService
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
import org.springframework.mock.web.MockMultipartFile import org.springframework.mock.web.MockMultipartFile
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MaterialServiceTest : class MaterialServiceTest :
AbstractExternalNamedModelServiceTest<Material, MaterialSaveDto, MaterialUpdateDto, MaterialService, MaterialRepository>() { AbstractExternalNamedModelServiceTest<Material, MaterialSaveDto, MaterialUpdateDto, MaterialService, MaterialRepository>() {
override val repository: MaterialRepository = mock() override val repository: MaterialRepository = mock()

View File

@ -7,11 +7,13 @@ import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.MaterialTypeRepository import dev.fyloz.colorrecipesexplorer.repository.MaterialTypeRepository
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MaterialTypeServiceTest : class MaterialTypeServiceTest :
AbstractExternalNamedModelServiceTest<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialTypeService, MaterialTypeRepository>() { AbstractExternalNamedModelServiceTest<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialTypeService, MaterialTypeRepository>() {
override val repository: MaterialTypeRepository = mock() override val repository: MaterialTypeRepository = mock()

View File

@ -4,12 +4,14 @@ import com.nhaarman.mockitokotlin2.*
import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.MixMaterialRepository import dev.fyloz.colorrecipesexplorer.repository.MixMaterialRepository
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertNotEquals import kotlin.test.assertNotEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MixMaterialServiceTest : AbstractModelServiceTest<MixMaterial, MixMaterialService, MixMaterialRepository>() { class MixMaterialServiceTest : AbstractModelServiceTest<MixMaterial, MixMaterialService, MixMaterialRepository>() {
override val repository: MixMaterialRepository = mock() override val repository: MixMaterialRepository = mock()
private val materialService: MaterialService = mock() private val materialService: MaterialService = mock()

View File

@ -5,9 +5,11 @@ import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.MixRepository import dev.fyloz.colorrecipesexplorer.repository.MixRepository
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MixServiceTest : AbstractExternalModelServiceTest<Mix, MixSaveDto, MixUpdateDto, MixService, MixRepository>() { class MixServiceTest : AbstractExternalModelServiceTest<Mix, MixSaveDto, MixUpdateDto, MixService, MixRepository>() {
override val repository: MixRepository = mock() override val repository: MixRepository = mock()
private val recipeService: RecipeService = mock() private val recipeService: RecipeService = mock()

View File

@ -7,10 +7,12 @@ import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.MixTypeRepository import dev.fyloz.colorrecipesexplorer.repository.MixTypeRepository
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MixTypeServiceTest : AbstractNamedModelServiceTest<MixType, MixTypeService, MixTypeRepository>() { class MixTypeServiceTest : AbstractNamedModelServiceTest<MixType, MixTypeService, MixTypeRepository>() {
override val repository: MixTypeRepository = mock() override val repository: MixTypeRepository = mock()
private val materialService: MaterialService = mock() private val materialService: MaterialService = mock()

View File

@ -1,12 +1,16 @@
package dev.fyloz.colorrecipesexplorer.service package dev.fyloz.colorrecipesexplorer.service
import com.nhaarman.mockitokotlin2.* import com.nhaarman.mockitokotlin2.*
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.model.account.group
import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository
import dev.fyloz.colorrecipesexplorer.service.files.FileService import dev.fyloz.colorrecipesexplorer.service.files.FileService
import io.mockk.* import io.mockk.*
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test 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.mock.web.MockMultipartFile
import org.springframework.web.multipart.MultipartFile import org.springframework.web.multipart.MultipartFile
import java.io.File import java.io.File
@ -14,12 +18,13 @@ import kotlin.test.assertEquals
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class RecipeServiceTest : class RecipeServiceTest :
AbstractExternalModelServiceTest<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeService, RecipeRepository>() { AbstractExternalModelServiceTest<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeService, RecipeRepository>() {
override val repository: RecipeRepository = mock() override val repository: RecipeRepository = mock()
private val companyService: CompanyService = mock() private val companyService: CompanyService = mock()
private val mixService: MixService = mock() private val mixService: MixService = mock()
private val groupService: EmployeeGroupService = mock() private val groupService: GroupService = mock()
private val recipeStepService: RecipeStepService = mock() private val recipeStepService: RecipeStepService = mock()
override val service: RecipeService = override val service: RecipeService =
spy(RecipeServiceImpl(repository, companyService, mixService, recipeStepService, groupService, mock())) spy(RecipeServiceImpl(repository, companyService, mixService, recipeStepService, groupService, mock()))
@ -56,6 +61,32 @@ class RecipeServiceTest :
assertFalse(found) 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 = service.existsByNameAndCompany(entity.name, company)
assertEquals(it, exists)
}
}
// 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 = service.getAllByName(entity.name)
assertEquals(recipes, found)
}
// getAllByCompany() // getAllByCompany()
@Test @Test
@ -73,14 +104,40 @@ class RecipeServiceTest :
@Test @Test
override fun `save(dto) calls and returns save() with the created entity`() { override fun `save(dto) calls and returns save() with the created entity`() {
whenever(companyService.getById(company.id!!)).doReturn(company) 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 } }) withBaseSaveDtoTest(entity, entitySaveDto, service, { 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)
with(assertThrows<AlreadyExistsException> { service.save(entitySaveDto) }) {
this.assertErrorCode("company-name")
}
}
// update() // update()
@Test @Test
override fun `update(dto) calls and returns update() with the created entity`() = 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() }) withBaseUpdateDtoTest(entity, entityUpdateDto, service, { 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(name).whenever(entityUpdateDto).name
with(assertThrows<AlreadyExistsException> { service.update(entityUpdateDto) }) {
this.assertErrorCode("company-name")
}
}
// updatePublicData() // updatePublicData()
@ -88,9 +145,9 @@ class RecipeServiceTest :
fun `updatePublicData() updates the notes of a recipe groups information according to the RecipePublicDataDto`() { fun `updatePublicData() updates the notes of a recipe groups information according to the RecipePublicDataDto`() {
val recipe = recipe( val recipe = recipe(
id = 0L, groupsInformation = setOf( id = 0L, groupsInformation = setOf(
recipeGroupInformation(id = 0L, group = employeeGroup(id = 1L), note = "Old note"), recipeGroupInformation(id = 0L, group = group(id = 1L), note = "Old note"),
recipeGroupInformation(id = 1L, group = employeeGroup(id = 2L), note = "Another note"), recipeGroupInformation(id = 1L, group = group(id = 2L), note = "Another note"),
recipeGroupInformation(id = 2L, group = employeeGroup(id = 3L), note = "Up to date note") recipeGroupInformation(id = 2L, group = group(id = 3L), note = "Up to date note")
) )
) )
val notes = setOf( val notes = setOf(

View File

@ -2,11 +2,14 @@ package dev.fyloz.colorrecipesexplorer.service
import com.nhaarman.mockitokotlin2.* import com.nhaarman.mockitokotlin2.*
import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.model.account.group
import dev.fyloz.colorrecipesexplorer.repository.RecipeStepRepository import dev.fyloz.colorrecipesexplorer.repository.RecipeStepRepository
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
import kotlin.test.assertTrue import kotlin.test.assertTrue
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class RecipeStepServiceTest : class RecipeStepServiceTest :
AbstractModelServiceTest<RecipeStep, RecipeStepService, RecipeStepRepository>() { AbstractModelServiceTest<RecipeStep, RecipeStepService, RecipeStepRepository>() {
override val repository: RecipeStepRepository = mock() override val repository: RecipeStepRepository = mock()
@ -80,7 +83,7 @@ class RecipeStepServiceTest :
private fun withGroupInformation(steps: MutableSet<RecipeStep>? = null, test: RecipeGroupInformation.() -> Unit) { private fun withGroupInformation(steps: MutableSet<RecipeStep>? = null, test: RecipeGroupInformation.() -> Unit) {
recipeGroupInformation( recipeGroupInformation(
group = employeeGroup(id = 0L), group = group(id = 0L),
steps = steps ?: mutableSetOf( steps = steps ?: mutableSetOf(
recipeStep(id = 0L, position = 1), recipeStep(id = 0L, position = 1),
recipeStep(id = 1L, position = 2), recipeStep(id = 1L, position = 2),

View File

@ -1,6 +1,10 @@
package dev.fyloz.colorrecipesexplorer.service.files package dev.fyloz.colorrecipesexplorer.service.files
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
import dev.fyloz.colorrecipesexplorer.repository.TouchUpKitRepository
import dev.fyloz.colorrecipesexplorer.service.touchupkit.TOUCH_UP_TEXT_EN
import dev.fyloz.colorrecipesexplorer.service.touchupkit.TOUCH_UP_TEXT_FR
import dev.fyloz.colorrecipesexplorer.service.touchupkit.TouchUpKitServiceImpl
import dev.fyloz.colorrecipesexplorer.utils.PdfDocument import dev.fyloz.colorrecipesexplorer.utils.PdfDocument
import dev.fyloz.colorrecipesexplorer.utils.toByteArrayResource import dev.fyloz.colorrecipesexplorer.utils.toByteArrayResource
import io.mockk.* import io.mockk.*
@ -10,13 +14,14 @@ import org.springframework.core.io.ByteArrayResource
import kotlin.test.assertEquals import kotlin.test.assertEquals
private class TouchUpKitServiceTestContext { private class TouchUpKitServiceTestContext {
val touchUpKitRepository = mockk<TouchUpKitRepository>()
val fileService = mockk<FileService> { val fileService = mockk<FileService> {
every { write(any<ByteArrayResource>(), any(), any()) } just Runs every { write(any<ByteArrayResource>(), any(), any()) } just Runs
} }
val creProperties = mockk<CreProperties> { val creProperties = mockk<CreProperties> {
every { cacheGeneratedFiles } returns false every { cacheGeneratedFiles } returns false
} }
val touchUpKitService = spyk(TouchUpKitServiceImpl(fileService, creProperties)) val touchUpKitService = spyk(TouchUpKitServiceImpl(fileService, touchUpKitRepository, creProperties))
val pdfDocumentData = mockk<ByteArrayResource>() val pdfDocumentData = mockk<ByteArrayResource>()
val pdfDocument = mockk<PdfDocument> { val pdfDocument = mockk<PdfDocument> {
mockkStatic(PdfDocument::toByteArrayResource) mockkStatic(PdfDocument::toByteArrayResource)