diff --git a/pom.xml b/pom.xml index f74e5f3..0e6568c 100644 --- a/pom.xml +++ b/pom.xml @@ -120,6 +120,18 @@ + + org.apache.maven.plugins + maven-compiler-plugin + 3.12.1 + + ${java.version} + ${java.version}  + + -parameters + + + diff --git a/src/main/java/dev/waltercrdz/api/ecommerce/orders/infrastructure/in/error/ErrorHandler.java b/src/main/java/dev/waltercrdz/api/ecommerce/orders/infrastructure/in/error/ErrorHandler.java index 28cec7a..6b928a4 100644 --- a/src/main/java/dev/waltercrdz/api/ecommerce/orders/infrastructure/in/error/ErrorHandler.java +++ b/src/main/java/dev/waltercrdz/api/ecommerce/orders/infrastructure/in/error/ErrorHandler.java @@ -7,6 +7,10 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.NoHandlerFoundException; import dev.waltercrdz.api.ecommerce.shared.domain.exception.DatabaseConnectionException; import dev.waltercrdz.api.ecommerce.shared.domain.exception.ErrorCode; @@ -20,43 +24,58 @@ public class ErrorHandler { private static final Logger LOGGER = LoggerFactory.getLogger(ErrorHandler.class); @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity handleIllegalArgumentException(IllegalArgumentException e) { + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ResponseBody + public ApiError handleIllegalArgumentException(IllegalArgumentException e) { LOGGER.error("Illegal argument: ", e); - return ResponseEntity - .status(HttpStatus.BAD_REQUEST) - .body(ApiError.fromException(ErrorCode.ILLEGAL_ARGUMENT, e)); + return ApiError.fromException(ErrorCode.ILLEGAL_ARGUMENT, e); } @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ResponseBody + public ApiError handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { LOGGER.error("Argument Not Valid: ", e); - return ResponseEntity - .status(HttpStatus.BAD_REQUEST) - .body(ApiError.fromException(ErrorCode.ILLEGAL_ARGUMENT, e)); + return ApiError.fromException(ErrorCode.ILLEGAL_ARGUMENT, e); + } + + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ResponseBody + public ApiError handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) { + LOGGER.error("Argument Not Valid: ", e); + return ApiError.fromException(ErrorCode.ILLEGAL_ARGUMENT, e); } @ExceptionHandler(DatabaseConnectionException.class) - public ResponseEntity handleDatabaseConnectionException(DatabaseConnectionException e) { + @ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE) + @ResponseBody + public ApiError handleDatabaseConnectionException(DatabaseConnectionException e) { LOGGER.error("Database connection: ", e); - return ResponseEntity - .status(HttpStatus.SERVICE_UNAVAILABLE) - .body(ApiError.fromDomainException(e)); + return ApiError.fromDomainException(e); } @ExceptionHandler(ProductNotFoundException.class) - public ResponseEntity handleProductNotFoundException(ProductNotFoundException e) { + @ResponseStatus(HttpStatus.NOT_FOUND) + @ResponseBody + public ApiError handleProductNotFoundException(ProductNotFoundException e) { LOGGER.error("Product Not Found: ", e); - return ResponseEntity - .status(HttpStatus.BAD_REQUEST) - .body(ApiError.fromDomainException(e)); + return ApiError.fromDomainException(e); } @ExceptionHandler(NotEnoughStockException.class) - public ResponseEntity handleNotEnoughStockException(NotEnoughStockException e) { + @ResponseStatus(HttpStatus.CONFLICT) + @ResponseBody + public ApiError handleNotEnoughStockException(NotEnoughStockException e) { LOGGER.error("Insufficient Stock: ", e); - return ResponseEntity - .status(HttpStatus.CONFLICT) - .body(ApiError.fromDomainException(e)); + return ApiError.fromDomainException(e); + } + + @ExceptionHandler(NoHandlerFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + @ResponseBody + public ApiError handleNoHandlerFoundException(NoHandlerFoundException e) { + return ApiError.fromException(ErrorCode.NOT_FOUND, e); } @ExceptionHandler(Exception.class) diff --git a/src/main/java/dev/waltercrdz/api/ecommerce/products/infrastructure/in/controller/ProductController.java b/src/main/java/dev/waltercrdz/api/ecommerce/products/infrastructure/in/controller/ProductController.java index 7cbfb89..dc8453d 100644 --- a/src/main/java/dev/waltercrdz/api/ecommerce/products/infrastructure/in/controller/ProductController.java +++ b/src/main/java/dev/waltercrdz/api/ecommerce/products/infrastructure/in/controller/ProductController.java @@ -5,6 +5,7 @@ import java.util.stream.Collectors; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -23,7 +24,7 @@ public ProductController(ProductFinder finder) { } @GetMapping("/{id}") - public ProductResponse getProduct(UUID id) { + public ProductResponse getProduct(@PathVariable UUID id) { final var product = this.finder.find(id); return ProductMapper.toResponse(product); } diff --git a/src/main/java/dev/waltercrdz/api/ecommerce/shared/domain/exception/ErrorCode.java b/src/main/java/dev/waltercrdz/api/ecommerce/shared/domain/exception/ErrorCode.java index 1b71890..dc497be 100644 --- a/src/main/java/dev/waltercrdz/api/ecommerce/shared/domain/exception/ErrorCode.java +++ b/src/main/java/dev/waltercrdz/api/ecommerce/shared/domain/exception/ErrorCode.java @@ -6,6 +6,7 @@ public enum ErrorCode { NOT_ENOUGH_STOCK("NOT_ENOUGH_STOCK"), ILLEGAL_ARGUMENT("ILLEGAL_ARGUMENT"), MISSING_PARAMETER("MISSING_PARAMETER"), + NOT_FOUND("NOT_FOUND"), INTERNAL_SERVER_ERROR("INTERNAL_SERVER_ERROR"); private final String code; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index afd1b29..66c5976 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -10,6 +10,9 @@ aws: spring: application: name: e-commerce + web: + resources: + add-mappings: false datasource: url: ${DB_URL} driver-class-name: org.postgresql.Driver diff --git a/src/test/java/dev/waltercrdz/api/ecommerce/orders/infrastructure/utils/ProductMother.java b/src/test/java/dev/waltercrdz/api/ecommerce/orders/infrastructure/utils/ProductMother.java new file mode 100644 index 0000000..99a813a --- /dev/null +++ b/src/test/java/dev/waltercrdz/api/ecommerce/orders/infrastructure/utils/ProductMother.java @@ -0,0 +1,27 @@ +package dev.waltercrdz.api.ecommerce.orders.infrastructure.utils; + +import java.math.BigDecimal; +import java.util.UUID; + +import dev.waltercrdz.api.ecommerce.products.infrastructure.in.dto.ProductResponse; + +public class ProductMother { + + public static final String INVALID_PRODUCT_ID = "invalid_product_id"; + public static final UUID NON_EXISTENT_PRODUCT_ID = UUID.fromString("f7b3b3b4-3b1b-4b3b-8b3b-3b1b3b1b3b1f"); + public static final UUID PRODUCT_ID_1 = UUID.fromString("f7b3b3b4-3b1b-4b3b-8b3b-3b1b3b1b3b1b"); + + // ('f7b3b3b4-3b1b-4b3b-8b3b-3b1b3b1b3b1b', 'Product 1', 'Description 1', 10.00, 10), + // ('f7b3b3b4-3b1b-4b3b-8b3b-3b1b3b1b3b1c', 'Product 2', 'Description 2', 20.00, 0), + // ('f7b3b3b4-3b1b-4b3b-8b3b-3b1b3b1b3b1d', 'Product 3', 'Description 3', 30.00, 5) + + public static ProductResponse createProduct1() { + return new ProductResponse( + PRODUCT_ID_1, + "Product 1", + "Description 1", + BigDecimal.valueOf(10.00), + 10 + ); + } +} diff --git a/src/test/java/dev/waltercrdz/api/ecommerce/products/infrastructure/in/controller/ProductControllerTest.java b/src/test/java/dev/waltercrdz/api/ecommerce/products/infrastructure/in/controller/ProductControllerTest.java new file mode 100644 index 0000000..b70888d --- /dev/null +++ b/src/test/java/dev/waltercrdz/api/ecommerce/products/infrastructure/in/controller/ProductControllerTest.java @@ -0,0 +1,66 @@ +package dev.waltercrdz.api.ecommerce.products.infrastructure.in.controller; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.springframework.http.MediaType; + +import dev.waltercrdz.api.ecommerce.orders.infrastructure.configuration.TestConfig; +import dev.waltercrdz.api.ecommerce.orders.infrastructure.utils.ProductMother; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@AutoConfigureMockMvc +@Import(TestConfig.class) +public class ProductControllerTest { + + private static final String URI_PRODUCT = "/products"; + + @Autowired + private MockMvc mockMvc; + + @Test + void shouldReturnAllProducts() throws Exception { + mockMvc.perform(get(URI_PRODUCT) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$.length()").value(3)); + } + + @Test + void shouldReturnProduct_whenValidProductIdIsProvided() throws Exception { + final var expectedProductResult = ProductMother.createProduct1(); + mockMvc.perform(get(URI_PRODUCT + "/" + expectedProductResult.id().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(expectedProductResult.id().toString())) + .andExpect(jsonPath("$.name").value(expectedProductResult.name())) + .andExpect(jsonPath("$.description").value(expectedProductResult.description())) + .andExpect(jsonPath("$.price").value(expectedProductResult.price().toString())) + .andExpect(jsonPath("$.stock").value(expectedProductResult.stock())); + } + + @Test + void shouldReturnNotFoundError_whenNonExistentProductIdIsProvided() throws Exception { + mockMvc.perform(get(URI_PRODUCT + "/" + ProductMother.NON_EXISTENT_PRODUCT_ID.toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + void shouldReturnBadRequestError_whenInvalidProductIdIsProvided() throws Exception { + mockMvc.perform(get(URI_PRODUCT + "/" + ProductMother.INVALID_PRODUCT_ID) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } +}