Merge branch 'eugenp:master' into master
This commit is contained in:
commit
35dadda533
|
@ -0,0 +1,3 @@
|
||||||
|
## Parent Boot 2
|
||||||
|
|
||||||
|
This is a parent module for all projects using Spring Boot 3.
|
|
@ -0,0 +1,97 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<artifactId>parent-boot-3</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
<name>parent-boot-3</name>
|
||||||
|
<packaging>pom</packaging>
|
||||||
|
<description>Parent for all Spring Boot 3 modules</description>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>com.baeldung</groupId>
|
||||||
|
<artifactId>parent-modules</artifactId>
|
||||||
|
<version>1.0.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit</groupId>
|
||||||
|
<artifactId>junit-bom</artifactId>
|
||||||
|
<version>${junit-jupiter.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-dependencies</artifactId>
|
||||||
|
<version>${spring-boot.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<pluginManagement>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<version>${spring-boot.version}</version>
|
||||||
|
<configuration>
|
||||||
|
<mainClass>${start-class}</mainClass>
|
||||||
|
<!-- this is necessary as we're not using the Boot parent -->
|
||||||
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>repackage</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</pluginManagement>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>spring-milestones</id>
|
||||||
|
<name>Spring Milestones</name>
|
||||||
|
<url>https://repo.spring.io/milestone</url>
|
||||||
|
<snapshots>
|
||||||
|
<enabled>false</enabled>
|
||||||
|
</snapshots>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
<pluginRepositories>
|
||||||
|
<pluginRepository>
|
||||||
|
<id>spring-milestones</id>
|
||||||
|
<name>Spring Milestones</name>
|
||||||
|
<url>https://repo.spring.io/milestone</url>
|
||||||
|
<snapshots>
|
||||||
|
<enabled>false</enabled>
|
||||||
|
</snapshots>
|
||||||
|
</pluginRepository>
|
||||||
|
</pluginRepositories>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<spring-boot.version>3.0.0-M3</spring-boot.version>
|
||||||
|
<junit-jupiter.version>5.8.2</junit-jupiter.version>
|
||||||
|
<maven-surefire-plugin.version>3.0.0-M7</maven-surefire-plugin.version>
|
||||||
|
<lombok.version>1.18.22</lombok.version>
|
||||||
|
<java.version>17</java.version>
|
||||||
|
<maven-pmd-plugin.version>3.17.0</maven-pmd-plugin.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
</project>
|
1
pom.xml
1
pom.xml
|
@ -1252,6 +1252,7 @@
|
||||||
<module>quarkus-modules/quarkus-jandex</module>
|
<module>quarkus-modules/quarkus-jandex</module>
|
||||||
<module>spring-boot-modules/spring-boot-cassandre</module>
|
<module>spring-boot-modules/spring-boot-cassandre</module>
|
||||||
<module>spring-boot-modules/spring-boot-camel</module>
|
<module>spring-boot-modules/spring-boot-camel</module>
|
||||||
|
<module>spring-boot-modules/spring-boot-3</module>
|
||||||
<module>testing-modules/testing-assertions</module>
|
<module>testing-modules/testing-assertions</module>
|
||||||
<module>persistence-modules/fauna</module>
|
<module>persistence-modules/fauna</module>
|
||||||
<module>lightrun</module>
|
<module>lightrun</module>
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>com.baeldung</groupId>
|
||||||
|
<artifactId>parent-boot-3</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
<relativePath>../../parent-boot-3</relativePath>
|
||||||
|
</parent>
|
||||||
|
<artifactId>spring-boot-3-sample</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
<name>spring-boot-3-sample</name>
|
||||||
|
<description>Demo project for Spring Boot</description>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-hateoas</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.h2database</groupId>
|
||||||
|
<artifactId>h2</artifactId>
|
||||||
|
<!-- Treiber: nicht gegen diese API programmieren, erst zur Laufzeit benötigt -->
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-devtools</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<!-- depends on javax.servlet, see https://github.com/springdoc/springdoc-openapi/issues/1284
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springdoc</groupId>
|
||||||
|
<artifactId>springdoc-openapi-ui</artifactId>
|
||||||
|
<version>1.6.9</version>
|
||||||
|
</dependency>
|
||||||
|
-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mapstruct</groupId>
|
||||||
|
<artifactId>mapstruct</artifactId>
|
||||||
|
<version>${mapstruct.version}</version>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<pluginManagement>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.mapstruct</groupId>
|
||||||
|
<artifactId>mapstruct-processor</artifactId>
|
||||||
|
<version>${mapstruct.version}</version>
|
||||||
|
</path>
|
||||||
|
<path>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>${lombok.version}</version>
|
||||||
|
</path>
|
||||||
|
<!-- This is needed when using Lombok 1.18.16 and above -->
|
||||||
|
<path>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok-mapstruct-binding</artifactId>
|
||||||
|
<version>0.2.0</version>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<excludes>
|
||||||
|
<exclude>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</exclude>
|
||||||
|
</excludes>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</pluginManagement>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<mapstruct.version>1.5.2.Final</mapstruct.version>
|
||||||
|
<start-class>com.baeldung.sample.TodoApplication</start-class>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
</project>
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.baeldung.sample;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
@SpringBootApplication
|
||||||
|
public class TodoApplication {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(TodoApplication.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
package com.baeldung.sample.boundary;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMethod;
|
||||||
|
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
import static java.util.Arrays.stream;
|
||||||
|
import static org.springframework.http.HttpHeaders.ACCEPT;
|
||||||
|
import static org.springframework.http.HttpHeaders.ACCEPT_LANGUAGE;
|
||||||
|
import static org.springframework.http.HttpHeaders.CONTENT_LANGUAGE;
|
||||||
|
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
|
||||||
|
import static org.springframework.http.HttpHeaders.IF_MATCH;
|
||||||
|
import static org.springframework.http.HttpHeaders.IF_NONE_MATCH;
|
||||||
|
import static org.springframework.http.HttpHeaders.LINK;
|
||||||
|
import static org.springframework.http.HttpHeaders.LOCATION;
|
||||||
|
import static org.springframework.http.HttpHeaders.ORIGIN;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class CorsConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public WebMvcConfigurer corsConfigurer(final CorsConfigurationData allowed) {
|
||||||
|
return new WebMvcConfigurer() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addCorsMappings(final CorsRegistry registry) {
|
||||||
|
registry.addMapping("/**")
|
||||||
|
.exposedHeaders(LOCATION, LINK)
|
||||||
|
// allow all HTTP request methods
|
||||||
|
.allowedMethods(stream(RequestMethod.values()).map(Enum::name).toArray(String[]::new)) //
|
||||||
|
// allow the commonly used headers
|
||||||
|
.allowedHeaders(ORIGIN, CONTENT_TYPE, CONTENT_LANGUAGE, ACCEPT, ACCEPT_LANGUAGE, IF_MATCH, IF_NONE_MATCH) //
|
||||||
|
// this is stage specific
|
||||||
|
.allowedOrigins(allowed.getOrigins())
|
||||||
|
.allowCredentials(allowed.isCredentials());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package com.baeldung.sample.boundary;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The properties from application.yml. You can specify them by the following snippet:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* server:
|
||||||
|
* endpoints:
|
||||||
|
* api:
|
||||||
|
* v1: /api/v1
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationProperties(prefix = "cors.allow")
|
||||||
|
@Data
|
||||||
|
public class CorsConfigurationData {
|
||||||
|
|
||||||
|
private String[] origins = { "*" };
|
||||||
|
private boolean credentials = false;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.baeldung.sample.boundary;
|
||||||
|
|
||||||
|
import com.baeldung.sample.control.NotFoundException;
|
||||||
|
import jakarta.validation.ValidationException;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
|
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
|
|
||||||
|
@ControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
@ExceptionHandler(NotFoundException.class)
|
||||||
|
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||||
|
protected void handleNotFoundException() {}
|
||||||
|
|
||||||
|
@ExceptionHandler({ValidationException.class, MethodArgumentNotValidException.class})
|
||||||
|
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
|
||||||
|
protected void handleValidationException() {}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package com.baeldung.sample.boundary;
|
||||||
|
|
||||||
|
import com.baeldung.sample.control.Todo;
|
||||||
|
import org.mapstruct.Mapper;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dieser Mapper kopiert die Informationen zwischen den Schichten.
|
||||||
|
*/
|
||||||
|
@Mapper(componentModel = "spring")
|
||||||
|
interface TodoDtoMapper {
|
||||||
|
|
||||||
|
TodoResponseDto map(Todo todo);
|
||||||
|
|
||||||
|
default String _mapStatus(Todo.Status status) {
|
||||||
|
return switch (status) {
|
||||||
|
case NEW -> "new";
|
||||||
|
case PROGRESS -> "progress";
|
||||||
|
case COMPLETED -> "completed";
|
||||||
|
case ARCHIVED -> "archived";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Todo map(TodoRequestDto todo, Long id);
|
||||||
|
|
||||||
|
default Todo.Status _mapStatus(String status) {
|
||||||
|
return null == status ? Todo.Status.NEW : switch (status) {
|
||||||
|
case "progress" -> Todo.Status.PROGRESS;
|
||||||
|
case "completed" -> Todo.Status.COMPLETED;
|
||||||
|
case "archived" -> Todo.Status.ARCHIVED;
|
||||||
|
default -> Todo.Status.NEW;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.baeldung.sample.boundary;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class TodoRequestDto {
|
||||||
|
@NotBlank
|
||||||
|
private String title;
|
||||||
|
private String description;
|
||||||
|
private LocalDate dueDate;
|
||||||
|
@Pattern(regexp = "new|progress|completed|archived")
|
||||||
|
private String status;
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.baeldung.sample.boundary;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class TodoResponseDto {
|
||||||
|
private Long id;
|
||||||
|
private String title;
|
||||||
|
private String description;
|
||||||
|
private LocalDate dueDate;
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
package com.baeldung.sample.boundary;
|
||||||
|
|
||||||
|
import com.baeldung.sample.control.NotFoundException;
|
||||||
|
import com.baeldung.sample.control.TodosService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
|
||||||
|
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
|
||||||
|
import static org.springframework.http.HttpStatus.NO_CONTENT;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/todos")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TodosController {
|
||||||
|
|
||||||
|
private static final String DEFAULT_MEDIA_TYPE = MediaType.APPLICATION_JSON_VALUE;
|
||||||
|
|
||||||
|
private final TodosService service;
|
||||||
|
// Mapping zwischen den Schichten
|
||||||
|
private final TodoDtoMapper mapper;
|
||||||
|
|
||||||
|
@GetMapping(produces = DEFAULT_MEDIA_TYPE)
|
||||||
|
public Collection<TodoResponseDto> findAll() {
|
||||||
|
return service.findAll().stream()
|
||||||
|
.map(mapper::map)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(value = "/{id}", produces = DEFAULT_MEDIA_TYPE)
|
||||||
|
public TodoResponseDto findById(
|
||||||
|
@PathVariable("id") final Long id
|
||||||
|
) {
|
||||||
|
// Action
|
||||||
|
return service.findById(id) //
|
||||||
|
.map(mapper::map) // map to dto
|
||||||
|
.orElseThrow(NotFoundException::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(consumes = DEFAULT_MEDIA_TYPE)
|
||||||
|
public ResponseEntity<TodoResponseDto> create(final @Valid @RequestBody TodoRequestDto item) {
|
||||||
|
// Action
|
||||||
|
final var todo = mapper.map(item, null);
|
||||||
|
final var newTodo = service.create(todo);
|
||||||
|
final var result = mapper.map(newTodo);
|
||||||
|
// Response
|
||||||
|
final URI locationHeader = linkTo(methodOn(TodosController.class).findById(result.getId())).toUri(); // HATEOAS
|
||||||
|
return ResponseEntity.created(locationHeader).body(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping(value = "{id}", consumes = DEFAULT_MEDIA_TYPE)
|
||||||
|
@ResponseStatus(NO_CONTENT)
|
||||||
|
public void update(
|
||||||
|
@PathVariable("id") final Long id,
|
||||||
|
@Valid @RequestBody final TodoRequestDto item
|
||||||
|
) {
|
||||||
|
service.update(mapper.map(item, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
@ResponseStatus(NO_CONTENT)
|
||||||
|
public void delete(
|
||||||
|
@PathVariable("id") final Long id
|
||||||
|
) {
|
||||||
|
service.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.baeldung.sample.control;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationProperties(prefix = "application.data")
|
||||||
|
@Data
|
||||||
|
public class DataInitializationConfigurationData {
|
||||||
|
|
||||||
|
private boolean initializeOnStartup = true;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.baeldung.sample.control;
|
||||||
|
|
||||||
|
public class NotFoundException extends RuntimeException {
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.baeldung.sample.control;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
public record Todo(
|
||||||
|
Long id,
|
||||||
|
@NotNull @Size(min = 1) String title,
|
||||||
|
String description,
|
||||||
|
LocalDate dueDate,
|
||||||
|
@NotNull Status status
|
||||||
|
) {
|
||||||
|
|
||||||
|
public enum Status {
|
||||||
|
NEW, PROGRESS, COMPLETED, ARCHIVED
|
||||||
|
}
|
||||||
|
|
||||||
|
public Todo(Long id, String title) {
|
||||||
|
this(id, title, null, null, Status.NEW);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.baeldung.sample.control;
|
||||||
|
|
||||||
|
import com.baeldung.sample.entity.TodoEntity;
|
||||||
|
import org.mapstruct.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dieser Mapper kopiert die Informationen zwischen den Schichten.
|
||||||
|
*/
|
||||||
|
@Mapper(componentModel = "spring")
|
||||||
|
interface TodoEntityMapper {
|
||||||
|
|
||||||
|
TodoEntity map(Todo todo);
|
||||||
|
|
||||||
|
Todo map(TodoEntity todo);
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.baeldung.sample.control;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.context.event.ContextRefreshedEvent;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TodosInitializer {
|
||||||
|
|
||||||
|
private final TodosService service;
|
||||||
|
/*
|
||||||
|
* we cannot use @Profile("default") because
|
||||||
|
* we are not able inject the bean during test
|
||||||
|
* depending from the profile activation
|
||||||
|
*/
|
||||||
|
private final DataInitializationConfigurationData config;
|
||||||
|
|
||||||
|
@EventListener(ContextRefreshedEvent.class)
|
||||||
|
public void initializeTodos() {
|
||||||
|
if (this.config.isInitializeOnStartup() && this.service.count() < 1) {
|
||||||
|
this.service.create(new Todo(null, "Deploy and run the application."));
|
||||||
|
this.service.create(new Todo(null, "Enter some TODO items!"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
package com.baeldung.sample.control;
|
||||||
|
|
||||||
|
import com.baeldung.sample.entity.TodosRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static java.util.stream.Collectors.toList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ein Service ist ein Singleton auf Control Layer, der in der Boundary von mehreren (REST) Controllern gemeinsam genutzt werden kann.
|
||||||
|
* Dieser hat keinen Bezug mehr zu HTTP.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TodosService {
|
||||||
|
|
||||||
|
private final TodoEntityMapper mapper;
|
||||||
|
private final TodosRepository repo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt die Anzahl an Datensätzen zurück.
|
||||||
|
* @return die Anzahl an Datensätzen
|
||||||
|
*/
|
||||||
|
long count() {
|
||||||
|
return repo.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt alle Todos zurück.
|
||||||
|
*
|
||||||
|
* @return eine unveränderliche Collection
|
||||||
|
*/
|
||||||
|
public Collection<Todo> findAll() {
|
||||||
|
return repo.findAll().stream()
|
||||||
|
.map(mapper::map)
|
||||||
|
.collect(toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Durchsucht die Todos nach einer ID.
|
||||||
|
*
|
||||||
|
* @param id die ID
|
||||||
|
* @return das Suchergebnis
|
||||||
|
*/
|
||||||
|
public Optional<Todo> findById(long id) {
|
||||||
|
return repo.findById(id)
|
||||||
|
.map(mapper::map);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fügt ein Item in den Datenbestand hinzu. Dabei wird eine ID generiert.
|
||||||
|
*
|
||||||
|
* @param item das anzulegende Item (ohne ID)
|
||||||
|
* @return das gespeicherte Item (mit ID)
|
||||||
|
* @throws IllegalArgumentException wenn das Item null oder die ID bereits belegt ist
|
||||||
|
*/
|
||||||
|
public Todo create(Todo item) {
|
||||||
|
if (null == item || null != item.id()) {
|
||||||
|
throw new IllegalArgumentException("item must exist without any id");
|
||||||
|
}
|
||||||
|
return mapper.map(repo.save(mapper.map(item)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktualisiert ein Item im Datenbestand.
|
||||||
|
*
|
||||||
|
* @param item das zu ändernde Item mit ID
|
||||||
|
* @throws IllegalArgumentException
|
||||||
|
* wenn das Item oder dessen ID nicht belegt ist
|
||||||
|
* @throws NotFoundException
|
||||||
|
* wenn das Element mit der ID nicht gefunden wird
|
||||||
|
*/
|
||||||
|
public void update(Todo item) {
|
||||||
|
if (null == item || null == item.id()) {
|
||||||
|
throw new IllegalArgumentException("item must exist with an id");
|
||||||
|
}
|
||||||
|
// remove separat, um nicht neue Einträge hinzuzufügen (put allein würde auch ersetzen)
|
||||||
|
if (repo.existsById(item.id())) {
|
||||||
|
repo.save(mapper.map(item));
|
||||||
|
} else {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entfernt ein Item aus dem Datenbestand.
|
||||||
|
*
|
||||||
|
* @param id die ID des zu löschenden Items
|
||||||
|
* @throws NotFoundException
|
||||||
|
* wenn das Element mit der ID nicht gefunden wird
|
||||||
|
*/
|
||||||
|
public void delete(long id) {
|
||||||
|
if (repo.existsById(id)) {
|
||||||
|
repo.deleteById(id);
|
||||||
|
} else {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
package com.baeldung.sample.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Enumerated;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Auch hier wird eine separate Klasse erstellt.
|
||||||
|
* Und auch hier geht es wieder um Unabhängigkeit der beiden Layer (Control und Persistence).
|
||||||
|
* So kann z.B. die Auflösung von Fremdschlüsseln (assignee, priority, topic) in der Persistence Layer erfolgen (JPA unterstützt das), oder aber auch erst in der Control Layer.
|
||||||
|
*/
|
||||||
|
@Entity(name = "todo")
|
||||||
|
@Table(name = "todos")
|
||||||
|
// we do not use @Data because hashCode() and equals() might influence JPA's behaviour
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@ToString
|
||||||
|
public class TodoEntity {
|
||||||
|
|
||||||
|
public enum StatusEntity {
|
||||||
|
NEW, PROGRESS, COMPLETED, ARCHIVED
|
||||||
|
}
|
||||||
|
|
||||||
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
|
@Id
|
||||||
|
private Long id;
|
||||||
|
@NotNull
|
||||||
|
@Size(min = 1)
|
||||||
|
private String title;
|
||||||
|
private String description;
|
||||||
|
private LocalDate dueDate;
|
||||||
|
@Enumerated
|
||||||
|
@NotNull
|
||||||
|
private StatusEntity status = StatusEntity.NEW;
|
||||||
|
|
||||||
|
public TodoEntity(Long id, String title) {
|
||||||
|
this.id = id;
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.baeldung.sample.entity;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface TodosRepository extends JpaRepository<TodoEntity, Long> {
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
spring:
|
||||||
|
mvc:
|
||||||
|
throw-exception-if-no-handler-found: true
|
||||||
|
jackson:
|
||||||
|
deserialization:
|
||||||
|
FAIL_ON_UNKNOWN_PROPERTIES: true
|
||||||
|
property-naming-strategy: SNAKE_CASE
|
||||||
|
jpa:
|
||||||
|
open-in-view: false
|
||||||
|
generate-ddl: true
|
||||||
|
show-sql: true
|
||||||
|
hibernate:
|
||||||
|
ddl-auto: update
|
||||||
|
properties:
|
||||||
|
hibernate:
|
||||||
|
dialect: org.hibernate.dialect.H2Dialect
|
||||||
|
# Custom Properties
|
||||||
|
cors:
|
||||||
|
allow:
|
||||||
|
origins: ${CORS_ALLOWED_ORIGINS:*}
|
||||||
|
credentials: ${CORS_ALLOW_CREDENTIALS:false}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.baeldung.sample.boundary;
|
||||||
|
|
||||||
|
import org.springframework.boot.test.context.TestConfiguration;
|
||||||
|
import org.springframework.context.annotation.ComponentScan;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class bundles all classes in the boundary layer.
|
||||||
|
* This includes also the generated mapper classes.
|
||||||
|
*/
|
||||||
|
@TestConfiguration
|
||||||
|
@ComponentScan(basePackageClasses = TodosBoundaryLayer.class)
|
||||||
|
public class TodosBoundaryLayer {
|
||||||
|
}
|
|
@ -0,0 +1,218 @@
|
||||||
|
package com.baeldung.sample.boundary;
|
||||||
|
|
||||||
|
import com.baeldung.sample.control.NotFoundException;
|
||||||
|
import com.baeldung.sample.control.Todo;
|
||||||
|
import com.baeldung.sample.control.TodosService;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.context.ContextConfiguration;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integrationstests der Todos REST API auf HTTP Layer. Die Anwendung wird als Black Box behandelt.
|
||||||
|
* Somit müssen alle Tests, die einen existierenden Datensatz benötigen, diesen im Test anlegen.
|
||||||
|
* Spring Boot startet den sog. "ApplicationContext" automatisch und bietet Möglichkeiten, die Anwendungsteile vom Test aus aufzurufen.
|
||||||
|
*/
|
||||||
|
@WebMvcTest
|
||||||
|
@ContextConfiguration(classes = TodosBoundaryLayer.class)
|
||||||
|
class TodosControllerApiIntegrationTest {
|
||||||
|
|
||||||
|
private static final String BASEURL = "/api/v1/todos"; // URL to Resource
|
||||||
|
private static final String DEFAULT_MEDIA_TYPE = MediaType.APPLICATION_JSON_VALUE;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
TodosService service;
|
||||||
|
@Autowired
|
||||||
|
MockMvc mvc; // testing by sending HTTP requests and verifying HTTP responses
|
||||||
|
@Autowired
|
||||||
|
ObjectMapper mapper; // used to render or parse JSON
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Testfall:
|
||||||
|
* - GET auf alle Todos -> 200 OK mit JSON
|
||||||
|
*/
|
||||||
|
@DisplayName("GET auf alle Daten (200 OK)")
|
||||||
|
@Test
|
||||||
|
void testFindAllTodos() throws Exception {
|
||||||
|
when(service.findAll()).thenReturn(List.of());
|
||||||
|
mvc
|
||||||
|
.perform(get(BASEURL).accept(DEFAULT_MEDIA_TYPE))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(content().contentType(DEFAULT_MEDIA_TYPE));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Testfall:
|
||||||
|
* - einzelnes Todos auslesen -> kein Fehler
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testFindById() throws Exception {
|
||||||
|
when(service.findById(1L))
|
||||||
|
.thenReturn(Optional.of(new Todo(1L, "test")));
|
||||||
|
mvc
|
||||||
|
.perform(get(BASEURL + "/1").accept(DEFAULT_MEDIA_TYPE))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(content().contentType(DEFAULT_MEDIA_TYPE))
|
||||||
|
.andExpect(jsonPath("$.title").value("test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Testfall:
|
||||||
|
* - einzelnes Todos auslesen -> 404
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testFindByIdNotExisting() throws Exception {
|
||||||
|
when(service.findById(1L))
|
||||||
|
.thenReturn(Optional.empty());
|
||||||
|
mvc
|
||||||
|
.perform(get(BASEURL + "/1").accept(DEFAULT_MEDIA_TYPE))
|
||||||
|
.andExpect(status().isNotFound());
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Testfall:
|
||||||
|
* - Anlegen eines Todos per POST -> 201 mit Location Header
|
||||||
|
*/
|
||||||
|
@DisplayName("POST liefert 201 mit Location-Header")
|
||||||
|
@Test
|
||||||
|
void testCreateTodo() throws Exception {
|
||||||
|
// etwas umständlich, die ID zu besetzen, wenn auf dem Service create() aufgerufen wird
|
||||||
|
when(service.create(any())).thenReturn(new Todo(5L, "test-todo"));
|
||||||
|
final var json = "{\"title\":\"test-todo\"}";
|
||||||
|
mvc
|
||||||
|
.perform(post(BASEURL).contentType(DEFAULT_MEDIA_TYPE).content(json))
|
||||||
|
.andExpect(status().isCreated())
|
||||||
|
.andExpect(header().exists(HttpHeaders.LOCATION))
|
||||||
|
.andExpect(jsonPath("$.id").value(5L));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Testfall:
|
||||||
|
* - Anlegen eines Todos per POST ohne Titel -> 422
|
||||||
|
*/
|
||||||
|
@DisplayName("POST erzeugt kein Todo, wenn kein Titel angegeben ist")
|
||||||
|
@Test
|
||||||
|
void testCreateTodoWithoutTitle() throws Exception {
|
||||||
|
final var json = "{}";
|
||||||
|
mvc
|
||||||
|
.perform(post(BASEURL).contentType(DEFAULT_MEDIA_TYPE).content(json))
|
||||||
|
.andExpect(status().isUnprocessableEntity());
|
||||||
|
verifyNoInteractions(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Testfall:
|
||||||
|
* - Anlegen eines Todos per POST mit ID -> 400
|
||||||
|
*/
|
||||||
|
@DisplayName("POST erzeugt kein Todo, wenn die ID mitgegeben wird (undefinierte Property)")
|
||||||
|
@Test
|
||||||
|
void testCreateTodoWithID() throws Exception {
|
||||||
|
final var json = "{\"id\":1, \"title\":\"test\"}";
|
||||||
|
mvc
|
||||||
|
.perform(post(BASEURL).contentType(DEFAULT_MEDIA_TYPE).content(json))
|
||||||
|
.andExpect(status().isBadRequest());
|
||||||
|
verifyNoInteractions(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Testfall:
|
||||||
|
* - Anlegen eines Todos per POST mit leerem Titel -> 400
|
||||||
|
*/
|
||||||
|
@DisplayName("POST erzeugt kein Todo, wenn Titel weniger als 1 Zeichen hat")
|
||||||
|
@Test
|
||||||
|
void testCreateTodoWithEmptyTitle() throws Exception {
|
||||||
|
final var newTodo = new TodoRequestDto();
|
||||||
|
newTodo.setTitle("");
|
||||||
|
final String json = mapper.writeValueAsString(newTodo);
|
||||||
|
this.mvc //
|
||||||
|
.perform(post(BASEURL).contentType(DEFAULT_MEDIA_TYPE).content(json)) //
|
||||||
|
.andExpect(status().isUnprocessableEntity());
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Testfall:
|
||||||
|
* - Ändern -> 204
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testUpdateTodo() throws Exception {
|
||||||
|
final var json = "{\"title\":\"test-todo\"}";
|
||||||
|
mvc
|
||||||
|
.perform(put(BASEURL + "/5").contentType(DEFAULT_MEDIA_TYPE).content(json))
|
||||||
|
.andExpect(status().isNoContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Testfall:
|
||||||
|
* - Ändern -> 404
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testUpdateTodoNotExisting() throws Exception {
|
||||||
|
doThrow(NotFoundException.class).when(service).update(any());
|
||||||
|
final var json = "{\"title\":\"test-todo\"}";
|
||||||
|
mvc
|
||||||
|
.perform(put(BASEURL + "/5").contentType(DEFAULT_MEDIA_TYPE).content(json))
|
||||||
|
.andExpect(status().isNotFound());
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Testfall:
|
||||||
|
* - Ändern des Todos mit leerem Titel
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
@DisplayName("PUT mit leerem Titel")
|
||||||
|
void testUpdateWithEmptyTitle() throws Exception {
|
||||||
|
// Act
|
||||||
|
final var json = "{}";
|
||||||
|
mvc
|
||||||
|
.perform(put(BASEURL + "/5").contentType(DEFAULT_MEDIA_TYPE).content(json))
|
||||||
|
.andExpect(status().isUnprocessableEntity());
|
||||||
|
verifyNoInteractions(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Testfall:
|
||||||
|
* - Löschen -> 204
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testDeleteTodo() throws Exception {
|
||||||
|
mvc
|
||||||
|
.perform(delete(BASEURL + "/5").contentType(DEFAULT_MEDIA_TYPE))
|
||||||
|
.andExpect(status().isNoContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Testfall:
|
||||||
|
* - Löschen -> 404
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testDeleteTodoNotExisting() throws Exception {
|
||||||
|
doThrow(NotFoundException.class).when(service).delete(anyLong());
|
||||||
|
mvc
|
||||||
|
.perform(delete(BASEURL + "/5").contentType(DEFAULT_MEDIA_TYPE))
|
||||||
|
.andExpect(status().isNotFound());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.baeldung.sample.control;
|
||||||
|
|
||||||
|
import org.springframework.boot.test.context.TestConfiguration;
|
||||||
|
import org.springframework.context.annotation.ComponentScan;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class bundles all classes in the control layer.
|
||||||
|
* This includes also the generated mapper classes.
|
||||||
|
*/
|
||||||
|
@TestConfiguration
|
||||||
|
@ComponentScan(basePackageClasses = TodosControlLayer.class)
|
||||||
|
public class TodosControlLayer {
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.baeldung.sample.control;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.atLeastOnce;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
class TodosInitializerActivationIntegrationTest {
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
TodosService service;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void serviceHasEmptyData() {
|
||||||
|
when(service.count()).thenReturn(0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void serviceHasNoFurtherInteractions() {
|
||||||
|
verifyNoMoreInteractions(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("data initialization is invoked on default profile")
|
||||||
|
void testIsInvoked() {
|
||||||
|
verify(service).count();
|
||||||
|
verify(service, atLeastOnce()).create(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package com.baeldung.sample.control;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.context.TestPropertySource;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@AutoConfigureTestDatabase
|
||||||
|
@Transactional
|
||||||
|
@TestPropertySource(
|
||||||
|
properties = {
|
||||||
|
"application.data.initialize-on-startup=false"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
class TodosServiceDatabaseIntegrationTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
TodosService service;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void assertEmpty() {
|
||||||
|
assertThat(service.count())
|
||||||
|
.isZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreate() {
|
||||||
|
service.create(new Todo(null, "test"));
|
||||||
|
assertThat(service.count())
|
||||||
|
.isEqualTo(1);
|
||||||
|
assertThat(service.findAll())
|
||||||
|
.hasSize(1)
|
||||||
|
.element(0).extracting(Todo::title).isEqualTo("test");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testFindById() {
|
||||||
|
final var todo = service.create(new Todo(null, "test"));
|
||||||
|
final var result = service.findById(todo.id());
|
||||||
|
assertThat(result)
|
||||||
|
.isNotEmpty()
|
||||||
|
.get().usingRecursiveComparison().isEqualTo(todo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDelete() {
|
||||||
|
final var todo = service.create(new Todo(null, "test"));
|
||||||
|
final var id = todo.id();
|
||||||
|
service.delete(id);
|
||||||
|
final var result = service.findById(id);
|
||||||
|
assertThat(result).isEmpty();
|
||||||
|
assertThatThrownBy(() -> service.delete(id))
|
||||||
|
.isInstanceOf(NotFoundException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,126 @@
|
||||||
|
package com.baeldung.sample.control;
|
||||||
|
|
||||||
|
import com.baeldung.sample.entity.TodoEntity;
|
||||||
|
import com.baeldung.sample.entity.TodosRepository;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.Answers;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.refEq;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integrationstests des TodosServices. Die Anwendung wird als Black Box behandelt.
|
||||||
|
* Somit müssen alle Tests, die einen existierenden Datensatz benötigen, diesen im Test anlegen.
|
||||||
|
*/
|
||||||
|
@SpringBootTest(classes = TodosControlLayer.class)
|
||||||
|
class TodosServiceIntegrationTest {
|
||||||
|
|
||||||
|
// dieses Objekt wird als Mock instruiert
|
||||||
|
// Answers.RETURNS_MOCKS ist notwendig, da Methoden in Service-Init-Methode bereits aufgerufen werden
|
||||||
|
@MockBean(answer = Answers.RETURNS_MOCKS)
|
||||||
|
TodosRepository repo;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
TodosService service;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Testfall:
|
||||||
|
* - alle Todos auslesen -> kein Fehler
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testFindAllTodos() {
|
||||||
|
when(repo.findAll()).thenReturn(List.of(new TodoEntity(1L, "test")));
|
||||||
|
final var result = service.findAll();
|
||||||
|
assertThat(result).hasSize(1) //
|
||||||
|
.element(0).extracting(Todo::id, Todo::title).containsExactly(1L, "test");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Testfall:
|
||||||
|
* - Anlegen eines Todos -> ID besetzt
|
||||||
|
* - Auslesen -> gefunden mit entsprechenden Werten
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("ConstantConditions")
|
||||||
|
void testCreateTodo() {
|
||||||
|
final var newTodo = new Todo(null, "test-todo");
|
||||||
|
when(repo.save(any())).thenReturn(new TodoEntity(5L, "test-todo"));
|
||||||
|
// create
|
||||||
|
final var result = this.service.create(newTodo);
|
||||||
|
// find out id
|
||||||
|
assertThat(result).extracting(Todo::id).isEqualTo(5L);
|
||||||
|
verify(repo).save(refEq(new TodoEntity(null, "test-todo")));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Testfall:
|
||||||
|
* - Ändern eines bestehenden Todos
|
||||||
|
* - Aufruf der Repo-Methode und Rückgabewert prüfen
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("ConstantConditions")
|
||||||
|
void testUpdateExisting() {
|
||||||
|
final var todo = new Todo(5L, "test-todo");
|
||||||
|
when(repo.existsById(todo.id())).thenReturn(true);
|
||||||
|
// Test
|
||||||
|
this.service.update(todo);
|
||||||
|
// Assert
|
||||||
|
verify(repo).save(refEq(new TodoEntity(5L, "test-todo")));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Testfall:
|
||||||
|
* - Ändern eines nicht existenten Todos
|
||||||
|
* - Aufruf der Repo-Methode und Rückgabewert prüfen
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testUpdateNotExisting() {
|
||||||
|
final var todo = new Todo(5L, "test-todo");
|
||||||
|
when(repo.existsById(todo.id())).thenReturn(false);
|
||||||
|
// Test+Assert
|
||||||
|
assertThatThrownBy(() -> this.service.update(todo))
|
||||||
|
.isInstanceOf(NotFoundException.class);
|
||||||
|
verify(repo).existsById(todo.id());
|
||||||
|
verifyNoMoreInteractions(repo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Testfall:
|
||||||
|
* - Löschen eines existenten Todos
|
||||||
|
* - Aufruf der Repo-Methode und Rückgabewert prüfen
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testDeleteExisting() {
|
||||||
|
when(repo.existsById(5L)).thenReturn(true);
|
||||||
|
// Test
|
||||||
|
this.service.delete(5L);
|
||||||
|
// Assert
|
||||||
|
verify(repo).deleteById(5L);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Testfall:
|
||||||
|
* - Löschen eines nicht existenten Todos
|
||||||
|
* - Aufruf der Repo-Methode und Rückgabewert prüfen
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testDeleteNotExisting() {
|
||||||
|
when(repo.existsById(5L)).thenReturn(false);
|
||||||
|
// Test+Assert
|
||||||
|
assertThatThrownBy(() -> this.service.delete(5L))
|
||||||
|
.isInstanceOf(NotFoundException.class);
|
||||||
|
verify(repo).existsById(5L);
|
||||||
|
verifyNoMoreInteractions(repo);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.baeldung.sample.shared;
|
||||||
|
|
||||||
|
import org.springframework.boot.test.context.TestConfiguration;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
|
||||||
|
|
||||||
|
@TestConfiguration
|
||||||
|
public class EnableBeanValidation {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public MethodValidationPostProcessor validator() {
|
||||||
|
return new MethodValidationPostProcessor();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue