From b8bfee1b4567f9408d1166541765dd310ebe1aed Mon Sep 17 00:00:00 2001 From: yevr19 Date: Mon, 11 Sep 2023 00:01:46 +0200 Subject: [PATCH 1/2] test(product): add web layer tests for product package --- .../handler/GlobalExceptionHandler.java | 3 +- .../product/api/SingleProductProvider.java | 2 +- .../exception/ProductNotFoundException.java | 2 +- .../handler/ProductExceptionHandler.java | 27 +++ .../endpoint/ProductsEndpointTest.java | 201 ++++++++++++++++++ .../product/util/ProductUtilStub.java | 85 ++++++++ 6 files changed, 317 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/zufar/onlinestore/product/exception/handler/ProductExceptionHandler.java create mode 100644 src/test/java/com/zufar/onlinestore/product/endpoint/ProductsEndpointTest.java create mode 100644 src/test/java/com/zufar/onlinestore/product/util/ProductUtilStub.java diff --git a/src/main/java/com/zufar/onlinestore/common/exception/handler/GlobalExceptionHandler.java b/src/main/java/com/zufar/onlinestore/common/exception/handler/GlobalExceptionHandler.java index 9a457257..381c27e5 100644 --- a/src/main/java/com/zufar/onlinestore/common/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/zufar/onlinestore/common/exception/handler/GlobalExceptionHandler.java @@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import java.time.LocalDateTime; +import java.util.Collections; import java.util.List; import java.util.stream.Stream; @@ -45,7 +46,7 @@ private List collectErrorMessages(Exception exception) { .getBindingResult() .getAllErrors().stream() .map(DefaultMessageSourceResolvable::getDefaultMessage) - .toList() : List.of(exception.getMessage()); + .toList() : Collections.singletonList(exception.getMessage()); } private String buildErrorDescription(Exception exception) { diff --git a/src/main/java/com/zufar/onlinestore/product/api/SingleProductProvider.java b/src/main/java/com/zufar/onlinestore/product/api/SingleProductProvider.java index abacf1ee..b8f8216a 100644 --- a/src/main/java/com/zufar/onlinestore/product/api/SingleProductProvider.java +++ b/src/main/java/com/zufar/onlinestore/product/api/SingleProductProvider.java @@ -30,4 +30,4 @@ public ProductInfoDto getProductById(final UUID productId) { return new ProductNotFoundException(productId); }); } -} \ No newline at end of file +} diff --git a/src/main/java/com/zufar/onlinestore/product/exception/ProductNotFoundException.java b/src/main/java/com/zufar/onlinestore/product/exception/ProductNotFoundException.java index 892864f7..dd2dc6e1 100644 --- a/src/main/java/com/zufar/onlinestore/product/exception/ProductNotFoundException.java +++ b/src/main/java/com/zufar/onlinestore/product/exception/ProductNotFoundException.java @@ -10,7 +10,7 @@ public class ProductNotFoundException extends RuntimeException { private final UUID productId; public ProductNotFoundException(final UUID productId) { - super(String.format("The product with productId = %s is not found.",productId)); + super(String.format("The product with productId = %s is not found.", productId)); this.productId = productId; } } diff --git a/src/main/java/com/zufar/onlinestore/product/exception/handler/ProductExceptionHandler.java b/src/main/java/com/zufar/onlinestore/product/exception/handler/ProductExceptionHandler.java new file mode 100644 index 00000000..4be74165 --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/product/exception/handler/ProductExceptionHandler.java @@ -0,0 +1,27 @@ +package com.zufar.onlinestore.product.exception.handler; + +import com.zufar.onlinestore.common.exception.handler.GlobalExceptionHandler; +import com.zufar.onlinestore.common.response.ApiResponse; +import com.zufar.onlinestore.product.exception.ProductNotFoundException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RequiredArgsConstructor +@RestControllerAdvice +@Slf4j +public class ProductExceptionHandler extends GlobalExceptionHandler { + + @ExceptionHandler(ProductNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ApiResponse handleProductNotFoundException(final ProductNotFoundException exception) { + ApiResponse apiResponse = buildResponse(exception, HttpStatus.NOT_FOUND); + log.error("Handle product not found exception: failed: messages: {}, description: {}.", + apiResponse.messages(), apiResponse.description()); + + return apiResponse; + } +} diff --git a/src/test/java/com/zufar/onlinestore/product/endpoint/ProductsEndpointTest.java b/src/test/java/com/zufar/onlinestore/product/endpoint/ProductsEndpointTest.java new file mode 100644 index 00000000..def644dd --- /dev/null +++ b/src/test/java/com/zufar/onlinestore/product/endpoint/ProductsEndpointTest.java @@ -0,0 +1,201 @@ +package com.zufar.onlinestore.product.endpoint; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.zufar.onlinestore.common.response.ApiResponse; +import com.zufar.onlinestore.product.api.ProductApi; +import com.zufar.onlinestore.product.dto.ProductInfoDto; +import com.zufar.onlinestore.product.dto.ProductListWithPaginationInfoDto; +import com.zufar.onlinestore.product.exception.ProductNotFoundException; +import com.zufar.onlinestore.security.jwt.filter.JwtAuthenticationProvider; +import org.instancio.Instancio; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.UUID; + +import static com.zufar.onlinestore.product.endpoint.ProductsEndpoint.PRODUCTS_URL; +import static com.zufar.onlinestore.product.util.ProductUtilStub.buildSampleProducts; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(controllers = ProductsEndpoint.class, excludeAutoConfiguration = {SecurityAutoConfiguration.class}) +class ProductsEndpointTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private ProductApi productApi; + + @MockBean + private JwtAuthenticationProvider provider; + + private ProductInfoDto productInfo; + + private ProductListWithPaginationInfoDto productList; + + @Test + void whenGetProductsSortedByNameDescThenReturnProducts() throws Exception { + int page = 0; + int size = 10; + String sortAttribute = "name"; + Sort.Direction sortDirection = Sort.Direction.DESC; + ProductListWithPaginationInfoDto products = buildSampleProducts(page, size, sortAttribute, sortDirection); + + when(productApi.getProducts(page, size, sortAttribute, sortDirection.name())).thenReturn(products); + + mockMvc.perform(get(PRODUCTS_URL) + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size)) + .param("sort_attribute", sortAttribute) + .param("sort_direction", sortDirection.name()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.products[0].name").value("Product E")) + .andExpect(jsonPath("$.totalElements").value(products.totalElements())); + + verify(productApi).getProducts(page, size, sortAttribute, sortDirection.name()); + } + + @Nested + class ProductsEndpointTests { + @BeforeEach + void setUp() { + productInfo = Instancio.create(ProductInfoDto.class); + productList = buildSampleProducts(0, 10, "name", Sort.Direction.ASC); + } + + @Test + void whenGetProductByIdThenReturn200() throws Exception { + UUID productId = UUID.randomUUID(); + + when(productApi.getProduct(productId)).thenReturn(productInfo); + + mockMvc.perform(get(PRODUCTS_URL + "/{productId}", productId) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(productInfo.id().toString())); + + verify(productApi).getProduct(productId); + } + + @Test + void whenGetProductsThenReturn200() throws Exception { + int page = 1; + int size = 10; + String sortAttribute = "name"; + String defaultDirection = Sort.Direction.ASC.name(); + + when(productApi.getProducts(page, size, sortAttribute, defaultDirection)).thenReturn(productList); + + mockMvc.perform(get(PRODUCTS_URL) + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size)) + .param("sort_attribute", sortAttribute) + .param("sort_direction", defaultDirection) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.products.size()").value(productList.products().size())); + + verify(productApi).getProducts(page, size, sortAttribute, defaultDirection); + } + + @Test + void whenNullProductIdThenReturn404() throws Exception { + UUID productId = UUID.randomUUID(); + String errorDescription = buildErrorDescription( + ProductsEndpoint.class.getName() + ); + + when(productApi.getProduct(productId)).thenThrow(new ProductNotFoundException(productId)); + + MvcResult mvcResult = mockMvc.perform(get(PRODUCTS_URL + "/{productId}", productId)) + .andExpect(status().isNotFound()) + .andReturn(); + + String actualResponse = mvcResult.getResponse().getContentAsString(); + ApiResponse expectedResponse = createExpectedErrorResponse(errorDescription, productId); + String expectedResponseBody = objectMapper.writeValueAsString(expectedResponse); + + assertThat(actualResponse).isEqualTo(expectedResponseBody); + + verify(productApi).getProduct(productId); + } + + @Test + void whenGetProductsSortedByNameAscThenReturnProducts() throws Exception { + int page = 0; + int size = 10; + String sortAttribute = "name"; + Sort.Direction sortDirection = Sort.Direction.ASC; + + when(productApi.getProducts(page, size, sortAttribute, sortDirection.name())).thenReturn(productList); + + mockMvc.perform(get(PRODUCTS_URL) + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size)) + .param("sort_attribute", sortAttribute) + .param("sort_direction", sortDirection.name()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.products[0].name").value("Product A")) + .andExpect(jsonPath("$.totalElements").value(productList.totalElements())); + + verify(productApi).getProducts(page, size, sortAttribute, sortDirection.name()); + } + + @Test + void whenFirstPageRetrievedThenReturn200() throws Exception { + int page = 0; + int size = 10; + + when(productApi.getProducts(page, size, null, null)).thenReturn(productList); + + mockMvc.perform(get(PRODUCTS_URL) + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page").value(page)); + + verify(productApi).getProducts(page, size, null, null); + } + + private String buildErrorDescription(String className) { + final int problematicCodeLine = 33; + return String.format("Operation was failed in method: %s that belongs to the class: %s. Problematic code line: %d", + "getProductById", className, problematicCodeLine); + } + + private ApiResponse createExpectedErrorResponse(String errorDescription, UUID productId) { + return new ApiResponse<>( + null, + Collections.singletonList(String.format("The product with productId = %s is not found.", productId)), + errorDescription, + HttpStatus.NOT_FOUND.value(), + LocalDateTime.now()); + } + } +} diff --git a/src/test/java/com/zufar/onlinestore/product/util/ProductUtilStub.java b/src/test/java/com/zufar/onlinestore/product/util/ProductUtilStub.java new file mode 100644 index 00000000..eb34900e --- /dev/null +++ b/src/test/java/com/zufar/onlinestore/product/util/ProductUtilStub.java @@ -0,0 +1,85 @@ +package com.zufar.onlinestore.product.util; + +import com.zufar.onlinestore.product.dto.ProductInfoDto; +import com.zufar.onlinestore.product.dto.ProductListWithPaginationInfoDto; +import org.springframework.data.domain.Sort; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.UUID; + +public class ProductUtilStub { + + public static ProductListWithPaginationInfoDto buildSampleProducts(Integer page, Integer size, String sortAttribute, Sort.Direction sortDirection) { + List products = generateSampleProducts(); + List sortedProducts = sortProducts(products, sortAttribute, sortDirection); + + return new ProductListWithPaginationInfoDto(sortedProducts, page, size, (long) sortedProducts.size(), calculateTotalPages(products.size(), size)); + } + + private static List generateSampleProducts() { + List products = new ArrayList<>(); + + products.add(new ProductInfoDto( + UUID.randomUUID(), + "Product A", + "Description for Product A", + BigDecimal.valueOf(10.50), + 20 + )); + + products.add(new ProductInfoDto( + UUID.randomUUID(), + "Product B", + "Description for Product B", + BigDecimal.valueOf(15.75), + 50 + )); + + products.add(new ProductInfoDto( + UUID.randomUUID(), + "Product C", + "Description for Product C", + BigDecimal.valueOf(20.00), + 30 + )); + + products.add(new ProductInfoDto( + UUID.randomUUID(), + "Product D", + "Description for Product D", + BigDecimal.valueOf(5.25), + 40 + )); + + products.add(new ProductInfoDto( + UUID.randomUUID(), + "Product E", + "Description for Product E", + BigDecimal.valueOf(12.00), + 60 + )); + return products; + } + + private static List sortProducts(List products, String sortAttribute, Sort.Direction direction) { + Comparator comparator = switch (sortAttribute.toLowerCase()) { + case "name" -> Comparator.comparing(ProductInfoDto::name); + case "description" -> Comparator.comparing(ProductInfoDto::description); + case "price" -> Comparator.comparing(ProductInfoDto::price); + case "quantity" -> Comparator.comparing(ProductInfoDto::quantity); + default -> throw new IllegalArgumentException("Unsupported sort attribute: " + sortAttribute); + }; + + if (direction == Sort.Direction.DESC) { + comparator = comparator.reversed(); + } + return products.stream().sorted(comparator).toList(); + } + + private static Integer calculateTotalPages(Integer totalElements, Integer size) { + return (int) Math.ceil((double) totalElements / size); + } +} From a151c7c239ff23db805850a977513f6d8fab828b Mon Sep 17 00:00:00 2001 From: yevr19 Date: Sat, 16 Sep 2023 22:58:38 +0200 Subject: [PATCH 2/2] test(product): add web layer tests for product package --- .../endpoint/ProductsEndpointTest.java | 224 ++++++++---------- ...{ProductUtilStub.java => ProductStub.java} | 24 +- 2 files changed, 117 insertions(+), 131 deletions(-) rename src/test/java/com/zufar/onlinestore/product/util/{ProductUtilStub.java => ProductStub.java} (77%) diff --git a/src/test/java/com/zufar/onlinestore/product/endpoint/ProductsEndpointTest.java b/src/test/java/com/zufar/onlinestore/product/endpoint/ProductsEndpointTest.java index def644dd..a10ddfa3 100644 --- a/src/test/java/com/zufar/onlinestore/product/endpoint/ProductsEndpointTest.java +++ b/src/test/java/com/zufar/onlinestore/product/endpoint/ProductsEndpointTest.java @@ -9,7 +9,6 @@ import com.zufar.onlinestore.security.jwt.filter.JwtAuthenticationProvider; import org.instancio.Instancio; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; @@ -25,7 +24,7 @@ import java.util.UUID; import static com.zufar.onlinestore.product.endpoint.ProductsEndpoint.PRODUCTS_URL; -import static com.zufar.onlinestore.product.util.ProductUtilStub.buildSampleProducts; +import static com.zufar.onlinestore.product.util.ProductStub.buildSampleProducts; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -53,6 +52,12 @@ class ProductsEndpointTest { private ProductListWithPaginationInfoDto productList; + @BeforeEach + void setUp() { + productInfo = Instancio.create(ProductInfoDto.class); + productList = buildSampleProducts(0, 10, "name", Sort.Direction.ASC); + } + @Test void whenGetProductsSortedByNameDescThenReturnProducts() throws Exception { int page = 0; @@ -77,125 +82,100 @@ void whenGetProductsSortedByNameDescThenReturnProducts() throws Exception { verify(productApi).getProducts(page, size, sortAttribute, sortDirection.name()); } - @Nested - class ProductsEndpointTests { - @BeforeEach - void setUp() { - productInfo = Instancio.create(ProductInfoDto.class); - productList = buildSampleProducts(0, 10, "name", Sort.Direction.ASC); - } - - @Test - void whenGetProductByIdThenReturn200() throws Exception { - UUID productId = UUID.randomUUID(); - - when(productApi.getProduct(productId)).thenReturn(productInfo); - - mockMvc.perform(get(PRODUCTS_URL + "/{productId}", productId) - .contentType(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.id").value(productInfo.id().toString())); - - verify(productApi).getProduct(productId); - } - - @Test - void whenGetProductsThenReturn200() throws Exception { - int page = 1; - int size = 10; - String sortAttribute = "name"; - String defaultDirection = Sort.Direction.ASC.name(); - - when(productApi.getProducts(page, size, sortAttribute, defaultDirection)).thenReturn(productList); - - mockMvc.perform(get(PRODUCTS_URL) - .param("page", String.valueOf(page)) - .param("size", String.valueOf(size)) - .param("sort_attribute", sortAttribute) - .param("sort_direction", defaultDirection) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$.products.size()").value(productList.products().size())); - - verify(productApi).getProducts(page, size, sortAttribute, defaultDirection); - } - - @Test - void whenNullProductIdThenReturn404() throws Exception { - UUID productId = UUID.randomUUID(); - String errorDescription = buildErrorDescription( - ProductsEndpoint.class.getName() - ); - - when(productApi.getProduct(productId)).thenThrow(new ProductNotFoundException(productId)); - - MvcResult mvcResult = mockMvc.perform(get(PRODUCTS_URL + "/{productId}", productId)) - .andExpect(status().isNotFound()) - .andReturn(); - - String actualResponse = mvcResult.getResponse().getContentAsString(); - ApiResponse expectedResponse = createExpectedErrorResponse(errorDescription, productId); - String expectedResponseBody = objectMapper.writeValueAsString(expectedResponse); - - assertThat(actualResponse).isEqualTo(expectedResponseBody); - - verify(productApi).getProduct(productId); - } - - @Test - void whenGetProductsSortedByNameAscThenReturnProducts() throws Exception { - int page = 0; - int size = 10; - String sortAttribute = "name"; - Sort.Direction sortDirection = Sort.Direction.ASC; - - when(productApi.getProducts(page, size, sortAttribute, sortDirection.name())).thenReturn(productList); - - mockMvc.perform(get(PRODUCTS_URL) - .param("page", String.valueOf(page)) - .param("size", String.valueOf(size)) - .param("sort_attribute", sortAttribute) - .param("sort_direction", sortDirection.name()) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$.products[0].name").value("Product A")) - .andExpect(jsonPath("$.totalElements").value(productList.totalElements())); - - verify(productApi).getProducts(page, size, sortAttribute, sortDirection.name()); - } - - @Test - void whenFirstPageRetrievedThenReturn200() throws Exception { - int page = 0; - int size = 10; - - when(productApi.getProducts(page, size, null, null)).thenReturn(productList); - - mockMvc.perform(get(PRODUCTS_URL) - .param("page", String.valueOf(page)) - .param("size", String.valueOf(size)) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.page").value(page)); - - verify(productApi).getProducts(page, size, null, null); - } - - private String buildErrorDescription(String className) { - final int problematicCodeLine = 33; - return String.format("Operation was failed in method: %s that belongs to the class: %s. Problematic code line: %d", - "getProductById", className, problematicCodeLine); - } - - private ApiResponse createExpectedErrorResponse(String errorDescription, UUID productId) { - return new ApiResponse<>( - null, - Collections.singletonList(String.format("The product with productId = %s is not found.", productId)), - errorDescription, - HttpStatus.NOT_FOUND.value(), - LocalDateTime.now()); - } + + @Test + void whenGetProductByIdThenReturn200() throws Exception { + UUID productId = UUID.randomUUID(); + + when(productApi.getProduct(productId)).thenReturn(productInfo); + + mockMvc.perform(get(PRODUCTS_URL + "/{productId}", productId) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(productInfo.id().toString())); + + verify(productApi).getProduct(productId); + } + + @Test + void whenGetProductsThenReturn200() throws Exception { + int page = 1; + int size = 10; + String sortAttribute = "name"; + String defaultDirection = Sort.Direction.ASC.name(); + + when(productApi.getProducts(page, size, sortAttribute, defaultDirection)).thenReturn(productList); + + mockMvc.perform(get(PRODUCTS_URL) + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size)) + .param("sort_attribute", sortAttribute) + .param("sort_direction", defaultDirection) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.products.size()").value(productList.products().size())); + + verify(productApi).getProducts(page, size, sortAttribute, defaultDirection); + } + + @Test + void whenNullProductIdThenReturn404() throws Exception { + UUID productId = UUID.randomUUID(); + String errorDescription = buildErrorDescription( + ProductsEndpoint.class.getName() + ); + + when(productApi.getProduct(productId)).thenThrow(new ProductNotFoundException(productId)); + + MvcResult mvcResult = mockMvc.perform(get(PRODUCTS_URL + "/{productId}", productId)) + .andExpect(status().isNotFound()) + .andReturn(); + + String actualResponse = mvcResult.getResponse().getContentAsString(); + ApiResponse expectedResponse = createExpectedErrorResponse(errorDescription, productId); + String expectedResponseBody = objectMapper.writeValueAsString(expectedResponse); + + assertThat(actualResponse).isEqualTo(expectedResponseBody); + + verify(productApi).getProduct(productId); + } + + @Test + void whenGetProductsSortedByNameAscThenReturnProducts() throws Exception { + int page = 0; + int size = 10; + String sortAttribute = "name"; + Sort.Direction sortDirection = Sort.Direction.ASC; + + when(productApi.getProducts(page, size, sortAttribute, sortDirection.name())).thenReturn(productList); + + mockMvc.perform(get(PRODUCTS_URL) + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size)) + .param("sort_attribute", sortAttribute) + .param("sort_direction", sortDirection.name()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.products[0].name").value("Product A")) + .andExpect(jsonPath("$.totalElements").value(productList.totalElements())); + + verify(productApi).getProducts(page, size, sortAttribute, sortDirection.name()); + } + + private String buildErrorDescription(String className) { + final int problematicCodeLine = 33; + return String.format("Operation was failed in method: %s that belongs to the class: %s. Problematic code line: %d", + "getProductById", className, problematicCodeLine); + } + + private ApiResponse createExpectedErrorResponse(String errorDescription, UUID productId) { + return new ApiResponse<>( + null, + Collections.singletonList(String.format("The product with productId = %s is not found.", productId)), + errorDescription, + HttpStatus.NOT_FOUND.value(), + LocalDateTime.now()); } } diff --git a/src/test/java/com/zufar/onlinestore/product/util/ProductUtilStub.java b/src/test/java/com/zufar/onlinestore/product/util/ProductStub.java similarity index 77% rename from src/test/java/com/zufar/onlinestore/product/util/ProductUtilStub.java rename to src/test/java/com/zufar/onlinestore/product/util/ProductStub.java index eb34900e..44c9298d 100644 --- a/src/test/java/com/zufar/onlinestore/product/util/ProductUtilStub.java +++ b/src/test/java/com/zufar/onlinestore/product/util/ProductStub.java @@ -8,9 +8,10 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.Map; import java.util.UUID; -public class ProductUtilStub { +public class ProductStub { public static ProductListWithPaginationInfoDto buildSampleProducts(Integer page, Integer size, String sortAttribute, Sort.Direction sortDirection) { List products = generateSampleProducts(); @@ -65,13 +66,18 @@ private static List generateSampleProducts() { } private static List sortProducts(List products, String sortAttribute, Sort.Direction direction) { - Comparator comparator = switch (sortAttribute.toLowerCase()) { - case "name" -> Comparator.comparing(ProductInfoDto::name); - case "description" -> Comparator.comparing(ProductInfoDto::description); - case "price" -> Comparator.comparing(ProductInfoDto::price); - case "quantity" -> Comparator.comparing(ProductInfoDto::quantity); - default -> throw new IllegalArgumentException("Unsupported sort attribute: " + sortAttribute); - }; + Map> attributeToComparator = Map.of( + "name", Comparator.comparing(ProductInfoDto::name), + "description", Comparator.comparing(ProductInfoDto::description), + "price", Comparator.comparing(ProductInfoDto::price), + "quantity", Comparator.comparing(ProductInfoDto::quantity) + ); + + Comparator comparator = attributeToComparator.getOrDefault(sortAttribute.toLowerCase(), null); + + if (comparator == null) { + throw new IllegalArgumentException("Unsupported sort attribute: " + sortAttribute); + } if (direction == Sort.Direction.DESC) { comparator = comparator.reversed(); @@ -80,6 +86,6 @@ private static List sortProducts(List products, } private static Integer calculateTotalPages(Integer totalElements, Integer size) { - return (int) Math.ceil((double) totalElements / size); + return (totalElements + size - 1) / size; } }