From fb71937de83f68febb31c4dd2a7087b40af610de Mon Sep 17 00:00:00 2001 From: Aravind Venkatesan <43500365+aravinve@users.noreply.github.com> Date: Wed, 3 Aug 2022 23:52:43 +0800 Subject: [PATCH] [API] - Add search mocks api based on specified column - search mocks based on the search query string and the search column such as name, description or route - #179 --- .../mimock/common/constants/UrlConfig.java | 1 + .../controller/SearchMocksController.java | 46 ++++++++ .../manage/mimocks/enums/SearchColumn.java | 13 +++ .../mimocks/service/SearchMocksService.java | 10 ++ .../service/SearchMocksServiceImpl.java | 48 ++++++++ .../mimock/repository/MocksRepository.java | 4 + .../controller/SearchMocksControllerTest.java | 105 ++++++++++++++++++ .../service/SearchMocksServiceTest.java | 78 +++++++++++++ 8 files changed, 305 insertions(+) create mode 100644 mimock-backend/src/main/java/com/arbindo/mimock/manage/mimocks/controller/SearchMocksController.java create mode 100644 mimock-backend/src/main/java/com/arbindo/mimock/manage/mimocks/enums/SearchColumn.java create mode 100644 mimock-backend/src/main/java/com/arbindo/mimock/manage/mimocks/service/SearchMocksService.java create mode 100644 mimock-backend/src/main/java/com/arbindo/mimock/manage/mimocks/service/SearchMocksServiceImpl.java create mode 100644 mimock-backend/src/test/java/com/arbindo/mimock/manage/mimocks/controller/SearchMocksControllerTest.java create mode 100644 mimock-backend/src/test/java/com/arbindo/mimock/manage/mimocks/service/SearchMocksServiceTest.java diff --git a/mimock-backend/src/main/java/com/arbindo/mimock/common/constants/UrlConfig.java b/mimock-backend/src/main/java/com/arbindo/mimock/common/constants/UrlConfig.java index e02cd897..a478958d 100644 --- a/mimock-backend/src/main/java/com/arbindo/mimock/common/constants/UrlConfig.java +++ b/mimock-backend/src/main/java/com/arbindo/mimock/common/constants/UrlConfig.java @@ -42,6 +42,7 @@ private UrlConfig() { // region MOCKS public static final String MOCKS = "/mocks"; public static final String MOCKS_PAGEABLE = "/filter"; + public static final String MOCKS_SEARCH = "/search"; public static final String MOCKS_CSV_EXPORT = "/csv/export"; public static final String MOCKS_CSV_TEMPLATE_EXPORT = "/csv/template/export"; // endregion diff --git a/mimock-backend/src/main/java/com/arbindo/mimock/manage/mimocks/controller/SearchMocksController.java b/mimock-backend/src/main/java/com/arbindo/mimock/manage/mimocks/controller/SearchMocksController.java new file mode 100644 index 00000000..989181d9 --- /dev/null +++ b/mimock-backend/src/main/java/com/arbindo/mimock/manage/mimocks/controller/SearchMocksController.java @@ -0,0 +1,46 @@ +package com.arbindo.mimock.manage.mimocks.controller; + +import com.arbindo.mimock.common.constants.UrlConfig; +import com.arbindo.mimock.entities.Mock; +import com.arbindo.mimock.manage.mimocks.mapper.ResponseModelMapper; +import com.arbindo.mimock.manage.mimocks.models.response.ListMocksResponse; +import com.arbindo.mimock.manage.mimocks.service.SearchMocksService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.SortDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@Log4j2 +@RequestMapping(UrlConfig.MOCKS_PATH) +@SecurityRequirement(name = UrlConfig.SWAGGER_BEARER_AUTH_KEY) +@Tag(name = "Mock Management", description = "Handles operations related to mock resource.") +public class SearchMocksController { + + @Autowired + private SearchMocksService searchMocksService; + + @Operation(summary = "Search and List Mocks As Pageable", description = "Searches for mocks and lists them in " + + "pageable format based on the search query provided", tags = {"Mock Management"}) + @GetMapping(UrlConfig.MOCKS_SEARCH) + public ResponseEntity> searchMocks(@SortDefault(sort = "createdAt", + direction = Sort.Direction.DESC) Pageable pageable, @RequestParam(required = true) String searchColumnString, + @RequestParam(required = true) String searchQuery) { + Page mockPageable = searchMocksService.searchMocks(pageable, searchColumnString, searchQuery); + if(mockPageable == null){ + return ResponseEntity.badRequest().build(); + } + Page responsePage = mockPageable.map(ResponseModelMapper::map); + return ResponseEntity.ok(responsePage); + } +} diff --git a/mimock-backend/src/main/java/com/arbindo/mimock/manage/mimocks/enums/SearchColumn.java b/mimock-backend/src/main/java/com/arbindo/mimock/manage/mimocks/enums/SearchColumn.java new file mode 100644 index 00000000..0403e9c9 --- /dev/null +++ b/mimock-backend/src/main/java/com/arbindo/mimock/manage/mimocks/enums/SearchColumn.java @@ -0,0 +1,13 @@ +package com.arbindo.mimock.manage.mimocks.enums; + +public enum SearchColumn { + NAME("NAME"), + DESCRIPTION("DESCRIPTION"), + ROUTE("ROUTE"); + + public final String value; + + private SearchColumn(String value) { + this.value = value; + } +} diff --git a/mimock-backend/src/main/java/com/arbindo/mimock/manage/mimocks/service/SearchMocksService.java b/mimock-backend/src/main/java/com/arbindo/mimock/manage/mimocks/service/SearchMocksService.java new file mode 100644 index 00000000..6c5e69ae --- /dev/null +++ b/mimock-backend/src/main/java/com/arbindo/mimock/manage/mimocks/service/SearchMocksService.java @@ -0,0 +1,10 @@ +package com.arbindo.mimock.manage.mimocks.service; + +import com.arbindo.mimock.entities.Mock; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface SearchMocksService { + + Page searchMocks(Pageable pageable, String searchColumnString, String searchQuery); +} diff --git a/mimock-backend/src/main/java/com/arbindo/mimock/manage/mimocks/service/SearchMocksServiceImpl.java b/mimock-backend/src/main/java/com/arbindo/mimock/manage/mimocks/service/SearchMocksServiceImpl.java new file mode 100644 index 00000000..cc262839 --- /dev/null +++ b/mimock-backend/src/main/java/com/arbindo/mimock/manage/mimocks/service/SearchMocksServiceImpl.java @@ -0,0 +1,48 @@ +package com.arbindo.mimock.manage.mimocks.service; + +import com.arbindo.mimock.entities.Mock; +import com.arbindo.mimock.manage.mimocks.enums.SearchColumn; +import com.arbindo.mimock.repository.MocksRepository; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.extern.log4j.Log4j2; +import org.apache.logging.log4j.Level; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@Log4j2 +@Builder +@AllArgsConstructor +public class SearchMocksServiceImpl implements SearchMocksService{ + + @Autowired + private MocksRepository mocksRepository; + + @Override + public Page searchMocks(Pageable pageable, String searchColumnString, String searchQuery) { + try { + SearchColumn searchColumn = SearchColumn.valueOf(searchColumnString); + String sanitizedSearchQuery = searchQuery + .replaceAll("\n", "") + .replaceAll("\r", "") + .trim(); + switch (searchColumn){ + case NAME: + return mocksRepository.findAllByMockNameIgnoreCaseContaining(sanitizedSearchQuery, pageable); + case DESCRIPTION: + return mocksRepository.findAllByDescriptionIgnoreCaseContaining(sanitizedSearchQuery, pageable); + case ROUTE: + return mocksRepository.findAllByRouteIgnoreCaseContaining(sanitizedSearchQuery, pageable); + default: + return mocksRepository.findAll(pageable); + } + } catch(Exception e) { + log.log(Level.DEBUG, e.getMessage()); + } + return null; + } + +} diff --git a/mimock-backend/src/main/java/com/arbindo/mimock/repository/MocksRepository.java b/mimock-backend/src/main/java/com/arbindo/mimock/repository/MocksRepository.java index 90521051..e7be686b 100644 --- a/mimock-backend/src/main/java/com/arbindo/mimock/repository/MocksRepository.java +++ b/mimock-backend/src/main/java/com/arbindo/mimock/repository/MocksRepository.java @@ -42,6 +42,10 @@ Page findAllByEntityStatusAndHttpMethodAndTextualResponseIsNullAndBinaryRe List findAllByEntityStatusAndDeletedAt(EntityStatus entityStatus, ZonedDateTime deletedAt); + Page findAllByMockNameIgnoreCaseContaining(String searchQuery, Pageable pageable); + Page findAllByDescriptionIgnoreCaseContaining(String searchQuery, Pageable pageable); + Page findAllByRouteIgnoreCaseContaining(String searchQuery, Pageable pageable); + Optional findOneByMockName(String mockName); Optional findOneByRouteAndHttpMethodAndQueryParamValuesAndRequestBodiesForMockAndRequestHeadersAndDeletedAtIsNull diff --git a/mimock-backend/src/test/java/com/arbindo/mimock/manage/mimocks/controller/SearchMocksControllerTest.java b/mimock-backend/src/test/java/com/arbindo/mimock/manage/mimocks/controller/SearchMocksControllerTest.java new file mode 100644 index 00000000..e1eaae78 --- /dev/null +++ b/mimock-backend/src/test/java/com/arbindo/mimock/manage/mimocks/controller/SearchMocksControllerTest.java @@ -0,0 +1,105 @@ +package com.arbindo.mimock.manage.mimocks.controller; + +import com.arbindo.mimock.common.constants.UrlConfig; +import com.arbindo.mimock.entities.Mock; +import com.arbindo.mimock.interceptor.DefaultHttpInterceptor; +import com.arbindo.mimock.manage.mimocks.models.response.ListMocksResponse; +import com.arbindo.mimock.manage.mimocks.service.SearchMocksService; +import com.arbindo.mimock.security.JwtRequestFilter; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +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.Pageable; +import org.springframework.jdbc.support.DatabaseStartupValidator; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import javax.sql.DataSource; + +import static com.arbindo.mimock.helpers.entities.MocksGenerator.generateMocksPageable; +import static com.arbindo.mimock.helpers.entities.MocksGenerator.getListMocksResponseInPageableFormat; +import static com.arbindo.mimock.helpers.general.JsonMapper.convertObjectToJsonString; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(value = SearchMocksController.class, excludeAutoConfiguration = { + SecurityAutoConfiguration.class, + UserDetailsServiceAutoConfiguration.class, +}) +@AutoConfigureMockMvc(addFilters = false) +@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class, JpaRepositoriesAutoConfiguration.class}) +public class SearchMocksControllerTest { + @Autowired + MockMvc mockMvc; + + @MockBean + SearchMocksService mockSearchService; + + @MockBean + DataSource mockDataSource; + + @MockBean + DatabaseStartupValidator mockDataStartupValidator; + + @MockBean + DefaultHttpInterceptor defaultHttpInterceptor; + + @MockBean + UserDetailsService userDetailsService; + + @MockBean + JwtRequestFilter jwtRequestFilter; + + @Test + void shouldReturnHttpOk_SearchMocks_ReturnMocksAsPageableForSearchQuery() throws Exception { + // Arrange + String route = UrlConfig.MOCKS_PATH + UrlConfig.MOCKS_SEARCH; + String expectedContentType = "application/json"; + Page expectedMocksFromDB = generateMocksPageable(); + Page expectedListMocksResponse = getListMocksResponseInPageableFormat(expectedMocksFromDB); + String expectedResponseBody = convertObjectToJsonString(expectedListMocksResponse); + + lenient().when(mockSearchService.searchMocks(any(Pageable.class), anyString(), anyString())) + .thenReturn(expectedMocksFromDB); + + // Act + MvcResult result = mockMvc.perform(get(route) + .param("searchColumnString", "NAME") + .param("searchQuery", "randomQueryString")) + .andExpect(status().isOk()) + .andExpect(content().contentType(expectedContentType)) + .andReturn(); + + // Assert + assertEquals(expectedResponseBody, result.getResponse().getContentAsString()); + } + + @Test + void shouldReturnHttpBadRequest_SearchMocks_UnableToGetMocksDueToInvalidSearchColumn() throws Exception { + // Arrange + String route = UrlConfig.MOCKS_PATH + UrlConfig.MOCKS_SEARCH; + + lenient().when(mockSearchService.searchMocks(any(Pageable.class), anyString(), anyString())) + .thenReturn(null); + + // Act & Assert + MvcResult result = mockMvc.perform(get(route) + .param("searchColumnString", "NAME") + .param("searchQuery", "randomQueryString")) + .andExpect(status().isBadRequest()) + .andReturn(); + + } +} diff --git a/mimock-backend/src/test/java/com/arbindo/mimock/manage/mimocks/service/SearchMocksServiceTest.java b/mimock-backend/src/test/java/com/arbindo/mimock/manage/mimocks/service/SearchMocksServiceTest.java new file mode 100644 index 00000000..731ffd78 --- /dev/null +++ b/mimock-backend/src/test/java/com/arbindo/mimock/manage/mimocks/service/SearchMocksServiceTest.java @@ -0,0 +1,78 @@ +package com.arbindo.mimock.manage.mimocks.service; + +import com.arbindo.mimock.entities.Mock; +import com.arbindo.mimock.repository.MocksRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EmptySource; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import static com.arbindo.mimock.helpers.entities.MocksGenerator.generateMocksPageable; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; + +@ExtendWith(MockitoExtension.class) +@SpringBootTest +public class SearchMocksServiceTest { + + @org.mockito.Mock + MocksRepository mockRepository; + + SearchMocksService searchMocksService; + + @BeforeEach + void setupMock(){ + this.searchMocksService = SearchMocksServiceImpl.builder() + .mocksRepository(mockRepository) + .build(); + } + + @ParameterizedTest + @EmptySource + @NullSource + void shouldReturnNull_WhenSearchColumnStringIsNullOrEmpty(String searchColumnString) { + // Act + Page result = searchMocksService.searchMocks(Pageable.unpaged(), searchColumnString, "randomQuery"); + + // Assert + assertNull(result); + } + + @ParameterizedTest + @ValueSource(strings = {"Test", "UUID", "RandomString"}) + void shouldReturnNull_WhenSearchColumnIsInvalid(String searchColumnString) { + // Act + Page result = searchMocksService.searchMocks(Pageable.unpaged(), searchColumnString, "randomQuery"); + + // Assert + assertNull(result); + } + + @ParameterizedTest + @ValueSource(strings = {"NAME", "DESCRIPTION", "ROUTE"}) + void shouldReturnNull_WhenSearchColumnIsValid_ReturnMocksAsPageable(String searchColumnString) { + // Arrange + Page expectedMocksFromDB = generateMocksPageable(); + lenient().when(mockRepository.findAllByMockNameIgnoreCaseContaining(anyString(), any(Pageable.class))) + .thenReturn(expectedMocksFromDB); + lenient().when(mockRepository.findAllByDescriptionIgnoreCaseContaining(anyString(), any(Pageable.class))) + .thenReturn(expectedMocksFromDB); + lenient().when(mockRepository.findAllByRouteIgnoreCaseContaining(anyString(), any(Pageable.class))) + .thenReturn(expectedMocksFromDB); + + // Act + Page result = searchMocksService.searchMocks(Pageable.unpaged(), searchColumnString, "randomQuery"); + + // Assert + assertNotNull(result); + assertEquals(expectedMocksFromDB, result); + } +}