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()); + } +}