diff --git a/boot-opensearch-sample/pom.xml b/boot-opensearch-sample/pom.xml
index 8f2594c68..a820b6bd2 100644
--- a/boot-opensearch-sample/pom.xml
+++ b/boot-opensearch-sample/pom.xml
@@ -56,6 +56,12 @@
org.springframework.boot
spring-boot-starter-web
+
+ org.opensearch.client
+ spring-data-opensearch-starter
+ 1.2.0
+
+
org.springframework.boot
spring-boot-devtools
@@ -83,11 +89,6 @@
spring-boot-testcontainers
test
-
- org.awaitility
- awaitility
- test
-
org.testcontainers
junit-jupiter
diff --git a/boot-opensearch-sample/src/main/java/com/example/opensearch/entities/Restaurant.java b/boot-opensearch-sample/src/main/java/com/example/opensearch/entities/Restaurant.java
new file mode 100644
index 000000000..dcb5e107a
--- /dev/null
+++ b/boot-opensearch-sample/src/main/java/com/example/opensearch/entities/Restaurant.java
@@ -0,0 +1,51 @@
+package com.example.opensearch.entities;
+
+import jakarta.validation.constraints.NotEmpty;
+import java.util.Objects;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.elasticsearch.annotations.Document;
+
+@Document(indexName = "restaurants")
+public class Restaurant {
+
+ @Id private Long id;
+
+ @NotEmpty(message = "Name cannot be empty")
+ private String name;
+
+ public Restaurant() {}
+
+ public Restaurant(Long id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Restaurant that = (Restaurant) o;
+ return Objects.equals(id, that.id) && Objects.equals(name, that.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, name);
+ }
+}
diff --git a/boot-opensearch-sample/src/main/java/com/example/opensearch/model/response/PagedResult.java b/boot-opensearch-sample/src/main/java/com/example/opensearch/model/response/PagedResult.java
new file mode 100644
index 000000000..63d396d82
--- /dev/null
+++ b/boot-opensearch-sample/src/main/java/com/example/opensearch/model/response/PagedResult.java
@@ -0,0 +1,27 @@
+package com.example.opensearch.model.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+import org.springframework.data.domain.Page;
+
+public record PagedResult(
+ List data,
+ long totalElements,
+ int pageNumber,
+ int totalPages,
+ @JsonProperty("isFirst") boolean isFirst,
+ @JsonProperty("isLast") boolean isLast,
+ @JsonProperty("hasNext") boolean hasNext,
+ @JsonProperty("hasPrevious") boolean hasPrevious) {
+ public PagedResult(Page page) {
+ this(
+ page.getContent(),
+ page.getTotalElements(),
+ page.getNumber() + 1,
+ page.getTotalPages(),
+ page.isFirst(),
+ page.isLast(),
+ page.hasNext(),
+ page.hasPrevious());
+ }
+}
diff --git a/boot-opensearch-sample/src/main/java/com/example/opensearch/repositories/RestaurantRepository.java b/boot-opensearch-sample/src/main/java/com/example/opensearch/repositories/RestaurantRepository.java
new file mode 100644
index 000000000..7e8158286
--- /dev/null
+++ b/boot-opensearch-sample/src/main/java/com/example/opensearch/repositories/RestaurantRepository.java
@@ -0,0 +1,10 @@
+package com.example.opensearch.repositories;
+
+import com.example.opensearch.entities.Restaurant;
+import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
+import org.springframework.data.repository.ListCrudRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface RestaurantRepository
+ extends ElasticsearchRepository, ListCrudRepository {}
diff --git a/boot-opensearch-sample/src/main/java/com/example/opensearch/services/RestaurantService.java b/boot-opensearch-sample/src/main/java/com/example/opensearch/services/RestaurantService.java
new file mode 100644
index 000000000..14f683e56
--- /dev/null
+++ b/boot-opensearch-sample/src/main/java/com/example/opensearch/services/RestaurantService.java
@@ -0,0 +1,51 @@
+package com.example.opensearch.services;
+
+import com.example.opensearch.entities.Restaurant;
+import com.example.opensearch.model.response.PagedResult;
+import com.example.opensearch.repositories.RestaurantRepository;
+import java.util.Optional;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@Transactional
+public class RestaurantService {
+
+ private final RestaurantRepository restaurantRepository;
+
+ @Autowired
+ public RestaurantService(RestaurantRepository restaurantRepository) {
+ this.restaurantRepository = restaurantRepository;
+ }
+
+ public PagedResult findAllRestaurants(
+ int pageNo, int pageSize, String sortBy, String sortDir) {
+ Sort sort =
+ sortDir.equalsIgnoreCase(Sort.Direction.ASC.name())
+ ? Sort.by(sortBy).ascending()
+ : Sort.by(sortBy).descending();
+
+ // create Pageable instance
+ Pageable pageable = PageRequest.of(pageNo, pageSize, sort);
+ Page restaurantsPage = restaurantRepository.findAll(pageable);
+
+ return new PagedResult<>(restaurantsPage);
+ }
+
+ public Optional findRestaurantById(Long id) {
+ return restaurantRepository.findById(id);
+ }
+
+ public Restaurant saveRestaurant(Restaurant restaurant) {
+ return restaurantRepository.save(restaurant);
+ }
+
+ public void deleteRestaurantById(Long id) {
+ restaurantRepository.deleteById(id);
+ }
+}
diff --git a/boot-opensearch-sample/src/main/java/com/example/opensearch/web/controllers/RestaurantController.java b/boot-opensearch-sample/src/main/java/com/example/opensearch/web/controllers/RestaurantController.java
new file mode 100644
index 000000000..e14d3b7c7
--- /dev/null
+++ b/boot-opensearch-sample/src/main/java/com/example/opensearch/web/controllers/RestaurantController.java
@@ -0,0 +1,100 @@
+package com.example.opensearch.web.controllers;
+
+import com.example.opensearch.entities.Restaurant;
+import com.example.opensearch.model.response.PagedResult;
+import com.example.opensearch.services.RestaurantService;
+import com.example.opensearch.utils.AppConstants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.validation.annotation.Validated;
+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.RequestParam;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/restaurants")
+public class RestaurantController {
+
+ private final Logger log = LoggerFactory.getLogger(this.getClass());
+
+ private final RestaurantService restaurantService;
+
+ @Autowired
+ public RestaurantController(RestaurantService restaurantService) {
+ this.restaurantService = restaurantService;
+ }
+
+ @GetMapping
+ public PagedResult getAllRestaurants(
+ @RequestParam(
+ value = "pageNo",
+ defaultValue = AppConstants.DEFAULT_PAGE_NUMBER,
+ required = false)
+ int pageNo,
+ @RequestParam(
+ value = "pageSize",
+ defaultValue = AppConstants.DEFAULT_PAGE_SIZE,
+ required = false)
+ int pageSize,
+ @RequestParam(
+ value = "sortBy",
+ defaultValue = AppConstants.DEFAULT_SORT_BY,
+ required = false)
+ String sortBy,
+ @RequestParam(
+ value = "sortDir",
+ defaultValue = AppConstants.DEFAULT_SORT_DIRECTION,
+ required = false)
+ String sortDir) {
+ return restaurantService.findAllRestaurants(pageNo, pageSize, sortBy, sortDir);
+ }
+
+ @GetMapping("/{id}")
+ public ResponseEntity getRestaurantById(@PathVariable Long id) {
+ return restaurantService
+ .findRestaurantById(id)
+ .map(ResponseEntity::ok)
+ .orElseGet(() -> ResponseEntity.notFound().build());
+ }
+
+ @PostMapping
+ @ResponseStatus(HttpStatus.CREATED)
+ public Restaurant createRestaurant(@RequestBody @Validated Restaurant restaurant) {
+ return restaurantService.saveRestaurant(restaurant);
+ }
+
+ @PutMapping("/{id}")
+ public ResponseEntity updateRestaurant(
+ @PathVariable Long id, @RequestBody Restaurant restaurant) {
+ return restaurantService
+ .findRestaurantById(id)
+ .map(
+ restaurantObj -> {
+ restaurant.setId(id);
+ return ResponseEntity.ok(restaurantService.saveRestaurant(restaurant));
+ })
+ .orElseGet(() -> ResponseEntity.notFound().build());
+ }
+
+ @DeleteMapping("/{id}")
+ public ResponseEntity deleteRestaurant(@PathVariable Long id) {
+ return restaurantService
+ .findRestaurantById(id)
+ .map(
+ restaurant -> {
+ restaurantService.deleteRestaurantById(id);
+ return ResponseEntity.ok(restaurant);
+ })
+ .orElseGet(() -> ResponseEntity.notFound().build());
+ }
+}
diff --git a/boot-opensearch-sample/src/test/java/com/example/opensearch/common/ContainersConfig.java b/boot-opensearch-sample/src/test/java/com/example/opensearch/common/ContainersConfig.java
index c61f7780b..0c8cb2339 100644
--- a/boot-opensearch-sample/src/test/java/com/example/opensearch/common/ContainersConfig.java
+++ b/boot-opensearch-sample/src/test/java/com/example/opensearch/common/ContainersConfig.java
@@ -1,6 +1,33 @@
package com.example.opensearch.common;
+import java.net.HttpURLConnection;
+import java.time.Duration;
import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.springframework.context.annotation.Bean;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
@TestConfiguration(proxyBeanMethods = false)
-public class ContainersConfig {}
+public class ContainersConfig {
+
+ @Bean
+ @ServiceConnection("elasticsearch")
+ GenericContainer> createOpenSearchContainer() {
+ return new GenericContainer<>("opensearchproject/opensearch:1.1.0")
+ .withEnv("discovery.type", "single-node")
+ .withEnv("DISABLE_SECURITY_PLUGIN", "true")
+ .withEnv("OPENSEARCH_JAVA_OPTS", "-Xms512m -Xmx512m")
+ .withExposedPorts(9200, 9600)
+ .waitingFor(
+ new HttpWaitStrategy()
+ .forPort(9200)
+ .forStatusCodeMatching(
+ response ->
+ response == HttpURLConnection.HTTP_OK
+ || response
+ == HttpURLConnection
+ .HTTP_UNAUTHORIZED)
+ .withStartupTimeout(Duration.ofMinutes(2)));
+ }
+}
diff --git a/boot-opensearch-sample/src/test/java/com/example/opensearch/services/RestaurantServiceTest.java b/boot-opensearch-sample/src/test/java/com/example/opensearch/services/RestaurantServiceTest.java
new file mode 100644
index 000000000..083c0d67a
--- /dev/null
+++ b/boot-opensearch-sample/src/test/java/com/example/opensearch/services/RestaurantServiceTest.java
@@ -0,0 +1,96 @@
+package com.example.opensearch.services;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.times;
+import static org.mockito.BDDMockito.verify;
+import static org.mockito.BDDMockito.willDoNothing;
+
+import com.example.opensearch.entities.Restaurant;
+import com.example.opensearch.model.response.PagedResult;
+import com.example.opensearch.repositories.RestaurantRepository;
+import java.util.List;
+import java.util.Optional;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+
+@ExtendWith(MockitoExtension.class)
+class RestaurantServiceTest {
+
+ @Mock private RestaurantRepository restaurantRepository;
+
+ @InjectMocks private RestaurantService restaurantService;
+
+ @Test
+ void findAllRestaurants() {
+ // given
+ Pageable pageable = PageRequest.of(0, 10, Sort.by(Sort.Direction.ASC, "id"));
+ Page restaurantPage = new PageImpl<>(List.of(getRestaurant()));
+ given(restaurantRepository.findAll(pageable)).willReturn(restaurantPage);
+
+ // when
+ PagedResult pagedResult =
+ restaurantService.findAllRestaurants(0, 10, "id", "asc");
+
+ // then
+ assertThat(pagedResult).isNotNull();
+ assertThat(pagedResult.data()).isNotEmpty().hasSize(1);
+ assertThat(pagedResult.hasNext()).isFalse();
+ assertThat(pagedResult.pageNumber()).isEqualTo(1);
+ assertThat(pagedResult.totalPages()).isEqualTo(1);
+ assertThat(pagedResult.isFirst()).isTrue();
+ assertThat(pagedResult.isLast()).isTrue();
+ assertThat(pagedResult.hasPrevious()).isFalse();
+ assertThat(pagedResult.totalElements()).isEqualTo(1);
+ }
+
+ @Test
+ void findRestaurantById() {
+ // given
+ given(restaurantRepository.findById(1L)).willReturn(Optional.of(getRestaurant()));
+ // when
+ Optional optionalRestaurant = restaurantService.findRestaurantById(1L);
+ // then
+ assertThat(optionalRestaurant).isPresent();
+ Restaurant restaurant = optionalRestaurant.get();
+ assertThat(restaurant.getId()).isEqualTo(1L);
+ assertThat(restaurant.getName()).isEqualTo("junitTest");
+ }
+
+ @Test
+ void saveRestaurant() {
+ // given
+ given(restaurantRepository.save(getRestaurant())).willReturn(getRestaurant());
+ // when
+ Restaurant persistedRestaurant = restaurantService.saveRestaurant(getRestaurant());
+ // then
+ assertThat(persistedRestaurant).isNotNull();
+ assertThat(persistedRestaurant.getId()).isEqualTo(1L);
+ assertThat(persistedRestaurant.getName()).isEqualTo("junitTest");
+ }
+
+ @Test
+ void deleteRestaurantById() {
+ // given
+ willDoNothing().given(restaurantRepository).deleteById(1L);
+ // when
+ restaurantService.deleteRestaurantById(1L);
+ // then
+ verify(restaurantRepository, times(1)).deleteById(1L);
+ }
+
+ private Restaurant getRestaurant() {
+ Restaurant restaurant = new Restaurant();
+ restaurant.setId(1L);
+ restaurant.setName("junitTest");
+ return restaurant;
+ }
+}
diff --git a/boot-opensearch-sample/src/test/java/com/example/opensearch/web/controllers/RestaurantControllerIT.java b/boot-opensearch-sample/src/test/java/com/example/opensearch/web/controllers/RestaurantControllerIT.java
new file mode 100644
index 000000000..61c0786e3
--- /dev/null
+++ b/boot-opensearch-sample/src/test/java/com/example/opensearch/web/controllers/RestaurantControllerIT.java
@@ -0,0 +1,128 @@
+package com.example.opensearch.web.controllers;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.Matchers.hasSize;
+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.header;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import com.example.opensearch.common.AbstractIntegrationTest;
+import com.example.opensearch.entities.Restaurant;
+import com.example.opensearch.repositories.RestaurantRepository;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+
+class RestaurantControllerIT extends AbstractIntegrationTest {
+
+ @Autowired private RestaurantRepository restaurantRepository;
+
+ private List restaurantList = null;
+
+ @BeforeEach
+ void setUp() {
+ restaurantRepository.deleteAll();
+
+ restaurantList = new ArrayList<>();
+ restaurantList.add(new Restaurant(null, "First Restaurant"));
+ restaurantList.add(new Restaurant(null, "Second Restaurant"));
+ restaurantList.add(new Restaurant(null, "Third Restaurant"));
+ restaurantList = restaurantRepository.saveAll(restaurantList);
+ }
+
+ @Test
+ void shouldFetchAllRestaurants() throws Exception {
+ this.mockMvc
+ .perform(get("/api/restaurants"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.data.size()", is(restaurantList.size())))
+ .andExpect(jsonPath("$.totalElements", is(3)))
+ .andExpect(jsonPath("$.pageNumber", is(1)))
+ .andExpect(jsonPath("$.totalPages", is(1)))
+ .andExpect(jsonPath("$.isFirst", is(true)))
+ .andExpect(jsonPath("$.isLast", is(true)))
+ .andExpect(jsonPath("$.hasNext", is(false)))
+ .andExpect(jsonPath("$.hasPrevious", is(false)));
+ }
+
+ @Test
+ void shouldFindRestaurantById() throws Exception {
+ Restaurant restaurant = restaurantList.get(0);
+ Long restaurantId = restaurant.getId();
+
+ this.mockMvc
+ .perform(get("/api/restaurants/{id}", restaurantId))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id", is(restaurant.getId()), Long.class))
+ .andExpect(jsonPath("$.name", is(restaurant.getName())));
+ }
+
+ @Test
+ void shouldCreateNewRestaurant() throws Exception {
+ Restaurant restaurant = new Restaurant(null, "New Restaurant");
+ this.mockMvc
+ .perform(
+ post("/api/restaurants")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(restaurant)))
+ .andExpect(status().isCreated())
+ .andExpect(jsonPath("$.id", notNullValue()))
+ .andExpect(jsonPath("$.name", is(restaurant.getName())));
+ }
+
+ @Test
+ void shouldReturn400WhenCreateNewRestaurantWithoutText() throws Exception {
+ Restaurant restaurant = new Restaurant(null, null);
+
+ this.mockMvc
+ .perform(
+ post("/api/restaurants")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(restaurant)))
+ .andExpect(status().isBadRequest())
+ .andExpect(header().string("Content-Type", is("application/problem+json")))
+ .andExpect(jsonPath("$.type", is("about:blank")))
+ .andExpect(jsonPath("$.title", is("Constraint Violation")))
+ .andExpect(jsonPath("$.status", is(400)))
+ .andExpect(jsonPath("$.detail", is("Invalid request content.")))
+ .andExpect(jsonPath("$.instance", is("/api/restaurants")))
+ .andExpect(jsonPath("$.violations", hasSize(1)))
+ .andExpect(jsonPath("$.violations[0].field", is("name")))
+ .andExpect(jsonPath("$.violations[0].message", is("Name cannot be empty")))
+ .andReturn();
+ }
+
+ @Test
+ void shouldUpdateRestaurant() throws Exception {
+ Restaurant restaurant = restaurantList.get(0);
+ restaurant.setName("Updated Restaurant");
+
+ this.mockMvc
+ .perform(
+ put("/api/restaurants/{id}", restaurant.getId())
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(restaurant)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id", is(restaurant.getId()), Long.class))
+ .andExpect(jsonPath("$.name", is(restaurant.getName())));
+ }
+
+ @Test
+ void shouldDeleteRestaurant() throws Exception {
+ Restaurant restaurant = restaurantList.get(0);
+
+ this.mockMvc
+ .perform(delete("/api/restaurants/{id}", restaurant.getId()))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id", is(restaurant.getId()), Long.class))
+ .andExpect(jsonPath("$.name", is(restaurant.getName())));
+ }
+}
diff --git a/boot-opensearch-sample/src/test/java/com/example/opensearch/web/controllers/RestaurantControllerTest.java b/boot-opensearch-sample/src/test/java/com/example/opensearch/web/controllers/RestaurantControllerTest.java
new file mode 100644
index 000000000..cf4f7b155
--- /dev/null
+++ b/boot-opensearch-sample/src/test/java/com/example/opensearch/web/controllers/RestaurantControllerTest.java
@@ -0,0 +1,192 @@
+package com.example.opensearch.web.controllers;
+
+import static com.example.opensearch.utils.AppConstants.PROFILE_TEST;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.Matchers.hasSize;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.doNothing;
+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.header;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import com.example.opensearch.entities.Restaurant;
+import com.example.opensearch.model.response.PagedResult;
+import com.example.opensearch.services.RestaurantService;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+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.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.web.servlet.MockMvc;
+
+@WebMvcTest(controllers = RestaurantController.class)
+@ActiveProfiles(PROFILE_TEST)
+class RestaurantControllerTest {
+
+ @Autowired private MockMvc mockMvc;
+
+ @MockBean private RestaurantService restaurantService;
+
+ @Autowired private ObjectMapper objectMapper;
+
+ private List restaurantList;
+
+ @BeforeEach
+ void setUp() {
+ this.restaurantList = new ArrayList<>();
+ this.restaurantList.add(new Restaurant(1L, "text 1"));
+ this.restaurantList.add(new Restaurant(2L, "text 2"));
+ this.restaurantList.add(new Restaurant(3L, "text 3"));
+ }
+
+ @Test
+ void shouldFetchAllRestaurants() throws Exception {
+ Page page = new PageImpl<>(restaurantList);
+ PagedResult restaurantPagedResult = new PagedResult<>(page);
+ given(restaurantService.findAllRestaurants(0, 10, "id", "asc"))
+ .willReturn(restaurantPagedResult);
+
+ this.mockMvc
+ .perform(get("/api/restaurants"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.data.size()", is(restaurantList.size())))
+ .andExpect(jsonPath("$.totalElements", is(3)))
+ .andExpect(jsonPath("$.pageNumber", is(1)))
+ .andExpect(jsonPath("$.totalPages", is(1)))
+ .andExpect(jsonPath("$.isFirst", is(true)))
+ .andExpect(jsonPath("$.isLast", is(true)))
+ .andExpect(jsonPath("$.hasNext", is(false)))
+ .andExpect(jsonPath("$.hasPrevious", is(false)));
+ }
+
+ @Test
+ void shouldFindRestaurantById() throws Exception {
+ Long restaurantId = 1L;
+ Restaurant restaurant = new Restaurant(restaurantId, "text 1");
+ given(restaurantService.findRestaurantById(restaurantId))
+ .willReturn(Optional.of(restaurant));
+
+ this.mockMvc
+ .perform(get("/api/restaurants/{id}", restaurantId))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.name", is(restaurant.getName())));
+ }
+
+ @Test
+ void shouldReturn404WhenFetchingNonExistingRestaurant() throws Exception {
+ Long restaurantId = 1L;
+ given(restaurantService.findRestaurantById(restaurantId)).willReturn(Optional.empty());
+
+ this.mockMvc
+ .perform(get("/api/restaurants/{id}", restaurantId))
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ void shouldCreateNewRestaurant() throws Exception {
+ given(restaurantService.saveRestaurant(any(Restaurant.class)))
+ .willAnswer((invocation) -> invocation.getArgument(0));
+
+ Restaurant restaurant = new Restaurant(1L, "some text");
+ this.mockMvc
+ .perform(
+ post("/api/restaurants")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(restaurant)))
+ .andExpect(status().isCreated())
+ .andExpect(jsonPath("$.id", notNullValue()))
+ .andExpect(jsonPath("$.name", is(restaurant.getName())));
+ }
+
+ @Test
+ void shouldReturn400WhenCreateNewRestaurantWithoutText() throws Exception {
+ Restaurant restaurant = new Restaurant(null, null);
+
+ this.mockMvc
+ .perform(
+ post("/api/restaurants")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(restaurant)))
+ .andExpect(status().isBadRequest())
+ .andExpect(header().string("Content-Type", is("application/problem+json")))
+ .andExpect(jsonPath("$.type", is("about:blank")))
+ .andExpect(jsonPath("$.title", is("Constraint Violation")))
+ .andExpect(jsonPath("$.status", is(400)))
+ .andExpect(jsonPath("$.detail", is("Invalid request content.")))
+ .andExpect(jsonPath("$.instance", is("/api/restaurants")))
+ .andExpect(jsonPath("$.violations", hasSize(1)))
+ .andExpect(jsonPath("$.violations[0].field", is("name")))
+ .andExpect(jsonPath("$.violations[0].message", is("Name cannot be empty")))
+ .andReturn();
+ }
+
+ @Test
+ void shouldUpdateRestaurant() throws Exception {
+ Long restaurantId = 1L;
+ Restaurant restaurant = new Restaurant(restaurantId, "Updated text");
+ given(restaurantService.findRestaurantById(restaurantId))
+ .willReturn(Optional.of(restaurant));
+ given(restaurantService.saveRestaurant(any(Restaurant.class)))
+ .willAnswer((invocation) -> invocation.getArgument(0));
+
+ this.mockMvc
+ .perform(
+ put("/api/restaurants/{id}", restaurant.getId())
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(restaurant)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.name", is(restaurant.getName())));
+ }
+
+ @Test
+ void shouldReturn404WhenUpdatingNonExistingRestaurant() throws Exception {
+ Long restaurantId = 1L;
+ given(restaurantService.findRestaurantById(restaurantId)).willReturn(Optional.empty());
+ Restaurant restaurant = new Restaurant(restaurantId, "Updated text");
+
+ this.mockMvc
+ .perform(
+ put("/api/restaurants/{id}", restaurantId)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(restaurant)))
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ void shouldDeleteRestaurant() throws Exception {
+ Long restaurantId = 1L;
+ Restaurant restaurant = new Restaurant(restaurantId, "Some text");
+ given(restaurantService.findRestaurantById(restaurantId))
+ .willReturn(Optional.of(restaurant));
+ doNothing().when(restaurantService).deleteRestaurantById(restaurant.getId());
+
+ this.mockMvc
+ .perform(delete("/api/restaurants/{id}", restaurant.getId()))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.name", is(restaurant.getName())));
+ }
+
+ @Test
+ void shouldReturn404WhenDeletingNonExistingRestaurant() throws Exception {
+ Long restaurantId = 1L;
+ given(restaurantService.findRestaurantById(restaurantId)).willReturn(Optional.empty());
+
+ this.mockMvc
+ .perform(delete("/api/restaurants/{id}", restaurantId))
+ .andExpect(status().isNotFound());
+ }
+}