diff --git a/Book-Store.pdf b/Book-Store.pdf new file mode 100644 index 0000000..b86a7da Binary files /dev/null and b/Book-Store.pdf differ diff --git a/Book-Store.png b/Book-Store.png new file mode 100644 index 0000000..28b4105 Binary files /dev/null and b/Book-Store.png differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..dff39e4 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# 🎯 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 +- 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. + +# 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/) diff --git a/pom.xml b/pom.xml index d17cb27..521749c 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 @@ -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 + @@ -60,6 +64,17 @@ 1.6.14 + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.1.0 + + + + io.swagger.core.v3 + swagger-annotations + 2.2.25 + io.jsonwebtoken @@ -121,11 +136,10 @@ test - - mysql - mysql-connector-java - 8.0.33 + com.mysql + mysql-connector-j + 9.1.0 @@ -224,6 +238,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/AuthenticationController.java b/src/main/java/mate/academy/springbootwebgreqit/controller/AuthenticationController.java index b689b75..da1d6fc 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/controller/AuthenticationController.java +++ b/src/main/java/mate/academy/springbootwebgreqit/controller/AuthenticationController.java @@ -1,5 +1,9 @@ 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 io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import mate.academy.springbootwebgreqit.dto.user.UserLoginRequestDto; @@ -13,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 @@ -20,15 +25,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); } } diff --git a/src/main/java/mate/academy/springbootwebgreqit/controller/BookController.java b/src/main/java/mate/academy/springbootwebgreqit/controller/BookController.java index ca81229..b859ffa 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,66 @@ 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')") - public BookDto updateBook(@PathVariable Long id, @RequestBody @Valid UpdateBookRequestDto updateBookRequestDto) { + @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..408ce00 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,65 @@ 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) { + 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 b9c4c97..a7b1a24 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java +++ b/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java @@ -1,9 +1,15 @@ 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.cartitem.CartItemRequestDto; +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; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -11,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 @@ -20,26 +25,46 @@ public class ShoppingCartController { private final ShoppingCartService shoppingCartService; - @GetMapping - public ShoppingCartDto getShoppingCartForCurrentUser(@RequestParam Long userId) { - return shoppingCartService.getShoppingCartForCurrentUser(userId); + @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("/{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"), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) @PostMapping public ShoppingCartDto addBookToShoppingCart(@RequestBody CartItemRequestDto cartItem, - @RequestParam Long userId) { - return shoppingCartService.addBookToShoppingCart(cartItem, userId); + 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, - @RequestParam Long userId) { - return shoppingCartService.updateCartItemQuantity(cartItemId, quantity, userId); + @RequestBody @Valid UpdateCartItemDto quantity) { + return shoppingCartService.updateCartItemQuantity(cartItemId, quantity); } + @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, @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/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/dto/shoppingcart/UpdateCartItemDto.java b/src/main/java/mate/academy/springbootwebgreqit/dto/shoppingcart/UpdateCartItemDto.java new file mode 100644 index 0000000..b4ecbfd --- /dev/null +++ b/src/main/java/mate/academy/springbootwebgreqit/dto/shoppingcart/UpdateCartItemDto.java @@ -0,0 +1,10 @@ +package mate.academy.springbootwebgreqit.dto.shoppingcart; + +import jakarta.validation.constraints.Positive; +import lombok.Data; + +@Data +public class UpdateCartItemDto { + @Positive + private int 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/model/User.java b/src/main/java/mate/academy/springbootwebgreqit/model/User.java index fe41696..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_card_id") - @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/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 2c2f319..5163ae3 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/repository/ShoppingCartRepository.java +++ b/src/main/java/mate/academy/springbootwebgreqit/repository/ShoppingCartRepository.java @@ -1,12 +1,18 @@ 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 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); + + Optional findByCartItems(Set cartItems); } 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/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java b/src/main/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java index ee79370..3345ae4 100644 --- a/src/main/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java +++ b/src/main/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java @@ -1,18 +1,24 @@ package mate.academy.springbootwebgreqit.service; import mate.academy.springbootwebgreqit.dto.cartitem.CartItemRequestDto; +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; +import org.springframework.security.core.Authentication; public interface ShoppingCartService { - ShoppingCartDto getShoppingCartForCurrentUser(Long userId); + ShoppingCartDto getShoppingCartForCurrentUser(Authentication authentication, + Long shoppingCartId); - ShoppingCartDto addBookToShoppingCart(CartItemRequestDto cartItem, Long userId); + ShoppingCartDto addBookToShoppingCart(CartItemRequestDto cartItem, + Authentication authentication); - ShoppingCartDto updateCartItemQuantity(Long cartItemId, int quantity, Long userId); + ShoppingCartDto updateCartItemQuantity(Long cartItemId, + UpdateCartItemDto quantity); - 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..7e5d9e8 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.UpdateCartItemDto; import mate.academy.springbootwebgreqit.dto.shoppingcart.ShoppingCartDto; import mate.academy.springbootwebgreqit.exception.EntityNotFoundException; import mate.academy.springbootwebgreqit.mapper.CartItemMapper; @@ -16,8 +17,10 @@ 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; +import java.util.Set; @Service @RequiredArgsConstructor @@ -31,20 +34,18 @@ 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, + Long shoppingCartId) { + 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)); 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: " + + user.getEmail()); } - Hibernate.initialize(shoppingCart.getCartItems()); - Hibernate.initialize(user.getRoles()); - shoppingCart.getCartItems().forEach(cartItem -> - Hibernate.initialize(cartItem.getBook().getCategories())); - shoppingCart.getCartItems().forEach(cartItem -> - Hibernate.initialize(cartItem.getBook().getCartItems())); return shoppingCartMapper.toDto(shoppingCart); } @@ -52,8 +53,11 @@ 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 IllegalArgumentException("User not found with id: " + + authentication.getName())); + ShoppingCart shoppingCart = shoppingCartRepository.findByUserId(user.getId()) .orElseThrow(() -> new EntityNotFoundException("Shopping cart not found")); Optional existingItemOpt = shoppingCart.getCartItems().stream() @@ -71,67 +75,54 @@ 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); } ); - 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); + shoppingCartRepository.save(shoppingCart); + return shoppingCartMapper.toDto(shoppingCart); } @Transactional @Override public ShoppingCartDto updateCartItemQuantity(Long cartItemId, - int quantity, Long userId) { - if (quantity <= 0) { + UpdateCartItemDto quantity) { + if (quantity.getQuantity() <= 0) { throw new EntityNotFoundException("Quantity must be a positive integer"); } - User user = userRepository.findById(userId) - .orElseThrow(() -> new EntityNotFoundException("User not found")); - ShoppingCart shoppingCart = user.getShoppingCart(); - if (shoppingCart == null) { - throw new EntityNotFoundException("Shopping cart not found for user with ID " - + userId); - } + 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)); CartItem cartItem = shoppingCart.getCartItems().stream() .filter(item -> item.getId().equals(cartItemId)) .findFirst() .orElseThrow(() -> new EntityNotFoundException("CartItem with ID " + cartItemId + " not found in the user's shopping cart")); - cartItem.setQuantity(quantity); - 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())); - + cartItem.setQuantity(quantity.getQuantity()); cartItemRepository.save(cartItem); - ShoppingCart savedShoppingCart = shoppingCartRepository.save(shoppingCart); + shoppingCartRepository.save(shoppingCart); - return shoppingCartMapper.toDto(savedShoppingCart); + return shoppingCartMapper.toDto(shoppingCart); } @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 IllegalArgumentException("User not found with id: " + + 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)) @@ -142,14 +133,6 @@ public ShoppingCartDto removeCartItem(Long cartItemId, Long userId) { 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/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/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/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..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_card_id - type: bigint - createTable: tableName: roles 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 new file mode 100644 index 0000000..74896cb --- /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.dto.shoppingcart.UpdateCartItemDto; +import mate.academy.springbootwebgreqit.security.CustomUserDetailService; +import mate.academy.springbootwebgreqit.service.ShoppingCartService; +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; +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 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.*; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class ShoppingCartControllerTest { + protected static MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private ShoppingCartService shoppingCartService; + @Autowired + private CustomUserDetailService customUserDetailService; + + @BeforeAll + static void beforeAll(@Autowired WebApplicationContext webApplicationContext) { + mockMvc = MockMvcBuilders + .webAppContextSetup(webApplicationContext) + .apply(springSecurity()) + .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"}) + @DisplayName("Get shopping cart for the current user") + void getShoppingCart_ShouldReturnCartForUser() throws Exception { + Long shoppingCartId = 1L; + mockMvc.perform(get("/cart/{shoppingCartId}", shoppingCartId) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.user.id").value(1L)) + .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"}) + @DisplayName("Add book to shopping cart") + void addBookToShoppingCart_ShouldReturnUpdatedCart() throws Exception { + CartItemRequestDto cartItemRequestDto = new CartItemRequestDto(); + cartItemRequestDto.setBookId(1L); + cartItemRequestDto.setQuantity(2); + + String jsonRequest = objectMapper.writeValueAsString(cartItemRequestDto); + + MvcResult result = mockMvc.perform(post("/cart") + .contentType(MediaType.APPLICATION_JSON) + .content(jsonRequest)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.cartItems[0].quantity") + .value(cartItemRequestDto.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"}) + @DisplayName("Update cart item quantity") + void updateCartItemQuantity_ShouldReturnUpdatedQuantity() throws Exception { + Long cartItemId = 1L; + UpdateCartItemDto quantityDto = new UpdateCartItemDto(); + quantityDto.setQuantity(3); + + String jsonRequest = objectMapper.writeValueAsString(quantityDto); + + mockMvc.perform(put("/cart/items/{cartItemId}", cartItemId) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonRequest)) + .andExpect(status().isOk()) + .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"}) + @DisplayName("Remove item from cart") + void removeItemFromCart_ShouldReturnEmptyCart() throws Exception { + Long cartItemId = 1L; + + mockMvc.perform(delete("/cart/items/{cartItemId}", cartItemId) + .contentType(MediaType.APPLICATION_JSON)) + .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 new file mode 100644 index 0000000..1e23d89 --- /dev/null +++ b/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java @@ -0,0 +1,180 @@ +package mate.academy.springbootwebgreqit.service; + +import mate.academy.springbootwebgreqit.dto.cartitem.CartItemRequestDto; +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; +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; +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.math.BigDecimal; +import java.util.Collections; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +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; + private UpdateCartItemDto requestUpdateQuantityDto; + private Category category; + + @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); + 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); + cartItem.setBook(book); + cartItem.setQuantity(2); + cartItem.setShoppingCart(shoppingCart); + shoppingCart.getCartItems().add(cartItem); + + authentication = mock(Authentication.class); + + requestUpdateQuantityDto = new UpdateCartItemDto(); + requestUpdateQuantityDto.setQuantity(3); + } + + @Test + void getShoppingCartForCurrentUser_ShouldReturnShoppingCartDto() { + when(userRepository.findByEmail(authentication.getName())).thenReturn(Optional.of(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(shoppingCartRepository).findById(shoppingCart.getId()); + verify(shoppingCartMapper).toDto(shoppingCart); + } + + @Test + void getShoppingCartForCurrentUser_ShouldThrowEntityNotFoundException_WhenShoppingCartNotFound() { + when(userRepository.findByEmail(authentication.getName())).thenReturn(Optional.of(user)); + when(shoppingCartRepository.findById(shoppingCart.getId())).thenReturn(Optional.empty()); + + assertThrows(EntityNotFoundException.class, + () -> shoppingCartService.getShoppingCartForCurrentUser(authentication, shoppingCart.getId())); + } + + @Test + 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 + void removeCartItem_ShouldRemoveItemAndReturnShoppingCartDto() { + shoppingCart.setCartItems(new HashSet<>(Set.of(cartItem))); + + 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); + + ShoppingCartDto result = shoppingCartService.removeCartItem(cartItem.getId(), authentication); + + assertNotNull(result); + 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); + + ShoppingCart result = shoppingCartService.createShoppingCart(user); + + assertNotNull(result); + assertEquals(user, result.getUser()); + verify(shoppingCartRepository).save(result); + } +} 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..63582c0 --- /dev/null +++ b/src/test/resources/data-sql/clear-tables-for-sh.sql @@ -0,0 +1,11 @@ +-- 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; diff --git a/src/test/resources/data-sql/clear-tables.sql b/src/test/resources/data-sql/clear-tables.sql index 2b2a583..106cba8 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; 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 new file mode 100644 index 0000000..67b9368 --- /dev/null +++ b/src/test/resources/data-sql/create-books.sql @@ -0,0 +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'); 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..cba9555 --- /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); 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'); 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..a34cfee --- /dev/null +++ b/src/test/resources/data-sql/create-users.sql @@ -0,0 +1,6 @@ +-- 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);