-
-
Notifications
You must be signed in to change notification settings - Fork 118
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test(product): add web layer tests for product package
- Loading branch information
Showing
6 changed files
with
317 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
src/main/java/com/zufar/onlinestore/product/exception/handler/ProductExceptionHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Void> handleProductNotFoundException(final ProductNotFoundException exception) { | ||
ApiResponse<Void> apiResponse = buildResponse(exception, HttpStatus.NOT_FOUND); | ||
log.error("Handle product not found exception: failed: messages: {}, description: {}.", | ||
apiResponse.messages(), apiResponse.description()); | ||
|
||
return apiResponse; | ||
} | ||
} |
201 changes: 201 additions & 0 deletions
201
src/test/java/com/zufar/onlinestore/product/endpoint/ProductsEndpointTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Void> 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<Void> 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()); | ||
} | ||
} | ||
} |
85 changes: 85 additions & 0 deletions
85
src/test/java/com/zufar/onlinestore/product/util/ProductUtilStub.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<ProductInfoDto> products = generateSampleProducts(); | ||
List<ProductInfoDto> sortedProducts = sortProducts(products, sortAttribute, sortDirection); | ||
|
||
return new ProductListWithPaginationInfoDto(sortedProducts, page, size, (long) sortedProducts.size(), calculateTotalPages(products.size(), size)); | ||
} | ||
|
||
private static List<ProductInfoDto> generateSampleProducts() { | ||
List<ProductInfoDto> 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<ProductInfoDto> sortProducts(List<ProductInfoDto> products, String sortAttribute, Sort.Direction direction) { | ||
Comparator<ProductInfoDto> 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); | ||
} | ||
} |