From 54ed9f3d27ae6db365f5c56cbd0ef00369100ad6 Mon Sep 17 00:00:00 2001 From: Greqit <146957474+qreqit@users.noreply.github.com> Date: Tue, 1 Oct 2024 18:58:51 +0100 Subject: [PATCH 01/21] Create README.md --- README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..ef8b1a3 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# πŸ“š Book Store Application +I have always loved books, especially those that help understand the meaning of human existence and provide fuel for thought. Inspired by this passion, I decided to create my own application, where users can purchase books from the comfort of their homes. + +# πŸš€ Technologies Used +- Spring Boot +- Spring Security +- Spring Data JPA +- JWT (JSON Web Token) +- Swagger +- Liquibase +- MapStruct +- Lombok +- Hibernate Validator +- MySQL +# πŸ“– API Endpoints +1. AuthenticationController +- ```POST /auth/registration``` – Register a new user. +- ```POST /auth/login``` – User authentication (login). +2. BookController +- ```GET /books``` – Retrieve all books with pagination (ADMIN access only). +- ```GET /books/{id}``` – Retrieve a book by its ID. +- ```POST /books``` – Create a new book (ADMIN access only). +- ```PUT /books/{id}``` – Update a book by its ID (ADMIN access only). +- ```DELETE /books/{id}``` – Delete a book by its ID (ADMIN access only). +- ```GET /books/search``` – Search books by parameters. +3. CategoryController +- ```POST /categories``` – Create a new category (ADMIN access only). +- ```GET /categories``` – Retrieve all categories. +- ```GET /categories/{id}``` – Retrieve a category by its ID. +- ```PUT /categories/{id}``` – Update a category by its ID (ADMIN access only). +- ```DELETE /categories/{id}``` – Delete a category by its ID (ADMIN access only). +- ```GET /categories/{id}/books``` – Retrieve all books in a category by its ID. +4. OrderController +- ```POST /orders``` – Create a new order (USER access only). +- ```GET /orders``` – Retrieve all orders of the logged-in user (USER access only). +- ```PATCH /orders/{id}``` – Update the status of an order (ADMIN access only). +- ```GET /orders/{orderId}/items``` – Retrieve all items from a specific order (USER access only). +- ```GET /orders/{orderId}/items/{itemId}``` – Retrieve a specific item from an order (USER access only). +5. ShoppingCartController +- ```GET /cart``` – Retrieve the current user's shopping cart. +- ```POST /cart``` – Add a book to the shopping cart. +- ```PUT /cart/items/{cartItemId}``` – Update the quantity of items in the cart. +- ```DELETE /cart/items/{cartItemId}``` – Remove an item from the cart. +# 🎯 Application Overview +This application provides functionalities for managing users, books, categories, orders, and shopping carts. The system ensures secure access with role-based authorization (USER and ADMIN). The app is designed for book sales, allowing users to: + +Create accounts and log in. +Purchase books conveniently and quickly from home. +Track the status of orders and know when they arrive at the post office. +This is my first large-scale application, and I faced numerous challenges during its development. Each part was difficult but immensely educational, and I gained a lot of knowledge that will be invaluable for my future projects. From e6ce08ddeeae6b8917930de34ef2300aee9525bb Mon Sep 17 00:00:00 2001 From: Greqit <146957474+qreqit@users.noreply.github.com> Date: Tue, 1 Oct 2024 20:55:21 +0200 Subject: [PATCH 02/21] Add files via upload --- Book-Store.pdf | Bin 0 -> 37093 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Book-Store.pdf diff --git a/Book-Store.pdf b/Book-Store.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b86a7dacdd56d96200bd60667f0d154e06920406 GIT binary patch literal 37093 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 z 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 KDJVo_MgKol4k1YZ literal 0 HcmV?d00001 From 491c2baed4866a0dee11e474728ecbe0c6ab95be Mon Sep 17 00:00:00 2001 From: Greqit <146957474+qreqit@users.noreply.github.com> Date: Tue, 1 Oct 2024 19:58:48 +0100 Subject: [PATCH 03/21] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ef8b1a3..1571783 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # πŸ“š Book Store Application I have always loved books, especially those that help understand the meaning of human existence and provide fuel for thought. Inspired by this passion, I decided to create my own application, where users can purchase books from the comfort of their homes. - +![Book-Store](Book-Store.pdf) # πŸš€ Technologies Used - Spring Boot - Spring Security From 5657d2917861609d721106fb3cde329792a37c8c Mon Sep 17 00:00:00 2001 From: Greqit <146957474+qreqit@users.noreply.github.com> Date: Tue, 1 Oct 2024 21:05:43 +0200 Subject: [PATCH 04/21] Add files via upload --- Book-Store.png | Bin 0 -> 155932 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Book-Store.png diff --git a/Book-Store.png b/Book-Store.png new file mode 100644 index 0000000000000000000000000000000000000000..28b4105b5a2fd5cf7c37efb75a977af80e7c1edf GIT binary patch literal 155932 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 literal 0 HcmV?d00001 From 2654704f35860bf28d73dbf886749bce65b9f0b2 Mon Sep 17 00:00:00 2001 From: Greqit <146957474+qreqit@users.noreply.github.com> Date: Tue, 1 Oct 2024 20:06:28 +0100 Subject: [PATCH 05/21] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1571783..e1c29f5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # πŸ“š Book Store Application I have always loved books, especially those that help understand the meaning of human existence and provide fuel for thought. Inspired by this passion, I decided to create my own application, where users can purchase books from the comfort of their homes. -![Book-Store](Book-Store.pdf) +![Book-Store](./Book-Store.png) # πŸš€ Technologies Used - Spring Boot - Spring Security From 58d038e85e66286df013df0ac3dab423ecb3c1fb Mon Sep 17 00:00:00 2001 From: Greqit <146957474+qreqit@users.noreply.github.com> Date: Tue, 1 Oct 2024 20:31:56 +0100 Subject: [PATCH 06/21] Add link with video owerview of Program Functionality --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index e1c29f5..af83657 100644 --- a/README.md +++ b/README.md @@ -48,3 +48,6 @@ Create accounts and log in. Purchase books conveniently and quickly from home. Track the status of orders and know when they arrive at the post office. This is my first large-scale application, and I faced numerous challenges during its development. Each part was difficult but immensely educational, and I gained a lot of knowledge that will be invaluable for my future projects. + +# πŸ“ΉVideo Overview of Program Functionality +You can also find a video of the program at this link: https://www.loom.com/share/0c519d24efc04b64acf9a3e9096b40cb?sid=e3750a8b-c044-4b13-b382-535497228e21 From 32b2f4c57d88c09cd669f22e2664b8b9f5b67ee7 Mon Sep 17 00:00:00 2001 From: Greqit <146957474+qreqit@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:20:51 +0100 Subject: [PATCH 07/21] Update README.md --- README.md | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index af83657..dff39e4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ -# πŸ“š Book Store Application -I have always loved books, especially those that help understand the meaning of human existence and provide fuel for thought. Inspired by this passion, I decided to create my own application, where users can purchase books from the comfort of their homes. -![Book-Store](./Book-Store.png) +# 🎯 Project Overview +This application provides functionalities for managing users, books, categories, orders, and shopping carts. The system ensures secure access with role-based authorization (USER and ADMIN). The app is designed for book sales, allowing users to: + +Create accounts and log in. +Purchase books conveniently and quickly from home. +Track the status of orders and know when they arrive at the post office. +This is my first large-scale application, and I faced numerous challenges during its development. Each part was difficult but immensely educational, and I gained a lot of knowledge that will be invaluable for my future projects. + # πŸš€ Technologies Used - Spring Boot - Spring Security @@ -12,6 +17,7 @@ I have always loved books, especially those that help understand the meaning of - Lombok - Hibernate Validator - MySQL + # πŸ“– API Endpoints 1. AuthenticationController - ```POST /auth/registration``` – Register a new user. @@ -41,13 +47,17 @@ I have always loved books, especially those that help understand the meaning of - ```POST /cart``` – Add a book to the shopping cart. - ```PUT /cart/items/{cartItemId}``` – Update the quantity of items in the cart. - ```DELETE /cart/items/{cartItemId}``` – Remove an item from the cart. -# 🎯 Application Overview -This application provides functionalities for managing users, books, categories, orders, and shopping carts. The system ensures secure access with role-based authorization (USER and ADMIN). The app is designed for book sales, allowing users to: -Create accounts and log in. -Purchase books conveniently and quickly from home. -Track the status of orders and know when they arrive at the post office. -This is my first large-scale application, and I faced numerous challenges during its development. Each part was difficult but immensely educational, and I gained a lot of knowledge that will be invaluable for my future projects. +# Models and relations + +![Book-Store](./Book-Store.png) + +# πŸ“š Getting Started +I have always loved books, especially those that help understand the meaning of human existence and provide fuel for thought. Inspired by this passion, I decided to create my own application, where users can purchase books from the comfort of their homes. # πŸ“ΉVideo Overview of Program Functionality You can also find a video of the program at this link: https://www.loom.com/share/0c519d24efc04b64acf9a3e9096b40cb?sid=e3750a8b-c044-4b13-b382-535497228e21 + +# Contacts +- Email: greqit.work@gmail.com +- [LinkedIn](https://www.linkedin.com/in/ivan-prystaia-7099a22b1/) From 35268467f46180b264b92e6702afdb71af86e03f Mon Sep 17 00:00:00 2001 From: greqit Date: Wed, 6 Nov 2024 12:49:10 +0100 Subject: [PATCH 08/21] add tests --- pom.xml | 2 +- src/main/resources/application.properties | 2 +- .../ShoppingCartControllerTest.java | 123 ++++++++++++ .../service/ShoppingCartServiceTest.java | 189 ++++++++++++++++++ src/test/resources/data-sql/clear-tables.sql | 4 +- .../insert-tables-for-shoppingcart.sql | 18 ++ 6 files changed, 335 insertions(+), 3 deletions(-) create mode 100644 src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java create mode 100644 src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java create mode 100644 src/test/resources/data-sql/insert-tables-for-shoppingcart.sql diff --git a/pom.xml b/pom.xml index d17cb27..1263652 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ 3.3.1 - mare.academy + mate.academy Spring-Boot-web 0.0.1-SNAPSHOT Spring-Boot-web diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a6d1391..7da5384 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -4,7 +4,7 @@ spring.datasource.url=jdbc:mysql://localhost:3306/book-store spring.datasource.username=root spring.datasource.password=1234567 -spring.jpa.hibernate.ddl-auto=update +spring.jpa.hibernate.ddl-auto=validate spring.jpa.show-sql=true spring.jpa.open-in-view=false diff --git a/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java b/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java new file mode 100644 index 0000000..1a2100a --- /dev/null +++ b/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java @@ -0,0 +1,123 @@ +package mate.academy.springbootwebgreqit.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import mate.academy.springbootwebgreqit.dto.cartitem.CartItemRequestDto; +import mate.academy.springbootwebgreqit.model.Book; +import mate.academy.springbootwebgreqit.model.User; +import mate.academy.springbootwebgreqit.service.ShoppingCartService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import java.math.BigDecimal; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class ShoppingCartControllerTest { + protected static MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private ShoppingCartService shoppingCartService; + + private User user; + private Book book; + + @BeforeEach + void setUp() { + // БтворСння користувача Ρ‚Π° ΠΊΠ½ΠΈΠ³ΠΈ для тСстів + user = new User(); + user.setId(1L); + user.setFirstName("john"); + user.setLastName("doe"); + user.setPassword("password"); + + book = new Book(); + book.setId(1L); + book.setTitle("Sample Book"); + book.setAuthor("John Author"); + book.setIsbn("1234567890"); + book.setPrice(BigDecimal.valueOf(10.99)); + book.setDescription("A sample book description."); + book.setCoverImage("https://example.com/book-cover.jpg"); + + // ΠœΠΎΠΊΡ–Π½Π³ Π°Π±ΠΎ створСння залСТностСй Π² класі ShoppingCartService. + } + + @Sql(scripts = "/data-sql/clear-tables.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + @WithMockUser(username = "john_doe", roles = {"USER"}) + @Test + @DisplayName("Get shopping cart for the current user") + void getShoppingCart_ShouldReturnCartForUser() throws Exception { + mockMvc.perform(get("/cart") + .param("userId", "1") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.userId").value(1L)) + .andExpect(jsonPath("$.items").isArray()) + .andReturn(); + } + + @Sql(scripts = {"/data-sql/clear-tables.sql", "/data-sql/insert-tables-for-shoppingcart.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @WithMockUser(username = "john_doe", roles = {"USER"}) + @Test + @DisplayName("Add book to shopping cart") + void addBookToShoppingCart_ShouldReturnUpdatedCart() throws Exception { + CartItemRequestDto cartItemRequestDto = new CartItemRequestDto(); + cartItemRequestDto.setBookId(1L); // Використовуємо ID ΠΊΠ½ΠΈΠ³ΠΈ, яка Π±ΡƒΠ»Π° Π΄ΠΎΠ΄Π°Π½Π° Π² Π±Π°Π·Ρƒ + cartItemRequestDto.setQuantity(2); + + String jsonRequest = objectMapper.writeValueAsString(cartItemRequestDto); + + MvcResult result = mockMvc.perform(post("/cart") + .param("userId", "1") + .contentType(MediaType.APPLICATION_JSON) + .content(jsonRequest)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.items[0].book.title").value("wallet")) // ΠŸΠ΅Ρ€Π΅Π²Ρ–Ρ€ΡΡ”ΠΌΠΎ, Ρ‡ΠΈ ΠΏΡ€Π°Π²ΠΈΠ»ΡŒΠ½Π° ΠΊΠ½ΠΈΠ³Π° + .andExpect(jsonPath("$.items[0].quantity").value(2)) // ΠŸΠ΅Ρ€Π΅Π²Ρ–Ρ€ΡΡ”ΠΌΠΎ ΠΊΡ–Π»ΡŒΠΊΡ–ΡΡ‚ΡŒ + .andReturn(); + } + + + @Sql(scripts = "/data-sql/clear-tables.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + @WithMockUser(username = "john_doe", roles = {"USER"}) + @Test + @DisplayName("Update cart item quantity") + void updateCartItemQuantity_ShouldReturnUpdatedQuantity() throws Exception { + Long cartItemId = 1L; + int newQuantity = 3; + + mockMvc.perform(put("/cart/items/{cartItemId}", cartItemId) + .param("quantity", String.valueOf(newQuantity)) + .param("userId", "1") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.items[0].quantity").value(newQuantity)) + .andReturn(); + } + + @Sql(scripts = "/data-sql/clear-tables.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + @WithMockUser(username = "john_doe", roles = {"USER"}) + @Test + @DisplayName("Remove item from cart") + void removeItemFromCart_ShouldReturnEmptyCart() throws Exception { + Long cartItemId = 1L; + + mockMvc.perform(delete("/cart/items/{cartItemId}", cartItemId) + .param("userId", "1") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.items").isEmpty()) + .andReturn(); + } +} diff --git a/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java b/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java new file mode 100644 index 0000000..10a97a7 --- /dev/null +++ b/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java @@ -0,0 +1,189 @@ +package mate.academy.springbootwebgreqit.service; + +import mate.academy.springbootwebgreqit.dto.cartitem.CartItemRequestDto; +import mate.academy.springbootwebgreqit.dto.shoppingcart.ShoppingCartDto; +import mate.academy.springbootwebgreqit.exception.EntityNotFoundException; +import mate.academy.springbootwebgreqit.mapper.CartItemMapper; +import mate.academy.springbootwebgreqit.mapper.ShoppingCartMapper; +import mate.academy.springbootwebgreqit.model.Book; +import mate.academy.springbootwebgreqit.model.CartItem; +import mate.academy.springbootwebgreqit.model.ShoppingCart; +import mate.academy.springbootwebgreqit.model.User; +import mate.academy.springbootwebgreqit.repository.BookRepository; +import mate.academy.springbootwebgreqit.repository.CartItemRepository; +import mate.academy.springbootwebgreqit.repository.ShoppingCartRepository; +import mate.academy.springbootwebgreqit.repository.UserRepository; +import mate.academy.springbootwebgreqit.service.impl.ShoppingCartServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ShoppingCartServiceTest { + + @Mock + private CartItemRepository cartItemRepository; + @Mock + private ShoppingCartRepository shoppingCartRepository; + @Mock + private ShoppingCartMapper shoppingCartMapper; + @Mock + private CartItemMapper cartItemMapper; + @Mock + private BookRepository bookRepository; + @Mock + private UserRepository userRepository; + + @InjectMocks + private ShoppingCartServiceImpl shoppingCartService; + + private User user; + private ShoppingCart shoppingCart; + private ShoppingCartDto shoppingCartDto; + private CartItemRequestDto cartItemRequestDto; + private Book book; + private CartItem cartItem; + + @BeforeEach + void setUp() { + user = new User(); + user.setId(1L); + + shoppingCart = new ShoppingCart(); + shoppingCart.setUser(user); + + shoppingCartDto = new ShoppingCartDto(); + + cartItemRequestDto = new CartItemRequestDto(); + cartItemRequestDto.setBookId(1L); + cartItemRequestDto.setQuantity(2); + + book = new Book(); + book.setId(1L); + + cartItem = new CartItem(); + cartItem.setId(1L); + cartItem.setBook(book); + cartItem.setQuantity(2); + cartItem.setShoppingCart(shoppingCart); + } + + @Test + void getShoppingCartForCurrentUser_ShouldReturnShoppingCartDto() { + user.setShoppingCart(shoppingCart); + // Arrange + when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); + when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); + + // Act + ShoppingCartDto result = shoppingCartService.getShoppingCartForCurrentUser(user.getId()); + + // Assert + assertNotNull(result); + verify(userRepository).findById(user.getId()); + verify(shoppingCartMapper).toDto(shoppingCart); + } + + @Test + void getShoppingCartForCurrentUser_ShouldThrowEntityNotFoundException_WhenShoppingCartNotFound() { + // Arrange + User user1 = new User(); + user1.setId(2L); + user1.setShoppingCart(null); + when(userRepository.findById(user1.getId())).thenReturn(Optional.of(user1)); + + // Act & Assert + assertThrows(EntityNotFoundException.class, + () -> shoppingCartService.getShoppingCartForCurrentUser(user1.getId())); + } + + + @Test + void addBookToShoppingCart_ShouldAddNewBookAndReturnShoppingCartDto() { + // Arrange + when(shoppingCartRepository.findByUserId(user.getId())).thenReturn(Optional.of(shoppingCart)); + when(bookRepository.findById(cartItemRequestDto.getBookId())).thenReturn(Optional.of(book)); + when(cartItemMapper.toModel(cartItemRequestDto)).thenReturn(cartItem); + when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); + + // Mocking save method to return updated shopping cart + when(shoppingCartRepository.save(any(ShoppingCart.class))).thenReturn(shoppingCart); + + // Act + ShoppingCartDto result = shoppingCartService.addBookToShoppingCart(cartItemRequestDto, user.getId()); + + // Assert + assertNotNull(result); + verify(shoppingCartRepository).findByUserId(user.getId()); + verify(cartItemMapper).toModel(cartItemRequestDto); + verify(bookRepository).findById(cartItemRequestDto.getBookId()); + verify(shoppingCartRepository).save(shoppingCart); + + // Optional: Verify that cart items were updated + assertEquals(1, shoppingCart.getCartItems().size()); // assuming one item is added + } + + + @Test + void updateCartItemQuantity_ShouldUpdateQuantityAndReturnShoppingCartDto() { + user.setShoppingCart(shoppingCart); + // Arrange + shoppingCart.getCartItems().add(cartItem); + + // ΠœΠΎΠΊΡƒΠ²Π°Π½Π½Ρ Ρ€Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€Ρ–Ρ—Π² + when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); // ΠŸΠ΅Ρ€Π΅Π²Ρ–Ρ€ΠΊΠ° Π½Π° Π½Π°ΡΠ²Π½Ρ–ΡΡ‚ΡŒ користувача + when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); // ΠœΠΎΠΊΡƒΠ²Π°Π½Π½Ρ пСрСтворСння кошика Π² DTO + when(cartItemRepository.save(cartItem)).thenReturn(cartItem); // ΠœΠΎΠΊΡƒΠ²Π°Π½Π½Ρ збСрСТСння Π·ΠΌΡ–Π½Π΅Π½ΠΎΡ— ΠΊΡ–Π»ΡŒΠΊΠΎΡΡ‚Ρ– Ρ‚ΠΎΠ²Π°Ρ€Ρƒ + when(shoppingCartRepository.save(shoppingCart)).thenReturn(shoppingCart); + + // Act + ShoppingCartDto result = shoppingCartService.updateCartItemQuantity(cartItem.getId(), 3, user.getId()); + + // Assert + assertNotNull(result); // ΠŸΠ΅Ρ€Π΅Π²Ρ–Ρ€ΠΊΠ°, Ρ‰ΠΎ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ Π½Π΅ null + assertEquals(3, cartItem.getQuantity()); // ΠŸΠ΅Ρ€Π΅Π²Ρ–Ρ€ΠΊΠ° Π·ΠΌΡ–Π½ΠΈ ΠΊΡ–Π»ΡŒΠΊΠΎΡΡ‚Ρ– Ρ‚ΠΎΠ²Π°Ρ€Ρƒ + verify(cartItemRepository).save(cartItem); // ΠŸΠ΅Ρ€Π΅Π²Ρ–Ρ€ΠΊΠ° Π²ΠΈΠΊΠ»ΠΈΠΊΡƒ ΠΌΠ΅Ρ‚ΠΎΠ΄Ρƒ збСрСТСння + } + + + + @Test + void removeCartItem_ShouldRemoveItemAndReturnShoppingCartDto() { + // Arrange + shoppingCart.getCartItems().add(cartItem); + when(shoppingCartRepository.findByUserId(user.getId())).thenReturn(Optional.of(shoppingCart)); + when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); + when(shoppingCartRepository.save(shoppingCart)).thenReturn(shoppingCart); + + // ΠœΠΎΠΊΡƒΠ²Π°Π½Π½Ρ для ΠΌΠ΅Ρ‚ΠΎΠ΄Ρƒ toDto + when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); + + // Act + ShoppingCartDto result = shoppingCartService.removeCartItem(cartItem.getId(), user.getId()); + + // Assert + assertNotNull(result); + assertTrue(shoppingCart.getCartItems().isEmpty()); + verify(shoppingCartRepository).save(shoppingCart); + } + + + @Test + void createShoppingCart_ShouldCreateAndReturnShoppingCart() { + // Act + ShoppingCart result = shoppingCartService.createShoppingCart(user); + + // Assert + assertNotNull(result); + assertEquals(user, result.getUser()); + verify(shoppingCartRepository).save(result); + } +} diff --git a/src/test/resources/data-sql/clear-tables.sql b/src/test/resources/data-sql/clear-tables.sql index 2b2a583..7f3c9eb 100644 --- a/src/test/resources/data-sql/clear-tables.sql +++ b/src/test/resources/data-sql/clear-tables.sql @@ -1,3 +1,5 @@ DELETE FROM books_categories; DELETE FROM books; -DELETE FROM categories; \ No newline at end of file +DELETE FROM categories; +DELETE FROM shopping_carts; +DELETE FROM users; \ No newline at end of file diff --git a/src/test/resources/data-sql/insert-tables-for-shoppingcart.sql b/src/test/resources/data-sql/insert-tables-for-shoppingcart.sql new file mode 100644 index 0000000..47f813b --- /dev/null +++ b/src/test/resources/data-sql/insert-tables-for-shoppingcart.sql @@ -0,0 +1,18 @@ +-- Вставка ΠΊΠ°Ρ‚Π΅Π³ΠΎΡ€Ρ–Ρ— (якщо таблиця ΠΊΠ°Ρ‚Π΅Π³ΠΎΡ€Ρ–ΠΉ існує) +INSERT INTO categories (id, name, description) VALUES (1, 'Fiction', 'Fiction books'); + +-- Вставка ΠΊΠ½ΠΈΠ³ΠΈ +INSERT INTO books (id, title, author, isbn, price, description, cover_image) +VALUES (1, 'Sample Book', 'John Author', '1234567890', 10.99, 'A sample book description.', 'https://example.com/book-cover.jpg'); + +-- Вставка користувача +INSERT INTO users (id, first_name, last_name, password) +VALUES (1, 'john', 'doe', 'password'); + +-- Вставка Π·Π²'язку ΠΊΠ½ΠΈΠ³ΠΈ Π· ΠΊΠ°Ρ‚Π΅Π³ΠΎΡ€Ρ–Ρ”ΡŽ (якщо таблиця books_categories існує) +INSERT INTO books_categories (book_id, category_id) +VALUES (1, 1); + +-- Вставка Ρ‚ΠΎΠ²Π°Ρ€Ρƒ Π² кошик (якщо Ρ” Ρ‚Π°ΠΊΠ° таблиця для кошика ΠΏΠΎΠΊΡƒΠΏΠΎΠΊ) +INSERT INTO shopping_cart (user_id, book_id, quantity) +VALUES (1, 1, 2); -- Для тСсту додавання ΠΊΠ½ΠΈΠ³ΠΈ Π· ΠΊΡ–Π»ΡŒΠΊΡ–ΡΡ‚ΡŽ 2 From fe55ff32eb7d80129c6d3d2e86b69ac14c915992 Mon Sep 17 00:00:00 2001 From: greqit Date: Wed, 6 Nov 2024 16:47:25 +0100 Subject: [PATCH 09/21] 1 of 4 tests work in shoppingCartControllerTests --- .../ShoppingCartControllerTest.java | 79 ++++++++++--------- .../data-sql/clear-tables-for-sh.sql | 4 + src/test/resources/data-sql/create-books.sql | 1 + .../resources/data-sql/create-cart-item.sql | 1 + .../data-sql/create-shopping-carts.sql | 1 + src/test/resources/data-sql/create-users.sql | 1 + .../insert-tables-for-shoppingcart.sql | 18 ----- 7 files changed, 49 insertions(+), 56 deletions(-) create mode 100644 src/test/resources/data-sql/clear-tables-for-sh.sql create mode 100644 src/test/resources/data-sql/create-books.sql create mode 100644 src/test/resources/data-sql/create-cart-item.sql create mode 100644 src/test/resources/data-sql/create-shopping-carts.sql create mode 100644 src/test/resources/data-sql/create-users.sql delete mode 100644 src/test/resources/data-sql/insert-tables-for-shoppingcart.sql diff --git a/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java b/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java index 1a2100a..f196aae 100644 --- a/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java +++ b/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java @@ -3,9 +3,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import mate.academy.springbootwebgreqit.dto.cartitem.CartItemRequestDto; import mate.academy.springbootwebgreqit.model.Book; +import mate.academy.springbootwebgreqit.model.CartItem; +import mate.academy.springbootwebgreqit.model.ShoppingCart; import mate.academy.springbootwebgreqit.model.User; import mate.academy.springbootwebgreqit.service.ShoppingCartService; -import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -15,7 +17,9 @@ import org.springframework.test.context.jdbc.Sql; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; -import java.math.BigDecimal; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -31,49 +35,43 @@ class ShoppingCartControllerTest { private User user; private Book book; - - @BeforeEach - void setUp() { - // БтворСння користувача Ρ‚Π° ΠΊΠ½ΠΈΠ³ΠΈ для тСстів - user = new User(); - user.setId(1L); - user.setFirstName("john"); - user.setLastName("doe"); - user.setPassword("password"); - - book = new Book(); - book.setId(1L); - book.setTitle("Sample Book"); - book.setAuthor("John Author"); - book.setIsbn("1234567890"); - book.setPrice(BigDecimal.valueOf(10.99)); - book.setDescription("A sample book description."); - book.setCoverImage("https://example.com/book-cover.jpg"); - - // ΠœΠΎΠΊΡ–Π½Π³ Π°Π±ΠΎ створСння залСТностСй Π² класі ShoppingCartService. + private CartItem cartItem; + private ShoppingCart shoppingCart; + + @BeforeAll + static void beforeAll(@Autowired WebApplicationContext webApplicationContext) { + mockMvc = MockMvcBuilders + .webAppContextSetup(webApplicationContext) + .apply(springSecurity()) + .build(); } - @Sql(scripts = "/data-sql/clear-tables.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + @Sql(scripts = {"/data-sql/create-books.sql", "/data-sql/create-users.sql", + "/data-sql/create-shopping-carts.sql", "/data-sql/create-cart-item.sql"}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) @WithMockUser(username = "john_doe", roles = {"USER"}) @Test @DisplayName("Get shopping cart for the current user") void getShoppingCart_ShouldReturnCartForUser() throws Exception { mockMvc.perform(get("/cart") - .param("userId", "1") - .contentType(MediaType.APPLICATION_JSON)) + .param("userId", "1") // Ensure the userId parameter is passed + .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.userId").value(1L)) - .andExpect(jsonPath("$.items").isArray()) - .andReturn(); + .andExpect(jsonPath("$.user.id").value(1L)) + .andExpect(jsonPath("$.cartItems[0].book.id").value(1L)); } - @Sql(scripts = {"/data-sql/clear-tables.sql", "/data-sql/insert-tables-for-shoppingcart.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"/data-sql/create-books.sql", "/data-sql/create-users.sql", + "/data-sql/create-shopping-carts.sql"}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) @WithMockUser(username = "john_doe", roles = {"USER"}) @Test @DisplayName("Add book to shopping cart") void addBookToShoppingCart_ShouldReturnUpdatedCart() throws Exception { CartItemRequestDto cartItemRequestDto = new CartItemRequestDto(); - cartItemRequestDto.setBookId(1L); // Використовуємо ID ΠΊΠ½ΠΈΠ³ΠΈ, яка Π±ΡƒΠ»Π° Π΄ΠΎΠ΄Π°Π½Π° Π² Π±Π°Π·Ρƒ + cartItemRequestDto.setBookId(1L); cartItemRequestDto.setQuantity(2); String jsonRequest = objectMapper.writeValueAsString(cartItemRequestDto); @@ -83,13 +81,15 @@ void addBookToShoppingCart_ShouldReturnUpdatedCart() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(jsonRequest)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.items[0].book.title").value("wallet")) // ΠŸΠ΅Ρ€Π΅Π²Ρ–Ρ€ΡΡ”ΠΌΠΎ, Ρ‡ΠΈ ΠΏΡ€Π°Π²ΠΈΠ»ΡŒΠ½Π° ΠΊΠ½ΠΈΠ³Π° - .andExpect(jsonPath("$.items[0].quantity").value(2)) // ΠŸΠ΅Ρ€Π΅Π²Ρ–Ρ€ΡΡ”ΠΌΠΎ ΠΊΡ–Π»ΡŒΠΊΡ–ΡΡ‚ΡŒ + .andExpect(jsonPath("$.cartItems[0].book.id").value(cartItemRequestDto.getBookId())) + .andExpect(jsonPath("$.cartItems[0].quantity").value(cartItemRequestDto.getQuantity())) .andReturn(); } - - @Sql(scripts = "/data-sql/clear-tables.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + @Sql(scripts = {"/data-sql/create-books.sql", "/data-sql/create-users.sql", + "/data-sql/create-shopping-carts.sql", "/data-sql/create-cart-item.sql"}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) @WithMockUser(username = "john_doe", roles = {"USER"}) @Test @DisplayName("Update cart item quantity") @@ -100,13 +100,16 @@ void updateCartItemQuantity_ShouldReturnUpdatedQuantity() throws Exception { mockMvc.perform(put("/cart/items/{cartItemId}", cartItemId) .param("quantity", String.valueOf(newQuantity)) .param("userId", "1") - .contentType(MediaType.APPLICATION_JSON)) + .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.items[0].quantity").value(newQuantity)) + .andExpect(jsonPath("$.cartItems[0].quantity").value(newQuantity)) .andReturn(); } - @Sql(scripts = "/data-sql/clear-tables.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + @Sql(scripts = {"/data-sql/create-books.sql", "/data-sql/create-users.sql", + "/data-sql/create-shopping-carts.sql", "/data-sql/create-cart-item.sql"}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) @WithMockUser(username = "john_doe", roles = {"USER"}) @Test @DisplayName("Remove item from cart") @@ -117,7 +120,7 @@ void removeItemFromCart_ShouldReturnEmptyCart() throws Exception { .param("userId", "1") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.items").isEmpty()) + .andExpect(jsonPath("$.cartItems").isEmpty()) .andReturn(); } } diff --git a/src/test/resources/data-sql/clear-tables-for-sh.sql b/src/test/resources/data-sql/clear-tables-for-sh.sql new file mode 100644 index 0000000..d72624e --- /dev/null +++ b/src/test/resources/data-sql/clear-tables-for-sh.sql @@ -0,0 +1,4 @@ +DELETE FROM cart_items; +DELETE FROM shopping_carts; +DELETE FROM books; +DELETE FROM users; \ No newline at end of file diff --git a/src/test/resources/data-sql/create-books.sql b/src/test/resources/data-sql/create-books.sql new file mode 100644 index 0000000..bb7de75 --- /dev/null +++ b/src/test/resources/data-sql/create-books.sql @@ -0,0 +1 @@ +INSERT INTO books (id, title, author, isbn, price, description, cover_image) VALUES (1, 'Sample Book', 'John Author', '1234567890', 10.99, 'A sample book description.', 'https://example.com/book-cover.jpg'); diff --git a/src/test/resources/data-sql/create-cart-item.sql b/src/test/resources/data-sql/create-cart-item.sql new file mode 100644 index 0000000..f7abad1 --- /dev/null +++ b/src/test/resources/data-sql/create-cart-item.sql @@ -0,0 +1 @@ +INSERT INTO cart_items (id, shopping_cart_id, book_id, quantity) VALUES (1, 1, 1, 2); \ No newline at end of file diff --git a/src/test/resources/data-sql/create-shopping-carts.sql b/src/test/resources/data-sql/create-shopping-carts.sql new file mode 100644 index 0000000..c61a990 --- /dev/null +++ b/src/test/resources/data-sql/create-shopping-carts.sql @@ -0,0 +1 @@ +INSERT INTO shopping_carts (id, user_id) VALUES (1, 1); \ No newline at end of file diff --git a/src/test/resources/data-sql/create-users.sql b/src/test/resources/data-sql/create-users.sql new file mode 100644 index 0000000..68c5155 --- /dev/null +++ b/src/test/resources/data-sql/create-users.sql @@ -0,0 +1 @@ +INSERT INTO users (id, first_name, last_name, email, password, shipping_address) VALUES (1, 'John', 'Doe', 'john.doe@example.com', 'password123', '123 Main St, Springfield'); diff --git a/src/test/resources/data-sql/insert-tables-for-shoppingcart.sql b/src/test/resources/data-sql/insert-tables-for-shoppingcart.sql deleted file mode 100644 index 47f813b..0000000 --- a/src/test/resources/data-sql/insert-tables-for-shoppingcart.sql +++ /dev/null @@ -1,18 +0,0 @@ --- Вставка ΠΊΠ°Ρ‚Π΅Π³ΠΎΡ€Ρ–Ρ— (якщо таблиця ΠΊΠ°Ρ‚Π΅Π³ΠΎΡ€Ρ–ΠΉ існує) -INSERT INTO categories (id, name, description) VALUES (1, 'Fiction', 'Fiction books'); - --- Вставка ΠΊΠ½ΠΈΠ³ΠΈ -INSERT INTO books (id, title, author, isbn, price, description, cover_image) -VALUES (1, 'Sample Book', 'John Author', '1234567890', 10.99, 'A sample book description.', 'https://example.com/book-cover.jpg'); - --- Вставка користувача -INSERT INTO users (id, first_name, last_name, password) -VALUES (1, 'john', 'doe', 'password'); - --- Вставка Π·Π²'язку ΠΊΠ½ΠΈΠ³ΠΈ Π· ΠΊΠ°Ρ‚Π΅Π³ΠΎΡ€Ρ–Ρ”ΡŽ (якщо таблиця books_categories існує) -INSERT INTO books_categories (book_id, category_id) -VALUES (1, 1); - --- Вставка Ρ‚ΠΎΠ²Π°Ρ€Ρƒ Π² кошик (якщо Ρ” Ρ‚Π°ΠΊΠ° таблиця для кошика ΠΏΠΎΠΊΡƒΠΏΠΎΠΊ) -INSERT INTO shopping_cart (user_id, book_id, quantity) -VALUES (1, 1, 2); -- Для тСсту додавання ΠΊΠ½ΠΈΠ³ΠΈ Π· ΠΊΡ–Π»ΡŒΠΊΡ–ΡΡ‚ΡŽ 2 From 307966d4d270eb1078dbef94c1bc21be403ec759 Mon Sep 17 00:00:00 2001 From: greqit Date: Thu, 7 Nov 2024 12:14:26 +0100 Subject: [PATCH 10/21] using authorization instead of userId --- .../controller/ShoppingCartController.java | 17 +- .../service/ShoppingCartService.java | 9 +- .../service/impl/ShoppingCartServiceImpl.java | 37 +- src/main/resources/application.properties | 2 +- .../ShoppingCartControllerTest.java | 11 +- .../service/ShoppingCartServiceTest.java | 378 +++++++++--------- 6 files changed, 229 insertions(+), 225 deletions(-) diff --git a/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java b/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java index b9c4c97..5765d3c 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java +++ b/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java @@ -4,6 +4,7 @@ import mate.academy.springbootwebgreqit.dto.cartitem.CartItemRequestDto; import mate.academy.springbootwebgreqit.dto.shoppingcart.ShoppingCartDto; import mate.academy.springbootwebgreqit.service.ShoppingCartService; +import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -21,25 +22,25 @@ public class ShoppingCartController { private final ShoppingCartService shoppingCartService; @GetMapping - public ShoppingCartDto getShoppingCartForCurrentUser(@RequestParam Long userId) { - return shoppingCartService.getShoppingCartForCurrentUser(userId); + public ShoppingCartDto getShoppingCartForCurrentUser(Authentication authentication) { + return shoppingCartService.getShoppingCartForCurrentUser(authentication); } @PostMapping public ShoppingCartDto addBookToShoppingCart(@RequestBody CartItemRequestDto cartItem, - @RequestParam Long userId) { - return shoppingCartService.addBookToShoppingCart(cartItem, userId); + Authentication authentication) { + return shoppingCartService.addBookToShoppingCart(cartItem, authentication); } @PutMapping("/items/{cartItemId}") public ShoppingCartDto updateCartItemQuantity(@PathVariable Long cartItemId, @RequestParam int quantity, - @RequestParam Long userId) { - return shoppingCartService.updateCartItemQuantity(cartItemId, quantity, userId); + Authentication authentication) { + return shoppingCartService.updateCartItemQuantity(cartItemId, quantity, authentication); } @DeleteMapping("/items/{cartItemId}") - public void removeCartItem(@PathVariable Long cartItemId, @RequestParam Long userId) { - shoppingCartService.removeCartItem(cartItemId, userId); + public void removeCartItem(@PathVariable Long cartItemId, Authentication authentication) { + shoppingCartService.removeCartItem(cartItemId, authentication); } } diff --git a/src/main/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java b/src/main/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java index ee79370..dd8b052 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java +++ b/src/main/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java @@ -4,15 +4,16 @@ import mate.academy.springbootwebgreqit.dto.shoppingcart.ShoppingCartDto; import mate.academy.springbootwebgreqit.model.ShoppingCart; import mate.academy.springbootwebgreqit.model.User; +import org.springframework.security.core.Authentication; public interface ShoppingCartService { - ShoppingCartDto getShoppingCartForCurrentUser(Long userId); + ShoppingCartDto getShoppingCartForCurrentUser(Authentication authentication); - ShoppingCartDto addBookToShoppingCart(CartItemRequestDto cartItem, Long userId); + ShoppingCartDto addBookToShoppingCart(CartItemRequestDto cartItem, Authentication authentication); - ShoppingCartDto updateCartItemQuantity(Long cartItemId, int quantity, Long userId); + ShoppingCartDto updateCartItemQuantity(Long cartItemId, int quantity, Authentication authentication); - ShoppingCartDto removeCartItem(Long cartItemId, Long userId); + ShoppingCartDto removeCartItem(Long cartItemId, Authentication authentication); ShoppingCart createShoppingCart(User user); } diff --git a/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java b/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java index 64a0119..982ff82 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java +++ b/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java @@ -16,6 +16,7 @@ import mate.academy.springbootwebgreqit.repository.UserRepository; import mate.academy.springbootwebgreqit.service.ShoppingCartService; import org.hibernate.Hibernate; +import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; import java.util.Optional; @@ -31,16 +32,16 @@ public class ShoppingCartServiceImpl implements ShoppingCartService { @Transactional @Override - public ShoppingCartDto getShoppingCartForCurrentUser(Long userId) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new IllegalArgumentException("User not found")); - ShoppingCart shoppingCart = user.getShoppingCart(); + public ShoppingCartDto getShoppingCartForCurrentUser(Authentication authentication) { + User authUser = userRepository.findByEmail(authentication.getName()) + .orElseThrow(() -> new EntityNotFoundException("User not found with email: " + authentication.getName())); + ShoppingCart shoppingCart = authUser.getShoppingCart(); if (shoppingCart == null) { - throw new EntityNotFoundException("Shopping cart not found for user with ID " - + userId); + throw new EntityNotFoundException("Shopping cart not found for user with email: " + + authUser.getEmail()); } Hibernate.initialize(shoppingCart.getCartItems()); - Hibernate.initialize(user.getRoles()); + Hibernate.initialize(authUser.getRoles()); shoppingCart.getCartItems().forEach(cartItem -> Hibernate.initialize(cartItem.getBook().getCategories())); shoppingCart.getCartItems().forEach(cartItem -> @@ -52,8 +53,10 @@ public ShoppingCartDto getShoppingCartForCurrentUser(Long userId) { @Override public ShoppingCartDto addBookToShoppingCart( CartItemRequestDto cartItemDto, - Long userId) { - ShoppingCart shoppingCart = shoppingCartRepository.findByUserId(userId) + Authentication authentication) { + User user = userRepository.findByEmail(authentication.getName()) + .orElseThrow(() -> new EntityNotFoundException("User not found with email: " + authentication.getName())); + ShoppingCart shoppingCart = shoppingCartRepository.findByUserId(user.getId()) .orElseThrow(() -> new EntityNotFoundException("Shopping cart not found")); Optional existingItemOpt = shoppingCart.getCartItems().stream() @@ -92,17 +95,17 @@ public ShoppingCartDto addBookToShoppingCart( @Transactional @Override public ShoppingCartDto updateCartItemQuantity(Long cartItemId, - int quantity, Long userId) { + int quantity, Authentication authentication) { if (quantity <= 0) { throw new EntityNotFoundException("Quantity must be a positive integer"); } - User user = userRepository.findById(userId) - .orElseThrow(() -> new EntityNotFoundException("User not found")); + User user = userRepository.findByEmail(authentication.getName()) + .orElseThrow(() -> new EntityNotFoundException("User not found with email: " + authentication.getName())); ShoppingCart shoppingCart = user.getShoppingCart(); if (shoppingCart == null) { throw new EntityNotFoundException("Shopping cart not found for user with ID " - + userId); + + user.getId()); } CartItem cartItem = shoppingCart.getCartItems().stream() .filter(item -> item.getId().equals(cartItemId)) @@ -127,11 +130,11 @@ public ShoppingCartDto updateCartItemQuantity(Long cartItemId, @Transactional @Override - public ShoppingCartDto removeCartItem(Long cartItemId, Long userId) { - ShoppingCart shoppingCart = shoppingCartRepository.findByUserId(userId) + public ShoppingCartDto removeCartItem(Long cartItemId, Authentication authentication) { + User user = userRepository.findByEmail(authentication.getName()) + .orElseThrow(() -> new EntityNotFoundException("User not found with email: " + authentication.getName())); + ShoppingCart shoppingCart = shoppingCartRepository.findByUserId(user.getId()) .orElseThrow(() -> new EntityNotFoundException("Shopping cart not found")); - User user = userRepository.findById(userId) - .orElseThrow(() -> new EntityNotFoundException("User not found")); CartItem cartItem = shoppingCart.getCartItems().stream() .filter(item -> item.getId().equals(cartItemId)) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 7da5384..a6d1391 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -4,7 +4,7 @@ spring.datasource.url=jdbc:mysql://localhost:3306/book-store spring.datasource.username=root spring.datasource.password=1234567 -spring.jpa.hibernate.ddl-auto=validate +spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.open-in-view=false diff --git a/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java b/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java index f196aae..509b099 100644 --- a/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java +++ b/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java @@ -50,7 +50,7 @@ static void beforeAll(@Autowired WebApplicationContext webApplicationContext) { "/data-sql/create-shopping-carts.sql", "/data-sql/create-cart-item.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) - @WithMockUser(username = "john_doe", roles = {"USER"}) + @WithMockUser(username = "first_name", roles = {"USER"}) @Test @DisplayName("Get shopping cart for the current user") void getShoppingCart_ShouldReturnCartForUser() throws Exception { @@ -59,7 +59,8 @@ void getShoppingCart_ShouldReturnCartForUser() throws Exception { .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.user.id").value(1L)) - .andExpect(jsonPath("$.cartItems[0].book.id").value(1L)); + .andExpect(jsonPath("$.cartItems[0].book.id").value(1L)) + .andReturn(); } @Sql(scripts = {"/data-sql/create-books.sql", "/data-sql/create-users.sql", @@ -110,7 +111,7 @@ void updateCartItemQuantity_ShouldReturnUpdatedQuantity() throws Exception { "/data-sql/create-shopping-carts.sql", "/data-sql/create-cart-item.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) - @WithMockUser(username = "john_doe", roles = {"USER"}) + @WithMockUser(username = "john_doe") @Test @DisplayName("Remove item from cart") void removeItemFromCart_ShouldReturnEmptyCart() throws Exception { @@ -119,8 +120,6 @@ void removeItemFromCart_ShouldReturnEmptyCart() throws Exception { mockMvc.perform(delete("/cart/items/{cartItemId}", cartItemId) .param("userId", "1") .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.cartItems").isEmpty()) - .andReturn(); + .andExpect(status().isOk()); } } diff --git a/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java b/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java index 10a97a7..2fb74a1 100644 --- a/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java +++ b/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java @@ -1,189 +1,189 @@ -package mate.academy.springbootwebgreqit.service; - -import mate.academy.springbootwebgreqit.dto.cartitem.CartItemRequestDto; -import mate.academy.springbootwebgreqit.dto.shoppingcart.ShoppingCartDto; -import mate.academy.springbootwebgreqit.exception.EntityNotFoundException; -import mate.academy.springbootwebgreqit.mapper.CartItemMapper; -import mate.academy.springbootwebgreqit.mapper.ShoppingCartMapper; -import mate.academy.springbootwebgreqit.model.Book; -import mate.academy.springbootwebgreqit.model.CartItem; -import mate.academy.springbootwebgreqit.model.ShoppingCart; -import mate.academy.springbootwebgreqit.model.User; -import mate.academy.springbootwebgreqit.repository.BookRepository; -import mate.academy.springbootwebgreqit.repository.CartItemRepository; -import mate.academy.springbootwebgreqit.repository.ShoppingCartRepository; -import mate.academy.springbootwebgreqit.repository.UserRepository; -import mate.academy.springbootwebgreqit.service.impl.ShoppingCartServiceImpl; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class ShoppingCartServiceTest { - - @Mock - private CartItemRepository cartItemRepository; - @Mock - private ShoppingCartRepository shoppingCartRepository; - @Mock - private ShoppingCartMapper shoppingCartMapper; - @Mock - private CartItemMapper cartItemMapper; - @Mock - private BookRepository bookRepository; - @Mock - private UserRepository userRepository; - - @InjectMocks - private ShoppingCartServiceImpl shoppingCartService; - - private User user; - private ShoppingCart shoppingCart; - private ShoppingCartDto shoppingCartDto; - private CartItemRequestDto cartItemRequestDto; - private Book book; - private CartItem cartItem; - - @BeforeEach - void setUp() { - user = new User(); - user.setId(1L); - - shoppingCart = new ShoppingCart(); - shoppingCart.setUser(user); - - shoppingCartDto = new ShoppingCartDto(); - - cartItemRequestDto = new CartItemRequestDto(); - cartItemRequestDto.setBookId(1L); - cartItemRequestDto.setQuantity(2); - - book = new Book(); - book.setId(1L); - - cartItem = new CartItem(); - cartItem.setId(1L); - cartItem.setBook(book); - cartItem.setQuantity(2); - cartItem.setShoppingCart(shoppingCart); - } - - @Test - void getShoppingCartForCurrentUser_ShouldReturnShoppingCartDto() { - user.setShoppingCart(shoppingCart); - // Arrange - when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); - when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); - - // Act - ShoppingCartDto result = shoppingCartService.getShoppingCartForCurrentUser(user.getId()); - - // Assert - assertNotNull(result); - verify(userRepository).findById(user.getId()); - verify(shoppingCartMapper).toDto(shoppingCart); - } - - @Test - void getShoppingCartForCurrentUser_ShouldThrowEntityNotFoundException_WhenShoppingCartNotFound() { - // Arrange - User user1 = new User(); - user1.setId(2L); - user1.setShoppingCart(null); - when(userRepository.findById(user1.getId())).thenReturn(Optional.of(user1)); - - // Act & Assert - assertThrows(EntityNotFoundException.class, - () -> shoppingCartService.getShoppingCartForCurrentUser(user1.getId())); - } - - - @Test - void addBookToShoppingCart_ShouldAddNewBookAndReturnShoppingCartDto() { - // Arrange - when(shoppingCartRepository.findByUserId(user.getId())).thenReturn(Optional.of(shoppingCart)); - when(bookRepository.findById(cartItemRequestDto.getBookId())).thenReturn(Optional.of(book)); - when(cartItemMapper.toModel(cartItemRequestDto)).thenReturn(cartItem); - when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); - - // Mocking save method to return updated shopping cart - when(shoppingCartRepository.save(any(ShoppingCart.class))).thenReturn(shoppingCart); - - // Act - ShoppingCartDto result = shoppingCartService.addBookToShoppingCart(cartItemRequestDto, user.getId()); - - // Assert - assertNotNull(result); - verify(shoppingCartRepository).findByUserId(user.getId()); - verify(cartItemMapper).toModel(cartItemRequestDto); - verify(bookRepository).findById(cartItemRequestDto.getBookId()); - verify(shoppingCartRepository).save(shoppingCart); - - // Optional: Verify that cart items were updated - assertEquals(1, shoppingCart.getCartItems().size()); // assuming one item is added - } - - - @Test - void updateCartItemQuantity_ShouldUpdateQuantityAndReturnShoppingCartDto() { - user.setShoppingCart(shoppingCart); - // Arrange - shoppingCart.getCartItems().add(cartItem); - - // ΠœΠΎΠΊΡƒΠ²Π°Π½Π½Ρ Ρ€Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€Ρ–Ρ—Π² - when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); // ΠŸΠ΅Ρ€Π΅Π²Ρ–Ρ€ΠΊΠ° Π½Π° Π½Π°ΡΠ²Π½Ρ–ΡΡ‚ΡŒ користувача - when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); // ΠœΠΎΠΊΡƒΠ²Π°Π½Π½Ρ пСрСтворСння кошика Π² DTO - when(cartItemRepository.save(cartItem)).thenReturn(cartItem); // ΠœΠΎΠΊΡƒΠ²Π°Π½Π½Ρ збСрСТСння Π·ΠΌΡ–Π½Π΅Π½ΠΎΡ— ΠΊΡ–Π»ΡŒΠΊΠΎΡΡ‚Ρ– Ρ‚ΠΎΠ²Π°Ρ€Ρƒ - when(shoppingCartRepository.save(shoppingCart)).thenReturn(shoppingCart); - - // Act - ShoppingCartDto result = shoppingCartService.updateCartItemQuantity(cartItem.getId(), 3, user.getId()); - - // Assert - assertNotNull(result); // ΠŸΠ΅Ρ€Π΅Π²Ρ–Ρ€ΠΊΠ°, Ρ‰ΠΎ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ Π½Π΅ null - assertEquals(3, cartItem.getQuantity()); // ΠŸΠ΅Ρ€Π΅Π²Ρ–Ρ€ΠΊΠ° Π·ΠΌΡ–Π½ΠΈ ΠΊΡ–Π»ΡŒΠΊΠΎΡΡ‚Ρ– Ρ‚ΠΎΠ²Π°Ρ€Ρƒ - verify(cartItemRepository).save(cartItem); // ΠŸΠ΅Ρ€Π΅Π²Ρ–Ρ€ΠΊΠ° Π²ΠΈΠΊΠ»ΠΈΠΊΡƒ ΠΌΠ΅Ρ‚ΠΎΠ΄Ρƒ збСрСТСння - } - - - - @Test - void removeCartItem_ShouldRemoveItemAndReturnShoppingCartDto() { - // Arrange - shoppingCart.getCartItems().add(cartItem); - when(shoppingCartRepository.findByUserId(user.getId())).thenReturn(Optional.of(shoppingCart)); - when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); - when(shoppingCartRepository.save(shoppingCart)).thenReturn(shoppingCart); - - // ΠœΠΎΠΊΡƒΠ²Π°Π½Π½Ρ для ΠΌΠ΅Ρ‚ΠΎΠ΄Ρƒ toDto - when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); - - // Act - ShoppingCartDto result = shoppingCartService.removeCartItem(cartItem.getId(), user.getId()); - - // Assert - assertNotNull(result); - assertTrue(shoppingCart.getCartItems().isEmpty()); - verify(shoppingCartRepository).save(shoppingCart); - } - - - @Test - void createShoppingCart_ShouldCreateAndReturnShoppingCart() { - // Act - ShoppingCart result = shoppingCartService.createShoppingCart(user); - - // Assert - assertNotNull(result); - assertEquals(user, result.getUser()); - verify(shoppingCartRepository).save(result); - } -} +//package mate.academy.springbootwebgreqit.service; +// +//import mate.academy.springbootwebgreqit.dto.cartitem.CartItemRequestDto; +//import mate.academy.springbootwebgreqit.dto.shoppingcart.ShoppingCartDto; +//import mate.academy.springbootwebgreqit.exception.EntityNotFoundException; +//import mate.academy.springbootwebgreqit.mapper.CartItemMapper; +//import mate.academy.springbootwebgreqit.mapper.ShoppingCartMapper; +//import mate.academy.springbootwebgreqit.model.Book; +//import mate.academy.springbootwebgreqit.model.CartItem; +//import mate.academy.springbootwebgreqit.model.ShoppingCart; +//import mate.academy.springbootwebgreqit.model.User; +//import mate.academy.springbootwebgreqit.repository.BookRepository; +//import mate.academy.springbootwebgreqit.repository.CartItemRepository; +//import mate.academy.springbootwebgreqit.repository.ShoppingCartRepository; +//import mate.academy.springbootwebgreqit.repository.UserRepository; +//import mate.academy.springbootwebgreqit.service.impl.ShoppingCartServiceImpl; +//import org.junit.jupiter.api.BeforeEach; +//import org.junit.jupiter.api.Test; +//import org.junit.jupiter.api.extension.ExtendWith; +//import org.mockito.InjectMocks; +//import org.mockito.Mock; +//import org.mockito.junit.jupiter.MockitoExtension; +// +//import java.util.Optional; +// +//import static org.junit.jupiter.api.Assertions.*; +//import static org.mockito.Mockito.*; +// +//@ExtendWith(MockitoExtension.class) +//class ShoppingCartServiceTest { +// +// @Mock +// private CartItemRepository cartItemRepository; +// @Mock +// private ShoppingCartRepository shoppingCartRepository; +// @Mock +// private ShoppingCartMapper shoppingCartMapper; +// @Mock +// private CartItemMapper cartItemMapper; +// @Mock +// private BookRepository bookRepository; +// @Mock +// private UserRepository userRepository; +// +// @InjectMocks +// private ShoppingCartServiceImpl shoppingCartService; +// +// private User user; +// private ShoppingCart shoppingCart; +// private ShoppingCartDto shoppingCartDto; +// private CartItemRequestDto cartItemRequestDto; +// private Book book; +// private CartItem cartItem; +// +// @BeforeEach +// void setUp() { +// user = new User(); +// user.setId(1L); +// +// shoppingCart = new ShoppingCart(); +// shoppingCart.setUser(user); +// +// shoppingCartDto = new ShoppingCartDto(); +// +// cartItemRequestDto = new CartItemRequestDto(); +// cartItemRequestDto.setBookId(1L); +// cartItemRequestDto.setQuantity(2); +// +// book = new Book(); +// book.setId(1L); +// +// cartItem = new CartItem(); +// cartItem.setId(1L); +// cartItem.setBook(book); +// cartItem.setQuantity(2); +// cartItem.setShoppingCart(shoppingCart); +// } +// +//// @Test +//// void getShoppingCartForCurrentUser_ShouldReturnShoppingCartDto() { +//// user.setShoppingCart(shoppingCart); +//// // Arrange +//// when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); +//// when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); +//// +//// // Act +//// ShoppingCartDto result = shoppingCartService.getShoppingCartForCurrentUser(); +//// +//// // Assert +//// assertNotNull(result); +//// verify(userRepository).findById(user.getId()); +//// verify(shoppingCartMapper).toDto(shoppingCart); +//// } +// +//// @Test +//// void getShoppingCartForCurrentUser_ShouldThrowEntityNotFoundException_WhenShoppingCartNotFound() { +//// // Arrange +//// User user1 = new User(); +//// user1.setId(2L); +//// user1.setShoppingCart(null); +//// when(userRepository.findById(user1.getId())).thenReturn(Optional.of(user1)); +//// +//// // Act & Assert +//// assertThrows(EntityNotFoundException.class, +//// () -> shoppingCartService.getShoppingCartForCurrentUser(user1.getId())); +//// } +// +// +// @Test +// void addBookToShoppingCart_ShouldAddNewBookAndReturnShoppingCartDto() { +// // Arrange +// when(shoppingCartRepository.findByUserId(user.getId())).thenReturn(Optional.of(shoppingCart)); +// when(bookRepository.findById(cartItemRequestDto.getBookId())).thenReturn(Optional.of(book)); +// when(cartItemMapper.toModel(cartItemRequestDto)).thenReturn(cartItem); +// when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); +// +// // Mocking save method to return updated shopping cart +// when(shoppingCartRepository.save(any(ShoppingCart.class))).thenReturn(shoppingCart); +// +// // Act +// ShoppingCartDto result = shoppingCartService.addBookToShoppingCart(cartItemRequestDto, user.getId()); +// +// // Assert +// assertNotNull(result); +// verify(shoppingCartRepository).findByUserId(user.getId()); +// verify(cartItemMapper).toModel(cartItemRequestDto); +// verify(bookRepository).findById(cartItemRequestDto.getBookId()); +// verify(shoppingCartRepository).save(shoppingCart); +// +// // Optional: Verify that cart items were updated +// assertEquals(1, shoppingCart.getCartItems().size()); // assuming one item is added +// } +// +// +// @Test +// void updateCartItemQuantity_ShouldUpdateQuantityAndReturnShoppingCartDto() { +// user.setShoppingCart(shoppingCart); +// // Arrange +// shoppingCart.getCartItems().add(cartItem); +// +// // ΠœΠΎΠΊΡƒΠ²Π°Π½Π½Ρ Ρ€Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€Ρ–Ρ—Π² +// when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); // ΠŸΠ΅Ρ€Π΅Π²Ρ–Ρ€ΠΊΠ° Π½Π° Π½Π°ΡΠ²Π½Ρ–ΡΡ‚ΡŒ користувача +// when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); // ΠœΠΎΠΊΡƒΠ²Π°Π½Π½Ρ пСрСтворСння кошика Π² DTO +// when(cartItemRepository.save(cartItem)).thenReturn(cartItem); // ΠœΠΎΠΊΡƒΠ²Π°Π½Π½Ρ збСрСТСння Π·ΠΌΡ–Π½Π΅Π½ΠΎΡ— ΠΊΡ–Π»ΡŒΠΊΠΎΡΡ‚Ρ– Ρ‚ΠΎΠ²Π°Ρ€Ρƒ +// when(shoppingCartRepository.save(shoppingCart)).thenReturn(shoppingCart); +// +// // Act +// ShoppingCartDto result = shoppingCartService.updateCartItemQuantity(cartItem.getId(), 3, user.getId()); +// +// // Assert +// assertNotNull(result); // ΠŸΠ΅Ρ€Π΅Π²Ρ–Ρ€ΠΊΠ°, Ρ‰ΠΎ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ Π½Π΅ null +// assertEquals(3, cartItem.getQuantity()); // ΠŸΠ΅Ρ€Π΅Π²Ρ–Ρ€ΠΊΠ° Π·ΠΌΡ–Π½ΠΈ ΠΊΡ–Π»ΡŒΠΊΠΎΡΡ‚Ρ– Ρ‚ΠΎΠ²Π°Ρ€Ρƒ +// verify(cartItemRepository).save(cartItem); // ΠŸΠ΅Ρ€Π΅Π²Ρ–Ρ€ΠΊΠ° Π²ΠΈΠΊΠ»ΠΈΠΊΡƒ ΠΌΠ΅Ρ‚ΠΎΠ΄Ρƒ збСрСТСння +// } +// +// +// +// @Test +// void removeCartItem_ShouldRemoveItemAndReturnShoppingCartDto() { +// // Arrange +// shoppingCart.getCartItems().add(cartItem); +// when(shoppingCartRepository.findByUserId(user.getId())).thenReturn(Optional.of(shoppingCart)); +// when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); +// when(shoppingCartRepository.save(shoppingCart)).thenReturn(shoppingCart); +// +// // ΠœΠΎΠΊΡƒΠ²Π°Π½Π½Ρ для ΠΌΠ΅Ρ‚ΠΎΠ΄Ρƒ toDto +// when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); +// +// // Act +// ShoppingCartDto result = shoppingCartService.removeCartItem(cartItem.getId(), user.getId()); +// +// // Assert +// assertNotNull(result); +// assertTrue(shoppingCart.getCartItems().isEmpty()); +// verify(shoppingCartRepository).save(shoppingCart); +// } +// +// +// @Test +// void createShoppingCart_ShouldCreateAndReturnShoppingCart() { +// // Act +// ShoppingCart result = shoppingCartService.createShoppingCart(user); +// +// // Assert +// assertNotNull(result); +// assertEquals(user, result.getUser()); +// verify(shoppingCartRepository).save(result); +// } +//} From c26605d7a56ac1496b0232e38ae1d1918902df2b Mon Sep 17 00:00:00 2001 From: greqit Date: Thu, 7 Nov 2024 12:33:07 +0100 Subject: [PATCH 11/21] refactor tests --- .../ShoppingCartControllerTest.java | 22 +- .../service/ShoppingCartServiceTest.java | 361 +++++++++--------- 2 files changed, 179 insertions(+), 204 deletions(-) diff --git a/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java b/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java index 509b099..93f214f 100644 --- a/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java +++ b/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java @@ -19,6 +19,7 @@ import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; + import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -33,11 +34,6 @@ class ShoppingCartControllerTest { @Autowired private ShoppingCartService shoppingCartService; - private User user; - private Book book; - private CartItem cartItem; - private ShoppingCart shoppingCart; - @BeforeAll static void beforeAll(@Autowired WebApplicationContext webApplicationContext) { mockMvc = MockMvcBuilders @@ -50,12 +46,11 @@ static void beforeAll(@Autowired WebApplicationContext webApplicationContext) { "/data-sql/create-shopping-carts.sql", "/data-sql/create-cart-item.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) - @WithMockUser(username = "first_name", roles = {"USER"}) + @WithMockUser(username = "john.doe@example.com", roles = {"USER"}) @Test @DisplayName("Get shopping cart for the current user") void getShoppingCart_ShouldReturnCartForUser() throws Exception { mockMvc.perform(get("/cart") - .param("userId", "1") // Ensure the userId parameter is passed .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.user.id").value(1L)) @@ -67,7 +62,7 @@ void getShoppingCart_ShouldReturnCartForUser() throws Exception { "/data-sql/create-shopping-carts.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) - @WithMockUser(username = "john_doe", roles = {"USER"}) + @WithMockUser(username = "john.doe@example.com", roles = {"USER"}) @Test @DisplayName("Add book to shopping cart") void addBookToShoppingCart_ShouldReturnUpdatedCart() throws Exception { @@ -78,7 +73,6 @@ void addBookToShoppingCart_ShouldReturnUpdatedCart() throws Exception { String jsonRequest = objectMapper.writeValueAsString(cartItemRequestDto); MvcResult result = mockMvc.perform(post("/cart") - .param("userId", "1") .contentType(MediaType.APPLICATION_JSON) .content(jsonRequest)) .andExpect(status().isOk()) @@ -91,7 +85,7 @@ void addBookToShoppingCart_ShouldReturnUpdatedCart() throws Exception { "/data-sql/create-shopping-carts.sql", "/data-sql/create-cart-item.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) - @WithMockUser(username = "john_doe", roles = {"USER"}) + @WithMockUser(username = "john.doe@example.com", roles = {"USER"}) @Test @DisplayName("Update cart item quantity") void updateCartItemQuantity_ShouldReturnUpdatedQuantity() throws Exception { @@ -100,8 +94,7 @@ void updateCartItemQuantity_ShouldReturnUpdatedQuantity() throws Exception { mockMvc.perform(put("/cart/items/{cartItemId}", cartItemId) .param("quantity", String.valueOf(newQuantity)) - .param("userId", "1") - .contentType(MediaType.APPLICATION_JSON)) + .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.cartItems[0].quantity").value(newQuantity)) .andReturn(); @@ -111,15 +104,14 @@ void updateCartItemQuantity_ShouldReturnUpdatedQuantity() throws Exception { "/data-sql/create-shopping-carts.sql", "/data-sql/create-cart-item.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) - @WithMockUser(username = "john_doe") + @WithMockUser(username = "john.doe@example.com", roles = {"USER"}) @Test @DisplayName("Remove item from cart") void removeItemFromCart_ShouldReturnEmptyCart() throws Exception { Long cartItemId = 1L; mockMvc.perform(delete("/cart/items/{cartItemId}", cartItemId) - .param("userId", "1") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()); } -} +} \ No newline at end of file diff --git a/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java b/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java index 2fb74a1..5f1b8c2 100644 --- a/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java +++ b/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java @@ -1,189 +1,172 @@ -//package mate.academy.springbootwebgreqit.service; -// -//import mate.academy.springbootwebgreqit.dto.cartitem.CartItemRequestDto; -//import mate.academy.springbootwebgreqit.dto.shoppingcart.ShoppingCartDto; -//import mate.academy.springbootwebgreqit.exception.EntityNotFoundException; -//import mate.academy.springbootwebgreqit.mapper.CartItemMapper; -//import mate.academy.springbootwebgreqit.mapper.ShoppingCartMapper; -//import mate.academy.springbootwebgreqit.model.Book; -//import mate.academy.springbootwebgreqit.model.CartItem; -//import mate.academy.springbootwebgreqit.model.ShoppingCart; -//import mate.academy.springbootwebgreqit.model.User; -//import mate.academy.springbootwebgreqit.repository.BookRepository; -//import mate.academy.springbootwebgreqit.repository.CartItemRepository; -//import mate.academy.springbootwebgreqit.repository.ShoppingCartRepository; -//import mate.academy.springbootwebgreqit.repository.UserRepository; -//import mate.academy.springbootwebgreqit.service.impl.ShoppingCartServiceImpl; -//import org.junit.jupiter.api.BeforeEach; -//import org.junit.jupiter.api.Test; -//import org.junit.jupiter.api.extension.ExtendWith; -//import org.mockito.InjectMocks; -//import org.mockito.Mock; -//import org.mockito.junit.jupiter.MockitoExtension; -// -//import java.util.Optional; -// -//import static org.junit.jupiter.api.Assertions.*; -//import static org.mockito.Mockito.*; -// -//@ExtendWith(MockitoExtension.class) -//class ShoppingCartServiceTest { -// -// @Mock -// private CartItemRepository cartItemRepository; -// @Mock -// private ShoppingCartRepository shoppingCartRepository; -// @Mock -// private ShoppingCartMapper shoppingCartMapper; -// @Mock -// private CartItemMapper cartItemMapper; -// @Mock -// private BookRepository bookRepository; -// @Mock -// private UserRepository userRepository; -// -// @InjectMocks -// private ShoppingCartServiceImpl shoppingCartService; -// -// private User user; -// private ShoppingCart shoppingCart; -// private ShoppingCartDto shoppingCartDto; -// private CartItemRequestDto cartItemRequestDto; -// private Book book; -// private CartItem cartItem; -// -// @BeforeEach -// void setUp() { -// user = new User(); -// user.setId(1L); -// -// shoppingCart = new ShoppingCart(); -// shoppingCart.setUser(user); -// -// shoppingCartDto = new ShoppingCartDto(); -// -// cartItemRequestDto = new CartItemRequestDto(); -// cartItemRequestDto.setBookId(1L); -// cartItemRequestDto.setQuantity(2); -// -// book = new Book(); -// book.setId(1L); -// -// cartItem = new CartItem(); -// cartItem.setId(1L); -// cartItem.setBook(book); -// cartItem.setQuantity(2); -// cartItem.setShoppingCart(shoppingCart); -// } -// -//// @Test -//// void getShoppingCartForCurrentUser_ShouldReturnShoppingCartDto() { -//// user.setShoppingCart(shoppingCart); -//// // Arrange -//// when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); -//// when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); -//// -//// // Act -//// ShoppingCartDto result = shoppingCartService.getShoppingCartForCurrentUser(); -//// -//// // Assert -//// assertNotNull(result); -//// verify(userRepository).findById(user.getId()); -//// verify(shoppingCartMapper).toDto(shoppingCart); -//// } -// -//// @Test -//// void getShoppingCartForCurrentUser_ShouldThrowEntityNotFoundException_WhenShoppingCartNotFound() { -//// // Arrange -//// User user1 = new User(); -//// user1.setId(2L); -//// user1.setShoppingCart(null); -//// when(userRepository.findById(user1.getId())).thenReturn(Optional.of(user1)); -//// -//// // Act & Assert -//// assertThrows(EntityNotFoundException.class, -//// () -> shoppingCartService.getShoppingCartForCurrentUser(user1.getId())); -//// } -// -// -// @Test -// void addBookToShoppingCart_ShouldAddNewBookAndReturnShoppingCartDto() { -// // Arrange -// when(shoppingCartRepository.findByUserId(user.getId())).thenReturn(Optional.of(shoppingCart)); -// when(bookRepository.findById(cartItemRequestDto.getBookId())).thenReturn(Optional.of(book)); -// when(cartItemMapper.toModel(cartItemRequestDto)).thenReturn(cartItem); -// when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); -// -// // Mocking save method to return updated shopping cart -// when(shoppingCartRepository.save(any(ShoppingCart.class))).thenReturn(shoppingCart); -// -// // Act -// ShoppingCartDto result = shoppingCartService.addBookToShoppingCart(cartItemRequestDto, user.getId()); -// -// // Assert -// assertNotNull(result); -// verify(shoppingCartRepository).findByUserId(user.getId()); -// verify(cartItemMapper).toModel(cartItemRequestDto); -// verify(bookRepository).findById(cartItemRequestDto.getBookId()); -// verify(shoppingCartRepository).save(shoppingCart); -// -// // Optional: Verify that cart items were updated -// assertEquals(1, shoppingCart.getCartItems().size()); // assuming one item is added -// } -// -// -// @Test -// void updateCartItemQuantity_ShouldUpdateQuantityAndReturnShoppingCartDto() { -// user.setShoppingCart(shoppingCart); -// // Arrange -// shoppingCart.getCartItems().add(cartItem); -// -// // ΠœΠΎΠΊΡƒΠ²Π°Π½Π½Ρ Ρ€Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€Ρ–Ρ—Π² -// when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); // ΠŸΠ΅Ρ€Π΅Π²Ρ–Ρ€ΠΊΠ° Π½Π° Π½Π°ΡΠ²Π½Ρ–ΡΡ‚ΡŒ користувача -// when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); // ΠœΠΎΠΊΡƒΠ²Π°Π½Π½Ρ пСрСтворСння кошика Π² DTO -// when(cartItemRepository.save(cartItem)).thenReturn(cartItem); // ΠœΠΎΠΊΡƒΠ²Π°Π½Π½Ρ збСрСТСння Π·ΠΌΡ–Π½Π΅Π½ΠΎΡ— ΠΊΡ–Π»ΡŒΠΊΠΎΡΡ‚Ρ– Ρ‚ΠΎΠ²Π°Ρ€Ρƒ -// when(shoppingCartRepository.save(shoppingCart)).thenReturn(shoppingCart); -// -// // Act -// ShoppingCartDto result = shoppingCartService.updateCartItemQuantity(cartItem.getId(), 3, user.getId()); -// -// // Assert -// assertNotNull(result); // ΠŸΠ΅Ρ€Π΅Π²Ρ–Ρ€ΠΊΠ°, Ρ‰ΠΎ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ Π½Π΅ null -// assertEquals(3, cartItem.getQuantity()); // ΠŸΠ΅Ρ€Π΅Π²Ρ–Ρ€ΠΊΠ° Π·ΠΌΡ–Π½ΠΈ ΠΊΡ–Π»ΡŒΠΊΠΎΡΡ‚Ρ– Ρ‚ΠΎΠ²Π°Ρ€Ρƒ -// verify(cartItemRepository).save(cartItem); // ΠŸΠ΅Ρ€Π΅Π²Ρ–Ρ€ΠΊΠ° Π²ΠΈΠΊΠ»ΠΈΠΊΡƒ ΠΌΠ΅Ρ‚ΠΎΠ΄Ρƒ збСрСТСння -// } -// -// -// -// @Test -// void removeCartItem_ShouldRemoveItemAndReturnShoppingCartDto() { -// // Arrange -// shoppingCart.getCartItems().add(cartItem); -// when(shoppingCartRepository.findByUserId(user.getId())).thenReturn(Optional.of(shoppingCart)); -// when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); -// when(shoppingCartRepository.save(shoppingCart)).thenReturn(shoppingCart); -// -// // ΠœΠΎΠΊΡƒΠ²Π°Π½Π½Ρ для ΠΌΠ΅Ρ‚ΠΎΠ΄Ρƒ toDto -// when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); -// -// // Act -// ShoppingCartDto result = shoppingCartService.removeCartItem(cartItem.getId(), user.getId()); -// -// // Assert -// assertNotNull(result); -// assertTrue(shoppingCart.getCartItems().isEmpty()); -// verify(shoppingCartRepository).save(shoppingCart); -// } -// -// -// @Test -// void createShoppingCart_ShouldCreateAndReturnShoppingCart() { -// // Act -// ShoppingCart result = shoppingCartService.createShoppingCart(user); -// -// // Assert -// assertNotNull(result); -// assertEquals(user, result.getUser()); -// verify(shoppingCartRepository).save(result); -// } -//} +package mate.academy.springbootwebgreqit.service; + +import mate.academy.springbootwebgreqit.dto.cartitem.CartItemRequestDto; +import mate.academy.springbootwebgreqit.dto.shoppingcart.ShoppingCartDto; +import mate.academy.springbootwebgreqit.exception.EntityNotFoundException; +import mate.academy.springbootwebgreqit.mapper.CartItemMapper; +import mate.academy.springbootwebgreqit.mapper.ShoppingCartMapper; +import mate.academy.springbootwebgreqit.model.Book; +import mate.academy.springbootwebgreqit.model.CartItem; +import mate.academy.springbootwebgreqit.model.ShoppingCart; +import mate.academy.springbootwebgreqit.model.User; +import mate.academy.springbootwebgreqit.repository.BookRepository; +import mate.academy.springbootwebgreqit.repository.CartItemRepository; +import mate.academy.springbootwebgreqit.repository.ShoppingCartRepository; +import mate.academy.springbootwebgreqit.repository.UserRepository; +import mate.academy.springbootwebgreqit.service.impl.ShoppingCartServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.Authentication; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ShoppingCartServiceTest { + + @Mock + private CartItemRepository cartItemRepository; + @Mock + private ShoppingCartRepository shoppingCartRepository; + @Mock + private ShoppingCartMapper shoppingCartMapper; + @Mock + private CartItemMapper cartItemMapper; + @Mock + private BookRepository bookRepository; + @Mock + private UserRepository userRepository; + + @InjectMocks + private ShoppingCartServiceImpl shoppingCartService; + + private User user; + private ShoppingCart shoppingCart; + private ShoppingCartDto shoppingCartDto; + private CartItemRequestDto cartItemRequestDto; + private Book book; + private CartItem cartItem; + private Authentication authentication; + + @BeforeEach + void setUp() { + user = new User(); + user.setId(1L); + user.setEmail("test@example.com"); + + shoppingCart = new ShoppingCart(); + shoppingCart.setId(1L); + shoppingCart.setUser(user); + + shoppingCartDto = new ShoppingCartDto(); + + cartItemRequestDto = new CartItemRequestDto(); + cartItemRequestDto.setBookId(1L); + cartItemRequestDto.setQuantity(2); + + book = new Book(); + book.setId(1L); + + cartItem = new CartItem(); + cartItem.setId(1L); + cartItem.setBook(book); + cartItem.setQuantity(2); + cartItem.setShoppingCart(shoppingCart); + + authentication = mock(Authentication.class); + } + + @Test + void getShoppingCartForCurrentUser_ShouldReturnShoppingCartDto() { + user.setShoppingCart(shoppingCart); + when(authentication.getName()).thenReturn(user.getEmail()); + when(userRepository.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); + when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); + + ShoppingCartDto result = shoppingCartService.getShoppingCartForCurrentUser(authentication); + + assertNotNull(result); + verify(userRepository).findByEmail(user.getEmail()); + verify(shoppingCartMapper).toDto(shoppingCart); + } + + @Test + void getShoppingCartForCurrentUser_ShouldThrowEntityNotFoundException_WhenShoppingCartNotFound() { + when(userRepository.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); + when(authentication.getName()).thenReturn(user.getEmail()); + + assertThrows(EntityNotFoundException.class, + () -> shoppingCartService.getShoppingCartForCurrentUser(authentication)); + } + + @Test + void addBookToShoppingCart_ShouldAddNewBookAndReturnShoppingCartDto() { + when(userRepository.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); + when(shoppingCartRepository.findByUserId(user.getId())).thenReturn(Optional.of(shoppingCart)); + when(bookRepository.findById(cartItemRequestDto.getBookId())).thenReturn(Optional.of(book)); + when(cartItemMapper.toModel(cartItemRequestDto)).thenReturn(cartItem); + when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); + when(shoppingCartRepository.save(shoppingCart)).thenReturn(shoppingCart); + when(authentication.getName()).thenReturn(user.getEmail()); + + ShoppingCartDto result = shoppingCartService.addBookToShoppingCart(cartItemRequestDto, authentication); + + assertNotNull(result); + verify(shoppingCartRepository).findByUserId(user.getId()); + verify(cartItemMapper).toModel(cartItemRequestDto); + verify(bookRepository).findById(cartItemRequestDto.getBookId()); + verify(shoppingCartRepository).save(shoppingCart); + assertEquals(1, shoppingCart.getCartItems().size()); + } + + @Test + void updateCartItemQuantity_ShouldUpdateQuantityAndReturnShoppingCartDto() { + user.setShoppingCart(shoppingCart); + shoppingCart.getCartItems().add(cartItem); + when(userRepository.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); + when(cartItemRepository.save(cartItem)).thenReturn(cartItem); + when(shoppingCartRepository.save(shoppingCart)).thenReturn(shoppingCart); + when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); + when(authentication.getName()).thenReturn(user.getEmail()); + + ShoppingCartDto result = shoppingCartService.updateCartItemQuantity(cartItem.getId(), 3, authentication); + + assertNotNull(result); + assertEquals(3, cartItem.getQuantity()); + verify(cartItemRepository).save(cartItem); + verify(shoppingCartMapper).toDto(shoppingCart); + } + + @Test + void removeCartItem_ShouldRemoveItemAndReturnShoppingCartDto() { + shoppingCart.getCartItems().add(cartItem); + when(userRepository.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); + when(shoppingCartRepository.findByUserId(user.getId())).thenReturn(Optional.of(shoppingCart)); + when(shoppingCartRepository.save(shoppingCart)).thenReturn(shoppingCart); + when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); + when(authentication.getName()).thenReturn(user.getEmail()); + + ShoppingCartDto result = shoppingCartService.removeCartItem(cartItem.getId(), authentication); + + assertNotNull(result); + assertTrue(shoppingCart.getCartItems().isEmpty()); + verify(shoppingCartRepository).save(shoppingCart); + } + + @Test + void createShoppingCart_ShouldCreateAndReturnShoppingCart() { + when(shoppingCartRepository.save(any(ShoppingCart.class))).thenReturn(shoppingCart); + + ShoppingCart result = shoppingCartService.createShoppingCart(user); + + assertNotNull(result); + assertEquals(user, result.getUser()); + verify(shoppingCartRepository).save(result); + } +} \ No newline at end of file From bb00fce3a9b1a42d24ff356a9c4b47736cfb288f Mon Sep 17 00:00:00 2001 From: greqit Date: Thu, 7 Nov 2024 14:16:36 +0100 Subject: [PATCH 12/21] job is done --- .../springbootwebgreqit/model/User.java | 2 +- ...reate-table-users-roles-shoppingcarts.yaml | 2 +- .../ShoppingCartControllerTest.java | 37 ++++++++++--------- .../data-sql/clear-tables-for-sh.sql | 10 +++++ src/test/resources/data-sql/create-books.sql | 3 +- .../data-sql/create-shopping-carts.sql | 1 - src/test/resources/data-sql/create-users.sql | 10 ++++- 7 files changed, 43 insertions(+), 22 deletions(-) delete mode 100644 src/test/resources/data-sql/create-shopping-carts.sql diff --git a/src/main/java/mate/academy/springbootwebgreqit/model/User.java b/src/main/java/mate/academy/springbootwebgreqit/model/User.java index fe41696..78c9463 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/model/User.java +++ b/src/main/java/mate/academy/springbootwebgreqit/model/User.java @@ -46,7 +46,7 @@ public class User implements UserDetails { @Column(name = "is_deleted") private boolean isDeleted = false; - @JoinColumn(name = "shopping_card_id") + @JoinColumn(name = "shopping_cart_id", nullable = false) @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) private ShoppingCart shoppingCart; diff --git a/src/main/resources/db/changelog/changes/02-create-table-users-roles-shoppingcarts.yaml b/src/main/resources/db/changelog/changes/02-create-table-users-roles-shoppingcarts.yaml index 1e36e67..0d1d600 100644 --- a/src/main/resources/db/changelog/changes/02-create-table-users-roles-shoppingcarts.yaml +++ b/src/main/resources/db/changelog/changes/02-create-table-users-roles-shoppingcarts.yaml @@ -42,7 +42,7 @@ databaseChangeLog: type: boolean defaultValue: false - column: - name: shopping_card_id + name: shopping_cart_id type: bigint - createTable: diff --git a/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java b/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java index 93f214f..a04494c 100644 --- a/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java +++ b/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java @@ -2,10 +2,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import mate.academy.springbootwebgreqit.dto.cartitem.CartItemRequestDto; -import mate.academy.springbootwebgreqit.model.Book; -import mate.academy.springbootwebgreqit.model.CartItem; -import mate.academy.springbootwebgreqit.model.ShoppingCart; -import mate.academy.springbootwebgreqit.model.User; +import mate.academy.springbootwebgreqit.security.CustomUserDetailService; import mate.academy.springbootwebgreqit.service.ShoppingCartService; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; @@ -19,7 +16,6 @@ import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; - import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -33,6 +29,8 @@ class ShoppingCartControllerTest { @Autowired private ShoppingCartService shoppingCartService; + @Autowired + private CustomUserDetailService customUserDetailService; @BeforeAll static void beforeAll(@Autowired WebApplicationContext webApplicationContext) { @@ -42,10 +40,11 @@ static void beforeAll(@Autowired WebApplicationContext webApplicationContext) { .build(); } - @Sql(scripts = {"/data-sql/create-books.sql", "/data-sql/create-users.sql", - "/data-sql/create-shopping-carts.sql", "/data-sql/create-cart-item.sql"}, + @Sql(scripts = {"/data-sql/create-books.sql", + "/data-sql/create-users.sql", "/data-sql/create-cart-item.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) - @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = + Sql.ExecutionPhase.AFTER_TEST_METHOD) @WithMockUser(username = "john.doe@example.com", roles = {"USER"}) @Test @DisplayName("Get shopping cart for the current user") @@ -58,10 +57,10 @@ void getShoppingCart_ShouldReturnCartForUser() throws Exception { .andReturn(); } - @Sql(scripts = {"/data-sql/create-books.sql", "/data-sql/create-users.sql", - "/data-sql/create-shopping-carts.sql"}, + @Sql(scripts = {"/data-sql/create-books.sql", "/data-sql/create-users.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) - @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = + Sql.ExecutionPhase.AFTER_TEST_METHOD) @WithMockUser(username = "john.doe@example.com", roles = {"USER"}) @Test @DisplayName("Add book to shopping cart") @@ -76,15 +75,18 @@ void addBookToShoppingCart_ShouldReturnUpdatedCart() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(jsonRequest)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.cartItems[0].book.id").value(cartItemRequestDto.getBookId())) - .andExpect(jsonPath("$.cartItems[0].quantity").value(cartItemRequestDto.getQuantity())) + .andExpect(jsonPath("$.cartItems[0].book.id") + .value(cartItemRequestDto.getBookId())) + .andExpect(jsonPath("$.cartItems[0].quantity") + .value(cartItemRequestDto.getQuantity())) .andReturn(); } @Sql(scripts = {"/data-sql/create-books.sql", "/data-sql/create-users.sql", - "/data-sql/create-shopping-carts.sql", "/data-sql/create-cart-item.sql"}, + "/data-sql/create-cart-item.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) - @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = + Sql.ExecutionPhase.AFTER_TEST_METHOD) @WithMockUser(username = "john.doe@example.com", roles = {"USER"}) @Test @DisplayName("Update cart item quantity") @@ -101,9 +103,10 @@ void updateCartItemQuantity_ShouldReturnUpdatedQuantity() throws Exception { } @Sql(scripts = {"/data-sql/create-books.sql", "/data-sql/create-users.sql", - "/data-sql/create-shopping-carts.sql", "/data-sql/create-cart-item.sql"}, + "/data-sql/create-cart-item.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) - @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = + Sql.ExecutionPhase.AFTER_TEST_METHOD) @WithMockUser(username = "john.doe@example.com", roles = {"USER"}) @Test @DisplayName("Remove item from cart") diff --git a/src/test/resources/data-sql/clear-tables-for-sh.sql b/src/test/resources/data-sql/clear-tables-for-sh.sql index d72624e..69f52e0 100644 --- a/src/test/resources/data-sql/clear-tables-for-sh.sql +++ b/src/test/resources/data-sql/clear-tables-for-sh.sql @@ -1,4 +1,14 @@ +-- Set shopping_cart_id to NULL in users table before deleting from shopping_carts +UPDATE users SET shopping_cart_id = NULL WHERE shopping_cart_id = 1; + +-- Delete from cart_items DELETE FROM cart_items; + +-- Delete from shopping_carts DELETE FROM shopping_carts; + +-- Delete from books DELETE FROM books; + +-- Delete from users; DELETE FROM users; \ No newline at end of file diff --git a/src/test/resources/data-sql/create-books.sql b/src/test/resources/data-sql/create-books.sql index bb7de75..7410765 100644 --- a/src/test/resources/data-sql/create-books.sql +++ b/src/test/resources/data-sql/create-books.sql @@ -1 +1,2 @@ -INSERT INTO books (id, title, author, isbn, price, description, cover_image) VALUES (1, 'Sample Book', 'John Author', '1234567890', 10.99, 'A sample book description.', 'https://example.com/book-cover.jpg'); +INSERT INTO books (id, title, author, isbn, price, description, cover_image) +VALUES (1, 'Sample Book', 'John Author', '1234567890', 10.99, 'A sample book description.', 'https://example.com/book-cover.jpg'); \ No newline at end of file diff --git a/src/test/resources/data-sql/create-shopping-carts.sql b/src/test/resources/data-sql/create-shopping-carts.sql deleted file mode 100644 index c61a990..0000000 --- a/src/test/resources/data-sql/create-shopping-carts.sql +++ /dev/null @@ -1 +0,0 @@ -INSERT INTO shopping_carts (id, user_id) VALUES (1, 1); \ No newline at end of file diff --git a/src/test/resources/data-sql/create-users.sql b/src/test/resources/data-sql/create-users.sql index 68c5155..9d70f23 100644 --- a/src/test/resources/data-sql/create-users.sql +++ b/src/test/resources/data-sql/create-users.sql @@ -1 +1,9 @@ -INSERT INTO users (id, first_name, last_name, email, password, shipping_address) VALUES (1, 'John', 'Doe', 'john.doe@example.com', 'password123', '123 Main St, Springfield'); +-- Insert user +INSERT INTO users (id, first_name, last_name, email, password, shipping_address) +VALUES (1, 'John', 'Doe', 'john.doe@example.com', 'password123', '123 Main St, Springfield'); + +-- Insert shopping cart +INSERT INTO shopping_carts (id, user_id) VALUES (1, 1); + +-- Update user's shopping_cart_id +UPDATE users SET shopping_cart_id = 1 WHERE id = 1; \ No newline at end of file From 4c73e3327a0b5d7b18e2c1d1cc6c1953a7ed186b Mon Sep 17 00:00:00 2001 From: greqit Date: Sat, 9 Nov 2024 12:36:29 +0100 Subject: [PATCH 13/21] Add Swagger annotations everywhere --- pom.xml | 12 ++++++ .../controller/AuthenticationController.java | 21 +++++++--- .../controller/BookController.java | 41 +++++++++++++++---- .../controller/CategoryController.java | 33 +++++++++++++++ .../controller/OrderController.java | 37 +++++++++++++---- .../controller/ShoppingCartController.java | 23 +++++++++++ 6 files changed, 147 insertions(+), 20 deletions(-) diff --git a/pom.xml b/pom.xml index 1263652..da192bd 100644 --- a/pom.xml +++ b/pom.xml @@ -60,6 +60,18 @@ 1.6.14 + + io.springfox + springfox-swagger-ui + 3.0.0 + + + + io.swagger.core.v3 + swagger-annotations + 2.2.25 + + io.jsonwebtoken diff --git a/src/main/java/mate/academy/springbootwebgreqit/controller/AuthenticationController.java b/src/main/java/mate/academy/springbootwebgreqit/controller/AuthenticationController.java index b689b75..a37ae35 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/controller/AuthenticationController.java +++ b/src/main/java/mate/academy/springbootwebgreqit/controller/AuthenticationController.java @@ -1,5 +1,8 @@ package mate.academy.springbootwebgreqit.controller; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import mate.academy.springbootwebgreqit.dto.user.UserLoginRequestDto; @@ -20,15 +23,23 @@ public class AuthenticationController { private final UserService userService; private final AuthenticationService authenticationService; + @Operation(summary = "Register a new user") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "User registered successfully"), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) @PostMapping("/registration") - public UserResponseDto register(@RequestBody - @Valid UserRegistrationRequestDto requestBody) { + public UserResponseDto register(@RequestBody @Valid UserRegistrationRequestDto requestBody) { return userService.register(requestBody); } + @Operation(summary = "Authenticate a user") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "User authenticated successfully"), + @ApiResponse(responseCode = "401", description = "Invalid credentials") + }) @PostMapping("/login") - public UserLoginResponseDto login(@RequestBody - @Valid UserLoginRequestDto response) { + public UserLoginResponseDto login(@RequestBody @Valid UserLoginRequestDto response) { return authenticationService.authenticate(response); } -} +} \ No newline at end of file diff --git a/src/main/java/mate/academy/springbootwebgreqit/controller/BookController.java b/src/main/java/mate/academy/springbootwebgreqit/controller/BookController.java index ca81229..7fc5983 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/controller/BookController.java +++ b/src/main/java/mate/academy/springbootwebgreqit/controller/BookController.java @@ -1,6 +1,8 @@ package mate.academy.springbootwebgreqit.controller; -import io.swagger.annotations.ApiOperation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import mate.academy.springbootwebgreqit.dto.BookDto; @@ -27,40 +29,65 @@ public class BookController { private final BookService bookService; @PreAuthorize("hasRole('ADMIN')") + @Operation(summary = "Get all books with pagination") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Books retrieved successfully"), + @ApiResponse(responseCode = "403", description = "Access denied") + }) @GetMapping - @ApiOperation(value = "get all books with pagination") public Page getAll(Pageable pageable) { return bookService.findAll(pageable); } + @Operation(summary = "Get book by id") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Book retrieved successfully"), + @ApiResponse(responseCode = "404", description = "Book not found") + }) @GetMapping("/{id}") - @ApiOperation(value = "Get book by id") public BookDto getBookById(@Valid @PathVariable Long id) { return bookService.findById(id); } @PreAuthorize("hasRole('ADMIN')") + @Operation(summary = "Create a book") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Book created successfully"), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) @PostMapping - @ApiOperation(value = "create a book") public BookDto createBook(@Valid @RequestBody CreateBookRequestDto requestDto) { return bookService.save(requestDto); } - @PutMapping("/{id}") @PreAuthorize("hasRole('ADMIN')") + @Operation(summary = "Update a book") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Book updated successfully"), + @ApiResponse(responseCode = "404", description = "Book not found") + }) + @PutMapping("/{id}") public BookDto updateBook(@PathVariable Long id, @RequestBody @Valid UpdateBookRequestDto updateBookRequestDto) { return bookService.update(updateBookRequestDto); } @PreAuthorize("hasRole('ADMIN')") + @Operation(summary = "Delete a book") + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "Book deleted successfully"), + @ApiResponse(responseCode = "404", description = "Book not found") + }) @DeleteMapping("/{id}") - @ApiOperation(value = "delete a book") public void deleteBook(@PathVariable Long id) { bookService.deleteById(id); } + @Operation(summary = "Search a book") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Books retrieved successfully"), + @ApiResponse(responseCode = "400", description = "Invalid search parameters") + }) @GetMapping("/search") - @ApiOperation(value = "search a book") public Page searchBooks(BookSearchParameters searchParameters, Pageable pageable) { return bookService.search(searchParameters, pageable); } diff --git a/src/main/java/mate/academy/springbootwebgreqit/controller/CategoryController.java b/src/main/java/mate/academy/springbootwebgreqit/controller/CategoryController.java index 8f184af..1f11500 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/controller/CategoryController.java +++ b/src/main/java/mate/academy/springbootwebgreqit/controller/CategoryController.java @@ -1,5 +1,8 @@ package mate.academy.springbootwebgreqit.controller; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import mate.academy.springbootwebgreqit.dto.BookDtoWithoutCategotyIds; @@ -26,34 +29,64 @@ public class CategoryController { private final CategoryService categoryService; @PreAuthorize("hasRole('ADMIN')") + @Operation(summary = "Create a new category") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Category created successfully"), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) @PostMapping public CategoryDto createCategory(@Valid @RequestBody CreateCategoryRequestDto categoryDto) { return categoryService.save(categoryDto); } @PreAuthorize("hasRole('ADMIN')") + @Operation(summary = "Get all categories") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Categories retrieved successfully"), + @ApiResponse(responseCode = "403", description = "Access denied") + }) @GetMapping public List getAll() { return categoryService.findAll(); } + @Operation(summary = "Get category by id") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Category retrieved successfully"), + @ApiResponse(responseCode = "404", description = "Category not found") + }) @GetMapping("/{id}") public CategoryDto getCategoryById(@Valid @PathVariable Long id) { return categoryService.getById(id); } @PreAuthorize("hasRole('ADMIN')") + @Operation(summary = "Update a category") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Category updated successfully"), + @ApiResponse(responseCode = "404", description = "Category not found") + }) @PutMapping("/{id}") public CategoryDto updateCategory(@PathVariable Long id, @RequestBody @Valid UpdateCategoryRequestDto categoryDto) { return categoryService.update(id, categoryDto); } @PreAuthorize("hasRole('ADMIN')") + @Operation(summary = "Delete a category") + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "Category deleted successfully"), + @ApiResponse(responseCode = "404", description = "Category not found") + }) @DeleteMapping("/{id}") public void deleteCategory(@PathVariable Long id) { categoryService.deleteById(id); } + @Operation(summary = "Get books by category id") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Books retrieved successfully"), + @ApiResponse(responseCode = "404", description = "Category not found") + }) @GetMapping("/{id}/books") public List getBooksByCategoryId(@PathVariable Long id) { return categoryService.findBooksByCategoryId(id); diff --git a/src/main/java/mate/academy/springbootwebgreqit/controller/OrderController.java b/src/main/java/mate/academy/springbootwebgreqit/controller/OrderController.java index 35fbf29..fb3c1c6 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/controller/OrderController.java +++ b/src/main/java/mate/academy/springbootwebgreqit/controller/OrderController.java @@ -1,6 +1,8 @@ package mate.academy.springbootwebgreqit.controller; -import io.swagger.annotations.ApiOperation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import mate.academy.springbootwebgreqit.dto.order.CreateOrderRequestDto; @@ -28,14 +30,22 @@ public class OrderController { private final OrderService orderService; @PostMapping - @ApiOperation("Make order") + @Operation(summary = "Make order") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Order created successfully"), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) @PreAuthorize("hasRole('USER')") public OrderResponseDto addOrder(@RequestParam Long userId, @RequestBody CreateOrderRequestDto createOrderRequestDto) { return orderService.addOrder(userId, createOrderRequestDto); } - @ApiOperation("Get all orders") + @Operation(summary = "Get all orders") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Orders retrieved successfully"), + @ApiResponse(responseCode = "403", description = "Access denied") + }) @GetMapping @PreAuthorize("hasRole('USER')") public List getAllOrders(@RequestParam Long userId) { @@ -44,26 +54,37 @@ public List getAllOrders(@RequestParam Long userId) { @PreAuthorize("hasRole('ADMIN')") @PatchMapping("/{id}") - @ApiOperation("Update order status by id") + @Operation(summary = "Update order status by id") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Order status updated successfully"), + @ApiResponse(responseCode = "404", description = "Order not found") + }) public OrderResponseDto updateOrderStatus(@PathVariable Long id, @RequestBody @Valid OrderRequestDto requestDto) { return orderService.updateOrderStatus(id, requestDto); } + @Operation(summary = "Get all items from order by order id") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Order items retrieved successfully"), + @ApiResponse(responseCode = "404", description = "Order not found") + }) @GetMapping("/{orderId}/items") - @ApiOperation("Get all items from order by order id") @PreAuthorize("hasRole('USER')") public List getAllItemsFromOrder(@PathVariable Long orderId, Authentication authentication) { return orderService.getAllItemsFromOrder(orderId); } + @Operation(summary = "Get item from order by order id and item id") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Order item retrieved successfully"), + @ApiResponse(responseCode = "404", description = "Order or item not found") + }) @GetMapping("{orderId}/items/{itemId}") - @ApiOperation("Get item from order by order id and item id") @PreAuthorize("hasRole('USER')") public OrderItemResponseDto getItemFromOrderById(@PathVariable Long orderId, - @PathVariable Long itemId, - Authentication authentication) { + @PathVariable Long itemId) { return orderService.getItemFromOrderById(orderId, itemId); } } diff --git a/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java b/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java index 5765d3c..6af2e75 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java +++ b/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java @@ -1,5 +1,8 @@ package mate.academy.springbootwebgreqit.controller; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import lombok.RequiredArgsConstructor; import mate.academy.springbootwebgreqit.dto.cartitem.CartItemRequestDto; import mate.academy.springbootwebgreqit.dto.shoppingcart.ShoppingCartDto; @@ -21,17 +24,32 @@ public class ShoppingCartController { private final ShoppingCartService shoppingCartService; + @Operation(summary = "Get shopping cart for current user") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Shopping cart retrieved successfully"), + @ApiResponse(responseCode = "403", description = "Access denied") + }) @GetMapping public ShoppingCartDto getShoppingCartForCurrentUser(Authentication authentication) { return shoppingCartService.getShoppingCartForCurrentUser(authentication); } + @Operation(summary = "Add book to shopping cart") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Book added to shopping cart successfully"), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) @PostMapping public ShoppingCartDto addBookToShoppingCart(@RequestBody CartItemRequestDto cartItem, Authentication authentication) { return shoppingCartService.addBookToShoppingCart(cartItem, authentication); } + @Operation(summary = "Update cart item quantity") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Cart item quantity updated successfully"), + @ApiResponse(responseCode = "404", description = "Cart item not found") + }) @PutMapping("/items/{cartItemId}") public ShoppingCartDto updateCartItemQuantity(@PathVariable Long cartItemId, @RequestParam int quantity, @@ -39,6 +57,11 @@ public ShoppingCartDto updateCartItemQuantity(@PathVariable Long cartItemId, return shoppingCartService.updateCartItemQuantity(cartItemId, quantity, authentication); } + @Operation(summary = "Remove cart item") + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "Cart item removed successfully"), + @ApiResponse(responseCode = "404", description = "Cart item not found") + }) @DeleteMapping("/items/{cartItemId}") public void removeCartItem(@PathVariable Long cartItemId, Authentication authentication) { shoppingCartService.removeCartItem(cartItemId, authentication); From 24d55e79c95ffc0d87228f6e0563dca48c7b6819 Mon Sep 17 00:00:00 2001 From: greqit Date: Sat, 9 Nov 2024 12:59:32 +0100 Subject: [PATCH 14/21] refactor main logic --- .../controller/ShoppingCartController.java | 11 +++++----- .../RequestUpdateQuantityDto.java | 8 +++++++ .../springbootwebgreqit/model/User.java | 6 ------ .../repository/ShoppingCartRepository.java | 4 ++++ .../service/ShoppingCartService.java | 5 +++-- .../service/impl/ShoppingCartServiceImpl.java | 21 +++++++++++++------ .../service/impl/UserServiceImpl.java | 2 +- ...reate-table-users-roles-shoppingcarts.yaml | 3 --- .../service/ShoppingCartServiceTest.java | 13 +++++++----- 9 files changed, 45 insertions(+), 28 deletions(-) create mode 100644 src/main/java/mate/academy/springbootwebgreqit/dto/shoppingcart/RequestUpdateQuantityDto.java diff --git a/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java b/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java index 6af2e75..3129a4e 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java +++ b/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java @@ -3,8 +3,10 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import mate.academy.springbootwebgreqit.dto.cartitem.CartItemRequestDto; +import mate.academy.springbootwebgreqit.dto.shoppingcart.RequestUpdateQuantityDto; import mate.academy.springbootwebgreqit.dto.shoppingcart.ShoppingCartDto; import mate.academy.springbootwebgreqit.service.ShoppingCartService; import org.springframework.security.core.Authentication; @@ -15,7 +17,6 @@ import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -30,8 +31,8 @@ public class ShoppingCartController { @ApiResponse(responseCode = "403", description = "Access denied") }) @GetMapping - public ShoppingCartDto getShoppingCartForCurrentUser(Authentication authentication) { - return shoppingCartService.getShoppingCartForCurrentUser(authentication); + public ShoppingCartDto getShoppingCartForCurrentUser(Authentication authentication, Long shoppingCartId) { + return shoppingCartService.getShoppingCartForCurrentUser(authentication, shoppingCartId); } @Operation(summary = "Add book to shopping cart") @@ -41,7 +42,7 @@ public ShoppingCartDto getShoppingCartForCurrentUser(Authentication authenticati }) @PostMapping public ShoppingCartDto addBookToShoppingCart(@RequestBody CartItemRequestDto cartItem, - Authentication authentication) { + @Valid Authentication authentication) { return shoppingCartService.addBookToShoppingCart(cartItem, authentication); } @@ -52,7 +53,7 @@ public ShoppingCartDto addBookToShoppingCart(@RequestBody CartItemRequestDto car }) @PutMapping("/items/{cartItemId}") public ShoppingCartDto updateCartItemQuantity(@PathVariable Long cartItemId, - @RequestParam int quantity, + @RequestBody RequestUpdateQuantityDto quantity, Authentication authentication) { return shoppingCartService.updateCartItemQuantity(cartItemId, quantity, authentication); } diff --git a/src/main/java/mate/academy/springbootwebgreqit/dto/shoppingcart/RequestUpdateQuantityDto.java b/src/main/java/mate/academy/springbootwebgreqit/dto/shoppingcart/RequestUpdateQuantityDto.java new file mode 100644 index 0000000..f8469ed --- /dev/null +++ b/src/main/java/mate/academy/springbootwebgreqit/dto/shoppingcart/RequestUpdateQuantityDto.java @@ -0,0 +1,8 @@ +package mate.academy.springbootwebgreqit.dto.shoppingcart; + +import lombok.Data; + +@Data +public class RequestUpdateQuantityDto { + private int quantity; +} diff --git a/src/main/java/mate/academy/springbootwebgreqit/model/User.java b/src/main/java/mate/academy/springbootwebgreqit/model/User.java index 78c9463..ce07cf5 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/model/User.java +++ b/src/main/java/mate/academy/springbootwebgreqit/model/User.java @@ -1,7 +1,6 @@ package mate.academy.springbootwebgreqit.model; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -11,7 +10,6 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinTable; import jakarta.persistence.ManyToMany; -import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import lombok.Getter; import lombok.Setter; @@ -46,10 +44,6 @@ public class User implements UserDetails { @Column(name = "is_deleted") private boolean isDeleted = false; - @JoinColumn(name = "shopping_cart_id", nullable = false) - @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) - private ShoppingCart shoppingCart; - @ManyToMany(fetch = FetchType.EAGER) @JoinTable( name = "users_roles", diff --git a/src/main/java/mate/academy/springbootwebgreqit/repository/ShoppingCartRepository.java b/src/main/java/mate/academy/springbootwebgreqit/repository/ShoppingCartRepository.java index 2c2f319..44a63a9 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/repository/ShoppingCartRepository.java +++ b/src/main/java/mate/academy/springbootwebgreqit/repository/ShoppingCartRepository.java @@ -1,12 +1,16 @@ package mate.academy.springbootwebgreqit.repository; +import mate.academy.springbootwebgreqit.model.CartItem; import mate.academy.springbootwebgreqit.model.ShoppingCart; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; +import java.util.Set; public interface ShoppingCartRepository extends JpaRepository { @EntityGraph(attributePaths = {"cartItems", "cartItems.book"}) Optional findByUserId(Long userId); + + Optional findByCartItems(Set cartItems); } diff --git a/src/main/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java b/src/main/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java index dd8b052..e4a8c0a 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java +++ b/src/main/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java @@ -1,17 +1,18 @@ package mate.academy.springbootwebgreqit.service; import mate.academy.springbootwebgreqit.dto.cartitem.CartItemRequestDto; +import mate.academy.springbootwebgreqit.dto.shoppingcart.RequestUpdateQuantityDto; import mate.academy.springbootwebgreqit.dto.shoppingcart.ShoppingCartDto; import mate.academy.springbootwebgreqit.model.ShoppingCart; import mate.academy.springbootwebgreqit.model.User; import org.springframework.security.core.Authentication; public interface ShoppingCartService { - ShoppingCartDto getShoppingCartForCurrentUser(Authentication authentication); + ShoppingCartDto getShoppingCartForCurrentUser(Authentication authentication, Long shoppingCartId); ShoppingCartDto addBookToShoppingCart(CartItemRequestDto cartItem, Authentication authentication); - ShoppingCartDto updateCartItemQuantity(Long cartItemId, int quantity, Authentication authentication); + ShoppingCartDto updateCartItemQuantity(Long cartItemId, RequestUpdateQuantityDto quantity, Authentication authentication); ShoppingCartDto removeCartItem(Long cartItemId, Authentication authentication); diff --git a/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java b/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java index 982ff82..11e5d76 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java +++ b/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java @@ -3,6 +3,7 @@ import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import mate.academy.springbootwebgreqit.dto.cartitem.CartItemRequestDto; +import mate.academy.springbootwebgreqit.dto.shoppingcart.RequestUpdateQuantityDto; import mate.academy.springbootwebgreqit.dto.shoppingcart.ShoppingCartDto; import mate.academy.springbootwebgreqit.exception.EntityNotFoundException; import mate.academy.springbootwebgreqit.mapper.CartItemMapper; @@ -19,6 +20,7 @@ import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; import java.util.Optional; +import java.util.Set; @Service @RequiredArgsConstructor @@ -32,10 +34,11 @@ public class ShoppingCartServiceImpl implements ShoppingCartService { @Transactional @Override - public ShoppingCartDto getShoppingCartForCurrentUser(Authentication authentication) { + public ShoppingCartDto getShoppingCartForCurrentUser(Authentication authentication, Long shoppingCartId) { User authUser = userRepository.findByEmail(authentication.getName()) .orElseThrow(() -> new EntityNotFoundException("User not found with email: " + authentication.getName())); - ShoppingCart shoppingCart = authUser.getShoppingCart(); + ShoppingCart shoppingCart =shoppingCartRepository.findById(shoppingCartId) + .orElseThrow(() -> new EntityNotFoundException("Shopping cart not found for user with id: " + shoppingCartId)); if (shoppingCart == null) { throw new EntityNotFoundException("Shopping cart not found for user with email: " + authUser.getEmail()); @@ -95,14 +98,20 @@ public ShoppingCartDto addBookToShoppingCart( @Transactional @Override public ShoppingCartDto updateCartItemQuantity(Long cartItemId, - int quantity, Authentication authentication) { - if (quantity <= 0) { + RequestUpdateQuantityDto quantity, Authentication authentication) { + if (quantity.getQuantity() <= 0) { throw new EntityNotFoundException("Quantity must be a positive integer"); } User user = userRepository.findByEmail(authentication.getName()) .orElseThrow(() -> new EntityNotFoundException("User not found with email: " + authentication.getName())); - ShoppingCart shoppingCart = user.getShoppingCart(); + CartItem cartItemToFindShoppingCart = cartItemRepository.findById(cartItemId) + .orElseThrow(() -> new EntityNotFoundException("Cart item not found with id: " + cartItemId)); + + ShoppingCart shoppingCart = shoppingCartRepository + .findByCartItems(Set.of(cartItemToFindShoppingCart)) + .orElseThrow(() -> new EntityNotFoundException("Shopping cart " + + "not found by cart item with id: " + cartItemToFindShoppingCart)); if (shoppingCart == null) { throw new EntityNotFoundException("Shopping cart not found for user with ID " + user.getId()); @@ -113,7 +122,7 @@ public ShoppingCartDto updateCartItemQuantity(Long cartItemId, .orElseThrow(() -> new EntityNotFoundException("CartItem with ID " + cartItemId + " not found in the user's shopping cart")); - cartItem.setQuantity(quantity); + cartItem.setQuantity(quantity.getQuantity()); Hibernate.initialize(shoppingCart.getCartItems()); Hibernate.initialize(user.getRoles()); diff --git a/src/main/java/mate/academy/springbootwebgreqit/service/impl/UserServiceImpl.java b/src/main/java/mate/academy/springbootwebgreqit/service/impl/UserServiceImpl.java index 6ae5234..41312a4 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/service/impl/UserServiceImpl.java +++ b/src/main/java/mate/academy/springbootwebgreqit/service/impl/UserServiceImpl.java @@ -48,7 +48,7 @@ public UserResponseDto register(UserRegistrationRequestDto requestDto) { user.setRoles(roles); userRepository.save(user); ShoppingCart shoppingCart = shoppingCartService.createShoppingCart(user); - user.setShoppingCart(shoppingCart); + shoppingCart.setUser(user); return userMapper.toDto(user); } } diff --git a/src/main/resources/db/changelog/changes/02-create-table-users-roles-shoppingcarts.yaml b/src/main/resources/db/changelog/changes/02-create-table-users-roles-shoppingcarts.yaml index 0d1d600..9c3c24f 100644 --- a/src/main/resources/db/changelog/changes/02-create-table-users-roles-shoppingcarts.yaml +++ b/src/main/resources/db/changelog/changes/02-create-table-users-roles-shoppingcarts.yaml @@ -41,9 +41,6 @@ databaseChangeLog: name: is_deleted type: boolean defaultValue: false - - column: - name: shopping_cart_id - type: bigint - createTable: tableName: roles diff --git a/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java b/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java index 5f1b8c2..0e4d262 100644 --- a/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java +++ b/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java @@ -1,6 +1,7 @@ package mate.academy.springbootwebgreqit.service; import mate.academy.springbootwebgreqit.dto.cartitem.CartItemRequestDto; +import mate.academy.springbootwebgreqit.dto.shoppingcart.RequestUpdateQuantityDto; import mate.academy.springbootwebgreqit.dto.shoppingcart.ShoppingCartDto; import mate.academy.springbootwebgreqit.exception.EntityNotFoundException; import mate.academy.springbootwebgreqit.mapper.CartItemMapper; @@ -53,6 +54,7 @@ class ShoppingCartServiceTest { private Book book; private CartItem cartItem; private Authentication authentication; + private RequestUpdateQuantityDto requestUpdateQuantityDto; @BeforeEach void setUp() { @@ -80,16 +82,18 @@ void setUp() { cartItem.setShoppingCart(shoppingCart); authentication = mock(Authentication.class); + + requestUpdateQuantityDto = new RequestUpdateQuantityDto(); + requestUpdateQuantityDto.setQuantity(3); } @Test void getShoppingCartForCurrentUser_ShouldReturnShoppingCartDto() { - user.setShoppingCart(shoppingCart); when(authentication.getName()).thenReturn(user.getEmail()); when(userRepository.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); - ShoppingCartDto result = shoppingCartService.getShoppingCartForCurrentUser(authentication); + ShoppingCartDto result = shoppingCartService.getShoppingCartForCurrentUser(authentication, shoppingCart.getId()); assertNotNull(result); verify(userRepository).findByEmail(user.getEmail()); @@ -102,7 +106,7 @@ void getShoppingCartForCurrentUser_ShouldThrowEntityNotFoundException_WhenShoppi when(authentication.getName()).thenReturn(user.getEmail()); assertThrows(EntityNotFoundException.class, - () -> shoppingCartService.getShoppingCartForCurrentUser(authentication)); + () -> shoppingCartService.getShoppingCartForCurrentUser(authentication, shoppingCart.getId())); } @Test @@ -127,7 +131,6 @@ void addBookToShoppingCart_ShouldAddNewBookAndReturnShoppingCartDto() { @Test void updateCartItemQuantity_ShouldUpdateQuantityAndReturnShoppingCartDto() { - user.setShoppingCart(shoppingCart); shoppingCart.getCartItems().add(cartItem); when(userRepository.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); when(cartItemRepository.save(cartItem)).thenReturn(cartItem); @@ -135,7 +138,7 @@ void updateCartItemQuantity_ShouldUpdateQuantityAndReturnShoppingCartDto() { when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); when(authentication.getName()).thenReturn(user.getEmail()); - ShoppingCartDto result = shoppingCartService.updateCartItemQuantity(cartItem.getId(), 3, authentication); + ShoppingCartDto result = shoppingCartService.updateCartItemQuantity(cartItem.getId(), requestUpdateQuantityDto, authentication); assertNotNull(result); assertEquals(3, cartItem.getQuantity()); From 43c7fe395e794b67bef8aaef9ff5e8bac4222174 Mon Sep 17 00:00:00 2001 From: greqit Date: Sat, 9 Nov 2024 13:23:30 +0100 Subject: [PATCH 15/21] tests refactor --- .../controller/ShoppingCartController.java | 5 +-- .../ShoppingCartControllerTest.java | 34 +++++++++++-------- .../service/ShoppingCartServiceTest.java | 8 +++-- .../data-sql/clear-tables-for-sh.sql | 3 -- src/test/resources/data-sql/create-users.sql | 3 -- 5 files changed, 29 insertions(+), 24 deletions(-) diff --git a/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java b/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java index 3129a4e..244d097 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java +++ b/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java @@ -17,6 +17,7 @@ import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -30,8 +31,8 @@ public class ShoppingCartController { @ApiResponse(responseCode = "200", description = "Shopping cart retrieved successfully"), @ApiResponse(responseCode = "403", description = "Access denied") }) - @GetMapping - public ShoppingCartDto getShoppingCartForCurrentUser(Authentication authentication, Long shoppingCartId) { + @GetMapping("/{shoppingCartId}") + public ShoppingCartDto getShoppingCartForCurrentUser(Authentication authentication,@PathVariable Long shoppingCartId) { return shoppingCartService.getShoppingCartForCurrentUser(authentication, shoppingCartId); } diff --git a/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java b/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java index a04494c..2c5ebf3 100644 --- a/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java +++ b/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import mate.academy.springbootwebgreqit.dto.cartitem.CartItemRequestDto; +import mate.academy.springbootwebgreqit.dto.shoppingcart.RequestUpdateQuantityDto; import mate.academy.springbootwebgreqit.security.CustomUserDetailService; import mate.academy.springbootwebgreqit.service.ShoppingCartService; import org.junit.jupiter.api.BeforeAll; @@ -40,16 +41,18 @@ static void beforeAll(@Autowired WebApplicationContext webApplicationContext) { .build(); } + + @Test @Sql(scripts = {"/data-sql/create-books.sql", "/data-sql/create-users.sql", "/data-sql/create-cart-item.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) @WithMockUser(username = "john.doe@example.com", roles = {"USER"}) - @Test @DisplayName("Get shopping cart for the current user") void getShoppingCart_ShouldReturnCartForUser() throws Exception { - mockMvc.perform(get("/cart") + Long shoppingCartId = 1L; + mockMvc.perform(get("/cart/{shoppingCartId}", shoppingCartId) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.user.id").value(1L)) @@ -57,12 +60,12 @@ void getShoppingCart_ShouldReturnCartForUser() throws Exception { .andReturn(); } + @Test @Sql(scripts = {"/data-sql/create-books.sql", "/data-sql/create-users.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) @WithMockUser(username = "john.doe@example.com", roles = {"USER"}) - @Test @DisplayName("Add book to shopping cart") void addBookToShoppingCart_ShouldReturnUpdatedCart() throws Exception { CartItemRequestDto cartItemRequestDto = new CartItemRequestDto(); @@ -82,33 +85,36 @@ void addBookToShoppingCart_ShouldReturnUpdatedCart() throws Exception { .andReturn(); } - @Sql(scripts = {"/data-sql/create-books.sql", "/data-sql/create-users.sql", - "/data-sql/create-cart-item.sql"}, + + @Test + @Sql(scripts = {"/data-sql/create-books.sql", + "/data-sql/create-users.sql", "/data-sql/create-cart-item.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) - @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = - Sql.ExecutionPhase.AFTER_TEST_METHOD) + @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) @WithMockUser(username = "john.doe@example.com", roles = {"USER"}) - @Test @DisplayName("Update cart item quantity") void updateCartItemQuantity_ShouldReturnUpdatedQuantity() throws Exception { Long cartItemId = 1L; - int newQuantity = 3; + RequestUpdateQuantityDto quantityDto = new RequestUpdateQuantityDto(); + quantityDto.setQuantity(3); + + String jsonRequest = objectMapper.writeValueAsString(quantityDto); mockMvc.perform(put("/cart/items/{cartItemId}", cartItemId) - .param("quantity", String.valueOf(newQuantity)) - .contentType(MediaType.APPLICATION_JSON)) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonRequest)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.cartItems[0].quantity").value(newQuantity)) + .andExpect(jsonPath("$.cartItems[0].quantity").value(quantityDto.getQuantity())) .andReturn(); } + @Test @Sql(scripts = {"/data-sql/create-books.sql", "/data-sql/create-users.sql", "/data-sql/create-cart-item.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) @WithMockUser(username = "john.doe@example.com", roles = {"USER"}) - @Test @DisplayName("Remove item from cart") void removeItemFromCart_ShouldReturnEmptyCart() throws Exception { Long cartItemId = 1L; @@ -117,4 +123,4 @@ void removeItemFromCart_ShouldReturnEmptyCart() throws Exception { .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()); } -} \ No newline at end of file +} diff --git a/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java b/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java index 0e4d262..3856ac6 100644 --- a/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java +++ b/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java @@ -24,6 +24,7 @@ import org.springframework.security.core.Authentication; import java.util.Optional; +import java.util.Set; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @@ -92,6 +93,7 @@ void getShoppingCartForCurrentUser_ShouldReturnShoppingCartDto() { when(authentication.getName()).thenReturn(user.getEmail()); when(userRepository.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); + when(shoppingCartRepository.findById(shoppingCart.getId())).thenReturn(Optional.of(shoppingCart)); ShoppingCartDto result = shoppingCartService.getShoppingCartForCurrentUser(authentication, shoppingCart.getId()); @@ -137,8 +139,10 @@ void updateCartItemQuantity_ShouldUpdateQuantityAndReturnShoppingCartDto() { when(shoppingCartRepository.save(shoppingCart)).thenReturn(shoppingCart); when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); when(authentication.getName()).thenReturn(user.getEmail()); + when(cartItemRepository.findById(cartItem.getId())).thenReturn(Optional.of(cartItem)); + when(shoppingCartRepository.findByCartItems(Set.of(cartItem))).thenReturn(Optional.of(shoppingCart)); - ShoppingCartDto result = shoppingCartService.updateCartItemQuantity(cartItem.getId(), requestUpdateQuantityDto, authentication); + ShoppingCartDto result = shoppingCartService.updateCartItemQuantity(cartItem.getId(), requestUpdateQuantityDto, authentication); assertNotNull(result); assertEquals(3, cartItem.getQuantity()); @@ -172,4 +176,4 @@ void createShoppingCart_ShouldCreateAndReturnShoppingCart() { assertEquals(user, result.getUser()); verify(shoppingCartRepository).save(result); } -} \ No newline at end of file +} diff --git a/src/test/resources/data-sql/clear-tables-for-sh.sql b/src/test/resources/data-sql/clear-tables-for-sh.sql index 69f52e0..baad394 100644 --- a/src/test/resources/data-sql/clear-tables-for-sh.sql +++ b/src/test/resources/data-sql/clear-tables-for-sh.sql @@ -1,6 +1,3 @@ --- Set shopping_cart_id to NULL in users table before deleting from shopping_carts -UPDATE users SET shopping_cart_id = NULL WHERE shopping_cart_id = 1; - -- Delete from cart_items DELETE FROM cart_items; diff --git a/src/test/resources/data-sql/create-users.sql b/src/test/resources/data-sql/create-users.sql index 9d70f23..a34cfee 100644 --- a/src/test/resources/data-sql/create-users.sql +++ b/src/test/resources/data-sql/create-users.sql @@ -4,6 +4,3 @@ VALUES (1, 'John', 'Doe', 'john.doe@example.com', 'password123', '123 Main St, S -- Insert shopping cart INSERT INTO shopping_carts (id, user_id) VALUES (1, 1); - --- Update user's shopping_cart_id -UPDATE users SET shopping_cart_id = 1 WHERE id = 1; \ No newline at end of file From f546164bfc94631c973ce58287b3b71be654f941 Mon Sep 17 00:00:00 2001 From: greqit Date: Sat, 9 Nov 2024 13:33:24 +0100 Subject: [PATCH 16/21] refactor code for checkstyle --- .../controller/AuthenticationController.java | 2 +- .../controller/BookController.java | 3 ++- .../controller/CategoryController.java | 3 ++- .../controller/ShoppingCartController.java | 6 ++--- .../category/CreateCategoryRequestDto.java | 1 - .../service/ShoppingCartService.java | 13 +++++++--- .../service/impl/ShoppingCartServiceImpl.java | 26 ++++++++++++------- 7 files changed, 34 insertions(+), 20 deletions(-) diff --git a/src/main/java/mate/academy/springbootwebgreqit/controller/AuthenticationController.java b/src/main/java/mate/academy/springbootwebgreqit/controller/AuthenticationController.java index a37ae35..b523b46 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/controller/AuthenticationController.java +++ b/src/main/java/mate/academy/springbootwebgreqit/controller/AuthenticationController.java @@ -42,4 +42,4 @@ public UserResponseDto register(@RequestBody @Valid UserRegistrationRequestDto r public UserLoginResponseDto login(@RequestBody @Valid UserLoginRequestDto response) { return authenticationService.authenticate(response); } -} \ No newline at end of file +} diff --git a/src/main/java/mate/academy/springbootwebgreqit/controller/BookController.java b/src/main/java/mate/academy/springbootwebgreqit/controller/BookController.java index 7fc5983..b859ffa 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/controller/BookController.java +++ b/src/main/java/mate/academy/springbootwebgreqit/controller/BookController.java @@ -67,7 +67,8 @@ public BookDto createBook(@Valid @RequestBody CreateBookRequestDto requestDto) { @ApiResponse(responseCode = "404", description = "Book not found") }) @PutMapping("/{id}") - public BookDto updateBook(@PathVariable Long id, @RequestBody @Valid UpdateBookRequestDto updateBookRequestDto) { + public BookDto updateBook(@PathVariable Long id, + @RequestBody @Valid UpdateBookRequestDto updateBookRequestDto) { return bookService.update(updateBookRequestDto); } diff --git a/src/main/java/mate/academy/springbootwebgreqit/controller/CategoryController.java b/src/main/java/mate/academy/springbootwebgreqit/controller/CategoryController.java index 1f11500..408ce00 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/controller/CategoryController.java +++ b/src/main/java/mate/academy/springbootwebgreqit/controller/CategoryController.java @@ -67,7 +67,8 @@ public CategoryDto getCategoryById(@Valid @PathVariable Long id) { @ApiResponse(responseCode = "404", description = "Category not found") }) @PutMapping("/{id}") - public CategoryDto updateCategory(@PathVariable Long id, @RequestBody @Valid UpdateCategoryRequestDto categoryDto) { + public CategoryDto updateCategory(@PathVariable Long id, + @RequestBody @Valid UpdateCategoryRequestDto categoryDto) { return categoryService.update(id, categoryDto); } diff --git a/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java b/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java index 244d097..49a8fe0 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java +++ b/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java @@ -17,7 +17,6 @@ import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -32,13 +31,14 @@ public class ShoppingCartController { @ApiResponse(responseCode = "403", description = "Access denied") }) @GetMapping("/{shoppingCartId}") - public ShoppingCartDto getShoppingCartForCurrentUser(Authentication authentication,@PathVariable Long shoppingCartId) { + public ShoppingCartDto getShoppingCartForCurrentUser(Authentication authentication, + @PathVariable Long shoppingCartId) { return shoppingCartService.getShoppingCartForCurrentUser(authentication, shoppingCartId); } @Operation(summary = "Add book to shopping cart") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Book added to shopping cart successfully"), + @ApiResponse(responseCode = "200", description = "Book added to shopping cart"), @ApiResponse(responseCode = "400", description = "Invalid input data") }) @PostMapping diff --git a/src/main/java/mate/academy/springbootwebgreqit/dto/category/CreateCategoryRequestDto.java b/src/main/java/mate/academy/springbootwebgreqit/dto/category/CreateCategoryRequestDto.java index 69f177b..43b6dd9 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/dto/category/CreateCategoryRequestDto.java +++ b/src/main/java/mate/academy/springbootwebgreqit/dto/category/CreateCategoryRequestDto.java @@ -1,7 +1,6 @@ package mate.academy.springbootwebgreqit.dto.category; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import lombok.Data; @Data diff --git a/src/main/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java b/src/main/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java index e4a8c0a..67824e5 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java +++ b/src/main/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java @@ -8,13 +8,18 @@ import org.springframework.security.core.Authentication; public interface ShoppingCartService { - ShoppingCartDto getShoppingCartForCurrentUser(Authentication authentication, Long shoppingCartId); + ShoppingCartDto getShoppingCartForCurrentUser(Authentication authentication, + Long shoppingCartId); - ShoppingCartDto addBookToShoppingCart(CartItemRequestDto cartItem, Authentication authentication); + ShoppingCartDto addBookToShoppingCart(CartItemRequestDto cartItem, + Authentication authentication); - ShoppingCartDto updateCartItemQuantity(Long cartItemId, RequestUpdateQuantityDto quantity, Authentication authentication); + ShoppingCartDto updateCartItemQuantity(Long cartItemId, + RequestUpdateQuantityDto quantity, + Authentication authentication); - ShoppingCartDto removeCartItem(Long cartItemId, Authentication authentication); + ShoppingCartDto removeCartItem(Long cartItemId, + Authentication authentication); ShoppingCart createShoppingCart(User user); } diff --git a/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java b/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java index 11e5d76..6af41a7 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java +++ b/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java @@ -34,11 +34,14 @@ public class ShoppingCartServiceImpl implements ShoppingCartService { @Transactional @Override - public ShoppingCartDto getShoppingCartForCurrentUser(Authentication authentication, Long shoppingCartId) { + public ShoppingCartDto getShoppingCartForCurrentUser(Authentication authentication, + Long shoppingCartId) { User authUser = userRepository.findByEmail(authentication.getName()) - .orElseThrow(() -> new EntityNotFoundException("User not found with email: " + authentication.getName())); - ShoppingCart shoppingCart =shoppingCartRepository.findById(shoppingCartId) - .orElseThrow(() -> new EntityNotFoundException("Shopping cart not found for user with id: " + shoppingCartId)); + .orElseThrow(() -> new EntityNotFoundException("User not found with email: " + + authentication.getName())); + ShoppingCart shoppingCart = shoppingCartRepository.findById(shoppingCartId) + .orElseThrow(() -> new EntityNotFoundException("Shopping cart not " + + "found for user with id: " + shoppingCartId)); if (shoppingCart == null) { throw new EntityNotFoundException("Shopping cart not found for user with email: " + authUser.getEmail()); @@ -58,7 +61,8 @@ public ShoppingCartDto addBookToShoppingCart( CartItemRequestDto cartItemDto, Authentication authentication) { User user = userRepository.findByEmail(authentication.getName()) - .orElseThrow(() -> new EntityNotFoundException("User not found with email: " + authentication.getName())); + .orElseThrow(() -> new EntityNotFoundException("User not found with email: " + + authentication.getName())); ShoppingCart shoppingCart = shoppingCartRepository.findByUserId(user.getId()) .orElseThrow(() -> new EntityNotFoundException("Shopping cart not found")); @@ -98,15 +102,18 @@ public ShoppingCartDto addBookToShoppingCart( @Transactional @Override public ShoppingCartDto updateCartItemQuantity(Long cartItemId, - RequestUpdateQuantityDto quantity, Authentication authentication) { + RequestUpdateQuantityDto quantity, + Authentication authentication) { if (quantity.getQuantity() <= 0) { throw new EntityNotFoundException("Quantity must be a positive integer"); } User user = userRepository.findByEmail(authentication.getName()) - .orElseThrow(() -> new EntityNotFoundException("User not found with email: " + authentication.getName())); + .orElseThrow(() -> new EntityNotFoundException("User not found with email: " + + authentication.getName())); CartItem cartItemToFindShoppingCart = cartItemRepository.findById(cartItemId) - .orElseThrow(() -> new EntityNotFoundException("Cart item not found with id: " + cartItemId)); + .orElseThrow(() -> new EntityNotFoundException("Cart item not found with id: " + + cartItemId)); ShoppingCart shoppingCart = shoppingCartRepository .findByCartItems(Set.of(cartItemToFindShoppingCart)) @@ -141,7 +148,8 @@ public ShoppingCartDto updateCartItemQuantity(Long cartItemId, @Override public ShoppingCartDto removeCartItem(Long cartItemId, Authentication authentication) { User user = userRepository.findByEmail(authentication.getName()) - .orElseThrow(() -> new EntityNotFoundException("User not found with email: " + authentication.getName())); + .orElseThrow(() -> new EntityNotFoundException("User not found with email: " + + authentication.getName())); ShoppingCart shoppingCart = shoppingCartRepository.findByUserId(user.getId()) .orElseThrow(() -> new EntityNotFoundException("Shopping cart not found")); From c401c4ae1bca13d17a33be6cae4198f7ac750915 Mon Sep 17 00:00:00 2001 From: greqit Date: Mon, 11 Nov 2024 19:13:04 +0100 Subject: [PATCH 17/21] refactor tests and service --- pom.xml | 25 ++++++ .../controller/ShoppingCartController.java | 6 +- ...uantityDto.java => UpdateCartItemDto.java} | 4 +- .../service/ShoppingCartService.java | 4 +- .../service/impl/ShoppingCartServiceImpl.java | 56 ++------------ .../ShoppingCartControllerTest.java | 4 +- .../service/ShoppingCartServiceTest.java | 76 ++++++++++--------- 7 files changed, 83 insertions(+), 92 deletions(-) rename src/main/java/mate/academy/springbootwebgreqit/dto/shoppingcart/{RequestUpdateQuantityDto.java => UpdateCartItemDto.java} (55%) diff --git a/pom.xml b/pom.xml index da192bd..895a930 100644 --- a/pom.xml +++ b/pom.xml @@ -39,6 +39,10 @@ 8.0.1.Final 3.3.2 0.11.5 + 3.1.1 + + https://raw.githubusercontent.com/mate-academy/style-guides/master/java/checkstyle.xml + @@ -236,6 +240,27 @@ + + org.apache.maven.plugins + maven-checkstyle-plugin + ${maven.checkstyle.plugin.version} + + + compile + + check + + + + + **/src/main/java/** + **/target/generated-sources/** + ${maven.checkstyle.plugin.configLocation} + true + true + false + + diff --git a/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java b/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java index 49a8fe0..93da0d9 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java +++ b/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java @@ -6,7 +6,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import mate.academy.springbootwebgreqit.dto.cartitem.CartItemRequestDto; -import mate.academy.springbootwebgreqit.dto.shoppingcart.RequestUpdateQuantityDto; +import mate.academy.springbootwebgreqit.dto.shoppingcart.UpdateCartItemDto; import mate.academy.springbootwebgreqit.dto.shoppingcart.ShoppingCartDto; import mate.academy.springbootwebgreqit.service.ShoppingCartService; import org.springframework.security.core.Authentication; @@ -43,7 +43,7 @@ public ShoppingCartDto getShoppingCartForCurrentUser(Authentication authenticati }) @PostMapping public ShoppingCartDto addBookToShoppingCart(@RequestBody CartItemRequestDto cartItem, - @Valid Authentication authentication) { + Authentication authentication) { return shoppingCartService.addBookToShoppingCart(cartItem, authentication); } @@ -54,7 +54,7 @@ public ShoppingCartDto addBookToShoppingCart(@RequestBody CartItemRequestDto car }) @PutMapping("/items/{cartItemId}") public ShoppingCartDto updateCartItemQuantity(@PathVariable Long cartItemId, - @RequestBody RequestUpdateQuantityDto quantity, + @RequestBody @Valid UpdateCartItemDto quantity, Authentication authentication) { return shoppingCartService.updateCartItemQuantity(cartItemId, quantity, authentication); } diff --git a/src/main/java/mate/academy/springbootwebgreqit/dto/shoppingcart/RequestUpdateQuantityDto.java b/src/main/java/mate/academy/springbootwebgreqit/dto/shoppingcart/UpdateCartItemDto.java similarity index 55% rename from src/main/java/mate/academy/springbootwebgreqit/dto/shoppingcart/RequestUpdateQuantityDto.java rename to src/main/java/mate/academy/springbootwebgreqit/dto/shoppingcart/UpdateCartItemDto.java index f8469ed..2c05da3 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/dto/shoppingcart/RequestUpdateQuantityDto.java +++ b/src/main/java/mate/academy/springbootwebgreqit/dto/shoppingcart/UpdateCartItemDto.java @@ -1,8 +1,10 @@ package mate.academy.springbootwebgreqit.dto.shoppingcart; +import jakarta.validation.constraints.NotNull; import lombok.Data; @Data -public class RequestUpdateQuantityDto { +public class UpdateCartItemDto { + @NotNull private int quantity; } diff --git a/src/main/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java b/src/main/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java index 67824e5..b736829 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java +++ b/src/main/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java @@ -1,7 +1,7 @@ package mate.academy.springbootwebgreqit.service; import mate.academy.springbootwebgreqit.dto.cartitem.CartItemRequestDto; -import mate.academy.springbootwebgreqit.dto.shoppingcart.RequestUpdateQuantityDto; +import mate.academy.springbootwebgreqit.dto.shoppingcart.UpdateCartItemDto; import mate.academy.springbootwebgreqit.dto.shoppingcart.ShoppingCartDto; import mate.academy.springbootwebgreqit.model.ShoppingCart; import mate.academy.springbootwebgreqit.model.User; @@ -15,7 +15,7 @@ ShoppingCartDto addBookToShoppingCart(CartItemRequestDto cartItem, Authentication authentication); ShoppingCartDto updateCartItemQuantity(Long cartItemId, - RequestUpdateQuantityDto quantity, + UpdateCartItemDto quantity, Authentication authentication); ShoppingCartDto removeCartItem(Long cartItemId, diff --git a/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java b/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java index 6af41a7..3a9de94 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java +++ b/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java @@ -3,7 +3,7 @@ import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import mate.academy.springbootwebgreqit.dto.cartitem.CartItemRequestDto; -import mate.academy.springbootwebgreqit.dto.shoppingcart.RequestUpdateQuantityDto; +import mate.academy.springbootwebgreqit.dto.shoppingcart.UpdateCartItemDto; import mate.academy.springbootwebgreqit.dto.shoppingcart.ShoppingCartDto; import mate.academy.springbootwebgreqit.exception.EntityNotFoundException; import mate.academy.springbootwebgreqit.mapper.CartItemMapper; @@ -36,22 +36,14 @@ public class ShoppingCartServiceImpl implements ShoppingCartService { @Override public ShoppingCartDto getShoppingCartForCurrentUser(Authentication authentication, Long shoppingCartId) { - User authUser = userRepository.findByEmail(authentication.getName()) - .orElseThrow(() -> new EntityNotFoundException("User not found with email: " - + authentication.getName())); + User user = (User) authentication.getPrincipal(); ShoppingCart shoppingCart = shoppingCartRepository.findById(shoppingCartId) .orElseThrow(() -> new EntityNotFoundException("Shopping cart not " + "found for user with id: " + shoppingCartId)); if (shoppingCart == null) { throw new EntityNotFoundException("Shopping cart not found for user with email: " - + authUser.getEmail()); + + user.getEmail()); } - Hibernate.initialize(shoppingCart.getCartItems()); - Hibernate.initialize(authUser.getRoles()); - shoppingCart.getCartItems().forEach(cartItem -> - Hibernate.initialize(cartItem.getBook().getCategories())); - shoppingCart.getCartItems().forEach(cartItem -> - Hibernate.initialize(cartItem.getBook().getCartItems())); return shoppingCartMapper.toDto(shoppingCart); } @@ -60,9 +52,7 @@ public ShoppingCartDto getShoppingCartForCurrentUser(Authentication authenticati public ShoppingCartDto addBookToShoppingCart( CartItemRequestDto cartItemDto, Authentication authentication) { - User user = userRepository.findByEmail(authentication.getName()) - .orElseThrow(() -> new EntityNotFoundException("User not found with email: " - + authentication.getName())); + User user = (User) authentication.getPrincipal(); ShoppingCart shoppingCart = shoppingCartRepository.findByUserId(user.getId()) .orElseThrow(() -> new EntityNotFoundException("Shopping cart not found")); @@ -81,8 +71,6 @@ public ShoppingCartDto addBookToShoppingCart( newCartItem.setShoppingCart(shoppingCart); newCartItem.setBook(bookRepository.findById(cartItemDto.getBookId()) .orElseThrow(() -> new EntityNotFoundException("Book not found"))); - Hibernate.initialize(shoppingCart.getUser()); - Hibernate.initialize(shoppingCart.getUser().getRoles()); newCartItem.setQuantity(cartItemDto.getQuantity()); newCartItem.setShoppingCart(shoppingCart); shoppingCart.getCartItems().add(newCartItem); @@ -90,26 +78,18 @@ public ShoppingCartDto addBookToShoppingCart( ); ShoppingCart savedshoppingCart = shoppingCartRepository.save(shoppingCart); - Hibernate.initialize(savedshoppingCart.getCartItems()); - savedshoppingCart.getCartItems().forEach(cartItem -> - Hibernate.initialize(cartItem.getBook().getCategories())); - savedshoppingCart.getCartItems().forEach(cartItem -> - Hibernate.initialize(cartItem.getBook().getCartItems())); - return shoppingCartMapper.toDto(savedshoppingCart); } @Transactional @Override public ShoppingCartDto updateCartItemQuantity(Long cartItemId, - RequestUpdateQuantityDto quantity, + UpdateCartItemDto quantity, Authentication authentication) { if (quantity.getQuantity() <= 0) { throw new EntityNotFoundException("Quantity must be a positive integer"); } - User user = userRepository.findByEmail(authentication.getName()) - .orElseThrow(() -> new EntityNotFoundException("User not found with email: " - + authentication.getName())); + User user = (User) authentication.getPrincipal(); CartItem cartItemToFindShoppingCart = cartItemRepository.findById(cartItemId) .orElseThrow(() -> new EntityNotFoundException("Cart item not found with id: " @@ -119,10 +99,6 @@ public ShoppingCartDto updateCartItemQuantity(Long cartItemId, .findByCartItems(Set.of(cartItemToFindShoppingCart)) .orElseThrow(() -> new EntityNotFoundException("Shopping cart " + "not found by cart item with id: " + cartItemToFindShoppingCart)); - if (shoppingCart == null) { - throw new EntityNotFoundException("Shopping cart not found for user with ID " - + user.getId()); - } CartItem cartItem = shoppingCart.getCartItems().stream() .filter(item -> item.getId().equals(cartItemId)) .findFirst() @@ -130,14 +106,6 @@ public ShoppingCartDto updateCartItemQuantity(Long cartItemId, + cartItemId + " not found in the user's shopping cart")); cartItem.setQuantity(quantity.getQuantity()); - Hibernate.initialize(shoppingCart.getCartItems()); - Hibernate.initialize(user.getRoles()); - - shoppingCart.getCartItems().forEach(ci -> - Hibernate.initialize(cartItem.getBook().getCategories())); - shoppingCart.getCartItems().forEach(ci -> - Hibernate.initialize(cartItem.getBook().getCartItems())); - cartItemRepository.save(cartItem); ShoppingCart savedShoppingCart = shoppingCartRepository.save(shoppingCart); @@ -147,9 +115,7 @@ public ShoppingCartDto updateCartItemQuantity(Long cartItemId, @Transactional @Override public ShoppingCartDto removeCartItem(Long cartItemId, Authentication authentication) { - User user = userRepository.findByEmail(authentication.getName()) - .orElseThrow(() -> new EntityNotFoundException("User not found with email: " - + authentication.getName())); + User user = (User) authentication.getPrincipal(); ShoppingCart shoppingCart = shoppingCartRepository.findByUserId(user.getId()) .orElseThrow(() -> new EntityNotFoundException("Shopping cart not found")); @@ -162,14 +128,6 @@ public ShoppingCartDto removeCartItem(Long cartItemId, Authentication authentica shoppingCart.getCartItems().remove(cartItem); ShoppingCart savedshoppingCart = shoppingCartRepository.save(shoppingCart); - Hibernate.initialize(shoppingCart.getCartItems()); - Hibernate.initialize(user.getRoles()); - - shoppingCart.getCartItems().forEach(ci -> - Hibernate.initialize(cartItem.getBook().getCategories())); - shoppingCart.getCartItems().forEach(ci -> - Hibernate.initialize(cartItem.getBook().getCartItems())); - return shoppingCartMapper.toDto(savedshoppingCart); } diff --git a/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java b/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java index 2c5ebf3..e4c7d84 100644 --- a/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java +++ b/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import mate.academy.springbootwebgreqit.dto.cartitem.CartItemRequestDto; -import mate.academy.springbootwebgreqit.dto.shoppingcart.RequestUpdateQuantityDto; +import mate.academy.springbootwebgreqit.dto.shoppingcart.UpdateCartItemDto; import mate.academy.springbootwebgreqit.security.CustomUserDetailService; import mate.academy.springbootwebgreqit.service.ShoppingCartService; import org.junit.jupiter.api.BeforeAll; @@ -95,7 +95,7 @@ void addBookToShoppingCart_ShouldReturnUpdatedCart() throws Exception { @DisplayName("Update cart item quantity") void updateCartItemQuantity_ShouldReturnUpdatedQuantity() throws Exception { Long cartItemId = 1L; - RequestUpdateQuantityDto quantityDto = new RequestUpdateQuantityDto(); + UpdateCartItemDto quantityDto = new UpdateCartItemDto(); quantityDto.setQuantity(3); String jsonRequest = objectMapper.writeValueAsString(quantityDto); diff --git a/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java b/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java index 3856ac6..a12b5eb 100644 --- a/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java +++ b/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java @@ -1,7 +1,7 @@ package mate.academy.springbootwebgreqit.service; import mate.academy.springbootwebgreqit.dto.cartitem.CartItemRequestDto; -import mate.academy.springbootwebgreqit.dto.shoppingcart.RequestUpdateQuantityDto; +import mate.academy.springbootwebgreqit.dto.shoppingcart.UpdateCartItemDto; import mate.academy.springbootwebgreqit.dto.shoppingcart.ShoppingCartDto; import mate.academy.springbootwebgreqit.exception.EntityNotFoundException; import mate.academy.springbootwebgreqit.mapper.CartItemMapper; @@ -23,6 +23,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.core.Authentication; +import java.util.HashSet; import java.util.Optional; import java.util.Set; @@ -30,7 +31,7 @@ import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) -class ShoppingCartServiceTest { +class ShoppingCartServiceImplTest { @Mock private CartItemRepository cartItemRepository; @@ -55,7 +56,7 @@ class ShoppingCartServiceTest { private Book book; private CartItem cartItem; private Authentication authentication; - private RequestUpdateQuantityDto requestUpdateQuantityDto; + private UpdateCartItemDto requestUpdateQuantityDto; @BeforeEach void setUp() { @@ -69,6 +70,7 @@ void setUp() { shoppingCartDto = new ShoppingCartDto(); + cartItemRequestDto = new CartItemRequestDto(); cartItemRequestDto.setBookId(1L); cartItemRequestDto.setQuantity(2); @@ -81,31 +83,31 @@ void setUp() { cartItem.setBook(book); cartItem.setQuantity(2); cartItem.setShoppingCart(shoppingCart); + shoppingCart.getCartItems().add(cartItem); authentication = mock(Authentication.class); - requestUpdateQuantityDto = new RequestUpdateQuantityDto(); + requestUpdateQuantityDto = new UpdateCartItemDto(); requestUpdateQuantityDto.setQuantity(3); } @Test void getShoppingCartForCurrentUser_ShouldReturnShoppingCartDto() { - when(authentication.getName()).thenReturn(user.getEmail()); - when(userRepository.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); - when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); + when(authentication.getPrincipal()).thenReturn(user); when(shoppingCartRepository.findById(shoppingCart.getId())).thenReturn(Optional.of(shoppingCart)); + when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); ShoppingCartDto result = shoppingCartService.getShoppingCartForCurrentUser(authentication, shoppingCart.getId()); assertNotNull(result); - verify(userRepository).findByEmail(user.getEmail()); + verify(shoppingCartRepository).findById(shoppingCart.getId()); verify(shoppingCartMapper).toDto(shoppingCart); } @Test void getShoppingCartForCurrentUser_ShouldThrowEntityNotFoundException_WhenShoppingCartNotFound() { - when(userRepository.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); - when(authentication.getName()).thenReturn(user.getEmail()); + when(authentication.getPrincipal()).thenReturn(user); + when(shoppingCartRepository.findById(shoppingCart.getId())).thenReturn(Optional.empty()); assertThrows(EntityNotFoundException.class, () -> shoppingCartService.getShoppingCartForCurrentUser(authentication, shoppingCart.getId())); @@ -113,59 +115,63 @@ void getShoppingCartForCurrentUser_ShouldThrowEntityNotFoundException_WhenShoppi @Test void addBookToShoppingCart_ShouldAddNewBookAndReturnShoppingCartDto() { - when(userRepository.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); + when(authentication.getPrincipal()).thenReturn(user); when(shoppingCartRepository.findByUserId(user.getId())).thenReturn(Optional.of(shoppingCart)); when(bookRepository.findById(cartItemRequestDto.getBookId())).thenReturn(Optional.of(book)); when(cartItemMapper.toModel(cartItemRequestDto)).thenReturn(cartItem); when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); when(shoppingCartRepository.save(shoppingCart)).thenReturn(shoppingCart); - when(authentication.getName()).thenReturn(user.getEmail()); ShoppingCartDto result = shoppingCartService.addBookToShoppingCart(cartItemRequestDto, authentication); assertNotNull(result); verify(shoppingCartRepository).findByUserId(user.getId()); - verify(cartItemMapper).toModel(cartItemRequestDto); verify(bookRepository).findById(cartItemRequestDto.getBookId()); + verify(cartItemMapper).toModel(cartItemRequestDto); verify(shoppingCartRepository).save(shoppingCart); - assertEquals(1, shoppingCart.getCartItems().size()); + verify(shoppingCartMapper).toDto(shoppingCart); } @Test - void updateCartItemQuantity_ShouldUpdateQuantityAndReturnShoppingCartDto() { - shoppingCart.getCartItems().add(cartItem); - when(userRepository.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); - when(cartItemRepository.save(cartItem)).thenReturn(cartItem); - when(shoppingCartRepository.save(shoppingCart)).thenReturn(shoppingCart); - when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); - when(authentication.getName()).thenReturn(user.getEmail()); - when(cartItemRepository.findById(cartItem.getId())).thenReturn(Optional.of(cartItem)); - when(shoppingCartRepository.findByCartItems(Set.of(cartItem))).thenReturn(Optional.of(shoppingCart)); - - ShoppingCartDto result = shoppingCartService.updateCartItemQuantity(cartItem.getId(), requestUpdateQuantityDto, authentication); +void updateCartItemQuantity_ShouldUpdateQuantityAndReturnShoppingCartDto() { + int newQuantity = requestUpdateQuantityDto.getQuantity(); + when(authentication.getPrincipal()).thenReturn(user); + when(userRepository.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); + when(cartItemRepository.findById(cartItem.getId())).thenReturn(Optional.of(cartItem)); + when(shoppingCartRepository.findByUserId(user.getId())).thenReturn(Optional.of(shoppingCart)); + when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); + when(cartItemRepository.save(cartItem)).thenReturn(cartItem); + + ShoppingCartDto result = shoppingCartService.updateCartItemQuantity(cartItem.getId(), requestUpdateQuantityDto, authentication); + + assertNotNull(result); + assertEquals(newQuantity, cartItem.getQuantity(), "Quantity should be updated in the cart item"); + verify(cartItemRepository).save(cartItem); + verify(shoppingCartRepository).save(shoppingCart); + verify(shoppingCartMapper).toDto(shoppingCart); + verifyNoMoreInteractions(cartItemRepository, shoppingCartRepository, shoppingCartMapper); +} - assertNotNull(result); - assertEquals(3, cartItem.getQuantity()); - verify(cartItemRepository).save(cartItem); - verify(shoppingCartMapper).toDto(shoppingCart); - } @Test void removeCartItem_ShouldRemoveItemAndReturnShoppingCartDto() { - shoppingCart.getCartItems().add(cartItem); - when(userRepository.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); + shoppingCart.setCartItems(new HashSet<>(Set.of(cartItem))); // Π”ΠΎΠ΄Π°Ρ”ΠΌΠΎ cartItem Π΄ΠΎ shoppingCart + + when(authentication.getPrincipal()).thenReturn(user); when(shoppingCartRepository.findByUserId(user.getId())).thenReturn(Optional.of(shoppingCart)); when(shoppingCartRepository.save(shoppingCart)).thenReturn(shoppingCart); when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); - when(authentication.getName()).thenReturn(user.getEmail()); ShoppingCartDto result = shoppingCartService.removeCartItem(cartItem.getId(), authentication); assertNotNull(result); - assertTrue(shoppingCart.getCartItems().isEmpty()); + assertFalse(shoppingCart.getCartItems().contains(cartItem), "Cart item should be removed from the cart"); + verify(shoppingCartRepository).findByUserId(user.getId()); verify(shoppingCartRepository).save(shoppingCart); + verify(shoppingCartMapper).toDto(shoppingCart); } + @Test void createShoppingCart_ShouldCreateAndReturnShoppingCart() { when(shoppingCartRepository.save(any(ShoppingCart.class))).thenReturn(shoppingCart); @@ -176,4 +182,4 @@ void createShoppingCart_ShouldCreateAndReturnShoppingCart() { assertEquals(user, result.getUser()); verify(shoppingCartRepository).save(result); } -} +} \ No newline at end of file From 29503563cce65d2f58dc86213f5f6eb451d5a6b8 Mon Sep 17 00:00:00 2001 From: greqit Date: Tue, 12 Nov 2024 16:48:01 +0100 Subject: [PATCH 18/21] fix --- .../controller/ShoppingCartController.java | 2 +- .../dto/shoppingcart/UpdateCartItemDto.java | 4 +- .../service/ShoppingCartService.java | 3 +- .../service/impl/ShoppingCartServiceImpl.java | 13 ++-- .../ShoppingCartControllerTest.java | 22 ++---- .../service/ShoppingCartServiceTest.java | 73 +++++++++++-------- .../data-sql/clear-tables-for-sh.sql | 2 +- src/test/resources/data-sql/clear-tables.sql | 2 +- src/test/resources/data-sql/create-book.sql | 2 +- src/test/resources/data-sql/create-books.sql | 2 +- .../resources/data-sql/create-cart-item.sql | 2 +- .../resources/data-sql/create-category.sql | 2 +- 12 files changed, 65 insertions(+), 64 deletions(-) diff --git a/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java b/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java index 93da0d9..ada144f 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java +++ b/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java @@ -56,7 +56,7 @@ public ShoppingCartDto addBookToShoppingCart(@RequestBody CartItemRequestDto car public ShoppingCartDto updateCartItemQuantity(@PathVariable Long cartItemId, @RequestBody @Valid UpdateCartItemDto quantity, Authentication authentication) { - return shoppingCartService.updateCartItemQuantity(cartItemId, quantity, authentication); + return shoppingCartService.updateCartItemQuantity(cartItemId, quantity); } @Operation(summary = "Remove cart item") diff --git a/src/main/java/mate/academy/springbootwebgreqit/dto/shoppingcart/UpdateCartItemDto.java b/src/main/java/mate/academy/springbootwebgreqit/dto/shoppingcart/UpdateCartItemDto.java index 2c05da3..b4ecbfd 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/dto/shoppingcart/UpdateCartItemDto.java +++ b/src/main/java/mate/academy/springbootwebgreqit/dto/shoppingcart/UpdateCartItemDto.java @@ -1,10 +1,10 @@ package mate.academy.springbootwebgreqit.dto.shoppingcart; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; import lombok.Data; @Data public class UpdateCartItemDto { - @NotNull + @Positive private int quantity; } diff --git a/src/main/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java b/src/main/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java index b736829..3345ae4 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java +++ b/src/main/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java @@ -15,8 +15,7 @@ ShoppingCartDto addBookToShoppingCart(CartItemRequestDto cartItem, Authentication authentication); ShoppingCartDto updateCartItemQuantity(Long cartItemId, - UpdateCartItemDto quantity, - Authentication authentication); + UpdateCartItemDto quantity); ShoppingCartDto removeCartItem(Long cartItemId, Authentication authentication); diff --git a/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java b/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java index 3a9de94..8668f95 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java +++ b/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java @@ -16,7 +16,6 @@ import mate.academy.springbootwebgreqit.repository.ShoppingCartRepository; import mate.academy.springbootwebgreqit.repository.UserRepository; import mate.academy.springbootwebgreqit.service.ShoppingCartService; -import org.hibernate.Hibernate; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; import java.util.Optional; @@ -77,19 +76,17 @@ public ShoppingCartDto addBookToShoppingCart( } ); - ShoppingCart savedshoppingCart = shoppingCartRepository.save(shoppingCart); - return shoppingCartMapper.toDto(savedshoppingCart); + shoppingCartRepository.save(shoppingCart); + return shoppingCartMapper.toDto(shoppingCart); } @Transactional @Override public ShoppingCartDto updateCartItemQuantity(Long cartItemId, - UpdateCartItemDto quantity, - Authentication authentication) { + UpdateCartItemDto quantity) { if (quantity.getQuantity() <= 0) { throw new EntityNotFoundException("Quantity must be a positive integer"); } - User user = (User) authentication.getPrincipal(); CartItem cartItemToFindShoppingCart = cartItemRepository.findById(cartItemId) .orElseThrow(() -> new EntityNotFoundException("Cart item not found with id: " @@ -107,9 +104,9 @@ public ShoppingCartDto updateCartItemQuantity(Long cartItemId, cartItem.setQuantity(quantity.getQuantity()); cartItemRepository.save(cartItem); - ShoppingCart savedShoppingCart = shoppingCartRepository.save(shoppingCart); + shoppingCartRepository.save(shoppingCart); - return shoppingCartMapper.toDto(savedShoppingCart); + return shoppingCartMapper.toDto(shoppingCart); } @Transactional diff --git a/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java b/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java index e4c7d84..0776c7e 100644 --- a/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java +++ b/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java @@ -21,6 +21,10 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +@Sql(scripts = {"/data-sql/create-books.sql", "/data-sql/create-users.sql"}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +@Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = + Sql.ExecutionPhase.AFTER_TEST_METHOD) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class ShoppingCartControllerTest { protected static MockMvc mockMvc; @@ -43,11 +47,8 @@ static void beforeAll(@Autowired WebApplicationContext webApplicationContext) { @Test - @Sql(scripts = {"/data-sql/create-books.sql", - "/data-sql/create-users.sql", "/data-sql/create-cart-item.sql"}, + @Sql(scripts = {"/data-sql/create-cart-item.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) - @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = - Sql.ExecutionPhase.AFTER_TEST_METHOD) @WithMockUser(username = "john.doe@example.com", roles = {"USER"}) @DisplayName("Get shopping cart for the current user") void getShoppingCart_ShouldReturnCartForUser() throws Exception { @@ -61,10 +62,6 @@ void getShoppingCart_ShouldReturnCartForUser() throws Exception { } @Test - @Sql(scripts = {"/data-sql/create-books.sql", "/data-sql/create-users.sql"}, - executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) - @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = - Sql.ExecutionPhase.AFTER_TEST_METHOD) @WithMockUser(username = "john.doe@example.com", roles = {"USER"}) @DisplayName("Add book to shopping cart") void addBookToShoppingCart_ShouldReturnUpdatedCart() throws Exception { @@ -87,10 +84,8 @@ void addBookToShoppingCart_ShouldReturnUpdatedCart() throws Exception { @Test - @Sql(scripts = {"/data-sql/create-books.sql", - "/data-sql/create-users.sql", "/data-sql/create-cart-item.sql"}, + @Sql(scripts = {"/data-sql/create-cart-item.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) - @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) @WithMockUser(username = "john.doe@example.com", roles = {"USER"}) @DisplayName("Update cart item quantity") void updateCartItemQuantity_ShouldReturnUpdatedQuantity() throws Exception { @@ -109,11 +104,8 @@ void updateCartItemQuantity_ShouldReturnUpdatedQuantity() throws Exception { } @Test - @Sql(scripts = {"/data-sql/create-books.sql", "/data-sql/create-users.sql", - "/data-sql/create-cart-item.sql"}, + @Sql(scripts = {"/data-sql/create-cart-item.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) - @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = - Sql.ExecutionPhase.AFTER_TEST_METHOD) @WithMockUser(username = "john.doe@example.com", roles = {"USER"}) @DisplayName("Remove item from cart") void removeItemFromCart_ShouldReturnEmptyCart() throws Exception { diff --git a/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java b/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java index a12b5eb..366ed79 100644 --- a/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java +++ b/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java @@ -8,6 +8,7 @@ import mate.academy.springbootwebgreqit.mapper.ShoppingCartMapper; import mate.academy.springbootwebgreqit.model.Book; import mate.academy.springbootwebgreqit.model.CartItem; +import mate.academy.springbootwebgreqit.model.Category; import mate.academy.springbootwebgreqit.model.ShoppingCart; import mate.academy.springbootwebgreqit.model.User; import mate.academy.springbootwebgreqit.repository.BookRepository; @@ -23,6 +24,8 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.core.Authentication; +import java.math.BigDecimal; +import java.util.Collections; import java.util.HashSet; import java.util.Optional; import java.util.Set; @@ -31,7 +34,7 @@ import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) -class ShoppingCartServiceImplTest { +class ShoppingCartServiceTest { @Mock private CartItemRepository cartItemRepository; @@ -57,6 +60,7 @@ class ShoppingCartServiceImplTest { private CartItem cartItem; private Authentication authentication; private UpdateCartItemDto requestUpdateQuantityDto; + private Category category; @BeforeEach void setUp() { @@ -74,9 +78,21 @@ void setUp() { cartItemRequestDto = new CartItemRequestDto(); cartItemRequestDto.setBookId(1L); cartItemRequestDto.setQuantity(2); + cartItemRequestDto.setShoppingCartId(shoppingCart.getId()); book = new Book(); book.setId(1L); + book.setTitle("wallet"); + book.setAuthor("John"); + book.setIsbn("9283234577892"); + book.setPrice(BigDecimal.valueOf(60.99)); + book.setDescription("Updated description"); + book.setCoverImage("https://example.com/updated-cover-.jpg"); + book.setCategories(Collections.singleton(category)); + + category = new Category(); + category.setId(1L); + category.setName("Roman"); cartItem = new CartItem(); cartItem.setId(1L); @@ -114,43 +130,40 @@ void getShoppingCartForCurrentUser_ShouldThrowEntityNotFoundException_WhenShoppi } @Test - void addBookToShoppingCart_ShouldAddNewBookAndReturnShoppingCartDto() { + void addWrongBookToShoppingCart_ShouldThrowNullPointerException() { + cartItemRequestDto.setBookId(17L); when(authentication.getPrincipal()).thenReturn(user); when(shoppingCartRepository.findByUserId(user.getId())).thenReturn(Optional.of(shoppingCart)); - when(bookRepository.findById(cartItemRequestDto.getBookId())).thenReturn(Optional.of(book)); - when(cartItemMapper.toModel(cartItemRequestDto)).thenReturn(cartItem); - when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); - when(shoppingCartRepository.save(shoppingCart)).thenReturn(shoppingCart); + when(bookRepository.findById(cartItemRequestDto.getBookId())).thenReturn(Optional.empty()); // Only essential stubs remain - ShoppingCartDto result = shoppingCartService.addBookToShoppingCart(cartItemRequestDto, authentication); + assertThrows(NullPointerException.class, () -> { + shoppingCartService.addBookToShoppingCart(cartItemRequestDto, authentication); + }); - assertNotNull(result); verify(shoppingCartRepository).findByUserId(user.getId()); verify(bookRepository).findById(cartItemRequestDto.getBookId()); - verify(cartItemMapper).toModel(cartItemRequestDto); - verify(shoppingCartRepository).save(shoppingCart); - verify(shoppingCartMapper).toDto(shoppingCart); + verify(shoppingCartRepository, never()).save(shoppingCart); // `save` should not be called if the book is not found } + + @Test -void updateCartItemQuantity_ShouldUpdateQuantityAndReturnShoppingCartDto() { - int newQuantity = requestUpdateQuantityDto.getQuantity(); - when(authentication.getPrincipal()).thenReturn(user); - when(userRepository.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); - when(cartItemRepository.findById(cartItem.getId())).thenReturn(Optional.of(cartItem)); - when(shoppingCartRepository.findByUserId(user.getId())).thenReturn(Optional.of(shoppingCart)); - when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); - when(cartItemRepository.save(cartItem)).thenReturn(cartItem); - - ShoppingCartDto result = shoppingCartService.updateCartItemQuantity(cartItem.getId(), requestUpdateQuantityDto, authentication); - - assertNotNull(result); - assertEquals(newQuantity, cartItem.getQuantity(), "Quantity should be updated in the cart item"); - verify(cartItemRepository).save(cartItem); - verify(shoppingCartRepository).save(shoppingCart); - verify(shoppingCartMapper).toDto(shoppingCart); - verifyNoMoreInteractions(cartItemRepository, shoppingCartRepository, shoppingCartMapper); -} + void updateCartItemQuantity_ShouldUpdateQuantityAndReturnShoppingCartDto() { + int newQuantity = requestUpdateQuantityDto.getQuantity(); + when(cartItemRepository.findById(cartItem.getId())).thenReturn(Optional.of(cartItem)); + when(cartItemRepository.save(cartItem)).thenReturn(cartItem); + when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); + when(shoppingCartRepository.findByCartItems(Set.of(cartItem))).thenReturn(Optional.of(shoppingCart)); + + ShoppingCartDto result = shoppingCartService.updateCartItemQuantity(cartItem.getId(), requestUpdateQuantityDto); + + assertNotNull(result); + assertEquals(newQuantity, cartItem.getQuantity(), "Quantity should be updated in the cart item"); + verify(cartItemRepository).save(cartItem); + verify(shoppingCartRepository).save(shoppingCart); + verify(shoppingCartMapper).toDto(shoppingCart); + verifyNoMoreInteractions(cartItemRepository, shoppingCartRepository, shoppingCartMapper); + } @Test @@ -182,4 +195,4 @@ void createShoppingCart_ShouldCreateAndReturnShoppingCart() { assertEquals(user, result.getUser()); verify(shoppingCartRepository).save(result); } -} \ No newline at end of file +} diff --git a/src/test/resources/data-sql/clear-tables-for-sh.sql b/src/test/resources/data-sql/clear-tables-for-sh.sql index baad394..63582c0 100644 --- a/src/test/resources/data-sql/clear-tables-for-sh.sql +++ b/src/test/resources/data-sql/clear-tables-for-sh.sql @@ -8,4 +8,4 @@ DELETE FROM shopping_carts; DELETE FROM books; -- Delete from users; -DELETE FROM users; \ No newline at end of file +DELETE FROM users; diff --git a/src/test/resources/data-sql/clear-tables.sql b/src/test/resources/data-sql/clear-tables.sql index 7f3c9eb..106cba8 100644 --- a/src/test/resources/data-sql/clear-tables.sql +++ b/src/test/resources/data-sql/clear-tables.sql @@ -2,4 +2,4 @@ DELETE FROM books_categories; DELETE FROM books; DELETE FROM categories; DELETE FROM shopping_carts; -DELETE FROM users; \ No newline at end of file +DELETE FROM users; diff --git a/src/test/resources/data-sql/create-book.sql b/src/test/resources/data-sql/create-book.sql index 811b040..e5cd4b1 100644 --- a/src/test/resources/data-sql/create-book.sql +++ b/src/test/resources/data-sql/create-book.sql @@ -2,4 +2,4 @@ INSERT INTO books (id, title, author, isbn, price, description, cover_image) VALUES (1, 'wallet', 'John', '9283234577892', 60.99, 'Updated description', 'https://example.com/updated-cover-.jpg'); INSERT INTO books_categories (book_id, category_id) -VALUES (1, 1); \ No newline at end of file +VALUES (1, 1); diff --git a/src/test/resources/data-sql/create-books.sql b/src/test/resources/data-sql/create-books.sql index 7410765..67b9368 100644 --- a/src/test/resources/data-sql/create-books.sql +++ b/src/test/resources/data-sql/create-books.sql @@ -1,2 +1,2 @@ INSERT INTO books (id, title, author, isbn, price, description, cover_image) -VALUES (1, 'Sample Book', 'John Author', '1234567890', 10.99, 'A sample book description.', 'https://example.com/book-cover.jpg'); \ No newline at end of file +VALUES (1, 'Sample Book', 'John Author', '1234567890', 10.99, 'A sample book description.', 'https://example.com/book-cover.jpg'); diff --git a/src/test/resources/data-sql/create-cart-item.sql b/src/test/resources/data-sql/create-cart-item.sql index f7abad1..cba9555 100644 --- a/src/test/resources/data-sql/create-cart-item.sql +++ b/src/test/resources/data-sql/create-cart-item.sql @@ -1 +1 @@ -INSERT INTO cart_items (id, shopping_cart_id, book_id, quantity) VALUES (1, 1, 1, 2); \ No newline at end of file +INSERT INTO cart_items (id, shopping_cart_id, book_id, quantity) VALUES (1, 1, 1, 2); diff --git a/src/test/resources/data-sql/create-category.sql b/src/test/resources/data-sql/create-category.sql index afd0770..b3d6282 100644 --- a/src/test/resources/data-sql/create-category.sql +++ b/src/test/resources/data-sql/create-category.sql @@ -1 +1 @@ -INSERT INTO categories (id, name, description) VALUES (1, 'Roman', 'Some description'); \ No newline at end of file +INSERT INTO categories (id, name, description) VALUES (1, 'Roman', 'Some description'); From 0bf46a0900e81a2ada5fc9ae4d06756d9b249119 Mon Sep 17 00:00:00 2001 From: greqit Date: Tue, 12 Nov 2024 16:56:26 +0100 Subject: [PATCH 19/21] fix service, cause getPrincipal broke app --- .../service/impl/ShoppingCartServiceImpl.java | 13 ++++++++--- .../ShoppingCartControllerTest.java | 22 +++++++++++++------ .../service/ShoppingCartServiceTest.java | 10 ++++----- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java b/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java index 8668f95..4651540 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java +++ b/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java @@ -35,7 +35,9 @@ public class ShoppingCartServiceImpl implements ShoppingCartService { @Override public ShoppingCartDto getShoppingCartForCurrentUser(Authentication authentication, Long shoppingCartId) { - User user = (User) authentication.getPrincipal(); + User user = userRepository.findByEmail(authentication.getName()) + .orElseThrow(() -> new IllegalArgumentException("User not found with id: " + + authentication.getName())); ShoppingCart shoppingCart = shoppingCartRepository.findById(shoppingCartId) .orElseThrow(() -> new EntityNotFoundException("Shopping cart not " + "found for user with id: " + shoppingCartId)); @@ -51,7 +53,9 @@ public ShoppingCartDto getShoppingCartForCurrentUser(Authentication authenticati public ShoppingCartDto addBookToShoppingCart( CartItemRequestDto cartItemDto, Authentication authentication) { - User user = (User) authentication.getPrincipal(); + User user = userRepository.findByEmail(authentication.getName()) + .orElseThrow(() -> new IllegalArgumentException("User not found with id: " + + authentication.getName())); ShoppingCart shoppingCart = shoppingCartRepository.findByUserId(user.getId()) .orElseThrow(() -> new EntityNotFoundException("Shopping cart not found")); @@ -112,7 +116,10 @@ public ShoppingCartDto updateCartItemQuantity(Long cartItemId, @Transactional @Override public ShoppingCartDto removeCartItem(Long cartItemId, Authentication authentication) { - User user = (User) authentication.getPrincipal(); + User user = userRepository.findByEmail(authentication.getName()) + .orElseThrow(() -> new IllegalArgumentException("User not found with id: " + + authentication.getName())); + ShoppingCart shoppingCart = shoppingCartRepository.findByUserId(user.getId()) .orElseThrow(() -> new EntityNotFoundException("Shopping cart not found")); diff --git a/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java b/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java index 0776c7e..e4c7d84 100644 --- a/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java +++ b/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java @@ -21,10 +21,6 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -@Sql(scripts = {"/data-sql/create-books.sql", "/data-sql/create-users.sql"}, - executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) -@Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = - Sql.ExecutionPhase.AFTER_TEST_METHOD) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class ShoppingCartControllerTest { protected static MockMvc mockMvc; @@ -47,8 +43,11 @@ static void beforeAll(@Autowired WebApplicationContext webApplicationContext) { @Test - @Sql(scripts = {"/data-sql/create-cart-item.sql"}, + @Sql(scripts = {"/data-sql/create-books.sql", + "/data-sql/create-users.sql", "/data-sql/create-cart-item.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = + Sql.ExecutionPhase.AFTER_TEST_METHOD) @WithMockUser(username = "john.doe@example.com", roles = {"USER"}) @DisplayName("Get shopping cart for the current user") void getShoppingCart_ShouldReturnCartForUser() throws Exception { @@ -62,6 +61,10 @@ void getShoppingCart_ShouldReturnCartForUser() throws Exception { } @Test + @Sql(scripts = {"/data-sql/create-books.sql", "/data-sql/create-users.sql"}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = + Sql.ExecutionPhase.AFTER_TEST_METHOD) @WithMockUser(username = "john.doe@example.com", roles = {"USER"}) @DisplayName("Add book to shopping cart") void addBookToShoppingCart_ShouldReturnUpdatedCart() throws Exception { @@ -84,8 +87,10 @@ void addBookToShoppingCart_ShouldReturnUpdatedCart() throws Exception { @Test - @Sql(scripts = {"/data-sql/create-cart-item.sql"}, + @Sql(scripts = {"/data-sql/create-books.sql", + "/data-sql/create-users.sql", "/data-sql/create-cart-item.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) @WithMockUser(username = "john.doe@example.com", roles = {"USER"}) @DisplayName("Update cart item quantity") void updateCartItemQuantity_ShouldReturnUpdatedQuantity() throws Exception { @@ -104,8 +109,11 @@ void updateCartItemQuantity_ShouldReturnUpdatedQuantity() throws Exception { } @Test - @Sql(scripts = {"/data-sql/create-cart-item.sql"}, + @Sql(scripts = {"/data-sql/create-books.sql", "/data-sql/create-users.sql", + "/data-sql/create-cart-item.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = + Sql.ExecutionPhase.AFTER_TEST_METHOD) @WithMockUser(username = "john.doe@example.com", roles = {"USER"}) @DisplayName("Remove item from cart") void removeItemFromCart_ShouldReturnEmptyCart() throws Exception { diff --git a/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java b/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java index 366ed79..9bba8f9 100644 --- a/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java +++ b/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java @@ -109,7 +109,7 @@ void setUp() { @Test void getShoppingCartForCurrentUser_ShouldReturnShoppingCartDto() { - when(authentication.getPrincipal()).thenReturn(user); + when(userRepository.findByEmail(authentication.getName())).thenReturn(Optional.of(user)); when(shoppingCartRepository.findById(shoppingCart.getId())).thenReturn(Optional.of(shoppingCart)); when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); @@ -122,7 +122,7 @@ void getShoppingCartForCurrentUser_ShouldReturnShoppingCartDto() { @Test void getShoppingCartForCurrentUser_ShouldThrowEntityNotFoundException_WhenShoppingCartNotFound() { - when(authentication.getPrincipal()).thenReturn(user); + when(userRepository.findByEmail(authentication.getName())).thenReturn(Optional.of(user)); when(shoppingCartRepository.findById(shoppingCart.getId())).thenReturn(Optional.empty()); assertThrows(EntityNotFoundException.class, @@ -132,7 +132,7 @@ void getShoppingCartForCurrentUser_ShouldThrowEntityNotFoundException_WhenShoppi @Test void addWrongBookToShoppingCart_ShouldThrowNullPointerException() { cartItemRequestDto.setBookId(17L); - when(authentication.getPrincipal()).thenReturn(user); + when(userRepository.findByEmail(authentication.getName())).thenReturn(Optional.of(user)); when(shoppingCartRepository.findByUserId(user.getId())).thenReturn(Optional.of(shoppingCart)); when(bookRepository.findById(cartItemRequestDto.getBookId())).thenReturn(Optional.empty()); // Only essential stubs remain @@ -168,9 +168,9 @@ void updateCartItemQuantity_ShouldUpdateQuantityAndReturnShoppingCartDto() { @Test void removeCartItem_ShouldRemoveItemAndReturnShoppingCartDto() { - shoppingCart.setCartItems(new HashSet<>(Set.of(cartItem))); // Π”ΠΎΠ΄Π°Ρ”ΠΌΠΎ cartItem Π΄ΠΎ shoppingCart + shoppingCart.setCartItems(new HashSet<>(Set.of(cartItem))); - when(authentication.getPrincipal()).thenReturn(user); + when(userRepository.findByEmail(authentication.getName())).thenReturn(Optional.of(user)); when(shoppingCartRepository.findByUserId(user.getId())).thenReturn(Optional.of(shoppingCart)); when(shoppingCartRepository.save(shoppingCart)).thenReturn(shoppingCart); when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto); From 1aae44ce06e63041797cdc6d6fbe784177a2c9b0 Mon Sep 17 00:00:00 2001 From: greqit Date: Fri, 15 Nov 2024 17:20:28 +0100 Subject: [PATCH 20/21] fix --- .../springbootwebgreqit/controller/ShoppingCartController.java | 3 +-- src/main/java/mate/academy/springbootwebgreqit/model/Book.java | 1 + .../java/mate/academy/springbootwebgreqit/model/CartItem.java | 1 + .../service/impl/ShoppingCartServiceImpl.java | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java b/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java index ada144f..a7b1a24 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java +++ b/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java @@ -54,8 +54,7 @@ public ShoppingCartDto addBookToShoppingCart(@RequestBody CartItemRequestDto car }) @PutMapping("/items/{cartItemId}") public ShoppingCartDto updateCartItemQuantity(@PathVariable Long cartItemId, - @RequestBody @Valid UpdateCartItemDto quantity, - Authentication authentication) { + @RequestBody @Valid UpdateCartItemDto quantity) { return shoppingCartService.updateCartItemQuantity(cartItemId, quantity); } diff --git a/src/main/java/mate/academy/springbootwebgreqit/model/Book.java b/src/main/java/mate/academy/springbootwebgreqit/model/Book.java index f2994c6..05635c2 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/model/Book.java +++ b/src/main/java/mate/academy/springbootwebgreqit/model/Book.java @@ -63,6 +63,7 @@ public class Book { ) @ToString.Exclude @EqualsAndHashCode.Exclude + @JsonIgnore private Set categories = new HashSet<>(); private boolean isDeleted = false; diff --git a/src/main/java/mate/academy/springbootwebgreqit/model/CartItem.java b/src/main/java/mate/academy/springbootwebgreqit/model/CartItem.java index 11aa5e4..88c053b 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/model/CartItem.java +++ b/src/main/java/mate/academy/springbootwebgreqit/model/CartItem.java @@ -28,6 +28,7 @@ public class CartItem { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "book_id", nullable = false) + @JsonIgnore private Book book; private int quantity; } diff --git a/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java b/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java index 4651540..7e5d9e8 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java +++ b/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java @@ -16,6 +16,7 @@ import mate.academy.springbootwebgreqit.repository.ShoppingCartRepository; import mate.academy.springbootwebgreqit.repository.UserRepository; import mate.academy.springbootwebgreqit.service.ShoppingCartService; +import org.hibernate.Hibernate; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; import java.util.Optional; From aab281e860c237ea4a7b75007c44bc1c58e31d63 Mon Sep 17 00:00:00 2001 From: greqit Date: Sat, 16 Nov 2024 18:58:57 +0100 Subject: [PATCH 21/21] fix docker --- pom.xml | 14 ++++++-------- .../controller/AuthenticationController.java | 2 ++ .../repository/OrderItemReposirory.java | 3 +++ .../repository/OrderRepository.java | 3 +++ .../repository/ShoppingCartRepository.java | 2 ++ .../security/CustomUserDetailService.java | 3 ++- src/main/resources/application.properties | 5 ++++- src/main/resources/liquibase.properties | 2 +- .../controller/ShoppingCartControllerTest.java | 3 --- .../service/ShoppingCartServiceTest.java | 18 ------------------ 10 files changed, 23 insertions(+), 32 deletions(-) diff --git a/pom.xml b/pom.xml index 895a930..521749c 100644 --- a/pom.xml +++ b/pom.xml @@ -65,9 +65,9 @@ - io.springfox - springfox-swagger-ui - 3.0.0 + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.1.0 @@ -76,7 +76,6 @@ 2.2.25 - io.jsonwebtoken jjwt-jackson @@ -137,11 +136,10 @@ test - - mysql - mysql-connector-java - 8.0.33 + com.mysql + mysql-connector-j + 9.1.0 diff --git a/src/main/java/mate/academy/springbootwebgreqit/controller/AuthenticationController.java b/src/main/java/mate/academy/springbootwebgreqit/controller/AuthenticationController.java index b523b46..da1d6fc 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/controller/AuthenticationController.java +++ b/src/main/java/mate/academy/springbootwebgreqit/controller/AuthenticationController.java @@ -3,6 +3,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import mate.academy.springbootwebgreqit.dto.user.UserLoginRequestDto; @@ -16,6 +17,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "Auth managemant", description = "Endpoint to auth") @RestController @RequestMapping("/auth") @RequiredArgsConstructor diff --git a/src/main/java/mate/academy/springbootwebgreqit/repository/OrderItemReposirory.java b/src/main/java/mate/academy/springbootwebgreqit/repository/OrderItemReposirory.java index 884389a..944044c 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/repository/OrderItemReposirory.java +++ b/src/main/java/mate/academy/springbootwebgreqit/repository/OrderItemReposirory.java @@ -2,8 +2,11 @@ import mate.academy.springbootwebgreqit.model.OrderItem; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + import java.util.Optional; +@Repository public interface OrderItemReposirory extends JpaRepository { Optional findByOrderId(Long orderId); diff --git a/src/main/java/mate/academy/springbootwebgreqit/repository/OrderRepository.java b/src/main/java/mate/academy/springbootwebgreqit/repository/OrderRepository.java index dbc4a49..bbe80a2 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/repository/OrderRepository.java +++ b/src/main/java/mate/academy/springbootwebgreqit/repository/OrderRepository.java @@ -3,8 +3,11 @@ import mate.academy.springbootwebgreqit.model.Order; import mate.academy.springbootwebgreqit.model.User; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + import java.util.List; +@Repository public interface OrderRepository extends JpaRepository { List findByUser(User user); } diff --git a/src/main/java/mate/academy/springbootwebgreqit/repository/ShoppingCartRepository.java b/src/main/java/mate/academy/springbootwebgreqit/repository/ShoppingCartRepository.java index 44a63a9..5163ae3 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/repository/ShoppingCartRepository.java +++ b/src/main/java/mate/academy/springbootwebgreqit/repository/ShoppingCartRepository.java @@ -4,10 +4,12 @@ import mate.academy.springbootwebgreqit.model.ShoppingCart; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; import java.util.Optional; import java.util.Set; +@Repository public interface ShoppingCartRepository extends JpaRepository { @EntityGraph(attributePaths = {"cartItems", "cartItems.book"}) Optional findByUserId(Long userId); diff --git a/src/main/java/mate/academy/springbootwebgreqit/security/CustomUserDetailService.java b/src/main/java/mate/academy/springbootwebgreqit/security/CustomUserDetailService.java index ddc3f33..a24ee83 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/security/CustomUserDetailService.java +++ b/src/main/java/mate/academy/springbootwebgreqit/security/CustomUserDetailService.java @@ -5,6 +5,7 @@ import mate.academy.springbootwebgreqit.repository.UserRepository; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; @Service @@ -15,6 +16,6 @@ public class CustomUserDetailService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String email) throws EntityNotFoundException { return userRepository.findByEmail(email) - .orElseThrow(() -> new EntityNotFoundException("Can't find user by email")); + .orElseThrow(() -> new UsernameNotFoundException("Can't find user by email")); } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a6d1391..64ba9e0 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,4 +1,3 @@ -spring.application.name=Spring-boot-web-greqit spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/book-store spring.datasource.username=root @@ -9,6 +8,10 @@ spring.jpa.show-sql=true spring.jpa.open-in-view=false spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect +spring.liquibase.change-log=/db/changelog/db.changelog-master.yaml + +springdoc.api-docs.enabled=true +springdoc.swagger-ui.enabled=true jwt.expiration=1800000 jwt.secret=haveAGoodDayIfYouReadIt243509349584398534025 diff --git a/src/main/resources/liquibase.properties b/src/main/resources/liquibase.properties index e83e494..2b801bc 100644 --- a/src/main/resources/liquibase.properties +++ b/src/main/resources/liquibase.properties @@ -1,4 +1,4 @@ url=jdbc:mysql://localhost:3306/book-store username=root password=1234567 -changelog-file=/db/chandelog/db.changelog-master.yaml +changelog-file=/db/changelog/db.changelog-master.yaml diff --git a/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java b/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java index e4c7d84..74896cb 100644 --- a/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java +++ b/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java @@ -56,7 +56,6 @@ void getShoppingCart_ShouldReturnCartForUser() throws Exception { .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.user.id").value(1L)) - .andExpect(jsonPath("$.cartItems[0].book.id").value(1L)) .andReturn(); } @@ -78,8 +77,6 @@ void addBookToShoppingCart_ShouldReturnUpdatedCart() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(jsonRequest)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.cartItems[0].book.id") - .value(cartItemRequestDto.getBookId())) .andExpect(jsonPath("$.cartItems[0].quantity") .value(cartItemRequestDto.getQuantity())) .andReturn(); diff --git a/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java b/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java index 9bba8f9..1e23d89 100644 --- a/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java +++ b/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java @@ -129,24 +129,6 @@ void getShoppingCartForCurrentUser_ShouldThrowEntityNotFoundException_WhenShoppi () -> shoppingCartService.getShoppingCartForCurrentUser(authentication, shoppingCart.getId())); } - @Test - void addWrongBookToShoppingCart_ShouldThrowNullPointerException() { - cartItemRequestDto.setBookId(17L); - when(userRepository.findByEmail(authentication.getName())).thenReturn(Optional.of(user)); - when(shoppingCartRepository.findByUserId(user.getId())).thenReturn(Optional.of(shoppingCart)); - when(bookRepository.findById(cartItemRequestDto.getBookId())).thenReturn(Optional.empty()); // Only essential stubs remain - - assertThrows(NullPointerException.class, () -> { - shoppingCartService.addBookToShoppingCart(cartItemRequestDto, authentication); - }); - - verify(shoppingCartRepository).findByUserId(user.getId()); - verify(bookRepository).findById(cartItemRequestDto.getBookId()); - verify(shoppingCartRepository, never()).save(shoppingCart); // `save` should not be called if the book is not found - } - - - @Test void updateCartItemQuantity_ShouldUpdateQuantityAndReturnShoppingCartDto() { int newQuantity = requestUpdateQuantityDto.getQuantity();