From 854d3c2c3e288b8758a33ebb9112ebf72410809d Mon Sep 17 00:00:00 2001 From: FyloZ Date: Thu, 7 Jan 2021 17:11:21 -0500 Subject: [PATCH] =?UTF-8?q?Ajout=20du=20support=20pour=20les=20=C3=A9tapes?= =?UTF-8?q?=20des=20recettes=20dans=20l'API=20REST.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gradle/buildOutputCleanup/outputFiles.bin | Bin 18911 -> 19037 bytes .../model/RecipeStep.java | 44 --- .../repository/StepRepository.java | 13 - .../service/model/RecipeService.java | 4 +- ...ervice.java => RecipeStepJavaService.java} | 10 +- .../config/WebSecurityConfig.kt | 183 +++++++---- .../model/AccountModel.kt | 287 ++++++++++-------- .../trial/colorrecipesexplorer/model/Model.kt | 1 + .../colorrecipesexplorer/model/RecipeStep.kt | 76 +++++ .../repository/RecipeStepRepository.kt | 8 + .../rest/CompanyController.kt | 6 +- .../rest/InventoryController.kt | 22 +- .../rest/MaterialTypeController.kt | 5 +- .../rest/RestApiController.kt | 1 + .../service/RecipeStepService.kt | 29 ++ .../repository/RecipeStepRepositoryTest.kt | 13 + .../service/RecipeStepServiceTest.kt | 46 +++ 17 files changed, 481 insertions(+), 267 deletions(-) delete mode 100644 src/main/java/dev/fyloz/trial/colorrecipesexplorer/model/RecipeStep.java delete mode 100644 src/main/java/dev/fyloz/trial/colorrecipesexplorer/repository/StepRepository.java rename src/main/java/dev/fyloz/trial/colorrecipesexplorer/service/model/{StepService.java => RecipeStepJavaService.java} (78%) create mode 100644 src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/model/RecipeStep.kt create mode 100644 src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/repository/RecipeStepRepository.kt create mode 100644 src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/service/RecipeStepService.kt create mode 100644 src/test/kotlin/dev/fyloz/trial/colorrecipesexplorer/repository/RecipeStepRepositoryTest.kt create mode 100644 src/test/kotlin/dev/fyloz/trial/colorrecipesexplorer/service/RecipeStepServiceTest.kt diff --git a/.gradle/buildOutputCleanup/outputFiles.bin b/.gradle/buildOutputCleanup/outputFiles.bin index 915ef22023f7b251beb479c79cd92366fb8f79c3..108fe023fa417b658aec1486713c09131641462f 100644 GIT binary patch delta 220 zcmcaVnepxv#tkMCnsV*GrtjMRnNxrP3_QyJL&1clYcE}vnPd+U_fntiDxn}SEjnou zYtz3{sG#@cScw3Qvz?DswH@lGKm*dk delta 67 zcmcaRh4KDm#tkMCg5vGJrtjMRnNxrP3_Qvw$4V$no+{z7nOD++adNDb#N=KnkI8SP Vlo>5ID*h1PXz)OEv!lm6MgS!V7fk>F diff --git a/src/main/java/dev/fyloz/trial/colorrecipesexplorer/model/RecipeStep.java b/src/main/java/dev/fyloz/trial/colorrecipesexplorer/model/RecipeStep.java deleted file mode 100644 index a88c8c3..0000000 --- a/src/main/java/dev/fyloz/trial/colorrecipesexplorer/model/RecipeStep.java +++ /dev/null @@ -1,44 +0,0 @@ -package dev.fyloz.trial.colorrecipesexplorer.model; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import lombok.*; -import org.jetbrains.annotations.Nullable; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Objects; - -@Entity -@Data -@RequiredArgsConstructor -@NoArgsConstructor -@AllArgsConstructor -public class RecipeStep implements Model { - - @Id - @GeneratedValue(strategy = GenerationType.SEQUENCE) - private Long id; - - @NonNull - @JsonIgnore - @ManyToOne - private Recipe recipe; - - @NonNull - @NotNull - private String message; - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - RecipeStep that = (RecipeStep) o; - return Objects.equals(recipe, that.recipe) && - Objects.equals(message, that.message); - } - - @Override - public int hashCode() { - return Objects.hash(recipe, message); - } -} diff --git a/src/main/java/dev/fyloz/trial/colorrecipesexplorer/repository/StepRepository.java b/src/main/java/dev/fyloz/trial/colorrecipesexplorer/repository/StepRepository.java deleted file mode 100644 index 31e10f8..0000000 --- a/src/main/java/dev/fyloz/trial/colorrecipesexplorer/repository/StepRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package dev.fyloz.trial.colorrecipesexplorer.repository; - -import dev.fyloz.trial.colorrecipesexplorer.model.Recipe; -import dev.fyloz.trial.colorrecipesexplorer.model.RecipeStep; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.List; - -public interface StepRepository extends JpaRepository { - - List findAllByRecipe(Recipe recipe); - -} diff --git a/src/main/java/dev/fyloz/trial/colorrecipesexplorer/service/model/RecipeService.java b/src/main/java/dev/fyloz/trial/colorrecipesexplorer/service/model/RecipeService.java index 1fe4237..9a5575e 100644 --- a/src/main/java/dev/fyloz/trial/colorrecipesexplorer/service/model/RecipeService.java +++ b/src/main/java/dev/fyloz/trial/colorrecipesexplorer/service/model/RecipeService.java @@ -21,7 +21,7 @@ public class RecipeService extends AbstractJavaService private CompanyJavaService companyService; private MixService mixService; - private StepService stepService; + private RecipeStepJavaService stepService; private ImagesService imagesService; public RecipeService() { @@ -44,7 +44,7 @@ public class RecipeService extends AbstractJavaService } @Autowired - public void setStepService(StepService stepService) { + public void setStepService(RecipeStepJavaService stepService) { this.stepService = stepService; } diff --git a/src/main/java/dev/fyloz/trial/colorrecipesexplorer/service/model/StepService.java b/src/main/java/dev/fyloz/trial/colorrecipesexplorer/service/model/RecipeStepJavaService.java similarity index 78% rename from src/main/java/dev/fyloz/trial/colorrecipesexplorer/service/model/StepService.java rename to src/main/java/dev/fyloz/trial/colorrecipesexplorer/service/model/RecipeStepJavaService.java index 995f77a..6337394 100644 --- a/src/main/java/dev/fyloz/trial/colorrecipesexplorer/service/model/StepService.java +++ b/src/main/java/dev/fyloz/trial/colorrecipesexplorer/service/model/RecipeStepJavaService.java @@ -2,8 +2,8 @@ package dev.fyloz.trial.colorrecipesexplorer.service.model; import dev.fyloz.trial.colorrecipesexplorer.model.Recipe; import dev.fyloz.trial.colorrecipesexplorer.model.RecipeStep; +import dev.fyloz.trial.colorrecipesexplorer.repository.RecipeStepRepository; import dev.fyloz.trial.colorrecipesexplorer.service.AbstractJavaService; -import dev.fyloz.trial.colorrecipesexplorer.repository.StepRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -12,15 +12,15 @@ import java.util.List; import java.util.stream.Collectors; @Service -public class StepService extends AbstractJavaService { +public class RecipeStepJavaService extends AbstractJavaService { - public StepService() { + public RecipeStepJavaService() { super(RecipeStep.class); } @Autowired - public void setStepDao(StepRepository stepRepository) { - this.repository = stepRepository; + public void setStepDao(RecipeStepRepository recipeStepRepository) { + this.repository = recipeStepRepository; } /** diff --git a/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/config/WebSecurityConfig.kt b/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/config/WebSecurityConfig.kt index 81bdd6f..49c7fd4 100644 --- a/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/config/WebSecurityConfig.kt +++ b/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/config/WebSecurityConfig.kt @@ -53,9 +53,10 @@ import javax.servlet.http.HttpServletResponse @EnableGlobalMethodSecurity(prePostEnabled = true) @EnableConfigurationProperties(SecurityConfigurationProperties::class) class WebSecurityConfig( - val restAuthenticationEntryPoint: RestAuthenticationEntryPoint, - val securityConfigurationProperties: SecurityConfigurationProperties, - val logger: Logger) : WebSecurityConfigurerAdapter() { + val restAuthenticationEntryPoint: RestAuthenticationEntryPoint, + val securityConfigurationProperties: SecurityConfigurationProperties, + val logger: Logger +) : WebSecurityConfigurerAdapter() { @Autowired private lateinit var userDetailsService: EmployeeUserDetailsServiceImpl @@ -82,12 +83,12 @@ class WebSecurityConfig( registerCorsConfiguration("/**", CorsConfiguration().apply { allowedOrigins = listOf("http://localhost:4200") // Angular development server allowedMethods = listOf( - HttpMethod.GET.name, - HttpMethod.POST.name, - HttpMethod.PUT.name, - HttpMethod.DELETE.name, - HttpMethod.OPTIONS.name, - HttpMethod.HEAD.name + HttpMethod.GET.name, + HttpMethod.POST.name, + HttpMethod.PUT.name, + HttpMethod.DELETE.name, + HttpMethod.OPTIONS.name, + HttpMethod.HEAD.name ) allowCredentials = true }.applyPermitDefaultValues()) @@ -96,20 +97,27 @@ class WebSecurityConfig( @PostConstruct fun createSystemUsers() { - fun createUser(credentials: SecurityConfigurationProperties.SystemUserCredentials?, firstName: String, lastName: String, permissions: List) { + fun createUser( + credentials: SecurityConfigurationProperties.SystemUserCredentials?, + firstName: String, + lastName: String, + permissions: List + ) { Assert.notNull(credentials, "No root user has been defined.") credentials!! Assert.notNull(credentials.id, "The root user has no identifier defined.") Assert.notNull(credentials.password, "The root user has no password defined.") if (!employeeService.existsById(credentials.id!!)) { - employeeService.save(Employee( + employeeService.save( + Employee( id = credentials.id!!, firstName = firstName, lastName = lastName, password = passwordEncoder().encode(credentials.password!!), isSystemUser = true, permissions = permissions.toMutableSet() - )) + ) + ) } } @@ -130,27 +138,43 @@ class WebSecurityConfig( } http - .cors() - .and() - .headers().frameOptions().disable() - .and() - .csrf().disable() - .authorizeRequests() - .antMatchers(HttpMethod.GET, "/").permitAll() - .antMatchers("/api/login").permitAll() - .antMatchers("/api/employee/logout").permitAll() - .antMatchers(HttpMethod.GET, "/api/employee/current").authenticated() - .generateAuthorizations() - .and() - .addFilter(JwtAuthenticationFilter(authenticationManager(), employeeService, securityConfigurationProperties)) - .addFilter(JwtAuthorizationFilter(userDetailsService, securityConfigurationProperties, authenticationManager())) - .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .cors() + .and() + .headers().frameOptions().disable() + .and() + .csrf().disable() + .authorizeRequests() + .antMatchers(HttpMethod.GET, "/").permitAll() + .antMatchers("/api/login").permitAll() + .antMatchers("/api/employee/logout").permitAll() + .antMatchers(HttpMethod.GET, "/api/employee/current").authenticated() + .generateAuthorizations() + .and() + .addFilter( + JwtAuthenticationFilter( + authenticationManager(), + employeeService, + securityConfigurationProperties + ) + ) + .addFilter( + JwtAuthorizationFilter( + userDetailsService, + securityConfigurationProperties, + authenticationManager() + ) + ) + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) } } @Component class RestAuthenticationEntryPoint : AuthenticationEntryPoint { - override fun commence(request: HttpServletRequest, response: HttpServletResponse, authException: AuthenticationException) = response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized") + override fun commence( + request: HttpServletRequest, + response: HttpServletResponse, + authException: AuthenticationException + ) = response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized") } class CorsFilter : Filter { @@ -172,9 +196,9 @@ const val defaultGroupCookieName = "Default-Group" val blacklistedJwtTokens = mutableListOf() class JwtAuthenticationFilter( - val authManager: AuthenticationManager, - val employeeService: EmployeeServiceImpl, - val securityConfigurationProperties: SecurityConfigurationProperties + val authManager: AuthenticationManager, + val employeeService: EmployeeServiceImpl, + val securityConfigurationProperties: SecurityConfigurationProperties ) : UsernamePasswordAuthenticationFilter() { init { setFilterProcessesUrl("/api/login") @@ -185,7 +209,12 @@ class JwtAuthenticationFilter( return authManager.authenticate(UsernamePasswordAuthenticationToken(loginRequest.id, loginRequest.password)) } - override fun successfulAuthentication(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain, authResult: Authentication) { + override fun successfulAuthentication( + request: HttpServletRequest, + response: HttpServletResponse, + chain: FilterChain, + authResult: Authentication + ) { val jwtSecret = securityConfigurationProperties.jwtSecret val jwtDuration = securityConfigurationProperties.jwtDuration Assert.notNull(jwtSecret, "No JWT secret has been defined.") @@ -195,25 +224,29 @@ class JwtAuthenticationFilter( val expirationMs = System.currentTimeMillis() + jwtDuration!! val expirationDate = Date(expirationMs) val token = Jwts.builder() - .setSubject(employeeId) - .setExpiration(expirationDate) - .signWith(SignatureAlgorithm.HS512, jwtSecret!!.toByteArray()) - .compact() + .setSubject(employeeId) + .setExpiration(expirationDate) + .signWith(SignatureAlgorithm.HS512, jwtSecret!!.toByteArray()) + .compact() response.addHeader("Access-Control-Expose-Headers", "X-Authentication-Expiration") - response.addHeader("Set-Cookie", "$authorizationCookieName=Bearer$token; Max-Age=${jwtDuration / 1000}; HttpOnly; Secure; SameSite=strict") + response.addHeader( + "Set-Cookie", + "$authorizationCookieName=Bearer$token; Max-Age=${jwtDuration / 1000}; HttpOnly; Secure; SameSite=strict" + ) response.addHeader(authorizationCookieName, "Bearer $token") response.addHeader("X-Authentication-Expiration", "$expirationMs") } } class JwtAuthorizationFilter( - val userDetailsService: EmployeeUserDetailsServiceImpl, - val securityConfigurationProperties: SecurityConfigurationProperties, - authenticationManager: AuthenticationManager + val userDetailsService: EmployeeUserDetailsServiceImpl, + val securityConfigurationProperties: SecurityConfigurationProperties, + authenticationManager: AuthenticationManager ) : BasicAuthenticationFilter(authenticationManager) { override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) { val authorizationCookie = WebUtils.getCookie(request, authorizationCookieName) - val authorizationValue = if (authorizationCookie != null) authorizationCookie.value else request.getHeader(authorizationCookieName) + val authorizationValue = + if (authorizationCookie != null) authorizationCookie.value else request.getHeader(authorizationCookieName) if (authorizationValue != null && authorizationValue.startsWith("Bearer") && authorizationValue !in blacklistedJwtTokens) { val authenticationToken = getAuthentication(authorizationValue) SecurityContextHolder.getContext().authentication = authenticationToken @@ -231,10 +264,10 @@ class JwtAuthorizationFilter( val jwtSecret = securityConfigurationProperties.jwtSecret Assert.notNull(jwtSecret, "No JWT secret has been defined.") val employeeId = Jwts.parser() - .setSigningKey(jwtSecret!!.toByteArray()) - .parseClaimsJws(token.replace("Bearer", "")) - .body - .subject + .setSigningKey(jwtSecret!!.toByteArray()) + .parseClaimsJws(token.replace("Bearer", "")) + .body + .subject return if (employeeId != null) getAuthenticationToken(employeeId) else null } @@ -254,47 +287,71 @@ class SecurityConfigurationProperties { } private enum class ControllerAuthorizations( - val antMatcher: String, - val permissions: Map + val antMatcher: String, + val permissions: Map ) { - MATERIALS("/api/material/**", mapOf( + MATERIALS( + "/api/material/**", mapOf( HttpMethod.GET to EmployeePermission.VIEW_MATERIAL, HttpMethod.POST to EmployeePermission.EDIT_MATERIAL, HttpMethod.PUT to EmployeePermission.EDIT_MATERIAL, HttpMethod.DELETE to EmployeePermission.REMOVE_MATERIAL - )), - MATERIAL_TYPES("/api/materialtype/**", mapOf( + ) + ), + MATERIAL_TYPES( + "/api/materialtype/**", mapOf( HttpMethod.GET to EmployeePermission.VIEW_MATERIAL_TYPE, HttpMethod.POST to EmployeePermission.EDIT_MATERIAL_TYPE, HttpMethod.PUT to EmployeePermission.EDIT_MATERIAL_TYPE, HttpMethod.DELETE to EmployeePermission.REMOVE_MATERIAL_TYPE - )), - COMPANY("/api/company/**", mapOf( + ) + ), + COMPANY( + "/api/company/**", mapOf( HttpMethod.GET to EmployeePermission.VIEW_COMPANY, HttpMethod.POST to EmployeePermission.EDIT_COMPANY, HttpMethod.PUT to EmployeePermission.EDIT_COMPANY, HttpMethod.DELETE to EmployeePermission.REMOVE_COMPANY - )), - SET_BROWSER_DEFAULT_GROUP("/api/employee/group/default/**", mapOf( + ) + ), + RECIPE_STEP( + "/api/recipe/**", mapOf( + HttpMethod.GET to EmployeePermission.VIEW_RECIPE, + HttpMethod.POST to EmployeePermission.EDIT_RECIPE, + HttpMethod.PUT to EmployeePermission.EDIT_RECIPE, + HttpMethod.DELETE to EmployeePermission.REMOVE_RECIPE + ) + ), + SET_BROWSER_DEFAULT_GROUP( + "/api/employee/group/default/**", mapOf( HttpMethod.GET to EmployeePermission.VIEW_EMPLOYEE_GROUP, HttpMethod.POST to EmployeePermission.SET_BROWSER_DEFAULT_GROUP - )), - EMPLOYEES_FOR_GROUP("/api/employee/group/*/employees", mapOf( + ) + ), + EMPLOYEES_FOR_GROUP( + "/api/employee/group/*/employees", mapOf( HttpMethod.GET to EmployeePermission.VIEW_EMPLOYEE - )), - EMPLOYEE_GROUP("/api/employee/group/**", mapOf( + ) + ), + EMPLOYEE_GROUP( + "/api/employee/group/**", mapOf( HttpMethod.GET to EmployeePermission.VIEW_EMPLOYEE_GROUP, HttpMethod.POST to EmployeePermission.EDIT_EMPLOYEE_GROUP, HttpMethod.PUT to EmployeePermission.EDIT_EMPLOYEE_GROUP, HttpMethod.DELETE to EmployeePermission.REMOVE_EMPLOYEE_GROUP - )), - EMPLOYEE_PASSWORD("/api/employee/*/password", mapOf( + ) + ), + EMPLOYEE_PASSWORD( + "/api/employee/*/password", mapOf( HttpMethod.PUT to EmployeePermission.EDIT_EMPLOYEE_PASSWORD - )), - EMPLOYEE("/api/employee/**", mapOf( + ) + ), + EMPLOYEE( + "/api/employee/**", mapOf( HttpMethod.GET to EmployeePermission.VIEW_EMPLOYEE, HttpMethod.POST to EmployeePermission.EDIT_EMPLOYEE, HttpMethod.PUT to EmployeePermission.EDIT_EMPLOYEE, HttpMethod.DELETE to EmployeePermission.REMOVE_EMPLOYEE - )) + ) + ) } diff --git a/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/model/AccountModel.kt b/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/model/AccountModel.kt index 07fd9a7..e85d4f8 100644 --- a/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/model/AccountModel.kt +++ b/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/model/AccountModel.kt @@ -24,82 +24,93 @@ private const val EMPLOYEE_PASSWORD_TOO_SHORT_MESSAGE = "Le mot de passe doit co @Entity data class Employee( - @Id - override val id: Long, + @Id + override val id: Long, - val firstName: String = "", + val firstName: String = "", - val lastName: String = "", + val lastName: String = "", - @JsonIgnore - val password: String = "", + @JsonIgnore + val password: String = "", - @JsonIgnore - val isDefaultGroupUser: Boolean = false, + @JsonIgnore + val isDefaultGroupUser: Boolean = false, - @JsonIgnore - val isSystemUser: Boolean = false, + @JsonIgnore + val isSystemUser: Boolean = false, - @field:ManyToOne - @Fetch(FetchMode.SELECT) - var group: EmployeeGroup? = null, + @field:ManyToOne + @Fetch(FetchMode.SELECT) + var group: EmployeeGroup? = null, - @Enumerated(EnumType.STRING) - @ElementCollection(fetch = FetchType.EAGER) - @Fetch(FetchMode.SUBSELECT) - @get:JsonIgnore - val permissions: MutableSet = mutableSetOf(), + @Enumerated(EnumType.STRING) + @ElementCollection(fetch = FetchType.EAGER) + @Fetch(FetchMode.SUBSELECT) + @get:JsonIgnore + val permissions: MutableSet = mutableSetOf(), - val lastLoginTime: LocalDateTime? = null + val lastLoginTime: LocalDateTime? = null ) : Model { @JsonProperty("permissions") fun getFlattenedPermissions(): Iterable = getPermissions() - override fun equals(other: Any?): Boolean = other is Employee && id == other.id && firstName == other.firstName && lastName == other.lastName + override fun equals(other: Any?): Boolean = + other is Employee && id == other.id && firstName == other.firstName && lastName == other.lastName + override fun hashCode(): Int = Objects.hash(id, firstName, lastName) } /** DTO for creating employees. Allows a [password] a [groupId]. */ open class EmployeeSaveDto( - @field:NotNull(message = EMPLOYEE_ID_NULL_MESSAGE) - val id: Long, + @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_FIRST_NAME_EMPTY_MESSAGE) + val firstName: String, - @field:NotBlank(message = EMPLOYEE_LAST_NAME_EMPTY_MESSAGE) - val lastName: 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, + @field:NotBlank(message = EMPLOYEE_PASSWORD_EMPTY_MESSAGE) + @field:Size(min = 8, message = EMPLOYEE_PASSWORD_TOO_SHORT_MESSAGE) + val password: String, - @field:ManyToOne - @Fetch(FetchMode.SELECT) - var groupId: Long? = null, + @field:ManyToOne + @Fetch(FetchMode.SELECT) + var groupId: Long? = null, - @Enumerated(EnumType.STRING) - val permissions: MutableSet = mutableSetOf() + @Enumerated(EnumType.STRING) + val permissions: MutableSet = mutableSetOf() ) : EntityDto { override fun toEntity(): Employee = - Employee(id, firstName, lastName, "", isDefaultGroupUser = false, isSystemUser = false, group = null, permissions = permissions) + Employee( + id, + firstName, + lastName, + "", + isDefaultGroupUser = false, + isSystemUser = false, + group = null, + permissions = permissions + ) } open class EmployeeUpdateDto( - @field:NotNull(message = EMPLOYEE_ID_NULL_MESSAGE) - val id: Long, + @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_FIRST_NAME_EMPTY_MESSAGE) + val firstName: String = "", - @field:NotBlank(message = EMPLOYEE_LAST_NAME_EMPTY_MESSAGE) - val lastName: String = "", + @field:NotBlank(message = EMPLOYEE_LAST_NAME_EMPTY_MESSAGE) + val lastName: String = "", - @Enumerated(EnumType.STRING) - val permissions: Set = mutableSetOf() + @Enumerated(EnumType.STRING) + val permissions: Set = mutableSetOf() ) : EntityDto { override fun toEntity(): Employee = - Employee(id, firstName, lastName, permissions = permissions.toMutableSet()) + Employee(id, firstName, lastName, permissions = permissions.toMutableSet()) } @@ -109,20 +120,20 @@ private const val GROUP_PERMISSIONS_EMPTY_MESSAGE = "Au moins une permission est @Entity data class EmployeeGroup( - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - override var id: Long? = null, + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + override var id: Long? = null, - @Column(unique = true) - override val name: String = "", + @Column(unique = true) + override val name: String = "", - @Enumerated(EnumType.STRING) - @ElementCollection(fetch = FetchType.EAGER) - val permissions: MutableSet = mutableSetOf(), + @Enumerated(EnumType.STRING) + @ElementCollection(fetch = FetchType.EAGER) + val permissions: MutableSet = mutableSetOf(), - @OneToMany - @JsonIgnore - val employees: MutableSet = mutableSetOf() + @OneToMany + @JsonIgnore + val employees: MutableSet = mutableSetOf() ) : NamedModel { fun getEmployeeCount() = employees.size @@ -131,30 +142,30 @@ data class EmployeeGroup( } open class EmployeeGroupSaveDto( - @field:NotBlank(message = GROUP_NAME_NULL_MESSAGE) - @field:Size(min = 3) - val name: String, + @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 + @field:Size(min = 1, message = GROUP_PERMISSIONS_EMPTY_MESSAGE) + val permissions: MutableSet ) : EntityDto { override fun toEntity(): EmployeeGroup = - EmployeeGroup(null, name, permissions) + EmployeeGroup(null, name, permissions) } open class EmployeeGroupUpdateDto( - @field:NotNull(message = GROUP_ID_NULL_MESSAGE) - val id: Long, + @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: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 + @field:Size(min = 1, message = GROUP_PERMISSIONS_EMPTY_MESSAGE) + val permissions: MutableSet ) : EntityDto { override fun toEntity(): EmployeeGroup = - EmployeeGroup(id, name, permissions) + EmployeeGroup(id, name, permissions) } @@ -166,11 +177,15 @@ enum class EmployeePermission(val impliedPermissions: List = VIEW_MATERIAL, VIEW_MATERIAL_TYPE, VIEW_COMPANY, - VIEW(listOf( + VIEW_RECIPE, + VIEW( + listOf( VIEW_MATERIAL, VIEW_MATERIAL_TYPE, - VIEW_COMPANY - )), + VIEW_COMPANY, + VIEW_RECIPE + ) + ), VIEW_EMPLOYEE, VIEW_EMPLOYEE_GROUP, @@ -178,12 +193,16 @@ enum class EmployeePermission(val impliedPermissions: List = EDIT_MATERIAL(listOf(VIEW_MATERIAL)), EDIT_MATERIAL_TYPE(listOf(VIEW_MATERIAL_TYPE)), EDIT_COMPANY(listOf(VIEW_COMPANY)), - EDIT(listOf( + EDIT_RECIPE(listOf(VIEW_RECIPE)), + EDIT( + listOf( EDIT_MATERIAL, EDIT_MATERIAL_TYPE, EDIT_COMPANY, + EDIT_RECIPE, VIEW - )), + ) + ), EDIT_EMPLOYEE(listOf(VIEW_EMPLOYEE)), EDIT_EMPLOYEE_PASSWORD(listOf(EDIT_EMPLOYEE)), EDIT_EMPLOYEE_GROUP(listOf(VIEW_EMPLOYEE_GROUP)), @@ -192,21 +211,28 @@ enum class EmployeePermission(val impliedPermissions: List = REMOVE_MATERIAL(listOf(EDIT_MATERIAL)), REMOVE_MATERIAL_TYPE(listOf(EDIT_MATERIAL_TYPE)), REMOVE_COMPANY(listOf(EDIT_COMPANY)), - REMOVE(listOf( + REMOVE_RECIPE(listOf(EDIT_RECIPE)), + REMOVE( + listOf( REMOVE_MATERIAL, REMOVE_MATERIAL_TYPE, REMOVE_COMPANY, + REMOVE_RECIPE, EDIT - )), + ) + ), REMOVE_EMPLOYEE(listOf(EDIT_EMPLOYEE)), REMOVE_EMPLOYEE_GROUP(listOf(EDIT_EMPLOYEE_GROUP)), // Others - SET_BROWSER_DEFAULT_GROUP(listOf( + SET_BROWSER_DEFAULT_GROUP( + listOf( VIEW_EMPLOYEE_GROUP - )), + ) + ), - ADMIN(listOf( + ADMIN( + listOf( REMOVE, SET_BROWSER_DEFAULT_GROUP, @@ -214,7 +240,8 @@ enum class EmployeePermission(val impliedPermissions: List = REMOVE_EMPLOYEE, EDIT_EMPLOYEE_PASSWORD, REMOVE_EMPLOYEE_GROUP, - )); + ) + ); operator fun contains(permission: EmployeePermission): Boolean { return permission == this || impliedPermissions.any { permission in it } @@ -250,70 +277,82 @@ private fun EmployeePermission.toAuthority(): GrantedAuthority { // ==== 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 = mutableSetOf(), - lastLoginTime: LocalDateTime? = null, - op: Employee.() -> Unit = {} -) = Employee(id, firstName, lastName, password, isDefaultGroupUser, isSystemUser, group, permissions, lastLoginTime).apply(op) + 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 = mutableSetOf(), + lastLoginTime: LocalDateTime? = null, + op: Employee.() -> Unit = {} +) = Employee( + id, + firstName, + lastName, + password, + isDefaultGroupUser, + isSystemUser, + group, + permissions, + lastLoginTime +).apply(op) fun employee( - employee: Employee, - newId: Long? = null + employee: Employee, + newId: Long? = null ) = with(employee) { - Employee(newId - ?: id, firstName, lastName, password, isDefaultGroupUser, isSystemUser, group, permissions, lastLoginTime) + Employee( + newId + ?: id, firstName, lastName, password, isDefaultGroupUser, isSystemUser, group, permissions, lastLoginTime + ) } fun employeeSaveDto( - passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(), - id: Long = 0L, - firstName: String = "firstName", - lastName: String = "lastName", - password: String = passwordEncoder.encode("password"), - groupId: Long? = null, - permissions: MutableSet = mutableSetOf(), - op: EmployeeSaveDto.() -> Unit = {} + passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(), + id: Long = 0L, + firstName: String = "firstName", + lastName: String = "lastName", + password: String = passwordEncoder.encode("password"), + groupId: Long? = null, + permissions: MutableSet = mutableSetOf(), + op: EmployeeSaveDto.() -> Unit = {} ) = EmployeeSaveDto(id, firstName, lastName, password, groupId, permissions).apply(op) fun employeeUpdateDto( - id: Long = 0L, - firstName: String = "firstName", - lastName: String = "lastName", - permissions: MutableSet = mutableSetOf(), - op: EmployeeUpdateDto.() -> Unit = {} + id: Long = 0L, + firstName: String = "firstName", + lastName: String = "lastName", + permissions: MutableSet = mutableSetOf(), + op: EmployeeUpdateDto.() -> Unit = {} ) = EmployeeUpdateDto(id, firstName, lastName, permissions).apply(op) fun employeeGroup( - id: Long? = null, - name: String = "name", - permissions: MutableSet = mutableSetOf(), - employees: MutableSet = mutableSetOf(), - op: EmployeeGroup.() -> Unit = {} + id: Long? = null, + name: String = "name", + permissions: MutableSet = mutableSetOf(), + employees: MutableSet = mutableSetOf(), + op: EmployeeGroup.() -> Unit = {} ) = EmployeeGroup(id, name, permissions, employees).apply(op) fun employeeGroup( - employeeGroup: EmployeeGroup, - newId: Long? = null, - newName: String? = null + employeeGroup: EmployeeGroup, + newId: Long? = null, + newName: String? = null ) = with(employeeGroup) { EmployeeGroup(newId ?: id, newName ?: name, permissions, employees) } fun employeeGroupSaveDto( - name: String = "name", - permissions: MutableSet = mutableSetOf(), - op: EmployeeGroupSaveDto.() -> Unit = {} + name: String = "name", + permissions: MutableSet = mutableSetOf(), + op: EmployeeGroupSaveDto.() -> Unit = {} ) = EmployeeGroupSaveDto(name, permissions).apply(op) fun employeeGroupUpdateDto( - id: Long = 0L, - name: String = "name", - permissions: MutableSet = mutableSetOf(), - op: EmployeeGroupUpdateDto.() -> Unit = {} + id: Long = 0L, + name: String = "name", + permissions: MutableSet = mutableSetOf(), + op: EmployeeGroupUpdateDto.() -> Unit = {} ) = EmployeeGroupUpdateDto(id, name, permissions).apply(op) diff --git a/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/model/Model.kt b/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/model/Model.kt index ce7f1e8..e37259c 100644 --- a/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/model/Model.kt +++ b/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/model/Model.kt @@ -1,5 +1,6 @@ package dev.fyloz.trial.colorrecipesexplorer.model +/** The model of a stored entity. Each model should implements its own equals and hashCode methods to keep compatibility with the legacy Java and Thymeleaf code. */ interface Model { val id: Long? } diff --git a/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/model/RecipeStep.kt b/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/model/RecipeStep.kt new file mode 100644 index 0000000..cb71189 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/model/RecipeStep.kt @@ -0,0 +1,76 @@ +package dev.fyloz.trial.colorrecipesexplorer.model + +import com.fasterxml.jackson.annotation.JsonIgnore +import dev.fyloz.trial.colorrecipesexplorer.model.validation.NullOrNotBlank +import java.util.* +import javax.persistence.* +import javax.validation.constraints.NotNull + +private const val RECIPE_STEP_ID_NULL_MESSAGE = "Un identifiant est requis" +private const val RECIPE_STEP_RECIPE_NULL_MESSAGE = "Une recette est requise" +private const val RECIPE_STEP_MESSAGE_NULL_MESSAGE = "Un message est requis" + +@Entity +data class RecipeStep( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + override val id: Long?, + + @JsonIgnore + @ManyToOne + val recipe: Recipe?, + + val message: String +) : Model { + constructor(recipe: Recipe?, message: String) : this(null, recipe, message) + + override fun equals(other: Any?): Boolean = + other is RecipeStep && other.recipe == recipe && other.message == message + + override fun hashCode(): Int = Objects.hash(recipe, message) +} + + +open class RecipeStepSaveDto( + @field:NotNull(message = RECIPE_STEP_RECIPE_NULL_MESSAGE) + val recipe: Recipe, + + @field:NotNull(message = RECIPE_STEP_MESSAGE_NULL_MESSAGE) + val message: String +) : EntityDto { + override fun toEntity(): RecipeStep = RecipeStep(null, recipe, message) +} + + +open class RecipeStepUpdateDto( + @field:NotNull(message = RECIPE_STEP_ID_NULL_MESSAGE) + val id: Long, + + val recipe: Recipe?, + + @field:NullOrNotBlank(message = RECIPE_STEP_MESSAGE_NULL_MESSAGE) + val message: String? +) : EntityDto { + override fun toEntity(): RecipeStep = RecipeStep(id, recipe, message ?: "") +} + +// ==== DSL ==== +fun recipeStep( + id: Long? = null, + recipe: Recipe? = TODO(), // TODO change default when recipe DSL is done + message: String = "message", + op: RecipeStep.() -> Unit = {} +) = RecipeStep(id, recipe, message).apply(op) + +fun recipeStepSaveDto( + recipe: Recipe = TODO(), // TODO change default when recipe DSL is done + message: String = "message", + op: RecipeStepSaveDto.() -> Unit = {} +) = RecipeStepSaveDto(recipe, message).apply(op) + +fun recipeStepUpdateDto( + id: Long = 0L, + recipe: Recipe? = TODO(), + message: String? = "message", + op: RecipeStepUpdateDto.() -> Unit = {} +) = RecipeStepUpdateDto(id, recipe, message).apply(op) diff --git a/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/repository/RecipeStepRepository.kt b/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/repository/RecipeStepRepository.kt new file mode 100644 index 0000000..c2fe927 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/repository/RecipeStepRepository.kt @@ -0,0 +1,8 @@ +package dev.fyloz.trial.colorrecipesexplorer.repository + +import dev.fyloz.trial.colorrecipesexplorer.model.RecipeStep +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface RecipeStepRepository : JpaRepository diff --git a/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/rest/CompanyController.kt b/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/rest/CompanyController.kt index e4028b4..261d724 100644 --- a/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/rest/CompanyController.kt +++ b/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/rest/CompanyController.kt @@ -13,4 +13,8 @@ private const val COMPANY_CONTROLLER_PATH = "api/company" @RestController @RequestMapping(COMPANY_CONTROLLER_PATH) @Profile("rest") -class CompanyController(companyService: CompanyService) : AbstractRestModelApiController(companyService, COMPANY_CONTROLLER_PATH) +class CompanyController(companyService: CompanyService) : + AbstractRestModelApiController( + companyService, + COMPANY_CONTROLLER_PATH + ) diff --git a/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/rest/InventoryController.kt b/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/rest/InventoryController.kt index 8abf418..b6638ec 100644 --- a/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/rest/InventoryController.kt +++ b/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/rest/InventoryController.kt @@ -1,11 +1,11 @@ -package dev.fyloz.trial.colorrecipesexplorer.rest - -import dev.fyloz.trial.colorrecipesexplorer.model.InventoryMaterial -import dev.fyloz.trial.colorrecipesexplorer.service.InventoryService -import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.RestController - -@RestController -class InventoryController(val inventoryService: InventoryService) { - fun getAllMaterials(): ResponseEntity> = ResponseEntity.ok(inventoryService.getAllMaterials()) -} +//package dev.fyloz.trial.colorrecipesexplorer.rest +// +//import dev.fyloz.trial.colorrecipesexplorer.model.InventoryMaterial +//import dev.fyloz.trial.colorrecipesexplorer.service.InventoryService +//import org.springframework.http.ResponseEntity +//import org.springframework.web.bind.annotation.RestController +// +//@RestController +//class InventoryController(val inventoryService: InventoryService) { +// fun getAllMaterials(): ResponseEntity> = ResponseEntity.ok(inventoryService.getAllMaterials()) +//} diff --git a/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/rest/MaterialTypeController.kt b/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/rest/MaterialTypeController.kt index 2827b00..ff05f70 100644 --- a/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/rest/MaterialTypeController.kt +++ b/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/rest/MaterialTypeController.kt @@ -13,8 +13,5 @@ private const val MATERIAL_TYPE_CONTROLLER_PATH = "api/materialtype" @RestController @RequestMapping(MATERIAL_TYPE_CONTROLLER_PATH) -@Profile("rest") class MaterialTypeController(materialTypeService: MaterialTypeService) : - AbstractRestModelApiController(materialTypeService, MATERIAL_TYPE_CONTROLLER_PATH) { - -} + AbstractRestModelApiController(materialTypeService, MATERIAL_TYPE_CONTROLLER_PATH) diff --git a/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/rest/RestApiController.kt b/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/rest/RestApiController.kt index 1cc1fd5..b08885f 100644 --- a/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/rest/RestApiController.kt +++ b/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/rest/RestApiController.kt @@ -4,6 +4,7 @@ import dev.fyloz.trial.colorrecipesexplorer.model.EntityDto import dev.fyloz.trial.colorrecipesexplorer.model.Model import dev.fyloz.trial.colorrecipesexplorer.service.ModelService import dev.fyloz.trial.colorrecipesexplorer.service.Service +import org.springframework.context.annotation.Profile import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* diff --git a/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/service/RecipeStepService.kt b/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/service/RecipeStepService.kt new file mode 100644 index 0000000..a612106 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/trial/colorrecipesexplorer/service/RecipeStepService.kt @@ -0,0 +1,29 @@ +package dev.fyloz.trial.colorrecipesexplorer.service + +import dev.fyloz.trial.colorrecipesexplorer.model.Recipe +import dev.fyloz.trial.colorrecipesexplorer.model.RecipeStep +import dev.fyloz.trial.colorrecipesexplorer.model.RecipeStepSaveDto +import dev.fyloz.trial.colorrecipesexplorer.model.RecipeStepUpdateDto +import dev.fyloz.trial.colorrecipesexplorer.repository.RecipeStepRepository +import org.springframework.stereotype.Service +import javax.transaction.Transactional + +interface RecipeStepService : ModelService { + /** Creates a step for the given [recipe] with the given [message]. */ + fun createForRecipe(recipe: Recipe, message: String): RecipeStep + + /** Creates several steps for the given [recipe] with the given [messages]. */ + fun createAllForRecipe(recipe: Recipe, messages: Collection): Collection +} + +@Service +class RecipeStepServiceImpl(recipeStepRepository: RecipeStepRepository) : + AbstractModelService(recipeStepRepository), + RecipeStepService { + override fun createForRecipe(recipe: Recipe, message: String): RecipeStep = + RecipeStep(recipe, message) + + @Transactional + override fun createAllForRecipe(recipe: Recipe, messages: Collection): Collection = + messages.map { createForRecipe(recipe, it) } +} diff --git a/src/test/kotlin/dev/fyloz/trial/colorrecipesexplorer/repository/RecipeStepRepositoryTest.kt b/src/test/kotlin/dev/fyloz/trial/colorrecipesexplorer/repository/RecipeStepRepositoryTest.kt new file mode 100644 index 0000000..60baca0 --- /dev/null +++ b/src/test/kotlin/dev/fyloz/trial/colorrecipesexplorer/repository/RecipeStepRepositoryTest.kt @@ -0,0 +1,13 @@ +package dev.fyloz.trial.colorrecipesexplorer.repository + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager + +@DataJpaTest +class RecipeStepRepositoryTest @Autowired constructor( + recipeStepRepository: RecipeStepRepository, + entityManager: TestEntityManager +) { + // Nothing for now +} diff --git a/src/test/kotlin/dev/fyloz/trial/colorrecipesexplorer/service/RecipeStepServiceTest.kt b/src/test/kotlin/dev/fyloz/trial/colorrecipesexplorer/service/RecipeStepServiceTest.kt new file mode 100644 index 0000000..4902c83 --- /dev/null +++ b/src/test/kotlin/dev/fyloz/trial/colorrecipesexplorer/service/RecipeStepServiceTest.kt @@ -0,0 +1,46 @@ +package dev.fyloz.trial.colorrecipesexplorer.service + +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.spy +import dev.fyloz.trial.colorrecipesexplorer.model.* +import dev.fyloz.trial.colorrecipesexplorer.repository.RecipeStepRepository +import org.junit.jupiter.api.Nested +import kotlin.test.assertEquals + +class RecipeStepServiceTest : + AbstractModelServiceTest() { + override val repository: RecipeStepRepository = mock() + override val service: RecipeStepService = spy(RecipeStepServiceImpl(repository)) + + override val entity: RecipeStep = recipeStep(id = 0L, recipe = TODO(), message = "message") + override val anotherEntity: RecipeStep = recipeStep(id = 1L, recipe = TODO(), message = "another message") + override val entitySaveDto: RecipeStepSaveDto = spy(recipeStepSaveDto(entity.recipe!!, entity.message)) + override val entityUpdateDto: RecipeStepUpdateDto = + spy(recipeStepUpdateDto(entity.id!!, entity.recipe, entity.message)) + + @Nested + inner class CreateForRecipe { + fun `returns a correct RecipeStep`() { + val step = recipeStep(null, entity.recipe, entity.message) + + val found = service.createForRecipe(entity.recipe!!, entity.message) + + assertEquals(step, found) + } + } + + @Nested + inner class CreateAllForRecipe { + fun `returns all correct RecipeSteps`() { + val steps = listOf( + recipeStep(null, entity.recipe, entity.message), + recipeStep(null, entity.recipe, anotherEntity.message) + ) + val messages = steps.map { it.message } + + val found = service.createAllForRecipe(entity.recipe!!, messages) + + assertEquals(steps, found) + } + } +}