diff --git a/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java b/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java index f99438c..6bd321a 100644 --- a/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java +++ b/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java @@ -15,7 +15,7 @@ import java.util.List; import java.util.UUID; -@CrossOrigin(origins = "http://localhost:3000") +@CrossOrigin(origins = "http://localhost:8080") @RestController @RequestMapping("/products") final class InventoryManagerController { diff --git a/src/test/java/com/encorazone/inventory_manager/controller/InventoryManagerControllerTests.java b/src/test/java/com/encorazone/inventory_manager/controller/InventoryManagerControllerTests.java index 27c10a4..b073c0c 100644 --- a/src/test/java/com/encorazone/inventory_manager/controller/InventoryManagerControllerTests.java +++ b/src/test/java/com/encorazone/inventory_manager/controller/InventoryManagerControllerTests.java @@ -1,19 +1,44 @@ package com.encorazone.inventory_manager.controller; - +import com.encorazone.inventory_manager.domain.*; import com.encorazone.inventory_manager.service.InventoryService; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.hasItems; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; +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.patch; +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.*; @SpringBootTest @AutoConfigureMockMvc @@ -21,15 +46,225 @@ public class InventoryManagerControllerTests { @Autowired - private MockMvc mockMvc; + private MockMvc mvc; + + @Autowired + ObjectMapper objectMapper; @MockitoBean private InventoryService inventoryService; @Test - void getAllProducts_shouldReturnDataFromDatabase() throws Exception { - mockMvc.perform(get("/products")) - .andExpect(status().isOk()); + @DisplayName("returns selected page with products when getting all products") + void getAll_returnsPage() throws Exception { + ProductListResponse payload = new ProductListResponse(List.of(sampleProductRes()), 1); + given(inventoryService.getAll(0, 10)).willReturn(payload); + + mvc.perform(get("/products")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.products", hasSize(1))) + .andExpect(jsonPath("$.totalPages", is(1))); + } + + @Test + @DisplayName("returns selected page according to de filter/sort orders") + void findByFilter_passesPageable() throws Exception { + ProductListResponse payload = new ProductListResponse(List.of(sampleProductRes()), 1); + ArgumentCaptor captor = ArgumentCaptor.forClass(Pageable.class); + Pageable expected = PageRequest.of(1, 10, Sort.by(Sort.Order.desc("price"))); + given(inventoryService.findByNameAndCategoryAndStockQuantity( + "Watermelon", "food", 0, expected)) + .willReturn(payload); + + mvc.perform(get("/products/filters") + .param("name", "Watermelon") + .param("category", "food") + .param("stockQuantity", "0") + .param("page", "1") + .param("size", "10") + .param("sort", "price,desc")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.products", hasSize(1))) + .andExpect(jsonPath("$.totalPages", is(1))); + verify(inventoryService) + .findByNameAndCategoryAndStockQuantity( + eq("Watermelon"), + eq("food"), + eq(0), + captor.capture()); + + Pageable p = captor.getValue(); + assert p.getPageNumber() == 1; + assert p.getPageSize() == 10; + Sort.Order o = p.getSort().getOrderFor("price"); + assert o != null && o.getDirection() == Sort.Direction.DESC; + } + + @Test + @DisplayName("returns small product descriptions corresponding to the created product") + void create_returnsShortResp() throws Exception { + ProductShortResponse created = sampleProductShort(); + given(inventoryService.create(nullable(Product.class))).willReturn(created); + + Product toCreate = sampleProduct(); + mvc.perform(post("/products") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(toCreate))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", notNullValue())) + .andExpect(jsonPath("$.name", is("Watermelon"))); + } + + @Test + @DisplayName("returns small product descriptions corresponding to the updated product") + void update_returnsShortResp() throws Exception { + ProductShortResponse updated = sampleProductShort(); + UUID id = updated.getId(); + given(inventoryService.update(eq(id), nullable(Product.class))).willReturn(Optional.of(updated)); + + mvc.perform(put("/products/{id}", id) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(sampleProduct()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(id.toString()))) + .andExpect(jsonPath("$.name", is("Watermelon"))); + } + + @Test + @DisplayName("returns 404-Not Found when no product id corresponds to the sent one - update") + void update_returnsNotFound() throws Exception { + UUID id = UUID.randomUUID(); + given(inventoryService.update(eq(id), nullable(Product.class))).willReturn(Optional.empty()); + + mvc.perform(put("/products/{id}", id) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(sampleProduct()))) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("returns small product descriptions corresponding to the marked out of stock product") + void markOutOfStock_returnsShortResp() throws Exception { + ProductShortResponse resp = sampleProductShort(); + UUID id = resp.getId(); + given(inventoryService.markOutOfStock(id)).willReturn(Optional.of(resp)); + + mvc.perform(patch("/products/{id}/outofstock", id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(id.toString()))) + .andExpect(jsonPath("$.name", is("Watermelon"))); + } + + @Test + @DisplayName("returns 404-Not Found when no product id corresponds to the sent one - markOutOfStock") + void markOutOfStock_returnsNotFound() throws Exception { + UUID id = UUID.randomUUID(); + given(inventoryService.markOutOfStock(id)).willReturn(Optional.empty()); + + mvc.perform(patch("/products/{id}/outofstock", id)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("returns small product descriptions corresponding to the restored stock product") + void restoreStock_returnsShortResp() throws Exception { + ProductShortResponse resp = sampleProductShort(); + UUID id = resp.getId(); + given(inventoryService.updateStock(id, 10)).willReturn(Optional.of(resp)); + + mvc.perform(patch("/products/{id}/instock", id).param("stockQuantity", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(id.toString()))) + .andExpect(jsonPath("$.name", is("Watermelon"))); + } + + @Test + @DisplayName("returns 404-Not Found when no product id corresponds to the sent one - restoreStock") + void restoreStock_returnsNotFound() throws Exception { + UUID id = UUID.randomUUID(); + given(inventoryService.updateStock(id, 10)).willReturn(Optional.empty()); + + mvc.perform(patch("/products/{id}/instock", id).param("stockQuantity", "10")) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("returns 204-No Content when deleting a product") + void delete_returnsNoContent() throws Exception { + UUID id = UUID.randomUUID(); + doNothing().when(inventoryService).delete(id); + + mvc.perform(delete("/products/{id}", id)) + .andExpect(status().isNoContent()); + } + + @Test + @DisplayName("returns a list with all the categories with products") + void fetchCategories_returnsCategories() throws Exception { + given(inventoryService.fetchCategories()).willReturn(Optional.of(List.of("food", "drinks"))); + + mvc.perform(get("/products/categories")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasItems("food", "drinks"))); } + @Test + @DisplayName("returns 404-Not Found when no category is available") + void fetchCategories_returnsNotFound() throws Exception { + given(inventoryService.fetchCategories()).willReturn(Optional.empty()); + + mvc.perform(get("/products/categories")) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("returns Inventory Summary when calling for metrics") + void fetchSummary_returnsInventorySummary() throws Exception { + InventorySummaryResponse row = new InventorySummaryResponse("food", 100L, BigDecimal.valueOf(2500), BigDecimal.valueOf(25)); + given(inventoryService.fetchInventorySummary()).willReturn(Optional.of(List.of(row))); + + mvc.perform(get("/products/summary")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].category", is("food"))); + } + + @Test + @DisplayName("returns 404-Not Found when no product in stock") + void fetchSummary_returnsNotFound() throws Exception { + given(inventoryService.fetchInventorySummary()).willReturn(Optional.empty()); + + mvc.perform(get("/products/summary")) + .andExpect(status().isNotFound()); + } + + private Product sampleProduct() { + Product p = new Product(); + p.setName("Melon"); + p.setCategory("food"); + p.setUnitPrice(BigDecimal.valueOf(25)); + p.setStockQuantity(5); + return p; + } + + private ProductResponse sampleProductRes() { + return new ProductResponse( + UUID.randomUUID(), + "Watermelon", + "food", + BigDecimal.valueOf(20), + null, + 10, + LocalDateTime.now(), + LocalDateTime.now()); + } + + private ProductShortResponse sampleProductShort() { + return new ProductShortResponse( + UUID.randomUUID(), + "Watermelon", + LocalDateTime.now(), + LocalDateTime.now()); + } } diff --git a/src/test/java/com/encorazone/inventory_manager/mapper/ProductMapperTests.java b/src/test/java/com/encorazone/inventory_manager/mapper/ProductMapperTests.java new file mode 100644 index 0000000..52eef7a --- /dev/null +++ b/src/test/java/com/encorazone/inventory_manager/mapper/ProductMapperTests.java @@ -0,0 +1,100 @@ +package com.encorazone.inventory_manager.mapper; + +import com.encorazone.inventory_manager.domain.Product; +import com.encorazone.inventory_manager.domain.InventorySummaryInterface; +import com.encorazone.inventory_manager.domain.ProductListResponse; +import com.encorazone.inventory_manager.domain.ProductResponse; +import com.encorazone.inventory_manager.domain.ProductShortResponse; +import com.encorazone.inventory_manager.domain.InventorySummaryResponse; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class ProductMapperTests { + + @Test + void toProductShortResponse_mapsFields() { + Product p = sampleProduct(); + ProductShortResponse r = ProductMapper.toProductShortResponse(p); + assertEquals(p.getId(), r.getId()); + assertEquals(p.getName(), r.getName()); + assertEquals(p.getCreationDate(), r.getCreationDate()); + assertEquals(p.getUpdateDate(), r.getUpdateDate()); + } + + @Test + void toProductResponse_mapsFields() { + Product p = sampleProduct(); + ProductResponse r = ProductMapper.toProductResponse(p); + assertEquals(p.getId(), r.getId()); + assertEquals(p.getName(), r.getName()); + assertEquals(p.getCategory(), r.getCategory()); + assertEquals(p.getUnitPrice(), r.getUnitPrice()); + assertEquals(p.getExpirationDate(), r.getExpirationDate()); + assertEquals(p.getStockQuantity(), r.getStockQuantity()); + assertEquals(p.getCreationDate(), r.getCreationDate()); + assertEquals(p.getUpdateDate(), r.getUpdateDate()); + } + + @Test + void toProductListResponse_mapsListAndTotalPages() { + Product p1 = sampleProduct(); + Product p2 = sampleProduct(); + p2.setId(UUID.randomUUID()); + p2.setName("Another"); + ProductListResponse r = ProductMapper.toProductListResponse(List.of(p1, p2), 7); + assertEquals(2, r.getProducts().size()); + assertEquals(7, r.getTotalPages()); + assertEquals(p1.getId(), r.getProducts().get(0).getId()); + assertEquals(p2.getId(), r.getProducts().get(1).getId()); + } + + @Test + void toInventorySummaryResponse_mapsFields() { + InventorySummaryInterface row = sampleSummaryRow("food", 12L, new BigDecimal("345.67"), new BigDecimal("28.81")); + InventorySummaryResponse r = ProductMapper.toInventorySummaryResponse(row); + assertEquals("food", r.getCategory()); + assertEquals(12L, Long.valueOf(r.getProductsInStock())); + assertEquals(new BigDecimal("345.67"), r.getValueInStock()); + assertEquals(new BigDecimal("28.81"), r.getAverageValue()); + } + + @Test + void toInventorySummaryResponseList_mapsList() { + List list = ProductMapper.toInventorySummaryResponseList(List.of( + sampleSummaryRow("food", 10L, new BigDecimal("100.00"), new BigDecimal("10.00")), + sampleSummaryRow("drinks", 5L, new BigDecimal("55.50"), new BigDecimal("11.10")) + )); + assertEquals(2, list.size()); + assertEquals("food", list.get(0).getCategory()); + assertEquals("drinks", list.get(1).getCategory()); + } + + private Product sampleProduct() { + Product p = new Product(); + p.setId(UUID.randomUUID()); + p.setName("Watermelon"); + p.setCategory("food"); + p.setUnitPrice(new BigDecimal("19.99")); + p.setExpirationDate(LocalDate.now().plusDays(30)); + p.setStockQuantity(7); + p.setCreationDate(LocalDateTime.now().minusDays(1)); + p.setUpdateDate(LocalDateTime.now()); + return p; + } + + private InventorySummaryInterface sampleSummaryRow(String category, Long inStock, BigDecimal value, BigDecimal avg) { + return new InventorySummaryInterface() { + @Override public String getCategory() { return category; } + @Override public Long getProductsInStock() { return inStock; } + @Override public BigDecimal getValueInStock() { return value; } + @Override public BigDecimal getAverageValue() { return avg; } + }; + } +} diff --git a/src/test/java/com/encorazone/inventory_manager/service/InventoryProductsFilterTests.java b/src/test/java/com/encorazone/inventory_manager/service/InventoryProductsFilterTests.java index edf236a..bff1b5f 100644 --- a/src/test/java/com/encorazone/inventory_manager/service/InventoryProductsFilterTests.java +++ b/src/test/java/com/encorazone/inventory_manager/service/InventoryProductsFilterTests.java @@ -1,4 +1,83 @@ package com.encorazone.inventory_manager.service; -public class InventoryProductsFilterTests { +import com.encorazone.inventory_manager.domain.Product; +import org.junit.jupiter.api.Test; +import org.springframework.data.jpa.domain.Specification; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.Path; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +class InventoryProductsFilterTests { + + @Test + void nameContains_builds_like_lowercased() { + @SuppressWarnings("unchecked") + Root root = mock(Root.class); + CriteriaQuery query = mock(CriteriaQuery.class); + CriteriaBuilder cb = mock(CriteriaBuilder.class); + @SuppressWarnings("unchecked") + Path path = (Path) mock(Path.class); + @SuppressWarnings("unchecked") Expression lower = (Expression) mock(Expression.class); + Predicate pred = mock(Predicate.class); + given(root.get("name")).willReturn((path)); + given(cb.lower(path)).willReturn(lower); + given(cb.like(lower, "%apple%")).willReturn(pred); + + Specification spec = InventoryProductsFilter.nameContains("Apple"); + assertSame(pred, spec.toPredicate(root, query, cb)); + } + + @Test + void categoryContains_returns_null_when_blank() { + Specification spec = InventoryProductsFilter.categoryContains(" "); + @SuppressWarnings("unchecked") + Root root = mock(Root.class); + CriteriaQuery query = mock(CriteriaQuery.class); + CriteriaBuilder cb = mock(CriteriaBuilder.class); + assertNull(spec.toPredicate(root, query, cb)); + } + + @Test + void quantityEquals_inStock_builds_gt_zero() { + @SuppressWarnings("unchecked") + Root root = mock(Root.class); + CriteriaQuery query = mock(CriteriaQuery.class); + CriteriaBuilder cb = mock(CriteriaBuilder.class); + @SuppressWarnings("unchecked") + Path path = (Path) mock(Path.class); + Predicate pred = mock(Predicate.class); + + given(root.get("stockQuantity")).willReturn(path); + given(cb.greaterThan(path, 0)).willReturn(pred); + + Specification spec = InventoryProductsFilter.quantityEquals(1); + assertNotNull(spec); + assertSame(pred, spec.toPredicate(root, query, cb)); + } + + @Test + void quantityEquals_outOfStock_builds_eq_zero() { + @SuppressWarnings("unchecked") + Root root = mock(Root.class); + CriteriaQuery query = mock(CriteriaQuery.class); + CriteriaBuilder cb = mock(CriteriaBuilder.class); + @SuppressWarnings("unchecked") + Path path = (Path) mock(Path.class); + Predicate pred = mock(Predicate.class); + + given(root.get("stockQuantity")).willReturn(path); + given(cb.equal(path, 0)).willReturn(pred); + + Specification spec = InventoryProductsFilter.quantityEquals(2); + assertNotNull(spec); + assertSame(pred, spec.toPredicate(root, query, cb)); + } }