routingFunction() {
- return route(GET("/p?ths"), serverRequest -> ok().body(fromValue("/p?ths"))).andRoute(GET("/test/{*id}"), serverRequest -> ok().body(fromValue(serverRequest.pathVariable("id"))))
- .andRoute(GET("/*card"), serverRequest -> ok().body(fromValue("/*card path was accessed")))
+ return route(GET("/t?st"), serverRequest -> ok().body(fromValue("Path /t?st is accessed"))).andRoute(GET("/test/{*id}"), serverRequest -> ok().body(fromValue(serverRequest.pathVariable("id"))))
+ .andRoute(GET("/baeldung/*Id"), serverRequest -> ok().body(fromValue("/baeldung/*Id path was accessed")))
.andRoute(GET("/{var1}_{var2}"), serverRequest -> ok().body(fromValue(serverRequest.pathVariable("var1") + " , " + serverRequest.pathVariable("var2"))))
.andRoute(GET("/{baeldung:[a-z]+}"), serverRequest -> ok().body(fromValue("/{baeldung:[a-z]+} was accessed and baeldung=" + serverRequest.pathVariable("baeldung"))))
- .and(RouterFunctions.resources("/files/{*filepaths}", new ClassPathResource("files/")));
+ .and(RouterFunctions.resources("/files/{*filepaths}", new ClassPathResource("files/")))
+ .and(RouterFunctions.resources("/resources/**", new ClassPathResource("resources/")));
}
WebServer start() throws Exception {
diff --git a/spring-5-reactive-modules/spring-5-reactive/src/main/resources/resources/test/test.txt b/spring-5-reactive-modules/spring-5-reactive/src/main/resources/resources/test/test.txt
new file mode 100644
index 0000000000..30d74d2584
--- /dev/null
+++ b/spring-5-reactive-modules/spring-5-reactive/src/main/resources/resources/test/test.txt
@@ -0,0 +1 @@
+test
\ No newline at end of file
diff --git a/spring-5-reactive-modules/spring-5-reactive/src/test/java/com/baeldung/reactive/urlmatch/ExploreSpring5URLPatternUsingRouterFunctionsIntegrationTest.java b/spring-5-reactive-modules/spring-5-reactive/src/test/java/com/baeldung/reactive/urlmatch/ExploreSpring5URLPatternUsingRouterFunctionsIntegrationTest.java
index 91721d2cef..a77a67c6ba 100644
--- a/spring-5-reactive-modules/spring-5-reactive/src/test/java/com/baeldung/reactive/urlmatch/ExploreSpring5URLPatternUsingRouterFunctionsIntegrationTest.java
+++ b/spring-5-reactive-modules/spring-5-reactive/src/test/java/com/baeldung/reactive/urlmatch/ExploreSpring5URLPatternUsingRouterFunctionsIntegrationTest.java
@@ -27,12 +27,12 @@ public class ExploreSpring5URLPatternUsingRouterFunctionsIntegrationTest {
@Test
public void givenRouter_whenGetPathWithSingleCharWildcard_thenGotPathPattern() throws Exception {
client.get()
- .uri("/paths")
+ .uri("/test")
.exchange()
.expectStatus()
.isOk()
.expectBody(String.class)
- .isEqualTo("/p?ths");
+ .isEqualTo("Path /t?st is accessed");
}
@Test
@@ -50,12 +50,12 @@ public class ExploreSpring5URLPatternUsingRouterFunctionsIntegrationTest {
public void givenRouter_whenGetMultipleCharWildcard_thenGotPathPattern() throws Exception {
client.get()
- .uri("/wildcard")
+ .uri("/baeldung/tutorialId")
.exchange()
.expectStatus()
.isOk()
.expectBody(String.class)
- .isEqualTo("/*card path was accessed");
+ .isEqualTo("/baeldung/*Id path was accessed");
}
@Test
@@ -107,4 +107,14 @@ public class ExploreSpring5URLPatternUsingRouterFunctionsIntegrationTest {
.isEqualTo("hello");
}
+ @Test
+ public void givenRouter_whenAccess_thenGot() throws Exception {
+ client.get()
+ .uri("/resources/test/test.txt")
+ .exchange()
+ .expectStatus()
+ .isOk()
+ .expectBody(String.class)
+ .isEqualTo("test");
+ }
}
diff --git a/spring-5-reactive-modules/spring-5-reactive/src/test/java/com/baeldung/reactive/urlmatch/PathPatternsUsingHandlerMethodIntegrationTest.java b/spring-5-reactive-modules/spring-5-reactive/src/test/java/com/baeldung/reactive/urlmatch/PathPatternsUsingHandlerMethodIntegrationTest.java
index d4c1cfe4c8..0b4607b54a 100644
--- a/spring-5-reactive-modules/spring-5-reactive/src/test/java/com/baeldung/reactive/urlmatch/PathPatternsUsingHandlerMethodIntegrationTest.java
+++ b/spring-5-reactive-modules/spring-5-reactive/src/test/java/com/baeldung/reactive/urlmatch/PathPatternsUsingHandlerMethodIntegrationTest.java
@@ -27,13 +27,21 @@ public class PathPatternsUsingHandlerMethodIntegrationTest {
@Test
public void givenHandlerMethod_whenMultipleURIVariablePattern_then200() {
- client.get()
- .uri("/spring5/ab/cd")
+ client.get()
+ .uri("/spring5/baeldung/tutorial")
.exchange()
.expectStatus()
.is2xxSuccessful()
.expectBody()
- .equals("/ab/cd");
+ .equals("/baeldung/tutorial");
+
+ client.get()
+ .uri("/spring5/baeldung")
+ .exchange()
+ .expectStatus()
+ .is2xxSuccessful()
+ .expectBody()
+ .equals("/baeldung");
}
@Test
diff --git a/spring-boot-modules/spring-boot-3/pom.xml b/spring-boot-modules/spring-boot-3/pom.xml
new file mode 100644
index 0000000000..556168e205
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3/pom.xml
@@ -0,0 +1,128 @@
+
+
+ 4.0.0
+
+ com.baeldung
+ parent-boot-3
+ 0.0.1-SNAPSHOT
+ ../../parent-boot-3
+
+ spring-boot-3-sample
+ 0.0.1-SNAPSHOT
+ spring-boot-3-sample
+ Demo project for Spring Boot
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-hateoas
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ com.h2database
+ h2
+
+ runtime
+
+
+ org.springframework.boot
+ spring-boot-devtools
+ runtime
+ true
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+ org.mapstruct
+ mapstruct
+ ${mapstruct.version}
+ true
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ org.mapstruct
+ mapstruct-processor
+ ${mapstruct.version}
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+
+ org.projectlombok
+ lombok-mapstruct-binding
+ 0.2.0
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+ 1.5.2.Final
+ com.baeldung.sample.TodoApplication
+
+
+
diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/TodoApplication.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/TodoApplication.java
new file mode 100644
index 0000000000..72c9c0e482
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/TodoApplication.java
@@ -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);
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/CorsConfiguration.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/CorsConfiguration.java
new file mode 100644
index 0000000000..e02e3d5442
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/CorsConfiguration.java
@@ -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());
+ }
+ };
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/CorsConfigurationData.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/CorsConfigurationData.java
new file mode 100644
index 0000000000..1c4ef55e6c
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/CorsConfigurationData.java
@@ -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:
+ *
+ *
+ * server:
+ * endpoints:
+ * api:
+ * v1: /api/v1
+ *
+ */
+@Configuration
+@ConfigurationProperties(prefix = "cors.allow")
+@Data
+public class CorsConfigurationData {
+
+ private String[] origins = { "*" };
+ private boolean credentials = false;
+
+}
diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/GlobalExceptionHandler.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/GlobalExceptionHandler.java
new file mode 100644
index 0000000000..eb5435656e
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/GlobalExceptionHandler.java
@@ -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() {}
+
+}
diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/TodoDtoMapper.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/TodoDtoMapper.java
new file mode 100644
index 0000000000..a870eade7b
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/TodoDtoMapper.java
@@ -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;
+ };
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/TodoRequestDto.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/TodoRequestDto.java
new file mode 100644
index 0000000000..fb7e646772
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/TodoRequestDto.java
@@ -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;
+}
diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/TodoResponseDto.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/TodoResponseDto.java
new file mode 100644
index 0000000000..140db7b296
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/TodoResponseDto.java
@@ -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;
+
+}
diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/TodosController.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/TodosController.java
new file mode 100644
index 0000000000..7efa7dfee3
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/boundary/TodosController.java
@@ -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 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 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);
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/DataInitializationConfigurationData.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/DataInitializationConfigurationData.java
new file mode 100644
index 0000000000..5871325714
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/DataInitializationConfigurationData.java
@@ -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;
+
+}
diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/NotFoundException.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/NotFoundException.java
new file mode 100644
index 0000000000..b06b54f547
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/NotFoundException.java
@@ -0,0 +1,5 @@
+package com.baeldung.sample.control;
+
+public class NotFoundException extends RuntimeException {
+
+}
diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/Todo.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/Todo.java
new file mode 100644
index 0000000000..a972a9a21a
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/Todo.java
@@ -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);
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/TodoEntityMapper.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/TodoEntityMapper.java
new file mode 100644
index 0000000000..37a74f7f85
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/TodoEntityMapper.java
@@ -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);
+
+}
diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/TodosInitializer.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/TodosInitializer.java
new file mode 100644
index 0000000000..0c7b76d165
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/TodosInitializer.java
@@ -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!"));
+ }
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/TodosService.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/TodosService.java
new file mode 100644
index 0000000000..e10ef6440f
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/control/TodosService.java
@@ -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 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 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();
+ }
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/entity/TodoEntity.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/entity/TodoEntity.java
new file mode 100644
index 0000000000..e551876584
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/entity/TodoEntity.java
@@ -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;
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/entity/TodosRepository.java b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/entity/TodosRepository.java
new file mode 100644
index 0000000000..9f305a61c2
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3/src/main/java/com/baeldung/sample/entity/TodosRepository.java
@@ -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 {
+
+}
diff --git a/spring-boot-modules/spring-boot-3/src/main/resources/application.yml b/spring-boot-modules/spring-boot-3/src/main/resources/application.yml
new file mode 100644
index 0000000000..9a966a5bbd
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3/src/main/resources/application.yml
@@ -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}
diff --git a/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/boundary/TodosBoundaryLayer.java b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/boundary/TodosBoundaryLayer.java
new file mode 100644
index 0000000000..39797bf2fb
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/boundary/TodosBoundaryLayer.java
@@ -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 {
+}
diff --git a/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/boundary/TodosControllerApiIntegrationTest.java b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/boundary/TodosControllerApiIntegrationTest.java
new file mode 100644
index 0000000000..680b6c85bb
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/boundary/TodosControllerApiIntegrationTest.java
@@ -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());
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/control/TodosControlLayer.java b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/control/TodosControlLayer.java
new file mode 100644
index 0000000000..ac07355eb9
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/control/TodosControlLayer.java
@@ -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 {
+}
diff --git a/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/control/TodosInitializerActivationIntegrationTest.java b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/control/TodosInitializerActivationIntegrationTest.java
new file mode 100644
index 0000000000..ff28756e8e
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/control/TodosInitializerActivationIntegrationTest.java
@@ -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());
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/control/TodosServiceDatabaseIntegrationTest.java b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/control/TodosServiceDatabaseIntegrationTest.java
new file mode 100644
index 0000000000..4344e4ec3f
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/control/TodosServiceDatabaseIntegrationTest.java
@@ -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);
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/control/TodosServiceIntegrationTest.java b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/control/TodosServiceIntegrationTest.java
new file mode 100644
index 0000000000..49bf34ac80
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/control/TodosServiceIntegrationTest.java
@@ -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);
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/shared/EnableBeanValidation.java b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/shared/EnableBeanValidation.java
new file mode 100644
index 0000000000..5780ac46fe
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3/src/test/java/com/baeldung/sample/shared/EnableBeanValidation.java
@@ -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();
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/jandexlib/JandexScannerService.java b/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/jandexlib/JandexScannerService.java
index fc10db223e..7ed41af65d 100644
--- a/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/jandexlib/JandexScannerService.java
+++ b/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/jandexlib/JandexScannerService.java
@@ -27,7 +27,9 @@ public class JandexScannerService implements SampleAnnotationScanner {
try {
final IndexReader reader = new IndexReader(appFile.getInputStream());
Index jandexFile = reader.read();
- final List appAnnotationList = jandexFile.getAnnotations(DotName.createSimple("com.baeldung.annotation.scanner.SampleAnnotation"));
+ final List appAnnotationList = jandexFile
+ .getAnnotations(DotName
+ .createSimple("com.baeldung.annotation.scanner.SampleAnnotation"));
List annotatedMethods = new ArrayList<>();
for (AnnotationInstance annotationInstance : appAnnotationList) {
if (annotationInstance.target()
@@ -48,7 +50,9 @@ public class JandexScannerService implements SampleAnnotationScanner {
try {
final IndexReader reader = new IndexReader(appFile.getInputStream());
Index jandexFile = reader.read();
- final List appAnnotationList = jandexFile.getAnnotations(DotName.createSimple("com.baeldung.annotation.scanner.SampleAnnotation"));
+ final List appAnnotationList = jandexFile
+ .getAnnotations(DotName
+ .createSimple("com.baeldung.annotation.scanner.SampleAnnotation"));
List annotatedClasses = new ArrayList<>();
for (AnnotationInstance annotationInstance : appAnnotationList) {
if (annotationInstance.target()
diff --git a/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/javareflectionlib/JavaReflectionsScannerService.java b/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/javareflectionlib/JavaReflectionsScannerService.java
index 2833bb1326..76a2293965 100644
--- a/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/javareflectionlib/JavaReflectionsScannerService.java
+++ b/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/javareflectionlib/JavaReflectionsScannerService.java
@@ -1,5 +1,6 @@
package com.baeldung.annotation.scanner.javareflectionlib;
+import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
@@ -37,9 +38,14 @@ public class JavaReflectionsScannerService implements SampleAnnotationScanner {
try {
Class> clazz = ClassLoader.getSystemClassLoader()
.loadClass("com.baeldung.annotation.scanner.SampleAnnotatedClass");
- SampleAnnotation classAnnotation = clazz.getAnnotation(SampleAnnotation.class);
List annotatedClasses = new ArrayList<>();
- annotatedClasses.add(classAnnotation.name());
+ Annotation[] classAnnotations = clazz.getAnnotations();
+ for (Annotation annotation : classAnnotations) {
+ if (annotation.annotationType()
+ .equals(SampleAnnotation.class)) {
+ annotatedClasses.add(((SampleAnnotation) annotation).name());
+ }
+ }
return Collections.unmodifiableList(annotatedClasses);
} catch (ClassNotFoundException e) {
throw new UnexpectedScanException(e);
diff --git a/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/reflectionslib/ReflectionsScannerService.java b/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/reflectionslib/ReflectionsScannerService.java
index 82a10e21aa..9cee4f19e2 100644
--- a/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/reflectionslib/ReflectionsScannerService.java
+++ b/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/reflectionslib/ReflectionsScannerService.java
@@ -16,7 +16,8 @@ public class ReflectionsScannerService implements SampleAnnotationScanner {
@Override
public List scanAnnotatedMethods() {
Reflections reflections = new Reflections("com.baeldung.annotation.scanner");
- Set methods = reflections.getMethodsAnnotatedWith(SampleAnnotation.class);
+ Set methods = reflections
+ .getMethodsAnnotatedWith(SampleAnnotation.class);
return methods.stream()
.map(method -> method.getAnnotation(SampleAnnotation.class)
.name())
@@ -26,7 +27,8 @@ public class ReflectionsScannerService implements SampleAnnotationScanner {
@Override
public List scanAnnotatedClasses() {
Reflections reflections = new Reflections("com.baeldung.annotation.scanner");
- Set> types = reflections.getTypesAnnotatedWith(SampleAnnotation.class);
+ Set> types = reflections
+ .getTypesAnnotatedWith(SampleAnnotation.class);
return types.stream()
.map(clazz -> clazz.getAnnotation(SampleAnnotation.class)
.name())
diff --git a/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/springcontextlib/SpringBeanAnnotationScannerService.java b/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/springcontextlib/SpringContextAnnotationScannerService.java
similarity index 80%
rename from spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/springcontextlib/SpringBeanAnnotationScannerService.java
rename to spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/springcontextlib/SpringContextAnnotationScannerService.java
index cd31ae686e..8d52067ccc 100644
--- a/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/springcontextlib/SpringBeanAnnotationScannerService.java
+++ b/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/springcontextlib/SpringContextAnnotationScannerService.java
@@ -17,7 +17,7 @@ import com.baeldung.annotation.scanner.SampleAnnotationScanner;
import com.baeldung.annotation.scanner.ScanNotSupportedException;
@Service
-public class SpringBeanAnnotationScannerService implements SampleAnnotationScanner {
+public class SpringContextAnnotationScannerService implements SampleAnnotationScanner {
@Override
public List scanAnnotatedMethods() {
throw new ScanNotSupportedException();
@@ -25,13 +25,16 @@ public class SpringBeanAnnotationScannerService implements SampleAnnotationScann
@Override
public List scanAnnotatedClasses() {
- ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
+ ClassPathScanningCandidateComponentProvider provider =
+ new ClassPathScanningCandidateComponentProvider(false);
provider.addIncludeFilter(new AnnotationTypeFilter(SampleAnnotation.class));
- Set beanDefs = provider.findCandidateComponents("com.baeldung.annotation.scanner");
+ Set beanDefs = provider
+ .findCandidateComponents("com.baeldung.annotation.scanner");
List annotatedBeans = new ArrayList<>();
for (BeanDefinition bd : beanDefs) {
if (bd instanceof AnnotatedBeanDefinition) {
- Map annotAttributeMap = ((AnnotatedBeanDefinition) bd).getMetadata()
+ Map annotAttributeMap = ((AnnotatedBeanDefinition) bd)
+ .getMetadata()
.getAnnotationAttributes(SampleAnnotation.class.getCanonicalName());
annotatedBeans.add(annotAttributeMap.get("name")
.toString());
diff --git a/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/springcorelib/SpringCoreAnnotationScannerService.java b/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/springcorelib/SpringCoreAnnotationScannerService.java
index f3421fe46d..14542a9104 100644
--- a/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/springcorelib/SpringCoreAnnotationScannerService.java
+++ b/spring-boot-modules/spring-boot-libraries-2/src/main/java/com/baeldung/annotation/scanner/springcorelib/SpringCoreAnnotationScannerService.java
@@ -8,21 +8,24 @@ import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.ClassUtils;
-import com.baeldung.annotation.scanner.SampleAnnotationScanner;
import com.baeldung.annotation.scanner.SampleAnnotatedClass;
import com.baeldung.annotation.scanner.SampleAnnotation;
+import com.baeldung.annotation.scanner.SampleAnnotationScanner;
import com.baeldung.annotation.scanner.ScanNotSupportedException;
@Service
public class SpringCoreAnnotationScannerService implements SampleAnnotationScanner {
@Override
public List scanAnnotatedMethods() {
- final Class> userClass = ClassUtils.getUserClass(SampleAnnotatedClass.class);
- return Arrays.stream(userClass.getMethods())
- .filter(method -> AnnotationUtils.getAnnotation(method, SampleAnnotation.class) != null)
+ Class> userClass = ClassUtils.getUserClass(SampleAnnotatedClass.class);
+ List annotatedMethods = Arrays.stream(userClass.getMethods())
+ .filter(method -> AnnotationUtils
+ .getAnnotation(method, SampleAnnotation.class) != null)
.map(method -> method.getAnnotation(SampleAnnotation.class)
.name())
.collect(Collectors.toList());
+
+ return annotatedMethods;
}
@Override
diff --git a/spring-boot-modules/spring-boot-libraries-2/src/test/java/com/baeldung/annotation/scanner/SampleAnnotationScannerUnitTest.java b/spring-boot-modules/spring-boot-libraries-2/src/test/java/com/baeldung/annotation/scanner/SampleAnnotationScannerUnitTest.java
index 80eca2b4c5..7c3d01b5e8 100644
--- a/spring-boot-modules/spring-boot-libraries-2/src/test/java/com/baeldung/annotation/scanner/SampleAnnotationScannerUnitTest.java
+++ b/spring-boot-modules/spring-boot-libraries-2/src/test/java/com/baeldung/annotation/scanner/SampleAnnotationScannerUnitTest.java
@@ -28,7 +28,8 @@ public class SampleAnnotationScannerUnitTest {
assertNotNull(annotatedClasses);
assertEquals(4, annotatedClasses.size());
- annotatedClasses.forEach(annotValue -> assertEquals("SampleAnnotatedClass", annotValue));
+ annotatedClasses.forEach(annotValue -> assertEquals("SampleAnnotatedClass",
+ annotValue));
}
@Test
@@ -41,7 +42,7 @@ public class SampleAnnotationScannerUnitTest {
assertNotNull(annotatedMethods);
assertEquals(3, annotatedMethods.size());
- annotatedMethods.forEach(annotValue -> assertEquals("annotatedMethod", annotValue));
+ annotatedMethods.forEach(annotValue -> assertEquals("annotatedMethod",
+ annotValue));
}
-
}
diff --git a/spring-boot-modules/spring-boot-mvc-3/pom.xml b/spring-boot-modules/spring-boot-mvc-3/pom.xml
index 43a492786e..6b0477cfc8 100644
--- a/spring-boot-modules/spring-boot-mvc-3/pom.xml
+++ b/spring-boot-modules/spring-boot-mvc-3/pom.xml
@@ -40,6 +40,11 @@
commons-io
${commons-io.version}
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-mvc-3/src/main/java/com/baeldung/produceimage/controller/DataProducerController.java b/spring-boot-modules/spring-boot-mvc-3/src/main/java/com/baeldung/produceimage/controller/DataProducerController.java
index ab233a8b60..218be68a45 100644
--- a/spring-boot-modules/spring-boot-mvc-3/src/main/java/com/baeldung/produceimage/controller/DataProducerController.java
+++ b/spring-boot-modules/spring-boot-mvc-3/src/main/java/com/baeldung/produceimage/controller/DataProducerController.java
@@ -1,9 +1,12 @@
package com.baeldung.produceimage.controller;
import org.apache.commons.io.IOUtils;
+import org.springframework.core.io.InputStreamResource;
import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import java.io.IOException;
@@ -29,6 +32,18 @@ public class DataProducerController {
return IOUtils.toByteArray(in);
}
+ @GetMapping("/get-image-dynamic-type")
+ @ResponseBody
+ public ResponseEntity getImageDynamicType(@RequestParam("jpg") boolean jpg) {
+ final MediaType contentType = jpg ? MediaType.IMAGE_JPEG : MediaType.IMAGE_PNG;
+ final InputStream in = jpg ?
+ getClass().getResourceAsStream("/com/baeldung/produceimage/image.jpg") :
+ getClass().getResourceAsStream("/com/baeldung/produceimage/image.png");
+ return ResponseEntity.ok()
+ .contentType(contentType)
+ .body(new InputStreamResource(in));
+ }
+
@GetMapping(value = "/get-file", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public @ResponseBody byte[] getFile() throws IOException {
final InputStream in = getClass().getResourceAsStream("/com/baeldung/produceimage/data.txt");
diff --git a/spring-boot-modules/spring-boot-mvc-3/src/main/resources/com/baeldung/produceimage/data.txt b/spring-boot-modules/spring-boot-mvc-3/src/main/resources/com/baeldung/produceimage/data.txt
new file mode 100644
index 0000000000..5a8297fc5e
--- /dev/null
+++ b/spring-boot-modules/spring-boot-mvc-3/src/main/resources/com/baeldung/produceimage/data.txt
@@ -0,0 +1 @@
+Hello Baeldung!
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-mvc-3/src/main/resources/com/baeldung/produceimage/image.jpg b/spring-boot-modules/spring-boot-mvc-3/src/main/resources/com/baeldung/produceimage/image.jpg
new file mode 100644
index 0000000000..db3abce0d6
Binary files /dev/null and b/spring-boot-modules/spring-boot-mvc-3/src/main/resources/com/baeldung/produceimage/image.jpg differ
diff --git a/spring-boot-modules/spring-boot-mvc-3/src/main/resources/com/baeldung/produceimage/image.png b/spring-boot-modules/spring-boot-mvc-3/src/main/resources/com/baeldung/produceimage/image.png
new file mode 100644
index 0000000000..16d218e334
Binary files /dev/null and b/spring-boot-modules/spring-boot-mvc-3/src/main/resources/com/baeldung/produceimage/image.png differ
diff --git a/spring-boot-modules/spring-boot-mvc-3/src/test/java/com/baeldung/etag/EtagIntegrationTest.java b/spring-boot-modules/spring-boot-mvc-3/src/test/java/com/baeldung/etag/EtagIntegrationTest.java
index 97de6d06f1..d7b50cb7fb 100644
--- a/spring-boot-modules/spring-boot-mvc-3/src/test/java/com/baeldung/etag/EtagIntegrationTest.java
+++ b/spring-boot-modules/spring-boot-mvc-3/src/test/java/com/baeldung/etag/EtagIntegrationTest.java
@@ -9,7 +9,6 @@ import org.assertj.core.util.Preconditions;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.web.server.LocalServerPort;
@@ -26,7 +25,6 @@ import static org.junit.Assert.assertTrue;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ComponentScan(basePackageClasses = WebConfig.class)
-@EnableAutoConfiguration
public class EtagIntegrationTest {
@LocalServerPort
diff --git a/spring-boot-modules/spring-boot-mvc-3/src/test/java/com/baeldung/produceimage/DataProducerControllerIntegrationTest.java b/spring-boot-modules/spring-boot-mvc-3/src/test/java/com/baeldung/produceimage/DataProducerControllerIntegrationTest.java
new file mode 100644
index 0000000000..eb459d88cc
--- /dev/null
+++ b/spring-boot-modules/spring-boot-mvc-3/src/test/java/com/baeldung/produceimage/DataProducerControllerIntegrationTest.java
@@ -0,0 +1,37 @@
+package com.baeldung.produceimage;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@SpringBootTest(classes = ImageApplication.class,
+ webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@AutoConfigureMockMvc
+public class DataProducerControllerIntegrationTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Test
+ void givenJpgTrue_whenGetImageDynamicType_ThenContentTypeIsJpg() throws Exception {
+ mockMvc.perform(get("/get-image-dynamic-type?jpg=true"))
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(MediaType.IMAGE_JPEG))
+ .andExpect(header().stringValues(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_JPEG_VALUE));
+ }
+
+ @Test
+ void givenJpgFalse_whenGetImageDynamicType_ThenContentTypeIsFalse() throws Exception {
+ mockMvc.perform(get("/get-image-dynamic-type?jpg=false"))
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(MediaType.IMAGE_PNG))
+ .andExpect(header().stringValues(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE));
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-mvc-3/src/test/java/com/baeldung/springvalidation/UserAccountUnitTest.java b/spring-boot-modules/spring-boot-mvc-3/src/test/java/com/baeldung/springvalidation/UserAccountIntegrationTest.java
similarity index 84%
rename from spring-boot-modules/spring-boot-mvc-3/src/test/java/com/baeldung/springvalidation/UserAccountUnitTest.java
rename to spring-boot-modules/spring-boot-mvc-3/src/test/java/com/baeldung/springvalidation/UserAccountIntegrationTest.java
index 507321bf8e..7c23153951 100644
--- a/spring-boot-modules/spring-boot-mvc-3/src/test/java/com/baeldung/springvalidation/UserAccountUnitTest.java
+++ b/spring-boot-modules/spring-boot-mvc-3/src/test/java/com/baeldung/springvalidation/UserAccountIntegrationTest.java
@@ -1,9 +1,7 @@
package com.baeldung.springvalidation;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -15,7 +13,7 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
-public class UserAccountUnitTest {
+public class UserAccountIntegrationTest {
@Autowired
private MockMvc mockMvc;
@@ -46,10 +44,8 @@ public class UserAccountUnitTest {
public void givenSaveBasicInfoStep1_whenIncorrectInput_thenError() throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders.post("/saveBasicInfoStep1")
.accept(MediaType.TEXT_HTML))
- // .param("name", "test123")
- // .param("password", "pass"))
.andExpect(model().errorCount(2))
- // .andExpect(view().name("error"))
+ .andExpect(view().name("error"))
.andExpect(status().isOk())
.andDo(print());
}
diff --git a/spring-boot-modules/spring-boot-springdoc/README.md b/spring-boot-modules/spring-boot-springdoc/README.md
index 733e31e698..3d0fd19ab1 100644
--- a/spring-boot-modules/spring-boot-springdoc/README.md
+++ b/spring-boot-modules/spring-boot-springdoc/README.md
@@ -6,3 +6,4 @@
- [Swagger @Api Description Is Deprecated](https://www.baeldung.com/java-swagger-api-description-deprecated)
- [Set List of Objects in Swagger API Response](https://www.baeldung.com/java-swagger-set-list-response)
- [Configure JWT Authentication for OpenAPI](https://www.baeldung.com/openapi-jwt-authentication)
+- [Apply Default Global SecurityScheme in springdoc-openapi](https://www.baeldung.com/spring-openapi-global-securityscheme)
diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/DefaultGlobalSecuritySchemeApplication.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/DefaultGlobalSecuritySchemeApplication.java
new file mode 100644
index 0000000000..1ce81a1e83
--- /dev/null
+++ b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/DefaultGlobalSecuritySchemeApplication.java
@@ -0,0 +1,32 @@
+package com.baeldung.defaultglobalsecurityscheme;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.web.SecurityFilterChain;
+
+import io.swagger.v3.oas.annotations.OpenAPIDefinition;
+import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
+import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
+import io.swagger.v3.oas.annotations.info.Info;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.security.SecurityScheme;
+
+@SpringBootApplication
+@OpenAPIDefinition(info = @Info(title = "Apply Default Global SecurityScheme in springdoc-openapi", version = "1.0.0"), security = { @SecurityRequirement(name = "api_key") })
+@SecurityScheme(type = SecuritySchemeType.APIKEY, name = "api_key", in = SecuritySchemeIn.HEADER)
+public class DefaultGlobalSecuritySchemeApplication {
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ return http.authorizeHttpRequests(authorizeRequests -> authorizeRequests.antMatchers("/api/auth/**", "/swagger-ui-custom.html", "/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**", "/webjars/**", "/swagger-ui/index.html", "/api-docs/**")
+ .permitAll()
+ .anyRequest()
+ .authenticated())
+ .build();
+ }
+
+ public static void main(String[] args) {
+ SpringApplication.run(DefaultGlobalSecuritySchemeApplication.class, args);
+ }
+}
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/controller/DefaultGlobalSecuritySchemeOpenApiController.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/controller/DefaultGlobalSecuritySchemeOpenApiController.java
new file mode 100644
index 0000000000..4ad7a2a2c3
--- /dev/null
+++ b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/controller/DefaultGlobalSecuritySchemeOpenApiController.java
@@ -0,0 +1,61 @@
+package com.baeldung.defaultglobalsecurityscheme.controller;
+
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+
+import javax.validation.Valid;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.baeldung.defaultglobalsecurityscheme.dto.LoginDto;
+import com.baeldung.defaultglobalsecurityscheme.dto.ApplicationExceptionDto;
+import com.baeldung.defaultglobalsecurityscheme.dto.PingResponseDto;
+import com.baeldung.defaultglobalsecurityscheme.dto.TokenDto;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.ExampleObject;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.security.SecurityRequirements;
+
+@RestController
+@RequestMapping("/")
+public class DefaultGlobalSecuritySchemeOpenApiController {
+ @RequestMapping(method = RequestMethod.POST, value = "/login", produces = { "application/json" }, consumes = { "application/json" })
+ @Operation(operationId = "login", responses = {
+ @ApiResponse(responseCode = "200", description = "api_key to be used in the secured-ping entry point", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = TokenDto.class)) }),
+ @ApiResponse(responseCode = "401", description = "Unauthorized request", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = ApplicationExceptionDto.class)) }) })
+ @SecurityRequirements()
+ public ResponseEntity login(@Parameter(name = "LoginDto", description = "Login") @Valid @RequestBody(required = true) LoginDto loginDto) {
+ TokenDto token = new TokenDto();
+ token.setRaw("Generated Token");
+ return ResponseEntity.ok(token);
+ }
+
+ @Operation(operationId = "ping", responses = {
+ @ApiResponse(responseCode = "200", description = "Ping that needs an api_key attribute in the header", content = {
+ @Content(mediaType = "application/json", schema = @Schema(implementation = PingResponseDto.class), examples = { @ExampleObject(value = "{ pong: '2022-06-17T18:30:33.465+02:00' }") }) }),
+ @ApiResponse(responseCode = "401", description = "Unauthorized request", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = ApplicationExceptionDto.class)) }),
+ @ApiResponse(responseCode = "403", description = "Forbidden request", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = ApplicationExceptionDto.class)) }) })
+ @RequestMapping(method = RequestMethod.GET, value = "/ping", produces = { "application/json" })
+ public ResponseEntity ping(@RequestHeader(name = "api_key", required = false) String api_key) {
+ int year = 2000;
+ int month = 1;
+ int dayOfMonth = 1;
+ int hour = 0;
+ int minute = 0;
+ int second = 0;
+ int nanoSeccond = 0;
+ ZoneOffset offset = ZoneOffset.UTC;
+ PingResponseDto response = new PingResponseDto();
+ response.setPong(OffsetDateTime.of(year, month, dayOfMonth, hour, minute, second, nanoSeccond, offset));
+ return ResponseEntity.ok(response);
+ }
+}
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/ApplicationExceptionDto.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/ApplicationExceptionDto.java
new file mode 100644
index 0000000000..5fb63793c4
--- /dev/null
+++ b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/ApplicationExceptionDto.java
@@ -0,0 +1,26 @@
+package com.baeldung.defaultglobalsecurityscheme.dto;
+
+public class ApplicationExceptionDto {
+ private long errorCode;
+ private String description;
+
+ public ApplicationExceptionDto() {
+ super();
+ }
+
+ public long getErrorCode() {
+ return errorCode;
+ }
+
+ public void setErrorCode(long errorCode) {
+ this.errorCode = errorCode;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+}
diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/LoginDto.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/LoginDto.java
new file mode 100644
index 0000000000..cf88cc4d98
--- /dev/null
+++ b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/LoginDto.java
@@ -0,0 +1,103 @@
+package com.baeldung.defaultglobalsecurityscheme.dto;
+
+import java.util.Objects;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+/**
+ * LoginDto
+ */
+
+@JsonTypeName("Login")
+public class LoginDto {
+
+ @JsonProperty("user")
+ private String user;
+
+ @JsonProperty("pass")
+ private String pass;
+
+ public LoginDto user(String user) {
+ this.user = user;
+ return this;
+ }
+
+ /**
+ * Get user
+ * @return user
+ */
+
+ @Schema(name = "user", required = true)
+ public String getUser() {
+ return user;
+ }
+
+ public void setUser(String user) {
+ this.user = user;
+ }
+
+ public LoginDto pass(String pass) {
+ this.pass = pass;
+ return this;
+ }
+
+ /**
+ * Get pass
+ * @return pass
+ */
+
+ @Schema(name = "pass", required = true)
+ public String getPass() {
+ return pass;
+ }
+
+ public void setPass(String pass) {
+ this.pass = pass;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ LoginDto login = (LoginDto) o;
+ return Objects.equals(this.user, login.user) && Objects.equals(this.pass, login.pass);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(user, pass);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class LoginDto {\n");
+ sb.append(" user: ")
+ .append(toIndentedString(user))
+ .append("\n");
+ sb.append(" pass: ")
+ .append(toIndentedString(pass))
+ .append("\n");
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Convert the given object to string with each line indented by 4 spaces
+ * (except the first line).
+ */
+ private String toIndentedString(Object o) {
+ if (o == null) {
+ return "null";
+ }
+ return o.toString()
+ .replace("\n", "\n ");
+ }
+}
diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/PingResponseDto.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/PingResponseDto.java
new file mode 100644
index 0000000000..0d367785d8
--- /dev/null
+++ b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/PingResponseDto.java
@@ -0,0 +1,84 @@
+package com.baeldung.defaultglobalsecurityscheme.dto;
+
+import java.time.OffsetDateTime;
+import java.util.Objects;
+
+import javax.validation.Valid;
+
+import org.springframework.format.annotation.DateTimeFormat;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+/**
+ * SecuredPingResponseDto
+ */
+
+@JsonTypeName("PingResponse")
+public class PingResponseDto {
+
+ @JsonProperty("pong")
+ @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
+ private OffsetDateTime pong;
+
+ public PingResponseDto pong(OffsetDateTime pong) {
+ this.pong = pong;
+ return this;
+ }
+
+ /**
+ * Get pong
+ * @return pong
+ */
+ @Valid
+ @Schema(name = "pong", required = false)
+ public OffsetDateTime getPong() {
+ return pong;
+ }
+
+ public void setPong(OffsetDateTime pong) {
+ this.pong = pong;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ PingResponseDto securedPingResponse = (PingResponseDto) o;
+ return Objects.equals(this.pong, securedPingResponse.pong);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(pong);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class PingResponseDto {\n");
+ sb.append(" pong: ")
+ .append(toIndentedString(pong))
+ .append("\n");
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Convert the given object to string with each line indented by 4 spaces
+ * (except the first line).
+ */
+ private String toIndentedString(Object o) {
+ if (o == null) {
+ return "null";
+ }
+ return o.toString()
+ .replace("\n", "\n ");
+ }
+}
diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/TokenDto.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/TokenDto.java
new file mode 100644
index 0000000000..d8f7daa09c
--- /dev/null
+++ b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/TokenDto.java
@@ -0,0 +1,57 @@
+package com.baeldung.defaultglobalsecurityscheme.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+/**
+ * LoginDto
+ */
+
+@JsonTypeName("Token")
+public class TokenDto {
+
+ @JsonProperty("raw")
+ private String raw;
+
+ @Schema(name = "raw", example = "app token")
+ public String getRaw() {
+ return raw;
+ }
+
+ public void setRaw(String raw) {
+ this.raw = raw;
+ }
+
+ @Override
+ public String toString() {
+ return "TokenDto [raw=" + raw + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((raw == null) ? 0 : raw.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ TokenDto other = (TokenDto) obj;
+ if (raw == null) {
+ if (other.raw != null)
+ return false;
+ } else if (!raw.equals(other.raw))
+ return false;
+ return true;
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-testing-2/src/test/java/com/baeldung/keycloaktestcontainers/IntegrationTest.java b/spring-boot-modules/spring-boot-testing-2/src/test/java/com/baeldung/keycloaktestcontainers/KeycloakTestContainers.java
similarity index 95%
rename from spring-boot-modules/spring-boot-testing-2/src/test/java/com/baeldung/keycloaktestcontainers/IntegrationTest.java
rename to spring-boot-modules/spring-boot-testing-2/src/test/java/com/baeldung/keycloaktestcontainers/KeycloakTestContainers.java
index 902c27b16e..44e24c98d1 100644
--- a/spring-boot-modules/spring-boot-testing-2/src/test/java/com/baeldung/keycloaktestcontainers/IntegrationTest.java
+++ b/spring-boot-modules/spring-boot-testing-2/src/test/java/com/baeldung/keycloaktestcontainers/KeycloakTestContainers.java
@@ -26,11 +26,11 @@ import org.springframework.web.reactive.function.client.WebClient;
import dasniko.testcontainers.keycloak.KeycloakContainer;
import io.restassured.RestAssured;
-@ContextConfiguration(initializers = { IntegrationTest.Initializer.class })
+@ContextConfiguration(initializers = { KeycloakTestContainers.Initializer.class })
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
-public abstract class IntegrationTest {
+public abstract class KeycloakTestContainers {
- private static final Logger LOGGER = LoggerFactory.getLogger(IntegrationTest.class.getName());
+ private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakTestContainers.class.getName());
@LocalServerPort
private int port;
diff --git a/spring-boot-modules/spring-boot-testing-2/src/test/java/com/baeldung/keycloaktestcontainers/UserControllerIntegrationTest.java b/spring-boot-modules/spring-boot-testing-2/src/test/java/com/baeldung/keycloaktestcontainers/UserControllerManualTest.java
similarity index 77%
rename from spring-boot-modules/spring-boot-testing-2/src/test/java/com/baeldung/keycloaktestcontainers/UserControllerIntegrationTest.java
rename to spring-boot-modules/spring-boot-testing-2/src/test/java/com/baeldung/keycloaktestcontainers/UserControllerManualTest.java
index 4015612860..817c8ae130 100644
--- a/spring-boot-modules/spring-boot-testing-2/src/test/java/com/baeldung/keycloaktestcontainers/UserControllerIntegrationTest.java
+++ b/spring-boot-modules/spring-boot-testing-2/src/test/java/com/baeldung/keycloaktestcontainers/UserControllerManualTest.java
@@ -5,7 +5,11 @@ import static org.hamcrest.Matchers.equalTo;
import org.junit.jupiter.api.Test;
-class UserControllerIntegrationTest extends IntegrationTest {
+/**
+ * Requires Docker running on the machine to run without errors
+ * Therefore, skipped from pipeline
+ */
+class UserControllerManualTest extends KeycloakTestContainers {
@Test
void givenAuthenticatedUser_whenGetMe_shouldReturnMyInfo() {
diff --git a/spring-cloud-modules/spring-cloud-archaius/pom.xml b/spring-cloud-modules/spring-cloud-archaius/pom.xml
index 1208daaba6..66b7bb9b19 100644
--- a/spring-cloud-modules/spring-cloud-archaius/pom.xml
+++ b/spring-cloud-modules/spring-cloud-archaius/pom.xml
@@ -16,11 +16,11 @@
- basic-config
additional-sources-simple
+ basic-config
+ dynamodb-config
extra-configs
jdbc-config
- dynamodb-config
zookeeper-config
diff --git a/spring-cloud-modules/spring-cloud-eureka/pom.xml b/spring-cloud-modules/spring-cloud-eureka/pom.xml
index 2a9c4f5fde..23523f2c2f 100644
--- a/spring-cloud-modules/spring-cloud-eureka/pom.xml
+++ b/spring-cloud-modules/spring-cloud-eureka/pom.xml
@@ -16,10 +16,11 @@
- spring-cloud-eureka-server
spring-cloud-eureka-client
+ spring-cloud-eureka-client-profiles
spring-cloud-eureka-feign-client
spring-cloud-eureka-feign-client-integration-test
+ spring-cloud-eureka-server
diff --git a/spring-cloud-modules/spring-cloud-hystrix/pom.xml b/spring-cloud-modules/spring-cloud-hystrix/pom.xml
index e1f30c25dc..41548ba36a 100644
--- a/spring-cloud-modules/spring-cloud-hystrix/pom.xml
+++ b/spring-cloud-modules/spring-cloud-hystrix/pom.xml
@@ -15,9 +15,9 @@
+ feign-rest-consumer
rest-producer
rest-consumer
- feign-rest-consumer
\ No newline at end of file
diff --git a/spring-cloud-modules/spring-cloud-netflix-sidecar/echo-demo/pom.xml b/spring-cloud-modules/spring-cloud-netflix-sidecar/echo-demo/pom.xml
index 60b6eab020..d8b1654eaa 100644
--- a/spring-cloud-modules/spring-cloud-netflix-sidecar/echo-demo/pom.xml
+++ b/spring-cloud-modules/spring-cloud-netflix-sidecar/echo-demo/pom.xml
@@ -9,7 +9,6 @@
com.baeldung.cloud
spring-cloud-netflix-sidecar
0.0.1-SNAPSHOT
- ../pom.xml
diff --git a/spring-cloud-modules/spring-cloud-netflix-sidecar/pom.xml b/spring-cloud-modules/spring-cloud-netflix-sidecar/pom.xml
index 0d95714bf9..3a72496279 100644
--- a/spring-cloud-modules/spring-cloud-netflix-sidecar/pom.xml
+++ b/spring-cloud-modules/spring-cloud-netflix-sidecar/pom.xml
@@ -11,10 +11,9 @@
Netflix Sidecar project for Spring Boot
- com.baeldung
- parent-boot-2
- 0.0.1-SNAPSHOT
- ../../parent-boot-2
+ com.baeldung.spring.cloud
+ spring-cloud-modules
+ 1.0.0-SNAPSHOT
diff --git a/spring-cloud-modules/spring-cloud-netflix-sidecar/sidecar-demo/pom.xml b/spring-cloud-modules/spring-cloud-netflix-sidecar/sidecar-demo/pom.xml
index 3c8d498456..200897652f 100644
--- a/spring-cloud-modules/spring-cloud-netflix-sidecar/sidecar-demo/pom.xml
+++ b/spring-cloud-modules/spring-cloud-netflix-sidecar/sidecar-demo/pom.xml
@@ -11,7 +11,6 @@
com.baeldung.cloud
spring-cloud-netflix-sidecar
0.0.1-SNAPSHOT
- ../pom.xml
diff --git a/spring-cloud-modules/spring-cloud-openfeign/pom.xml b/spring-cloud-modules/spring-cloud-openfeign/pom.xml
index 480663eb1c..5657577cb0 100644
--- a/spring-cloud-modules/spring-cloud-openfeign/pom.xml
+++ b/spring-cloud-modules/spring-cloud-openfeign/pom.xml
@@ -9,10 +9,9 @@
OpenFeign project for Spring Boot
- com.baeldung
- parent-boot-2
- 0.0.1-SNAPSHOT
- ../../parent-boot-2
+ com.baeldung.spring.cloud
+ spring-cloud-modules
+ 1.0.0-SNAPSHOT
diff --git a/spring-cloud-modules/spring-cloud-zuul/pom.xml b/spring-cloud-modules/spring-cloud-zuul/pom.xml
index e99cbdf355..b04a21da1c 100644
--- a/spring-cloud-modules/spring-cloud-zuul/pom.xml
+++ b/spring-cloud-modules/spring-cloud-zuul/pom.xml
@@ -17,9 +17,9 @@
spring-zuul-foos-resource
- spring-zuul-ui
- spring-zuul-rate-limiting
spring-zuul-post-filter
+ spring-zuul-rate-limiting
+ spring-zuul-ui
diff --git a/spring-jms/README.md b/spring-jms/README.md
index fdeb64953a..666e32fa4b 100644
--- a/spring-jms/README.md
+++ b/spring-jms/README.md
@@ -4,3 +4,4 @@ This module contains articles about Spring with JMS
### Relevant Articles:
- [Getting Started with Spring JMS](https://www.baeldung.com/spring-jms)
+- [Testing Spring JMS](https://www.baeldung.com/spring-jms-testing)
diff --git a/spring-jms/pom.xml b/spring-jms/pom.xml
index 09bf854221..ab202402f3 100644
--- a/spring-jms/pom.xml
+++ b/spring-jms/pom.xml
@@ -41,6 +41,25 @@
${spring-boot-test.version}
test
+
+
+ org.mockito
+ mockito-core
+ 4.6.1
+ test
+
+
+ org.apache.activemq.tooling
+ activemq-junit
+ 5.16.5
+ test
+
+
+ org.testcontainers
+ testcontainers
+ 1.17.3
+ test
+
diff --git a/spring-jms/src/main/java/com/baeldung/spring/jms/testing/JmsApplication.java b/spring-jms/src/main/java/com/baeldung/spring/jms/testing/JmsApplication.java
new file mode 100644
index 0000000000..0a89d422b4
--- /dev/null
+++ b/spring-jms/src/main/java/com/baeldung/spring/jms/testing/JmsApplication.java
@@ -0,0 +1,13 @@
+package com.baeldung.spring.jms.testing;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.annotation.ComponentScan;
+
+@ComponentScan
+public class JmsApplication {
+
+ public static void main(String[] args) {
+ ApplicationContext context = new AnnotationConfigApplicationContext(JmsApplication.class);
+ }
+}
diff --git a/spring-jms/src/main/java/com/baeldung/spring/jms/testing/JmsConfig.java b/spring-jms/src/main/java/com/baeldung/spring/jms/testing/JmsConfig.java
new file mode 100644
index 0000000000..92abb32861
--- /dev/null
+++ b/spring-jms/src/main/java/com/baeldung/spring/jms/testing/JmsConfig.java
@@ -0,0 +1,34 @@
+package com.baeldung.spring.jms.testing;
+
+import javax.jms.ConnectionFactory;
+
+import org.apache.activemq.ActiveMQConnectionFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.jms.annotation.EnableJms;
+import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
+import org.springframework.jms.config.JmsListenerContainerFactory;
+import org.springframework.jms.core.JmsTemplate;
+
+@Configuration
+@EnableJms
+public class JmsConfig {
+
+ @Bean
+ public JmsListenerContainerFactory> jmsListenerContainerFactory() {
+ DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
+ factory.setConnectionFactory(connectionFactory());
+ return factory;
+ }
+
+ @Bean
+ public ConnectionFactory connectionFactory() {
+ return new ActiveMQConnectionFactory("tcp://localhost:61616");
+ }
+
+ @Bean
+ public JmsTemplate jmsTemplate() {
+ return new JmsTemplate(connectionFactory());
+ }
+
+}
diff --git a/spring-jms/src/main/java/com/baeldung/spring/jms/testing/MessageListener.java b/spring-jms/src/main/java/com/baeldung/spring/jms/testing/MessageListener.java
new file mode 100644
index 0000000000..f66a99c876
--- /dev/null
+++ b/spring-jms/src/main/java/com/baeldung/spring/jms/testing/MessageListener.java
@@ -0,0 +1,20 @@
+package com.baeldung.spring.jms.testing;
+
+import javax.jms.JMSException;
+import javax.jms.TextMessage;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.jms.annotation.JmsListener;
+import org.springframework.stereotype.Component;
+
+@Component
+public class MessageListener {
+
+ private static final Logger logger = LoggerFactory.getLogger(MessageListener.class);
+
+ @JmsListener(destination = "queue-1")
+ public void sampleJmsListenerMethod(TextMessage message) throws JMSException {
+ logger.info("JMS listener received text message: {}", message.getText());
+ }
+}
diff --git a/spring-jms/src/main/java/com/baeldung/spring/jms/testing/MessageSender.java b/spring-jms/src/main/java/com/baeldung/spring/jms/testing/MessageSender.java
new file mode 100644
index 0000000000..6cb199b0e9
--- /dev/null
+++ b/spring-jms/src/main/java/com/baeldung/spring/jms/testing/MessageSender.java
@@ -0,0 +1,21 @@
+package com.baeldung.spring.jms.testing;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jms.core.JmsTemplate;
+import org.springframework.stereotype.Component;
+
+@Component
+public class MessageSender {
+
+ @Autowired
+ private JmsTemplate jmsTemplate;
+
+ private static final Logger logger = LoggerFactory.getLogger(MessageSender.class);
+
+ public void sendTextMessage(String destination, String message) {
+ logger.info("Sending message to {} destination with text {}", destination, message);
+ jmsTemplate.send(destination, s -> s.createTextMessage(message));
+ }
+}
diff --git a/spring-jms/src/test/java/com/baeldung/spring/jms/testing/EmbeddedActiveMqIntegrationTest.java b/spring-jms/src/test/java/com/baeldung/spring/jms/testing/EmbeddedActiveMqIntegrationTest.java
new file mode 100644
index 0000000000..6644ee50ac
--- /dev/null
+++ b/spring-jms/src/test/java/com/baeldung/spring/jms/testing/EmbeddedActiveMqIntegrationTest.java
@@ -0,0 +1,91 @@
+package com.baeldung.spring.jms.testing;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import javax.jms.ConnectionFactory;
+import javax.jms.JMSException;
+import javax.jms.TextMessage;
+
+import org.apache.activemq.ActiveMQConnectionFactory;
+import org.apache.activemq.junit.EmbeddedActiveMQBroker;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+import org.springframework.boot.test.mock.mockito.SpyBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.jms.annotation.EnableJms;
+import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
+import org.springframework.jms.config.JmsListenerContainerFactory;
+import org.springframework.jms.core.JmsTemplate;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import com.baeldung.spring.jms.testing.EmbeddedActiveMqIntegrationTest.TestConfiguration;
+
+@RunWith(SpringRunner.class)
+@ContextConfiguration(classes = { TestConfiguration.class })
+public class EmbeddedActiveMqIntegrationTest {
+
+ @ClassRule
+ public static EmbeddedActiveMQBroker embeddedBroker = new EmbeddedActiveMQBroker();
+
+ @SpyBean
+ private MessageListener messageListener;
+
+ @SpyBean
+ private MessageSender messageSender;
+
+ @Test
+ public void whenListening_thenReceivingCorrectMessage() throws JMSException {
+ String queueName = "queue-1";
+ String messageText = "Test message";
+
+ embeddedBroker.pushMessage(queueName, messageText);
+ assertEquals(1, embeddedBroker.getMessageCount(queueName));
+
+ ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(TextMessage.class);
+
+ Mockito.verify(messageListener, Mockito.timeout(100))
+ .sampleJmsListenerMethod(messageCaptor.capture());
+
+ TextMessage receivedMessage = messageCaptor.getValue();
+ assertEquals(messageText, receivedMessage.getText());
+ }
+
+ @Test
+ public void whenSendingMessage_thenCorrectQueueAndMessageText() throws JMSException {
+ String queueName = "queue-2";
+ String messageText = "Test message";
+
+ messageSender.sendTextMessage(queueName, messageText);
+
+ assertEquals(1, embeddedBroker.getMessageCount(queueName));
+ TextMessage sentMessage = embeddedBroker.peekTextMessage(queueName);
+ assertEquals(messageText, sentMessage.getText());
+ }
+
+ @Configuration
+ @EnableJms
+ static class TestConfiguration {
+ @Bean
+ public JmsListenerContainerFactory> jmsListenerContainerFactory() {
+ DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
+ factory.setConnectionFactory(connectionFactory());
+ return factory;
+ }
+
+ @Bean
+ public ConnectionFactory connectionFactory() {
+ return new ActiveMQConnectionFactory(embeddedBroker.getVmURL());
+ }
+
+ @Bean
+ public JmsTemplate jmsTemplate() {
+ return new JmsTemplate(connectionFactory());
+ }
+ }
+
+}
diff --git a/spring-jms/src/test/java/com/baeldung/spring/jms/testing/TestContainersActiveMqIntegrationTest.java b/spring-jms/src/test/java/com/baeldung/spring/jms/testing/TestContainersActiveMqIntegrationTest.java
new file mode 100644
index 0000000000..d117b90423
--- /dev/null
+++ b/spring-jms/src/test/java/com/baeldung/spring/jms/testing/TestContainersActiveMqIntegrationTest.java
@@ -0,0 +1,101 @@
+package com.baeldung.spring.jms.testing;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import javax.jms.ConnectionFactory;
+import javax.jms.JMSException;
+import javax.jms.Message;
+import javax.jms.TextMessage;
+
+import org.apache.activemq.ActiveMQConnectionFactory;
+import org.assertj.core.api.Assertions;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.mock.mockito.SpyBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.jms.annotation.EnableJms;
+import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
+import org.springframework.jms.config.JmsListenerContainerFactory;
+import org.springframework.jms.core.JmsTemplate;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.utility.DockerImageName;
+
+import com.baeldung.spring.jms.testing.TestContainersActiveMqIntegrationTest.TestConfiguration;
+
+@RunWith(SpringRunner.class)
+@ContextConfiguration(classes = { TestConfiguration.class, MessageSender.class })
+public class TestContainersActiveMqIntegrationTest {
+
+ @ClassRule
+ public static GenericContainer> activeMqContainer = new GenericContainer<>(DockerImageName.parse("rmohr/activemq:5.14.3")).withExposedPorts(61616);
+
+ @SpyBean
+ private MessageListener messageListener;
+
+ @Autowired
+ private MessageSender messageSender;
+
+ @Autowired
+ private JmsTemplate jmsTemplate;
+
+ @Test
+ public void whenListening_thenReceivingCorrectMessage() throws JMSException {
+ String queueName = "queue-1";
+ String messageText = "Test message";
+
+ jmsTemplate.send(queueName, s -> s.createTextMessage(messageText));
+
+ ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(TextMessage.class);
+
+ Mockito.verify(messageListener, Mockito.timeout(100))
+ .sampleJmsListenerMethod(messageCaptor.capture());
+
+ TextMessage receivedMessage = messageCaptor.getValue();
+ assertEquals(messageText, receivedMessage.getText());
+ }
+
+ @Test
+ public void whenSendingMessage_thenCorrectQueueAndMessageText() throws JMSException {
+ String queueName = "queue-2";
+ String messageText = "Test message";
+
+ messageSender.sendTextMessage(queueName, messageText);
+
+ Message sentMessage = jmsTemplate.receive(queueName);
+ Assertions.assertThat(sentMessage)
+ .isInstanceOf(TextMessage.class);
+
+ assertEquals(messageText, ((TextMessage) sentMessage).getText());
+ }
+
+ @Configuration
+ @EnableJms
+ static class TestConfiguration {
+ @Bean
+ public JmsListenerContainerFactory> jmsListenerContainerFactory() {
+ DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
+ factory.setConnectionFactory(connectionFactory());
+ return factory;
+ }
+
+ @Bean
+ public ConnectionFactory connectionFactory() {
+ String brokerUrlFormat = "tcp://%s:%d";
+ String brokerUrl = String.format(brokerUrlFormat, activeMqContainer.getHost(), activeMqContainer.getFirstMappedPort());
+ return new ActiveMQConnectionFactory(brokerUrl);
+ }
+
+ @Bean
+ public JmsTemplate jmsTemplate() {
+ return new JmsTemplate(connectionFactory());
+ }
+ }
+
+}
diff --git a/spring-security-modules/pom.xml b/spring-security-modules/pom.xml
index 2bd6d23058..83412d2252 100644
--- a/spring-security-modules/pom.xml
+++ b/spring-security-modules/pom.xml
@@ -32,6 +32,7 @@
spring-security-web-boot-1
spring-security-web-boot-2
spring-security-web-boot-3
+ spring-security-web-boot-4
spring-security-web-digest-auth
spring-security-web-login
spring-security-web-login-2
diff --git a/spring-security-modules/spring-security-web-boot-4/README.md b/spring-security-modules/spring-security-web-boot-4/README.md
new file mode 100644
index 0000000000..0856315682
--- /dev/null
+++ b/spring-security-modules/spring-security-web-boot-4/README.md
@@ -0,0 +1,10 @@
+## Spring Boot Security MVC
+
+This module contains articles about Spring Security with Spring MVC in Boot applications
+
+### The Course
+The "REST With Spring" Classes: http://github.learnspringsecurity.com
+
+### Relevant Articles:
+
+- More articles: [[<-- prev]](/spring-security-modules/spring-security-web-boot-3)
diff --git a/spring-security-modules/spring-security-web-boot-4/pom.xml b/spring-security-modules/spring-security-web-boot-4/pom.xml
new file mode 100644
index 0000000000..8dd56e1de2
--- /dev/null
+++ b/spring-security-modules/spring-security-web-boot-4/pom.xml
@@ -0,0 +1,39 @@
+
+
+ 4.0.0
+ spring-security-web-boot-4
+ 0.0.1-SNAPSHOT
+ spring-security-web-boot-4
+ jar
+ Spring Security MVC Boot - 4
+
+
+ com.baeldung
+ parent-boot-2
+ 0.0.1-SNAPSHOT
+ ../../parent-boot-2
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
+
+
\ No newline at end of file
diff --git a/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/securityfilterchain/SecurityFilterChainApplication.java b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/securityfilterchain/SecurityFilterChainApplication.java
new file mode 100644
index 0000000000..86f98b651b
--- /dev/null
+++ b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/securityfilterchain/SecurityFilterChainApplication.java
@@ -0,0 +1,14 @@
+package com.baeldung.securityfilterchain;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+
+@SpringBootApplication
+@EnableWebMvc
+public class SecurityFilterChainApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SecurityFilterChainApplication.class, args);
+ }
+}
diff --git a/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/securityfilterchain/configuration/SecurityConfig.java b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/securityfilterchain/configuration/SecurityConfig.java
new file mode 100644
index 0000000000..4d3bec2ad2
--- /dev/null
+++ b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/securityfilterchain/configuration/SecurityConfig.java
@@ -0,0 +1,50 @@
+package com.baeldung.securityfilterchain.configuration;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.SecurityFilterChain;
+
+@EnableWebSecurity
+@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
+public class SecurityConfig {
+
+ @Value("${spring.security.debug:false}")
+ boolean securityDebug;
+
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ http.csrf()
+ .disable()
+ .authorizeRequests()
+ .antMatchers(HttpMethod.DELETE)
+ .hasRole("ADMIN")
+ .antMatchers("/admin/**")
+ .hasAnyRole("ADMIN")
+ .antMatchers("/user/**")
+ .hasAnyRole("USER", "ADMIN")
+ .antMatchers("/login/**")
+ .anonymous()
+ .anyRequest()
+ .authenticated()
+ .and()
+ .httpBasic()
+ .and()
+ .sessionManagement()
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
+
+ return http.build();
+ }
+
+ @Bean
+ public WebSecurityCustomizer webSecurityCustomizer() {
+ return (web) -> web.debug(securityDebug)
+ .ignoring()
+ .antMatchers("/css/**", "/js/**", "/img/**", "/lib/**", "/favicon.ico");
+ }
+}
diff --git a/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/securityfilterchain/configuration/UserDetailServiceConfig.java b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/securityfilterchain/configuration/UserDetailServiceConfig.java
new file mode 100644
index 0000000000..6a614e888b
--- /dev/null
+++ b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/securityfilterchain/configuration/UserDetailServiceConfig.java
@@ -0,0 +1,31 @@
+package com.baeldung.securityfilterchain.configuration;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.provisioning.InMemoryUserDetailsManager;
+
+@Configuration
+public class UserDetailServiceConfig {
+
+ @Bean
+ public UserDetailsService userDetailsService(BCryptPasswordEncoder bCryptPasswordEncoder) {
+ InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
+ manager.createUser(User.withUsername("user")
+ .password(bCryptPasswordEncoder.encode("userPass"))
+ .roles("USER")
+ .build());
+ manager.createUser(User.withUsername("admin")
+ .password(bCryptPasswordEncoder.encode("adminPass"))
+ .roles("ADMIN", "USER")
+ .build());
+ return manager;
+ }
+
+ @Bean
+ public BCryptPasswordEncoder bCryptPasswordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+}
diff --git a/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/securityfilterchain/controller/ResourceController.java b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/securityfilterchain/controller/ResourceController.java
new file mode 100644
index 0000000000..e01d4ae9b3
--- /dev/null
+++ b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/securityfilterchain/controller/ResourceController.java
@@ -0,0 +1,34 @@
+package com.baeldung.securityfilterchain.controller;
+
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class ResourceController {
+ @GetMapping("/login")
+ public String loginEndpoint() {
+ return "Login!";
+ }
+
+ @GetMapping("/admin")
+ public String adminEndpoint() {
+ return "Admin!";
+ }
+
+ @GetMapping("/user")
+ public String userEndpoint() {
+ return "User!";
+ }
+
+ @GetMapping("/all")
+ public String allRolesEndpoint() {
+ return "All Roles!";
+ }
+
+ @DeleteMapping("/delete")
+ public String deleteEndpoint(@RequestBody String s) {
+ return "I am deleting " + s;
+ }
+}
diff --git a/spring-security-modules/spring-security-web-boot-4/src/test/java/com/baeldung/securityfilterchain/SecurityFilterChainIntegrationTest.java b/spring-security-modules/spring-security-web-boot-4/src/test/java/com/baeldung/securityfilterchain/SecurityFilterChainIntegrationTest.java
new file mode 100644
index 0000000000..e94b1b2f12
--- /dev/null
+++ b/spring-security-modules/spring-security-web-boot-4/src/test/java/com/baeldung/securityfilterchain/SecurityFilterChainIntegrationTest.java
@@ -0,0 +1,94 @@
+package com.baeldung.securityfilterchain;
+
+import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
+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.result.MockMvcResultMatchers.status;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.security.test.context.support.WithAnonymousUser;
+import org.springframework.security.test.context.support.WithUserDetails;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+@SpringBootTest(classes = SecurityFilterChainApplication.class)
+public class SecurityFilterChainIntegrationTest {
+ @Autowired
+ private WebApplicationContext context;
+
+ private MockMvc mvc;
+
+ @BeforeEach
+ public void setup() {
+ mvc = MockMvcBuilders.webAppContextSetup(context)
+ .apply(springSecurity())
+ .build();
+ }
+
+ @Test
+ @WithUserDetails(value = "admin")
+ public void whenAdminAccessUserEndpoint_thenOk() throws Exception {
+ mvc.perform(get("/user"))
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ @WithUserDetails(value = "admin")
+ public void whenAdminAccessAdminSecuredEndpoint_thenIsOk() throws Exception {
+ mvc.perform(get("/admin"))
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ @WithUserDetails(value = "admin")
+ public void whenAdminAccessDeleteSecuredEndpoint_thenIsOk() throws Exception {
+ mvc.perform(delete("/delete").content("{}"))
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ @WithAnonymousUser
+ public void whenAnonymousAccessLogin_thenOk() throws Exception {
+ mvc.perform(get("/login"))
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ @WithAnonymousUser
+ public void whenAnonymousAccessRestrictedEndpoint_thenIsUnauthorized() throws Exception {
+ mvc.perform(get("/all"))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ @WithUserDetails()
+ public void whenUserAccessUserSecuredEndpoint_thenOk() throws Exception {
+ mvc.perform(get("/user"))
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ @WithUserDetails()
+ public void whenUserAccessRestrictedEndpoint_thenOk() throws Exception {
+ mvc.perform(get("/all"))
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ @WithUserDetails()
+ public void whenUserAccessAdminSecuredEndpoint_thenIsForbidden() throws Exception {
+ mvc.perform(get("/admin"))
+ .andExpect(status().isForbidden());
+ }
+
+ @Test
+ @WithUserDetails()
+ public void whenUserAccessDeleteSecuredEndpoint_thenIsForbidden() throws Exception {
+ mvc.perform(delete("/delete"))
+ .andExpect(status().isForbidden());
+ }
+}
diff --git a/spring-security-modules/spring-security-web-boot-4/src/test/resources/application.properties b/spring-security-modules/spring-security-web-boot-4/src/test/resources/application.properties
new file mode 100644
index 0000000000..090ff54e92
--- /dev/null
+++ b/spring-security-modules/spring-security-web-boot-4/src/test/resources/application.properties
@@ -0,0 +1 @@
+spring.security.debug=true
\ No newline at end of file
diff --git a/spring-security-modules/spring-security-web-boot-4/src/test/resources/logback-test.xml b/spring-security-modules/spring-security-web-boot-4/src/test/resources/logback-test.xml
new file mode 100644
index 0000000000..c5a4b0ab1c
--- /dev/null
+++ b/spring-security-modules/spring-security-web-boot-4/src/test/resources/logback-test.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ [%d{ISO8601}]-[%thread] %-5level %logger - %msg%n
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-web-modules/spring-resttemplate-2/pom.xml b/spring-web-modules/spring-resttemplate-2/pom.xml
index b87b245da9..0228b1dc4f 100644
--- a/spring-web-modules/spring-resttemplate-2/pom.xml
+++ b/spring-web-modules/spring-resttemplate-2/pom.xml
@@ -12,7 +12,7 @@
com.baeldung
parent-boot-2
0.0.1-SNAPSHOT
- ../../parent-boot-2/pom.xml
+ ../../parent-boot-2