From 30685086c4f32cc7518e137ab6e57a59b187ce19 Mon Sep 17 00:00:00 2001 From: duongminhhieu Date: Sun, 2 Jun 2024 14:41:06 +0700 Subject: [PATCH 01/22] feat: find car by image use Gemini AI, chat with AI --- pom.xml | 17 ++- .../common/configs/GeminiConfig.java | 33 ++++++ .../common/exception/ErrorCode.java | 1 + .../product/ProductRepository.java | 6 + .../yasminiai/YasMiniAIController.java | 47 ++++++++ .../yasminiai/YasMiniAIService.java | 112 ++++++++++++++++++ 6 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/learning/yasminishop/common/configs/GeminiConfig.java create mode 100644 src/main/java/com/learning/yasminishop/yasminiai/YasMiniAIController.java create mode 100644 src/main/java/com/learning/yasminishop/yasminiai/YasMiniAIService.java diff --git a/pom.xml b/pom.xml index 77c6e23..f522069 100644 --- a/pom.xml +++ b/pom.xml @@ -25,6 +25,7 @@ 0.8.12 5.1.2 9.2.0 + 26.32.0 @@ -35,6 +36,15 @@ pom import + + + com.google.cloud + libraries-bom + ${google-cloud-bom.version} + pom + import + + @@ -106,6 +116,11 @@ spring-boot-starter-mail + + com.google.cloud + google-cloud-vertexai + + org.springframework.boot spring-boot-starter-test @@ -192,7 +207,7 @@ - + src/main/resources true diff --git a/src/main/java/com/learning/yasminishop/common/configs/GeminiConfig.java b/src/main/java/com/learning/yasminishop/common/configs/GeminiConfig.java new file mode 100644 index 0000000..d96d405 --- /dev/null +++ b/src/main/java/com/learning/yasminishop/common/configs/GeminiConfig.java @@ -0,0 +1,33 @@ +package com.learning.yasminishop.common.configs; + +import com.google.cloud.vertexai.VertexAI; +import com.google.cloud.vertexai.generativeai.ChatSession; +import com.google.cloud.vertexai.generativeai.GenerativeModel; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class GeminiConfig { + + private static final String MODEL_NAME = "gemini-1.5-flash"; + private static final String LOCATION = "asia-southeast1"; + private static final String PROJECT_ID = "yasmini"; + + @Bean + public VertexAI vertexAI() { + return new VertexAI(PROJECT_ID, LOCATION); + } + + @Bean + public GenerativeModel getModel(VertexAI vertexAI) { + return new GenerativeModel(MODEL_NAME, vertexAI); + } + + @Bean + public ChatSession chatSession(@Qualifier("getModel") GenerativeModel generativeModel) { + return new ChatSession(generativeModel); + } + + +} diff --git a/src/main/java/com/learning/yasminishop/common/exception/ErrorCode.java b/src/main/java/com/learning/yasminishop/common/exception/ErrorCode.java index 959fa6a..c1949b0 100644 --- a/src/main/java/com/learning/yasminishop/common/exception/ErrorCode.java +++ b/src/main/java/com/learning/yasminishop/common/exception/ErrorCode.java @@ -34,6 +34,7 @@ public enum ErrorCode { PRODUCT_STOCK_NOT_ENOUGH(1021, "Product stock is not enough", HttpStatus.BAD_REQUEST), CART_ITEM_NOT_FOUND(1022, "Cart item not found", HttpStatus.NOT_FOUND), ORDER_NOT_FOUND(1023, "Order not found", HttpStatus.NOT_FOUND), + GENERATIVE_AI_ERROR(1024, "Error in generative AI", HttpStatus.INTERNAL_SERVER_ERROR), // Constraint violation diff --git a/src/main/java/com/learning/yasminishop/product/ProductRepository.java b/src/main/java/com/learning/yasminishop/product/ProductRepository.java index 2c95008..37d8e39 100644 --- a/src/main/java/com/learning/yasminishop/product/ProductRepository.java +++ b/src/main/java/com/learning/yasminishop/product/ProductRepository.java @@ -3,6 +3,8 @@ import com.learning.yasminishop.common.entity.Product; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.List; @@ -10,6 +12,7 @@ @Repository public interface ProductRepository extends JpaRepository , JpaSpecificationExecutor { + boolean existsBySlug(String slug); boolean existsBySku(String sku); @@ -17,5 +20,8 @@ public interface ProductRepository extends JpaRepository , JpaS Optional findBySlug(String slug); List findTop10ByOrderByQuantityDesc(); + + @Query("SELECT p FROM Product p WHERE LOWER(p.name) LIKE LOWER(CONCAT('%', :keyword, '%'))") + List searchByKeyword(@Param("keyword") String keyword); } diff --git a/src/main/java/com/learning/yasminishop/yasminiai/YasMiniAIController.java b/src/main/java/com/learning/yasminishop/yasminiai/YasMiniAIController.java new file mode 100644 index 0000000..15e2295 --- /dev/null +++ b/src/main/java/com/learning/yasminishop/yasminiai/YasMiniAIController.java @@ -0,0 +1,47 @@ +package com.learning.yasminishop.yasminiai; + +import com.learning.yasminishop.common.dto.APIResponse; +import com.learning.yasminishop.product.dto.response.ProductResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@RestController +@RequestMapping("/ai") +@RequiredArgsConstructor +@Slf4j +public class YasMiniAIController { + + private final YasMiniAIService yasMiniAIService; + + + @PostMapping + public APIResponse> findCar(@RequestParam("file") MultipartFile file) { + + var response = yasMiniAIService.findCarByImage(file); + return APIResponse.>builder() + .result(response) + .build(); + } + + + @GetMapping("/{text}") + public APIResponse chat(@PathVariable String text) { + String chatResponse = yasMiniAIService.generateText(text); + return APIResponse.builder() + .result(chatResponse) + .build(); + } + + @GetMapping("history/{text}") + public APIResponse> getChatHistory(@PathVariable String text) { + + var response = yasMiniAIService.generateTextWithHistory(text); + return APIResponse.>builder() + .result(response) + .build(); + } +} diff --git a/src/main/java/com/learning/yasminishop/yasminiai/YasMiniAIService.java b/src/main/java/com/learning/yasminishop/yasminiai/YasMiniAIService.java new file mode 100644 index 0000000..7d1a6e6 --- /dev/null +++ b/src/main/java/com/learning/yasminishop/yasminiai/YasMiniAIService.java @@ -0,0 +1,112 @@ +package com.learning.yasminishop.yasminiai; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.cloud.vertexai.api.Content; +import com.google.cloud.vertexai.api.GenerateContentResponse; +import com.google.cloud.vertexai.api.Part; +import com.google.cloud.vertexai.generativeai.*; +import com.learning.yasminishop.common.entity.Product; +import com.learning.yasminishop.common.exception.AppException; +import com.learning.yasminishop.common.exception.ErrorCode; +import com.learning.yasminishop.product.ProductRepository; +import com.learning.yasminishop.product.dto.response.ProductResponse; +import com.learning.yasminishop.product.mapper.ProductMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +@Service +@RequiredArgsConstructor +@Slf4j +@Transactional(readOnly = true) +public class YasMiniAIService { + private final ChatSession chatSession; + + private final GenerativeModel generativeModel; + private final ProductRepository productRepository; + + private final ProductMapper productMapper; + + + @PreAuthorize("hasRole('USER')") + public String generateText(String prompt){ + try { + GenerateContentResponse generateContentResponse = chatSession.sendMessage(prompt); + return ResponseHandler.getText(generateContentResponse); + } catch (Exception e) { + log.error("Error generating text for prompt: {}", prompt, e); + throw new AppException(ErrorCode.GENERATIVE_AI_ERROR); + } + } + + @PreAuthorize("hasRole('USER')") + public List generateTextWithHistory(String prompt){ + try { + this.chatSession.sendMessage(prompt); + + List history = this.chatSession.getHistory(); + + return history.stream().flatMap(h -> h.getPartsList().stream()).map(Part::getText).toList(); + } catch (Exception e) { + log.error("Error generating text for prompt: {}", prompt, e); + throw new AppException(ErrorCode.GENERATIVE_AI_ERROR); + } + } + + // @PreAuthorize("hasRole('USER')") + public List findCarByImage(MultipartFile file){ + try { + var prompt = "Extract the name car to a list keyword and output them in JSON. If you don't find any information about the car, please output the list empty.\nExample response: [\"rolls\", \"royce\", \"wraith\"]"; + var content = this.generativeModel.generateContent( + ContentMaker.fromMultiModalData( + PartMaker.fromMimeTypeAndData(Objects.requireNonNull(file.getContentType()), file.getBytes()), + prompt + ) + ); + + String jsonContent = ResponseHandler.getText(content); + List keywords = convertJsonToList(jsonContent).stream() + .map(String::toLowerCase) + .toList(); + + log.info(keywords.toString()); + + Set results = new HashSet<>(); + for (String keyword : keywords) { + List products = productRepository.searchByKeyword(keyword); + results.addAll(products); + } + + return results.stream() + .map(productMapper::toProductResponse) + .toList(); + + } catch (Exception e) { + log.error("Error finding car by image", e); + throw new AppException(ErrorCode.GENERATIVE_AI_ERROR); + } + } + + private List convertJsonToList(String markdown) throws JsonProcessingException { + ObjectMapper objectMapper = new ObjectMapper(); + String parseJson = markdown; + if(markdown.contains("```json")){ + parseJson = extractJsonFromMarkdown(markdown); + } + return objectMapper.readValue(parseJson, List.class); + } + + private String extractJsonFromMarkdown(String markdown) { + return markdown.replace("```json\n", "").replace("\n```", ""); + } + +} From d3d6e5fa0e0a3039ea1bf4ffb049ef7abbc2a30c Mon Sep 17 00:00:00 2001 From: duongminhhieu Date: Sun, 2 Jun 2024 19:55:55 +0700 Subject: [PATCH 02/22] refactor: add preAuthorize for findCarByImage --- .../learning/yasminishop/yasminiai/YasMiniAIService.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/learning/yasminishop/yasminiai/YasMiniAIService.java b/src/main/java/com/learning/yasminishop/yasminiai/YasMiniAIService.java index 7d1a6e6..71689d8 100644 --- a/src/main/java/com/learning/yasminishop/yasminiai/YasMiniAIService.java +++ b/src/main/java/com/learning/yasminishop/yasminiai/YasMiniAIService.java @@ -62,7 +62,7 @@ public List generateTextWithHistory(String prompt){ } } - // @PreAuthorize("hasRole('USER')") + @PreAuthorize("hasRole('USER')") public List findCarByImage(MultipartFile file){ try { var prompt = "Extract the name car to a list keyword and output them in JSON. If you don't find any information about the car, please output the list empty.\nExample response: [\"rolls\", \"royce\", \"wraith\"]"; @@ -74,12 +74,11 @@ public List findCarByImage(MultipartFile file){ ); String jsonContent = ResponseHandler.getText(content); + log.info("Extracted keywords from image: {}", jsonContent); List keywords = convertJsonToList(jsonContent).stream() .map(String::toLowerCase) .toList(); - log.info(keywords.toString()); - Set results = new HashSet<>(); for (String keyword : keywords) { List products = productRepository.searchByKeyword(keyword); @@ -108,5 +107,4 @@ private List convertJsonToList(String markdown) throws JsonProcessingExc private String extractJsonFromMarkdown(String markdown) { return markdown.replace("```json\n", "").replace("\n```", ""); } - } From 1d1972ee07955e4efcd6bfaca43bdc30dc3cf51a Mon Sep 17 00:00:00 2001 From: duongminhhieu Date: Mon, 3 Jun 2024 10:27:55 +0700 Subject: [PATCH 03/22] chore: change name Environment in yml --- src/main/resources/application.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f829ca2..9dba81e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -89,7 +89,7 @@ application: username: ${ADMIN_USERNAME} password: ${ADMIN_PASSWORD} firebase: - json-file: ${FIREBASE_JSON_FILE} + json-file: ${FIREBASE_JSON_FILE_PATH} storage-bucket: ${FIREBASE_STORAGE_BUCKET} springdoc: @@ -133,7 +133,7 @@ application: username: ${ADMIN_USERNAME} password: ${ADMIN_PASSWORD} firebase: - json-file: ${FIREBASE_JSON_FILE} + json-file: ${FIREBASE_JSON_FILE_PATH} storage-bucket: ${FIREBASE_STORAGE_BUCKET} springdoc: From b60700a96bdfeed4bb08ab1749f870a950d49fb9 Mon Sep 17 00:00:00 2001 From: duongminhhieu Date: Mon, 3 Jun 2024 10:30:58 +0700 Subject: [PATCH 04/22] unit-test: add createProduct_validRequest_success for ProductServiceTest --- .../service/ProductServiceTest.java | 125 +++++++++++++++++- 1 file changed, 124 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/learning/yasminishop/service/ProductServiceTest.java b/src/test/java/com/learning/yasminishop/service/ProductServiceTest.java index b6fc6e5..7d7e01c 100644 --- a/src/test/java/com/learning/yasminishop/service/ProductServiceTest.java +++ b/src/test/java/com/learning/yasminishop/service/ProductServiceTest.java @@ -1,15 +1,35 @@ package com.learning.yasminishop.service; +import com.learning.yasminishop.category.CategoryRepository; +import com.learning.yasminishop.category.dto.response.CategoryResponse; +import com.learning.yasminishop.common.entity.Category; +import com.learning.yasminishop.common.entity.Product; +import com.learning.yasminishop.common.entity.Storage; import com.learning.yasminishop.product.ProductRepository; import com.learning.yasminishop.product.ProductService; +import com.learning.yasminishop.product.dto.request.ProductRequest; +import com.learning.yasminishop.product.dto.response.ProductAdminResponse; +import com.learning.yasminishop.product.mapper.ProductMapper; +import com.learning.yasminishop.storage.StorageRepository; +import com.learning.yasminishop.storage.dto.response.StorageResponse; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.TestPropertySource; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Set; + @SpringBootTest @Slf4j @TestPropertySource("/test.properties") @@ -18,14 +38,103 @@ class ProductServiceTest { @Autowired private ProductService productService; - @Autowired + @MockBean private ProductRepository productRepository; + @MockBean + private ProductMapper productMapper; + + @MockBean + private CategoryRepository categoryRepository; + @MockBean + private StorageRepository storageRepository; + + + private ProductRequest productCreation; + private Product product; + private ProductAdminResponse productAdminResponse; + private Category category1; + private Category category2; + private Storage image1; + private Storage image2; @BeforeEach void setUp() { + productCreation = ProductRequest.builder() + .name("Product 1") + .description("Product 1 description") + .price(BigDecimal.valueOf(1_000_000)) + .isAvailable(true) + .isFeatured(true) + .categoryIds(Set.of("category1", "category2")) + .slug("product-1") + .sku("sku-1") + .imageIds(Set.of("image1", "image2")) + .build(); + + category1 = Category.builder() + .id("category1") + .name("Category 1") + .build(); + category2 = Category.builder() + .id("category2") + .name("Category 2") + .build(); + image1 = Storage.builder() + .id("image1") + .url("image1-url") + .build(); + + image2 = Storage.builder() + .id("image2") + .url("image2-url") + .build(); + + product = Product.builder() + .id("product-1") + .name("Product 1") + .description("Product 1 description") + .price(BigDecimal.valueOf(1_000_000)) + .isAvailable(true) + .isFeatured(true) + .slug("product-1") + .sku("sku-1") + .images(Set.of(image1, image2)) + .categories(Set.of(category1, category2)) + .build(); + + CategoryResponse categoryResponse1 = CategoryResponse.builder() + .id("category1") + .name("Category 1") + .build(); + CategoryResponse categoryResponse2 = CategoryResponse.builder() + .id("category2") + .name("Category 2") + .build(); + + StorageResponse storageResponse1 = StorageResponse.builder() + .id("image1") + .url("image1-url") + .build(); + StorageResponse storageResponse2 = StorageResponse.builder() + .id("image2") + .url("image2-url") + .build(); + + productAdminResponse = ProductAdminResponse.builder() + .id("product-1") + .name("Product 1") + .description("Product 1 description") + .price(BigDecimal.valueOf(1_000_000)) + .isAvailable(true) + .isFeatured(true) + .slug("product-1") + .sku("sku-1") + .images(Set.of(storageResponse1, storageResponse2)) + .categories(Set.of(categoryResponse1, categoryResponse2)) + .build(); } @@ -33,10 +142,24 @@ void setUp() { @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) void createProduct_validRequest_success() { // GIVEN + when(productRepository.existsBySlug(any())).thenReturn(false); + when(productRepository.existsBySku(any())).thenReturn(false); + when(categoryRepository.findAllById(Set.of("category1", "category2"))).thenReturn(List.of(category1, category2)); + when(storageRepository.findAllById(Set.of("image1", "image2"))).thenReturn(List.of(image1, image2)); + when(productMapper.toProduct(any())).thenReturn(product); + when(productRepository.save(product)).thenReturn(product); + when(productMapper.toProductAdminResponse(product)).thenReturn(productAdminResponse); // WHEN + ProductAdminResponse response = productService.create(productCreation); // THEN + assertThat(response).isNotNull() + .hasFieldOrPropertyWithValue("id", "product-1"); + + verify(categoryRepository).findAllById(Set.of("category1", "category2")); + verify(storageRepository).findAllById(Set.of("image1", "image2")); + verify(productRepository).save(product); } From 06e4570721554295a158e73a6521c69b6105fe97 Mon Sep 17 00:00:00 2001 From: duongminhhieu Date: Mon, 3 Jun 2024 17:53:14 +0700 Subject: [PATCH 05/22] feat: set up connect SSE for feature notification + endpoint get all notification for customer --- .../security/SecurityConfiguration.java | 3 +- .../common/entity/Notification.java | 33 ++++++++ .../yasminishop/common/entity/User.java | 3 + .../notification/NotificationController.java | 36 ++++++++ .../notification/NotificationRepository.java | 12 +++ .../notification/NotificationService.java | 84 +++++++++++++++++++ .../dto/response/NotificationResponse.java | 29 +++++++ .../mapper/NotificationMapper.java | 10 +++ .../yasminishop/order/OrderService.java | 23 +++-- 9 files changed, 227 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/learning/yasminishop/common/entity/Notification.java create mode 100644 src/main/java/com/learning/yasminishop/notification/NotificationController.java create mode 100644 src/main/java/com/learning/yasminishop/notification/NotificationRepository.java create mode 100644 src/main/java/com/learning/yasminishop/notification/NotificationService.java create mode 100644 src/main/java/com/learning/yasminishop/notification/dto/response/NotificationResponse.java create mode 100644 src/main/java/com/learning/yasminishop/notification/mapper/NotificationMapper.java diff --git a/src/main/java/com/learning/yasminishop/common/configs/security/SecurityConfiguration.java b/src/main/java/com/learning/yasminishop/common/configs/security/SecurityConfiguration.java index ad49afe..d2668fe 100644 --- a/src/main/java/com/learning/yasminishop/common/configs/security/SecurityConfiguration.java +++ b/src/main/java/com/learning/yasminishop/common/configs/security/SecurityConfiguration.java @@ -32,7 +32,8 @@ public class SecurityConfiguration { "/categories/{slug}", "/products", "/products/{slug}", - "/rating" + "/rating", + "notifications/**", }; private static final String[] ALLOWED_METHODS = { diff --git a/src/main/java/com/learning/yasminishop/common/entity/Notification.java b/src/main/java/com/learning/yasminishop/common/entity/Notification.java new file mode 100644 index 0000000..4d1552e --- /dev/null +++ b/src/main/java/com/learning/yasminishop/common/entity/Notification.java @@ -0,0 +1,33 @@ +package com.learning.yasminishop.common.entity; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "t_notification") +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Notification extends AuditEntity{ + @Id + @GeneratedValue(strategy = GenerationType.UUID) + String id; + + @Column(columnDefinition = "TEXT") + String title; + + @Column(columnDefinition = "TEXT") + String content; + + String thumbnail; + + String link; + + Boolean isRead = false; + + @ManyToOne + User user; + +} diff --git a/src/main/java/com/learning/yasminishop/common/entity/User.java b/src/main/java/com/learning/yasminishop/common/entity/User.java index 32cbc97..1bd4821 100644 --- a/src/main/java/com/learning/yasminishop/common/entity/User.java +++ b/src/main/java/com/learning/yasminishop/common/entity/User.java @@ -47,4 +47,7 @@ public class User extends AuditEntity { @ManyToMany(fetch = FetchType.LAZY) private Set roles; + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + private Set notifications; + } diff --git a/src/main/java/com/learning/yasminishop/notification/NotificationController.java b/src/main/java/com/learning/yasminishop/notification/NotificationController.java new file mode 100644 index 0000000..2743332 --- /dev/null +++ b/src/main/java/com/learning/yasminishop/notification/NotificationController.java @@ -0,0 +1,36 @@ +package com.learning.yasminishop.notification; + +import com.learning.yasminishop.common.dto.APIResponse; +import com.learning.yasminishop.notification.dto.response.NotificationResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.List; + +@RestController +@RequestMapping("/notifications") +@RequiredArgsConstructor +@Slf4j +public class NotificationController { + + private final NotificationService notificationService; + + @GetMapping(path = "/subscribe/{token}", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public SseEmitter subscribe( @PathVariable String token) { + SseEmitter emitter = new SseEmitter(Long.MAX_VALUE); + notificationService.addEmitter(emitter, token); + return emitter; + } + + @GetMapping + public APIResponse> getNotifications() { + List notifications = notificationService.getNotifications(); + return APIResponse.>builder() + .result(notifications) + .build(); + } + +} diff --git a/src/main/java/com/learning/yasminishop/notification/NotificationRepository.java b/src/main/java/com/learning/yasminishop/notification/NotificationRepository.java new file mode 100644 index 0000000..7984a4a --- /dev/null +++ b/src/main/java/com/learning/yasminishop/notification/NotificationRepository.java @@ -0,0 +1,12 @@ +package com.learning.yasminishop.notification; + +import com.learning.yasminishop.common.entity.Notification; +import com.learning.yasminishop.common.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface NotificationRepository extends JpaRepository{ + + List findAllByUserOrderByCreatedByDesc(User user); +} diff --git a/src/main/java/com/learning/yasminishop/notification/NotificationService.java b/src/main/java/com/learning/yasminishop/notification/NotificationService.java new file mode 100644 index 0000000..bbc2af7 --- /dev/null +++ b/src/main/java/com/learning/yasminishop/notification/NotificationService.java @@ -0,0 +1,84 @@ +package com.learning.yasminishop.notification; + +import com.learning.yasminishop.common.configs.security.JwtService; +import com.learning.yasminishop.common.entity.Notification; +import com.learning.yasminishop.common.entity.User; +import com.learning.yasminishop.common.exception.AppException; +import com.learning.yasminishop.common.exception.ErrorCode; +import com.learning.yasminishop.notification.dto.response.NotificationResponse; +import com.learning.yasminishop.notification.mapper.NotificationMapper; +import com.learning.yasminishop.user.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Service +@RequiredArgsConstructor +@Slf4j +@Transactional(readOnly = true) +public class NotificationService { + + private final NotificationRepository notificationRepository; + private final UserRepository userRepository; + private final NotificationMapper notificationMapper; + + private final JwtService jwtService; + private final Map emitters = new ConcurrentHashMap<>(); + + + public void addEmitter(SseEmitter emitter, String token) { + + if(!jwtService.isTokenValid(token)) { + log.error("Token is not valid"); + emitter.completeWithError(new AppException(ErrorCode.INVALID_TOKEN)); + return; + } + + String email = jwtService.extractUserEmail(token); + log.info("User with email {} subscribed to notifications", email); + emitters.put(email, emitter); + } + + public void removeEmitter(String email) { + emitters.remove(email); + } + + public void sendNotification(String clientId, NotificationResponse notificationResponse) { + SseEmitter emitter = emitters.get(clientId); + if (emitter != null) { + try { + emitter.send(notificationResponse); + } catch (IOException e) { + emitter.completeWithError(e); + emitters.remove(clientId); + } + } + } + + public List getNotifications() { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new AppException(ErrorCode.USER_NOT_FOUND)); + + List notifications = notificationRepository.findAllByUserOrderByCreatedByDesc(user); + return notifications + .stream() + .map(notificationMapper::toNotificationResponse) + .toList(); + } + + public void createNotification(Notification notification) { + notificationRepository.save(notification); + sendNotification(notification.getUser().getEmail(), notificationMapper.toNotificationResponse(notification)); + } + +} diff --git a/src/main/java/com/learning/yasminishop/notification/dto/response/NotificationResponse.java b/src/main/java/com/learning/yasminishop/notification/dto/response/NotificationResponse.java new file mode 100644 index 0000000..387b65c --- /dev/null +++ b/src/main/java/com/learning/yasminishop/notification/dto/response/NotificationResponse.java @@ -0,0 +1,29 @@ +package com.learning.yasminishop.notification.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldDefaults; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@FieldDefaults(level = lombok.AccessLevel.PRIVATE) +public class NotificationResponse { + + String id; + String title; + String content; + String thumbnail; + String link; + Boolean isRead; + + String createdBy; + LocalDateTime createdDate; + String lastModifiedBy; + LocalDateTime lastModifiedDate; +} diff --git a/src/main/java/com/learning/yasminishop/notification/mapper/NotificationMapper.java b/src/main/java/com/learning/yasminishop/notification/mapper/NotificationMapper.java new file mode 100644 index 0000000..f040bab --- /dev/null +++ b/src/main/java/com/learning/yasminishop/notification/mapper/NotificationMapper.java @@ -0,0 +1,10 @@ +package com.learning.yasminishop.notification.mapper; + +import com.learning.yasminishop.common.entity.Notification; +import com.learning.yasminishop.notification.dto.response.NotificationResponse; +import org.mapstruct.Mapper; + +@Mapper(componentModel = "spring") +public interface NotificationMapper { + NotificationResponse toNotificationResponse(Notification notification); +} diff --git a/src/main/java/com/learning/yasminishop/order/OrderService.java b/src/main/java/com/learning/yasminishop/order/OrderService.java index 00aca38..8760417 100644 --- a/src/main/java/com/learning/yasminishop/order/OrderService.java +++ b/src/main/java/com/learning/yasminishop/order/OrderService.java @@ -2,13 +2,11 @@ import com.learning.yasminishop.cart.CartItemRepository; import com.learning.yasminishop.common.dto.PaginationResponse; -import com.learning.yasminishop.common.entity.CartItem; -import com.learning.yasminishop.common.entity.Order; -import com.learning.yasminishop.common.entity.OrderItem; -import com.learning.yasminishop.common.entity.User; +import com.learning.yasminishop.common.entity.*; import com.learning.yasminishop.common.enumeration.EOrderStatus; import com.learning.yasminishop.common.exception.AppException; import com.learning.yasminishop.common.exception.ErrorCode; +import com.learning.yasminishop.notification.NotificationService; import com.learning.yasminishop.order.dto.filter.OrderFilter; import com.learning.yasminishop.order.dto.request.OrderRequest; import com.learning.yasminishop.order.dto.response.OrderAdminResponse; @@ -39,6 +37,8 @@ public class OrderService { private final UserRepository userRepository; private final CartItemRepository cartItemRepository; + private final NotificationService notificationService; + private final OrderMapper orderMapper; @@ -75,7 +75,6 @@ public OrderResponse create(OrderRequest orderRequest) { } - @PreAuthorize("hasRole('USER')") public List getAllOrderByUser() { String email = SecurityContextHolder.getContext().getAuthentication().getName(); @@ -131,6 +130,8 @@ public void updateOrderStatus(String id, String status) { order.setStatus(EOrderStatus.valueOf(status)); orderRepository.save(order); + + sendNotification(order); } private Order createAOrder(OrderRequest orderRequest, User user, List cartItemsToOrder) { @@ -167,5 +168,17 @@ private Order createAOrder(OrderRequest orderRequest, User user, List return order; } + private void sendNotification(Order order) { + OrderItem orderItem = order.getOrderItems().stream().findFirst().orElseThrow(() -> new AppException(ErrorCode.ORDER_NOT_FOUND)); + Notification notification = Notification.builder() + .user(order.getUser()) + .title("Your order status has been updated") + .content("Your order status has been updated to " + order.getStatus()) + .isRead(false) + .thumbnail(orderItem.getProduct().getThumbnail()) + .build(); + + notificationService.createNotification(notification); + } } From 05b56d416eeb3e5de6817a1f841637f1e45e43f8 Mon Sep 17 00:00:00 2001 From: duongminhhieu Date: Tue, 4 Jun 2024 13:31:13 +0700 Subject: [PATCH 06/22] fix: change jpa method to findAllByUserOrderByCreatedDateDesc for NotificationRepository --- .../yasminishop/notification/NotificationRepository.java | 2 +- .../learning/yasminishop/notification/NotificationService.java | 2 +- .../notification/dto/response/NotificationResponse.java | 3 --- src/main/java/com/learning/yasminishop/order/OrderService.java | 1 + 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/learning/yasminishop/notification/NotificationRepository.java b/src/main/java/com/learning/yasminishop/notification/NotificationRepository.java index 7984a4a..ee078b1 100644 --- a/src/main/java/com/learning/yasminishop/notification/NotificationRepository.java +++ b/src/main/java/com/learning/yasminishop/notification/NotificationRepository.java @@ -8,5 +8,5 @@ public interface NotificationRepository extends JpaRepository{ - List findAllByUserOrderByCreatedByDesc(User user); + List findAllByUserOrderByCreatedDateDesc(User user); } diff --git a/src/main/java/com/learning/yasminishop/notification/NotificationService.java b/src/main/java/com/learning/yasminishop/notification/NotificationService.java index bbc2af7..15f5273 100644 --- a/src/main/java/com/learning/yasminishop/notification/NotificationService.java +++ b/src/main/java/com/learning/yasminishop/notification/NotificationService.java @@ -69,7 +69,7 @@ public List getNotifications() { User user = userRepository.findByEmail(email) .orElseThrow(() -> new AppException(ErrorCode.USER_NOT_FOUND)); - List notifications = notificationRepository.findAllByUserOrderByCreatedByDesc(user); + List notifications = notificationRepository.findAllByUserOrderByCreatedDateDesc(user); return notifications .stream() .map(notificationMapper::toNotificationResponse) diff --git a/src/main/java/com/learning/yasminishop/notification/dto/response/NotificationResponse.java b/src/main/java/com/learning/yasminishop/notification/dto/response/NotificationResponse.java index 387b65c..d118d9c 100644 --- a/src/main/java/com/learning/yasminishop/notification/dto/response/NotificationResponse.java +++ b/src/main/java/com/learning/yasminishop/notification/dto/response/NotificationResponse.java @@ -22,8 +22,5 @@ public class NotificationResponse { String link; Boolean isRead; - String createdBy; LocalDateTime createdDate; - String lastModifiedBy; - LocalDateTime lastModifiedDate; } diff --git a/src/main/java/com/learning/yasminishop/order/OrderService.java b/src/main/java/com/learning/yasminishop/order/OrderService.java index 8760417..075934e 100644 --- a/src/main/java/com/learning/yasminishop/order/OrderService.java +++ b/src/main/java/com/learning/yasminishop/order/OrderService.java @@ -176,6 +176,7 @@ private void sendNotification(Order order) { .content("Your order status has been updated to " + order.getStatus()) .isRead(false) .thumbnail(orderItem.getProduct().getThumbnail()) + .link("/order/" + order.getId()) .build(); notificationService.createNotification(notification); From 9eced9c0cbd85363b8aa28f6b6819f2897654829 Mon Sep 17 00:00:00 2001 From: duongminhhieu Date: Tue, 4 Jun 2024 13:32:22 +0700 Subject: [PATCH 07/22] refactor: delete function remove emitter --- .../yasminishop/notification/NotificationService.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/com/learning/yasminishop/notification/NotificationService.java b/src/main/java/com/learning/yasminishop/notification/NotificationService.java index 15f5273..72aeb6c 100644 --- a/src/main/java/com/learning/yasminishop/notification/NotificationService.java +++ b/src/main/java/com/learning/yasminishop/notification/NotificationService.java @@ -47,10 +47,6 @@ public void addEmitter(SseEmitter emitter, String token) { emitters.put(email, emitter); } - public void removeEmitter(String email) { - emitters.remove(email); - } - public void sendNotification(String clientId, NotificationResponse notificationResponse) { SseEmitter emitter = emitters.get(clientId); if (emitter != null) { From d9cff25021bafe0724e7c812bb8ddd353c8a78d0 Mon Sep 17 00:00:00 2001 From: duongminhhieu Date: Tue, 4 Jun 2024 16:43:56 +0700 Subject: [PATCH 08/22] fix: JPA never closes connections when the SSE close --- src/main/resources/application.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f829ca2..bac1be6 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -7,6 +7,8 @@ spring: multipart: max-file-size: 5MB max-request-size: 5MB + jpa: + open-in-view: false server: port: 8080 From 1b70aa9b821425c7dc60f23d8ba24a59fb6d8ec4 Mon Sep 17 00:00:00 2001 From: duongminhhieu Date: Wed, 5 Jun 2024 09:54:47 +0700 Subject: [PATCH 09/22] unit-test:add get by slug, getById and getAllforAdmin test product service --- .../service/ProductServiceTest.java | 122 +++++++++++++++++- 1 file changed, 116 insertions(+), 6 deletions(-) diff --git a/src/test/java/com/learning/yasminishop/service/ProductServiceTest.java b/src/test/java/com/learning/yasminishop/service/ProductServiceTest.java index 7d7e01c..fd25a88 100644 --- a/src/test/java/com/learning/yasminishop/service/ProductServiceTest.java +++ b/src/test/java/com/learning/yasminishop/service/ProductServiceTest.java @@ -2,13 +2,14 @@ import com.learning.yasminishop.category.CategoryRepository; import com.learning.yasminishop.category.dto.response.CategoryResponse; -import com.learning.yasminishop.common.entity.Category; -import com.learning.yasminishop.common.entity.Product; -import com.learning.yasminishop.common.entity.Storage; +import com.learning.yasminishop.common.dto.PaginationResponse; +import com.learning.yasminishop.common.entity.*; import com.learning.yasminishop.product.ProductRepository; import com.learning.yasminishop.product.ProductService; +import com.learning.yasminishop.product.dto.filter.ProductFilter; import com.learning.yasminishop.product.dto.request.ProductRequest; import com.learning.yasminishop.product.dto.response.ProductAdminResponse; +import com.learning.yasminishop.product.dto.response.ProductResponse; import com.learning.yasminishop.product.mapper.ProductMapper; import com.learning.yasminishop.storage.StorageRepository; import com.learning.yasminishop.storage.dto.response.StorageResponse; @@ -18,17 +19,23 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.TestPropertySource; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; import java.math.BigDecimal; import java.util.List; +import java.util.Optional; import java.util.Set; +import java.util.UUID; @SpringBootTest @Slf4j @@ -59,6 +66,9 @@ class ProductServiceTest { private Storage image1; private Storage image2; + private ProductResponse productResponse; + private ProductFilter productFilter; + @BeforeEach void setUp() { productCreation = ProductRequest.builder() @@ -92,6 +102,19 @@ void setUp() { .url("image2-url") .build(); + Set attributes = Set.of( + ProductAttribute.builder() + .id(UUID.randomUUID().toString()) + .name("Color") + .values(Set.of( + ProductAttributeValue.builder() + .id(UUID.randomUUID().toString()) + .value("Red") + .build() + )) + .build() + ); + product = Product.builder() .id("product-1") .name("Product 1") @@ -101,6 +124,9 @@ void setUp() { .isFeatured(true) .slug("product-1") .sku("sku-1") + .averageRating(4.0F) + .quantity(10L) + .attributes(attributes) .images(Set.of(image1, image2)) .categories(Set.of(category1, category2)) .build(); @@ -135,6 +161,35 @@ void setUp() { .images(Set.of(storageResponse1, storageResponse2)) .categories(Set.of(categoryResponse1, categoryResponse2)) .build(); + + productResponse = ProductResponse.builder() + .id("product-1") + .name("Product 1") + .description("Product 1 description") + .price(BigDecimal.valueOf(1_000_000)) + .isAvailable(true) + .isFeatured(true) + .slug("product-1") + .sku("sku-1") + .images(Set.of(storageResponse1, storageResponse2)) + .categories(Set.of(categoryResponse1, categoryResponse2)) + .build(); + + + productFilter = new ProductFilter(); + productFilter.setCategoryIds(new String[]{"category1", "category2"}); + + Category category1 = new Category(); + category1.setId("category1"); + Category category2 = new Category(); + category2.setId("category2"); + + Product product = new Product(); + product.setId("product-1"); + + ProductAdminResponse productAdminResponse = new ProductAdminResponse(); + productAdminResponse.setId("product-1"); + } @@ -162,5 +217,60 @@ void createProduct_validRequest_success() { verify(productRepository).save(product); } + @Test + void getBySlug_validSlug_success() { + // GIVEN + when(productRepository.findBySlug("product-1")).thenReturn(Optional.of(product)); + when(productMapper.toProductResponse(any())).thenReturn(productResponse); + + // WHEN + ProductResponse response = productService.getBySlug("product-1"); + + // THEN + assertThat(response).isNotNull() + .hasFieldOrPropertyWithValue("id", "product-1"); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void getById_validId_success() { + // GIVEN + when(productRepository.findById(any())).thenReturn(Optional.of(product)); + when(productMapper.toProductAdminResponse(any())).thenReturn(productAdminResponse); + + // WHEN + ProductAdminResponse productAdminResponse1 = productService.getById("product-1"); + + // THEN + assertThat(productAdminResponse1).isNotNull() + .hasFieldOrPropertyWithValue("id", "product-1"); + + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void getAllProductsForAdmin_validRequest_success() { + // GIVEN + + Pageable pageable = PageRequest.of(0, 5); + Page productPage = new PageImpl<>(List.of(product)); + + + when(categoryRepository.findAllById(anyList())).thenReturn(List.of(category1, category2)); + when(productRepository.findAll(any(Specification.class), eq(pageable))).thenReturn(productPage); + when(productMapper.toProductAdminResponse(any(Product.class))).thenReturn(productAdminResponse); + + // WHEN + PaginationResponse response = productService.getAllProductsForAdmin(productFilter, pageable); + + // THEN + assertThat(response).isNotNull(); + assertThat(response.getData().getFirst().getId()).isEqualTo("product-1"); + + verify(categoryRepository).findAllById(anyList()); + verify(productRepository).findAll(any(Specification.class), eq(pageable)); + verify(productMapper, times(1)).toProductAdminResponse(any(Product.class)); + } + } From 717be304c0afef113f3d9d268e5a34444034d765 Mon Sep 17 00:00:00 2001 From: duongminhhieu Date: Wed, 5 Jun 2024 11:04:12 +0700 Subject: [PATCH 10/22] feat: check relationship between product with (order, cart) when delete product --- .../yasminishop/common/entity/Product.java | 8 ++++++- .../common/exception/ErrorCode.java | 2 ++ .../yasminishop/product/ProductService.java | 21 +++++++++++++------ 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/learning/yasminishop/common/entity/Product.java b/src/main/java/com/learning/yasminishop/common/entity/Product.java index 2d8eabf..58ec33a 100644 --- a/src/main/java/com/learning/yasminishop/common/entity/Product.java +++ b/src/main/java/com/learning/yasminishop/common/entity/Product.java @@ -55,7 +55,13 @@ public class Product extends AuditEntity{ @ManyToMany private Set categories; - @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + @OneToMany(cascade = CascadeType.ALL) private Set images; + @OneToMany(cascade = CascadeType.ALL) + private Set orderItems; + + @OneToMany(mappedBy = "product", cascade = CascadeType.ALL) + private Set cartItems; + } diff --git a/src/main/java/com/learning/yasminishop/common/exception/ErrorCode.java b/src/main/java/com/learning/yasminishop/common/exception/ErrorCode.java index c1949b0..4d919f3 100644 --- a/src/main/java/com/learning/yasminishop/common/exception/ErrorCode.java +++ b/src/main/java/com/learning/yasminishop/common/exception/ErrorCode.java @@ -35,6 +35,8 @@ public enum ErrorCode { CART_ITEM_NOT_FOUND(1022, "Cart item not found", HttpStatus.NOT_FOUND), ORDER_NOT_FOUND(1023, "Order not found", HttpStatus.NOT_FOUND), GENERATIVE_AI_ERROR(1024, "Error in generative AI", HttpStatus.INTERNAL_SERVER_ERROR), + PRODUCT_IN_ORDER(1025, "Product is in order", HttpStatus.BAD_REQUEST), + PRODUCT_IN_CART(1026, "Product is in cart", HttpStatus.BAD_REQUEST), // Constraint violation diff --git a/src/main/java/com/learning/yasminishop/product/ProductService.java b/src/main/java/com/learning/yasminishop/product/ProductService.java index 29e916d..5886af7 100644 --- a/src/main/java/com/learning/yasminishop/product/ProductService.java +++ b/src/main/java/com/learning/yasminishop/product/ProductService.java @@ -42,8 +42,7 @@ public class ProductService { public PaginationResponse getAllProducts( ProductFilter productFilter, - Pageable pageable) - { + Pageable pageable) { // check if the categoryIds are valid List categories = categoryRepository.findAllById(List.of(productFilter.getCategoryIds())); @@ -123,8 +122,7 @@ public ProductAdminResponse getById(String id) { @PreAuthorize("hasRole('ADMIN')") public PaginationResponse getAllProductsForAdmin( ProductFilter productFilter, - Pageable pageable) - { + Pageable pageable) { // check if the categoryIds are valid List categories = categoryRepository.findAllById(List.of(productFilter.getCategoryIds())); @@ -195,8 +193,7 @@ public ProductAdminResponse update(String id, ProductRequest productUpdate) { productMapper.updateProduct(product, productUpdate); product.setCategories(new HashSet<>(categories)); - product.getImages().clear(); - product.getImages().addAll(images); // update images + product.setImages(new HashSet<>(images)); // update images product.setThumbnail(images.getFirst().getUrl()); return productMapper.toProductAdminResponse(productRepository.save(product)); @@ -211,6 +208,18 @@ public void delete(List ids) { if (products.size() != ids.size()) { throw new AppException(ErrorCode.PRODUCT_NOT_FOUND); } + + // check if the product is in any order, cart + for (Product product : products) { + + if (!product.getOrderItems().isEmpty()) { + throw new AppException(ErrorCode.PRODUCT_IN_ORDER); + } + if (!product.getCartItems().isEmpty()) { + throw new AppException(ErrorCode.PRODUCT_IN_CART); + } + } + productRepository.deleteAll(products); } From cb1278c97ccda598081a95f34039e2bd5d5cb712 Mon Sep 17 00:00:00 2001 From: duongminhhieu Date: Wed, 5 Jun 2024 11:09:13 +0700 Subject: [PATCH 11/22] unit-test: write all test for ProductServiceTest --- .../service/ProductServiceTest.java | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/src/test/java/com/learning/yasminishop/service/ProductServiceTest.java b/src/test/java/com/learning/yasminishop/service/ProductServiceTest.java index fd25a88..0ed514f 100644 --- a/src/test/java/com/learning/yasminishop/service/ProductServiceTest.java +++ b/src/test/java/com/learning/yasminishop/service/ProductServiceTest.java @@ -4,6 +4,7 @@ import com.learning.yasminishop.category.dto.response.CategoryResponse; import com.learning.yasminishop.common.dto.PaginationResponse; import com.learning.yasminishop.common.entity.*; +import com.learning.yasminishop.common.exception.AppException; import com.learning.yasminishop.product.ProductRepository; import com.learning.yasminishop.product.ProductService; import com.learning.yasminishop.product.dto.filter.ProductFilter; @@ -28,6 +29,7 @@ import org.springframework.test.context.TestPropertySource; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @@ -65,6 +67,10 @@ class ProductServiceTest { private Category category2; private Storage image1; private Storage image2; + private List categories; + private List images; + + private ProductRequest productUpdate; private ProductResponse productResponse; private ProductFilter productFilter; @@ -92,6 +98,9 @@ void setUp() { .id("category2") .name("Category 2") .build(); + + categories = List.of(category1, category2); + image1 = Storage.builder() .id("image1") .url("image1-url") @@ -102,6 +111,8 @@ void setUp() { .url("image2-url") .build(); + images = List.of(image1, image2); + Set attributes = Set.of( ProductAttribute.builder() .id(UUID.randomUUID().toString()) @@ -175,6 +186,17 @@ void setUp() { .categories(Set.of(categoryResponse1, categoryResponse2)) .build(); + productUpdate = ProductRequest.builder() + .name("Product 1") + .description("Product 1 description") + .price(BigDecimal.valueOf(1_000_000)) + .isAvailable(true) + .isFeatured(true) + .categoryIds(Set.of("category1", "category2")) + .slug("product-1") + .sku("sku-1") + .imageIds(Set.of("image1", "image2")) + .build(); productFilter = new ProductFilter(); productFilter.setCategoryIds(new String[]{"category1", "category2"}); @@ -272,5 +294,112 @@ void getAllProductsForAdmin_validRequest_success() { verify(productMapper, times(1)).toProductAdminResponse(any(Product.class)); } + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void toggleAvailability_validRequest_success() { + // GIVEN + List ids = List.of("product-1", "product-2"); + + Product product1 = new Product(); + product1.setId("product-1"); + product1.setIsAvailable(true); + + Product product2 = new Product(); + product2.setId("product-2"); + product2.setIsAvailable(false); + + List products = List.of(product1, product2); + + when(productRepository.findAllById(ids)).thenReturn(products); + + // WHEN + productService.toggleAvailability(ids); + + // THEN + verify(productRepository).findAllById(ids); + verify(productRepository).saveAll(products); + + // Check that the availability of the products has been toggled + assertThat(product1.getIsAvailable()).isFalse(); + assertThat(product2.getIsAvailable()).isTrue(); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void update_validRequest_success() { + // GIVEN + String id = "product-1"; + + when(productRepository.findById(id)).thenReturn(Optional.of(product)); + when(categoryRepository.existsBySlug(productUpdate.getSlug())).thenReturn(false); + when(categoryRepository.existsBySlug(productUpdate.getSku())).thenReturn(false); + when(categoryRepository.findAllById(anySet())).thenReturn(categories); + when(storageRepository.findAllById(anySet())).thenReturn(images); + when(productRepository.save(any(Product.class))).thenReturn(product); + when(productMapper.toProductAdminResponse(any(Product.class))).thenReturn(productAdminResponse); + + // WHEN + ProductAdminResponse response = productService.update(id, productUpdate); + + // THEN + assertThat(response).isNotNull(); + assertThat(response.getId()).isEqualTo("product-1"); + + verify(productRepository).findById(id); + verify(categoryRepository).findAllById(anySet()); + verify(storageRepository).findAllById(anySet()); + verify(productRepository).save(any(Product.class)); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void delete_allProductIdsExistAndNoneInOrderOrCart_success() { + // GIVEN + List ids = List.of("product-1"); + product.setOrderItems(Set.of()); + product.setCartItems(Set.of()); + List products = List.of(product); + + when(productRepository.findAllById(ids)).thenReturn(products); + + // WHEN + productService.delete(ids); + + // THEN + verify(productRepository).findAllById(ids); + verify(productRepository).deleteAll(products); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void delete_someProductIdsDoNotExist_throwsException() { + // GIVEN + List ids = List.of("product-1", "product-2"); + Product product1 = new Product(); + product1.setId("product-1"); + List products = List.of(product1); + + when(productRepository.findAllById(ids)).thenReturn(products); + + // THEN + assertThrows(AppException.class, () -> productService.delete(ids)); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void delete_productInOrderOrCart_throwsException() { + // GIVEN + List ids = List.of("product-1"); + Product product1 = new Product(); + product1.setId("product-1"); + product1.setOrderItems(Set.of(new OrderItem())); // product is in an order + List products = List.of(product1); + + when(productRepository.findAllById(ids)).thenReturn(products); + + // THEN + assertThrows(AppException.class, () -> productService.delete(ids)); + } + } From f0cadb8515c6f17ef8e9a555d3b5270b4d163745 Mon Sep 17 00:00:00 2001 From: duongminhhieu Date: Wed, 5 Jun 2024 11:59:11 +0700 Subject: [PATCH 12/22] refactor: check product isAvailable before create order --- .../java/com/learning/yasminishop/cart/CartItemRepository.java | 1 + src/main/java/com/learning/yasminishop/order/OrderService.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/learning/yasminishop/cart/CartItemRepository.java b/src/main/java/com/learning/yasminishop/cart/CartItemRepository.java index fc0e2c2..cff7711 100644 --- a/src/main/java/com/learning/yasminishop/cart/CartItemRepository.java +++ b/src/main/java/com/learning/yasminishop/cart/CartItemRepository.java @@ -13,4 +13,5 @@ public interface CartItemRepository extends JpaRepository{ Optional findByProductAndUser(Product product, User user); List findAllByUserOrderByLastModifiedDateDesc(User user); + List findAllByUserAndProductIsAvailable(User user, Boolean isAvailable); } diff --git a/src/main/java/com/learning/yasminishop/order/OrderService.java b/src/main/java/com/learning/yasminishop/order/OrderService.java index 075934e..d49af31 100644 --- a/src/main/java/com/learning/yasminishop/order/OrderService.java +++ b/src/main/java/com/learning/yasminishop/order/OrderService.java @@ -51,7 +51,7 @@ public OrderResponse create(OrderRequest orderRequest) { User user = userRepository.findByEmail(email).orElseThrow(() -> new AppException(ErrorCode.USER_NOT_FOUND)); // get the cart items of the user - List cartItems = cartItemRepository.findAllByUserOrderByLastModifiedDateDesc(user); + List cartItems = cartItemRepository.findAllByUserAndProductIsAvailable(user, true); Set cartItemRequestIds = orderRequest.getCartItemIds(); // Extract IDs from cartItems From 59dccacae2c4fa944e989d15be2cfa33099b87a9 Mon Sep 17 00:00:00 2001 From: duongminhhieu Date: Wed, 5 Jun 2024 15:50:07 +0700 Subject: [PATCH 13/22] unit-test: write all unit-test for CategoryController --- .../controller/CategoryControllerTest.java | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 src/test/java/com/learning/yasminishop/controller/CategoryControllerTest.java diff --git a/src/test/java/com/learning/yasminishop/controller/CategoryControllerTest.java b/src/test/java/com/learning/yasminishop/controller/CategoryControllerTest.java new file mode 100644 index 0000000..0b1e1bc --- /dev/null +++ b/src/test/java/com/learning/yasminishop/controller/CategoryControllerTest.java @@ -0,0 +1,194 @@ +package com.learning.yasminishop.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.learning.yasminishop.category.CategoryRepository; +import com.learning.yasminishop.category.dto.request.CategoryCreation; +import com.learning.yasminishop.category.dto.request.CategoryIds; +import com.learning.yasminishop.common.entity.Category; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import java.util.List; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@Slf4j +@AutoConfigureMockMvc +@TestPropertySource("/test.properties") +class CategoryControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private CategoryRepository categoryRepository; + + private CategoryCreation categoryCreation; + private Category category; + private CategoryIds categoryIds; + + @BeforeEach + void setUp() { + + categoryCreation = CategoryCreation.builder() + .name("Category Create") + .description("Category create description") + .slug("category-create") + .build(); + category = Category.builder() + .name("Category 1") + .description("Category 1 description") + .slug("category-1") + .isAvailable(true) + .build(); + + categoryIds = CategoryIds.builder() + .ids(List.of("category-1")) + .build(); + + } + + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void createCategory_validRequest_success() throws Exception { + + // GIVEN + ObjectMapper objectMapper = new ObjectMapper(); + String categoryCreationJson = objectMapper.writeValueAsString(categoryCreation); + + // WHEN THEN + mockMvc.perform(MockMvcRequestBuilders.post("/categories") + .contentType(MediaType.APPLICATION_JSON) + .content(categoryCreationJson)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("internalCode").value(1000)) + .andExpect(jsonPath("result.name").value("Category Create")) + .andExpect(jsonPath("result.description").value("Category create description")) + .andExpect(jsonPath("result.slug").value("category-create")); + } + + @Test + @DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD) + void getAllCategories_validRequest_success() throws Exception { + // GIVEN + categoryRepository.save(category); + + // WHEN THEN + mockMvc.perform(MockMvcRequestBuilders.get("/categories")) + .andExpect(status().isOk()) + .andExpect(jsonPath("internalCode").value(1000)) + .andExpect(jsonPath("result").isArray()); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + @DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD) + void getCategoryBySlug_validRequest_success() throws Exception { + // GIVEN + categoryRepository.save(category); + + + // WHEN THEN + mockMvc.perform(MockMvcRequestBuilders.get("/categories/slug/category-1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("internalCode").value(1000)) + .andExpect(jsonPath("result.name").value("Category 1")) + .andExpect(jsonPath("result.description").value("Category 1 description")) + .andExpect(jsonPath("result.slug").value("category-1")); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + @DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD) + void getCategoryById_validRequest_success() throws Exception { + // GIVEN + Category saved = categoryRepository.save(category); + String id = saved.getId(); + + // WHEN THEN + mockMvc.perform(MockMvcRequestBuilders.get("/categories/" + id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("internalCode").value(1000)) + .andExpect(jsonPath("result.id").value(id)) + .andExpect(jsonPath("result.name").value("Category 1")) + .andExpect(jsonPath("result.description").value("Category 1 description")) + .andExpect(jsonPath("result.slug").value("category-1")); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + @DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD) + void toggleAvailability_validRequest_success() throws Exception { + // GIVEN + ObjectMapper objectMapper = new ObjectMapper(); + Category saved = categoryRepository.save(category); + + categoryIds.setIds(List.of(saved.getId())); + String idsJson = objectMapper.writeValueAsString(categoryIds); + + // WHEN THEN + mockMvc.perform(MockMvcRequestBuilders.patch("/categories/toggle-availability") + .contentType(MediaType.APPLICATION_JSON) + .content(idsJson)) + .andExpect(status().isOk()) + .andExpect(jsonPath("internalCode").value(1000)) + .andExpect(jsonPath("message").value("Categories availability toggled successfully")); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + @DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD) + void deleteCategories_validRequest_success() throws Exception { + // GIVEN + ObjectMapper objectMapper = new ObjectMapper(); + Category saved = categoryRepository.save(category); + + categoryIds.setIds(List.of(saved.getId())); + String idsJson = objectMapper.writeValueAsString(categoryIds); + + // WHEN THEN + mockMvc.perform(MockMvcRequestBuilders.delete("/categories") + .contentType(MediaType.APPLICATION_JSON) + .content(idsJson)) + .andExpect(status().isOk()) + .andExpect(jsonPath("internalCode").value(1000)) + .andExpect(jsonPath("message").value("Category deleted successfully")); + } + + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + @DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD) + void getAllCategoriesForAdmin_validRequest_success() throws Exception { + // GIVEN + String name = "Category"; + boolean isAvailable = true; + int page = 1; + int itemsPerPage = 10; + + // WHEN THEN + mockMvc.perform(MockMvcRequestBuilders.get("/categories/admin") + .param("name", name) + .param("isAvailable", Boolean.toString(isAvailable)) + .param("page", Integer.toString(page)) + .param("itemsPerPage", Integer.toString(itemsPerPage))) + .andExpect(status().isOk()) + .andExpect(jsonPath("internalCode").value(1000)) + .andExpect(jsonPath("result").exists()); + } + + +} From 60c4abba8dafe91d432e67bcdd6e2be74eb23848 Mon Sep 17 00:00:00 2001 From: duongminhhieu Date: Wed, 5 Jun 2024 16:07:46 +0700 Subject: [PATCH 14/22] chore: change name, artifactId in pom.xml file --- pom.xml | 9 ++++----- .../learning/yasminishop/common/entity/Notification.java | 2 ++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index f522069..c15391d 100644 --- a/pom.xml +++ b/pom.xml @@ -9,10 +9,10 @@ com.learning - SpringSecurity + YasMiniShop 1.0.0 - SpringSecurity - SpringSecurity + YasMini + YasMini CarShop 21 1.18.30 @@ -198,10 +198,9 @@ + **/common/** **/dto/** - **/entity/** **/mapper/** - **/configs/** diff --git a/src/main/java/com/learning/yasminishop/common/entity/Notification.java b/src/main/java/com/learning/yasminishop/common/entity/Notification.java index 4d1552e..6b6df63 100644 --- a/src/main/java/com/learning/yasminishop/common/entity/Notification.java +++ b/src/main/java/com/learning/yasminishop/common/entity/Notification.java @@ -2,6 +2,7 @@ import jakarta.persistence.*; import lombok.*; +import lombok.experimental.FieldDefaults; @Entity @Table(name = "t_notification") @@ -10,6 +11,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) public class Notification extends AuditEntity{ @Id @GeneratedValue(strategy = GenerationType.UUID) From 8e8e36800e74cbdcc7b656baacd3154af9aca218 Mon Sep 17 00:00:00 2001 From: duongminhhieu Date: Wed, 5 Jun 2024 16:38:13 +0700 Subject: [PATCH 15/22] unit-test: write all unit-test for CategoryService --- .../service/CategoryServiceTest.java | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 src/test/java/com/learning/yasminishop/service/CategoryServiceTest.java diff --git a/src/test/java/com/learning/yasminishop/service/CategoryServiceTest.java b/src/test/java/com/learning/yasminishop/service/CategoryServiceTest.java new file mode 100644 index 0000000..6f8b421 --- /dev/null +++ b/src/test/java/com/learning/yasminishop/service/CategoryServiceTest.java @@ -0,0 +1,174 @@ +package com.learning.yasminishop.service; + +import com.learning.yasminishop.category.CategoryRepository; +import com.learning.yasminishop.category.CategoryService; +import com.learning.yasminishop.category.dto.request.CategoryCreation; +import com.learning.yasminishop.category.dto.request.CategoryUpdate; +import com.learning.yasminishop.category.dto.response.CategoryResponse; +import com.learning.yasminishop.common.entity.Category; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.TestPropertySource; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@SpringBootTest +@Slf4j +@TestPropertySource("/test.properties") +class CategoryServiceTest { + + @Autowired + private CategoryService categoryService; + + @MockBean + private CategoryRepository categoryRepository; + + private CategoryCreation categoryCreation; + + private Category category; + private CategoryUpdate categoryUpdate; + + @BeforeEach + void setUp() { + + categoryCreation = CategoryCreation.builder() + .name("Category Create") + .description("Category create description") + .slug("category-create") + .build(); + + category = Category.builder() + .id("cate1") + .name("Category 1") + .description("Category 1 description") + .slug("category-1") + .isAvailable(true) + .products(Set.of()) + .build(); + + categoryUpdate = CategoryUpdate.builder() + .name("Category update") + .description("Category 1 update description") + .slug("category-1") + .isAvailable(true) + .build(); + + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void createCategory_validRequest_success() { + // GIVEN + when(categoryRepository.existsBySlug(categoryCreation.getSlug())).thenReturn(false); + when(categoryRepository.save(any(Category.class))).thenReturn(category); + + // WHEN + CategoryResponse response = categoryService.create(categoryCreation); + + // THEN + assertThat(response).isNotNull() + .hasFieldOrPropertyWithValue("name", "Category 1") + .hasFieldOrPropertyWithValue("description", "Category 1 description") + .hasFieldOrPropertyWithValue("slug", "category-1"); + } + + @Test + void getAllCategories_validRequest_success() { + // GIVEN + when(categoryRepository.findAllByIsAvailable(true)).thenReturn(List.of(category)); + + // WHEN + List response = categoryService.getAllCategories(); + + // THEN + assertThat(response).isNotNull() + .hasSize(1) + .first() + .hasFieldOrPropertyWithValue("name", "Category 1") + .hasFieldOrPropertyWithValue("description", "Category 1 description") + .hasFieldOrPropertyWithValue("slug", "category-1"); + } + + @Test + void getBySlug_validRequest_success() { + // GIVEN + when(categoryRepository.findBySlug("category-1")).thenReturn(Optional.of(category)); + + // WHEN + CategoryResponse response = categoryService.getBySlug("category-1"); + + // THEN + assertThat(response).isNotNull() + .hasFieldOrPropertyWithValue("name", "Category 1") + .hasFieldOrPropertyWithValue("description", "Category 1 description") + .hasFieldOrPropertyWithValue("slug", "category-1"); + } + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void getCategory_validRequest_success() { + // GIVEN + when(categoryRepository.findById("cate1")).thenReturn(Optional.of(category)); + + // WHEN + var response = categoryService.getCategory("cate1"); + + // THEN + assertThat(response).isNotNull() + .hasFieldOrPropertyWithValue("name", "Category 1") + .hasFieldOrPropertyWithValue("description", "Category 1 description") + .hasFieldOrPropertyWithValue("slug", "category-1"); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void delete_validRequest_success() { + // GIVEN + when(categoryRepository.findAllById(List.of("cate1"))).thenReturn(List.of(category)); + + // WHEN + categoryService.delete(List.of("cate1")); + + // THEN + verify(categoryRepository).deleteAll(List.of(category)); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void toggleAvailability_validRequest_success() { + // GIVEN + when(categoryRepository.findAllById(List.of("cate1"))).thenReturn(List.of(category)); + + // WHEN + categoryService.toggleAvailability(List.of("cate1")); + + // THEN + verify(categoryRepository).saveAll(List.of(category)); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void update_validRequest_success() { + // GIVEN + when(categoryRepository.findById("cate1")).thenReturn(Optional.of(category)); + when(categoryRepository.save(any(Category.class))).thenReturn(category); + + // WHEN + var response = categoryService.update("cate1", categoryUpdate); + + // THEN + assertThat(response).isNotNull() + .hasFieldOrPropertyWithValue("name", "Category update"); + } +} From 7333208e1de0f931f7294957cdb350d301a577f9 Mon Sep 17 00:00:00 2001 From: duongminhhieu Date: Wed, 5 Jun 2024 17:34:31 +0700 Subject: [PATCH 16/22] unit-test: write all unit-test for CartController --- .../controller/CartControllerTest.java | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 src/test/java/com/learning/yasminishop/controller/CartControllerTest.java diff --git a/src/test/java/com/learning/yasminishop/controller/CartControllerTest.java b/src/test/java/com/learning/yasminishop/controller/CartControllerTest.java new file mode 100644 index 0000000..c8db9a7 --- /dev/null +++ b/src/test/java/com/learning/yasminishop/controller/CartControllerTest.java @@ -0,0 +1,179 @@ +package com.learning.yasminishop.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.learning.yasminishop.cart.CartItemRepository; +import com.learning.yasminishop.cart.dto.request.CartItemRequest; +import com.learning.yasminishop.common.entity.CartItem; +import com.learning.yasminishop.common.entity.Product; +import com.learning.yasminishop.common.entity.User; +import com.learning.yasminishop.product.ProductRepository; +import com.learning.yasminishop.user.UserRepository; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import java.math.BigDecimal; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@Slf4j +@AutoConfigureMockMvc +@TestPropertySource("/test.properties") +class CartControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ProductRepository productRepository; + + @Autowired + private CartItemRepository cartItemRepository; + + @Autowired + private UserRepository userRepository; + + private CartItemRequest cartItemRequest; + private Product product; + private User user; + private CartItem cartItem; + + @BeforeEach + void setUp() { + cartItemRequest = CartItemRequest.builder() + .productId("product-1") + .quantity(2) + .build(); + product = Product.builder() + .name("Product 1") + .description("Product 1 description") + .price(BigDecimal.valueOf(100)) + .quantity(10L) + .slug("product-1") + .sku("sku-1") + .isFeatured(true) + .isAvailable(true) + .build(); + user = User.builder() + .email("user@gmail.com") + .password("password") + .build(); + cartItem = CartItem.builder() + .product(product) + .quantity(2) + .price(BigDecimal.valueOf(200)) + .build(); + } + + @Test + @DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD) + @WithMockUser(username = "user@gmail.com") + void createCart_validRequest_success() throws Exception { + // GIVEN + ObjectMapper objectMapper = new ObjectMapper(); + Product savedProduct = productRepository.save(product); + userRepository.save(user); + cartItemRequest.setProductId(savedProduct.getId()); + String requestJson = objectMapper.writeValueAsString(cartItemRequest); + + // WHEN THEN + mockMvc.perform(MockMvcRequestBuilders.post("/carts") + .contentType(MediaType.APPLICATION_JSON) + .content(requestJson)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("internalCode").value(1000)) + .andExpect(jsonPath("result.product.name").value("Product 1")); + } + + @Test + @DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD) + @WithMockUser(username = "user@gmail.com") + void getAllCarts_validRequest_success() throws Exception { + // GIVEN + Product savedProduct = productRepository.save(product); + User savedUser = userRepository.save(user); + cartItem.setProduct(savedProduct); + cartItem.setUser(savedUser); + cartItemRepository.save(cartItem); + + + // WHEN THEN + mockMvc.perform(MockMvcRequestBuilders.get("/carts")) + .andExpect(status().isOk()) + .andExpect(jsonPath("internalCode").value(1000)) + .andExpect(jsonPath("result[0].product.name").value("Product 1")); + } + + @Test + @DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD) + @WithMockUser(username = "user@gmail.com") + void updateCart_validRequest_success() throws Exception { + // GIVEN + Product savedProduct = productRepository.save(product); + User savedUser = userRepository.save(user); + cartItem.setProduct(savedProduct); + cartItem.setUser(savedUser); + CartItem savedCartItem = cartItemRepository.save(cartItem); + String requestJson = "{\"quantity\": 5}"; + + // WHEN THEN + mockMvc.perform(MockMvcRequestBuilders.put("/carts/" + savedCartItem.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(requestJson)) + .andExpect(status().isOk()) + .andExpect(jsonPath("internalCode").value(1000)) + .andExpect(jsonPath("result.quantity").value(5)); + } + + @Test + @DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD) + @WithMockUser(username = "user@gmail.com") + void deleteCart_validRequest_success() throws Exception { + // GIVEN + Product savedProduct = productRepository.save(product); + User savedUser = userRepository.save(user); + cartItem.setProduct(savedProduct); + cartItem.setUser(savedUser); + CartItem savedCartItem = cartItemRepository.save(cartItem); + String requestJson = "{\"ids\": [\"" + savedCartItem.getId() + "\"]}"; + + // WHEN THEN + mockMvc.perform(MockMvcRequestBuilders.delete("/carts") + .contentType(MediaType.APPLICATION_JSON) + .content(requestJson)) + .andExpect(status().isOk()) + .andExpect(jsonPath("internalCode").value(1000)) + .andExpect(jsonPath("message").value("Cart items deleted successfully")); + } + + @Test + @DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD) + @WithMockUser(username = "user@gmail.com") + void getCartItemsByIds_validRequest_success() throws Exception { + // GIVEN + Product savedProduct = productRepository.save(product); + User savedUser = userRepository.save(user); + cartItem.setProduct(savedProduct); + cartItem.setUser(savedUser); + CartItem savedCartItem = cartItemRepository.save(cartItem); + + // WHEN THEN + mockMvc.perform(MockMvcRequestBuilders.get("/carts/get-by-ids") + .param("ids", savedCartItem.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("internalCode").value(1000)) + .andExpect(jsonPath("result[0].product.name").value("Product 1")); + } + +} From dd964f9c0a50ff330ac541aadbe347407011ba12 Mon Sep 17 00:00:00 2001 From: duongminhhieu Date: Thu, 6 Jun 2024 14:05:27 +0700 Subject: [PATCH 17/22] unit-test: write all unit-test for CartService --- .../yasminishop/service/CartServiceTest.java | 221 ++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 src/test/java/com/learning/yasminishop/service/CartServiceTest.java diff --git a/src/test/java/com/learning/yasminishop/service/CartServiceTest.java b/src/test/java/com/learning/yasminishop/service/CartServiceTest.java new file mode 100644 index 0000000..8a2231e --- /dev/null +++ b/src/test/java/com/learning/yasminishop/service/CartServiceTest.java @@ -0,0 +1,221 @@ +package com.learning.yasminishop.service; + +import com.learning.yasminishop.cart.CartItemRepository; +import com.learning.yasminishop.cart.CartItemService; +import com.learning.yasminishop.cart.dto.request.CartItemRequest; +import com.learning.yasminishop.cart.dto.request.CartItemUpdate; +import com.learning.yasminishop.cart.dto.response.CartItemResponse; +import com.learning.yasminishop.common.entity.CartItem; +import com.learning.yasminishop.common.entity.Product; +import com.learning.yasminishop.common.entity.User; +import com.learning.yasminishop.common.exception.AppException; +import com.learning.yasminishop.product.ProductRepository; +import com.learning.yasminishop.user.UserRepository; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.TestPropertySource; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import static org.assertj.core.api.Assertions.assertThat; + + +@SpringBootTest +@Slf4j +@TestPropertySource("/test.properties") +class CartServiceTest { + + @MockBean + private CartItemRepository cartItemRepository; + + @MockBean + private ProductRepository productRepository; + + @MockBean + private UserRepository userRepository; + + @Autowired + private CartItemService cartItemService; + + + private CartItemRequest cartItemRequest; + private Product product; + private User user; + private CartItem cartItem; + private CartItemUpdate cartItemUpdate; + private List cartIds; + private List cartItems; + + @BeforeEach + void setUp() { + + cartItemRequest = CartItemRequest.builder() + .productId("product-1") + .quantity(2) + .build(); + + product = Product.builder() + .name("Product 1") + .description("Product 1 description") + .price(BigDecimal.valueOf(100)) + .quantity(10L) + .slug("product-1") + .sku("sku-1") + .isFeatured(true) + .isAvailable(true) + .build(); + user = User.builder() + .email("user@test.com") + .password("password") + .build(); + cartItem = CartItem.builder() + .product(product) + .quantity(2) + .user(user) + .price(BigDecimal.valueOf(200)) + .build(); + + cartItemUpdate = CartItemUpdate.builder() + .quantity(3) + .build(); + cartIds = List.of("cart-1", "cart-2"); + cartItems = List.of(CartItem.builder() + .id("cart-1") + .product(product) + .quantity(2) + .user(user) + .price(BigDecimal.valueOf(200)) + .build(), + CartItem.builder() + .id("cart-2") + .product(product) + .quantity(2) + .user(user) + .price(BigDecimal.valueOf(200)) + .build()); + } + + @Nested + class HappyCase { + + @Test + @WithMockUser(username = "user@test.com") + void create_validRequest_success() { + // GIVEN + when(productRepository.findById(cartItemRequest.getProductId())).thenReturn(Optional.of(product)); + when(userRepository.findByEmail(any())).thenReturn(Optional.of(user)); + when(cartItemRepository.findByProductAndUser(product, user)).thenReturn(Optional.empty()); + when(cartItemRepository.save(any(CartItem.class))).thenReturn(cartItem); + + // WHEN + CartItemResponse result = cartItemService.create(cartItemRequest); + + // THEN + assertEquals(cartItem.getQuantity(), result.getQuantity()); + assertEquals(cartItem.getPrice(), result.getPrice()); + assertEquals(cartItem.getProduct().getId(), result.getProduct().getId()); + } + + @Test + @WithMockUser(username = "user@test.com") + void getAll_validRequest_success() { + // GIVEN + when(userRepository.findByEmail(any())).thenReturn(Optional.of(user)); + when(cartItemRepository.findAllByUserOrderByLastModifiedDateDesc(user)).thenReturn(java.util.List.of(cartItem)); + + // WHEN + var result = cartItemService.getAll(); + + // THEN + assertThat(result).isNotNull() + .hasSize(1) + .first() + .hasFieldOrPropertyWithValue("quantity", 2) + .hasFieldOrPropertyWithValue("price", BigDecimal.valueOf(200)); + } + + @Test + @WithMockUser(username = "user@test.com") + void update_validRequest_success() { + // GIVEN + when(userRepository.findByEmail(any())).thenReturn(Optional.of(user)); + when(cartItemRepository.findById(cartItem.getId())).thenReturn(Optional.of(cartItem)); + when(cartItemRepository.save(any(CartItem.class))).thenReturn(cartItem); + + // WHEN + var result = cartItemService.update(cartItem.getId(), cartItemUpdate); + + // THEN + assertEquals(cartItemUpdate.getQuantity(), result.getQuantity()); + } + + @Test + @WithMockUser(username = "user@test.com") + void delete_validRequest_success() { + // GIVEN + when(cartItemRepository.findAllById(cartIds)).thenReturn(cartItems); + when(userRepository.findByEmail(any())).thenReturn(Optional.of(user)); + + // WHEN + cartItemService.delete(cartIds); + + // THEN + verify(cartItemRepository).deleteAll(cartItems); + } + + @Test + @WithMockUser(username = "user@test.com") + void getCartByIds_validRequest_success() { + // GIVEN + when(cartItemRepository.findAllById(cartIds)).thenReturn(cartItems); + + // WHEN + var result = cartItemService.getCartByIds(cartIds); + + // THEN + assertThat(result).isNotNull() + .hasSize(2) + .first() + .hasFieldOrPropertyWithValue("id", "cart-1") + .hasFieldOrPropertyWithValue("quantity", 2); + } + + } + + @Nested + class UnhappyCase { + + @Test + @WithMockUser(username = "user@test.com") + void create_productNotFound_throwException() { + // GIVEN + when(productRepository.findById(cartItemRequest.getProductId())).thenReturn(Optional.empty()); + + // WHEN + var exception = assertThrows(AppException.class, () -> cartItemService.create(cartItemRequest)); + + // THEN + assertThat(exception.getErrorCode().getInternalCode()) + .isEqualTo(1009); + assertThat(exception.getErrorCode().getMessage()) + .isEqualTo("Product not found"); + } + + } + + +} From a2888e7ecaa2c585c21491cac2c4e12ac2f0a95a Mon Sep 17 00:00:00 2001 From: duongminhhieu Date: Thu, 6 Jun 2024 14:32:25 +0700 Subject: [PATCH 18/22] unit-test: refactor test code (add happy class and unhappiness) --- .../AuthenticationControllerTest.java | 373 ++++++++-------- .../controller/CategoryControllerTest.java | 1 - .../service/AuthenticationServiceTest.java | 401 +++++++++--------- .../service/CategoryServiceTest.java | 212 ++++----- .../service/PermissionServiceTest.java | 92 ++-- .../service/ProductServiceTest.java | 372 ++++++++-------- .../yasminishop/service/RoleServiceTest.java | 110 ++--- .../yasminishop/service/UserServiceTest.java | 249 +++++------ 8 files changed, 928 insertions(+), 882 deletions(-) diff --git a/src/test/java/com/learning/yasminishop/controller/AuthenticationControllerTest.java b/src/test/java/com/learning/yasminishop/controller/AuthenticationControllerTest.java index 578fac3..f65f4a4 100644 --- a/src/test/java/com/learning/yasminishop/controller/AuthenticationControllerTest.java +++ b/src/test/java/com/learning/yasminishop/controller/AuthenticationControllerTest.java @@ -10,6 +10,7 @@ import com.learning.yasminishop.auth.dto.response.TokenResponse; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -77,192 +78,200 @@ void setUp() { } - @Test - void register_validRequest_success() throws Exception { - // GIVEN - ObjectMapper objectMapper = new ObjectMapper(); - String registerRequestJson = objectMapper.writeValueAsString(registerRequest); + @Nested + class HappyCase { + @Test + void register_validRequest_success() throws Exception { + // GIVEN + ObjectMapper objectMapper = new ObjectMapper(); + String registerRequestJson = objectMapper.writeValueAsString(registerRequest); + + when(authenticationService.register(any(RegisterRequest.class))) + .thenReturn(authenticationResponse); + + // WHEN THEN + mockMvc.perform(MockMvcRequestBuilders.post("/auth/register") + .contentType(MediaType.APPLICATION_JSON) + .content(registerRequestJson)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("internalCode").value(1000)) + .andExpect(jsonPath("result.tokens.access_token").value("accessToken")) + .andExpect(jsonPath("result.tokens.refresh_token").value("refreshToken")); + + } + + @Test + void authenticate_validRequest_success() throws Exception { + + // GIVEN + ObjectMapper objectMapper = new ObjectMapper(); + String authenticationRequestJson = objectMapper.writeValueAsString(authenticationRequest); + + when(authenticationService.authenticate(any(AuthenticationRequest.class))) + .thenReturn(authenticationResponse); + + // WHEN THEN + mockMvc.perform(MockMvcRequestBuilders.post("/auth/authenticate") + .contentType(MediaType.APPLICATION_JSON) + .content(authenticationRequestJson)) + .andExpect(status().isOk()) + .andExpect(jsonPath("internalCode").value(1000)) + .andExpect(jsonPath("result.tokens.access_token").value("accessToken")) + .andExpect(jsonPath("result.tokens.refresh_token").value("refreshToken")); + + } + + @Test + void refresh_validRequest_success() throws Exception { + // GIVEN + ObjectMapper objectMapper = new ObjectMapper(); + String refreshRequestJson = objectMapper.writeValueAsString(refreshRequest); + + when(authenticationService.refresh(any(RefreshRequest.class))) + .thenReturn(authenticationResponse); + + // WHEN THEN + mockMvc.perform(MockMvcRequestBuilders.post("/auth/refresh") + .contentType(MediaType.APPLICATION_JSON) + .content(refreshRequestJson)) + .andExpect(status().isOk()) + .andExpect(jsonPath("internalCode").value(1000)) + .andExpect(jsonPath("result.tokens.access_token").value("accessToken")) + .andExpect(jsonPath("result.tokens.refresh_token").value("refreshToken")); + + } + + @Test + void logout_validRequest_success() throws Exception { + // GIVEN + ObjectMapper objectMapper = new ObjectMapper(); + String logoutRequestJson = objectMapper.writeValueAsString(logoutRequest); + + // WHEN THEN + mockMvc.perform(MockMvcRequestBuilders.post("/auth/logout") + .contentType(MediaType.APPLICATION_JSON) + .content(logoutRequestJson)) + .andExpect(status().isOk()) + .andExpect(jsonPath("internalCode").value(1000)) + .andExpect(jsonPath("result").value("Logout successful")); + } - when(authenticationService.register(any(RegisterRequest.class))) - .thenReturn(authenticationResponse); - - // WHEN THEN - mockMvc.perform(MockMvcRequestBuilders.post("/auth/register") - .contentType(MediaType.APPLICATION_JSON) - .content(registerRequestJson)) - .andExpect(status().isCreated()) - .andExpect(jsonPath("internalCode").value(1000)) - .andExpect(jsonPath("result.tokens.access_token").value("accessToken")) - .andExpect(jsonPath("result.tokens.refresh_token").value("refreshToken")); - - } - - @Test - void register_invalidEmail_fail() throws Exception { - // GIVEN - ObjectMapper objectMapper = new ObjectMapper(); - registerRequest.setEmail("invalidEmail"); - String registerRequestJson = objectMapper.writeValueAsString(registerRequest); - - // WHEN THEN - mockMvc.perform(MockMvcRequestBuilders.post("/auth/register") - .contentType(MediaType.APPLICATION_JSON) - .content(registerRequestJson)) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("internalCode").value(2002)) - .andExpect(jsonPath("message").value("Invalid email")); - } - - @Test - void authenticate_validRequest_success() throws Exception { - - // GIVEN - ObjectMapper objectMapper = new ObjectMapper(); - String authenticationRequestJson = objectMapper.writeValueAsString(authenticationRequest); - - when(authenticationService.authenticate(any(AuthenticationRequest.class))) - .thenReturn(authenticationResponse); - - // WHEN THEN - mockMvc.perform(MockMvcRequestBuilders.post("/auth/authenticate") - .contentType(MediaType.APPLICATION_JSON) - .content(authenticationRequestJson)) - .andExpect(status().isOk()) - .andExpect(jsonPath("internalCode").value(1000)) - .andExpect(jsonPath("result.tokens.access_token").value("accessToken")) - .andExpect(jsonPath("result.tokens.refresh_token").value("refreshToken")); - - } - - @Test - void authenticate_invalidEmail_fail() throws Exception { - // GIVEN - ObjectMapper objectMapper = new ObjectMapper(); - authenticationRequest.setEmail("invalidEmail"); - String authenticationRequestJson = objectMapper.writeValueAsString(authenticationRequest); - - // WHEN THEN - mockMvc.perform(MockMvcRequestBuilders.post("/auth/authenticate") - .contentType(MediaType.APPLICATION_JSON) - .content(authenticationRequestJson)) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("internalCode").value(2002)) - .andExpect(jsonPath("message").value("Invalid email")); - } - - @Test - void authenticate_invalidPassword_fail() throws Exception { - // GIVEN - ObjectMapper objectMapper = new ObjectMapper(); - authenticationRequest.setPassword("123"); - String authenticationRequestJson = objectMapper.writeValueAsString(authenticationRequest); - - // WHEN THEN - mockMvc.perform(MockMvcRequestBuilders.post("/auth/authenticate") - .contentType(MediaType.APPLICATION_JSON) - .content(authenticationRequestJson)) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("internalCode").value(2001)) - .andExpect(jsonPath("message").value("Password must be at least 6 characters")); - } - - @Test - void authenticate_nullPassword_fail() throws Exception { - // GIVEN - ObjectMapper objectMapper = new ObjectMapper(); - authenticationRequest.setPassword(null); - String authenticationRequestJson = objectMapper.writeValueAsString(authenticationRequest); - - // WHEN THEN - mockMvc.perform(MockMvcRequestBuilders.post("/auth/authenticate") - .contentType(MediaType.APPLICATION_JSON) - .content(authenticationRequestJson)) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("internalCode").value(2004)) - .andExpect(jsonPath("message").value("\"password\" must not be null")); } - @Test - void authenticate_nullEmail_fail() throws Exception { - // GIVEN - ObjectMapper objectMapper = new ObjectMapper(); - authenticationRequest.setEmail(null); - String authenticationRequestJson = objectMapper.writeValueAsString(authenticationRequest); - - // WHEN THEN - mockMvc.perform(MockMvcRequestBuilders.post("/auth/authenticate") - .contentType(MediaType.APPLICATION_JSON) - .content(authenticationRequestJson)) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("internalCode").value(2004)) - .andExpect(jsonPath("message").value("\"email\" must not be null")); + @Nested + class UnHappyCase { + @Test + void register_invalidEmail_fail() throws Exception { + // GIVEN + ObjectMapper objectMapper = new ObjectMapper(); + registerRequest.setEmail("invalidEmail"); + String registerRequestJson = objectMapper.writeValueAsString(registerRequest); + + // WHEN THEN + mockMvc.perform(MockMvcRequestBuilders.post("/auth/register") + .contentType(MediaType.APPLICATION_JSON) + .content(registerRequestJson)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("internalCode").value(2002)) + .andExpect(jsonPath("message").value("Invalid email")); + } + + @Test + void authenticate_invalidEmail_fail() throws Exception { + // GIVEN + ObjectMapper objectMapper = new ObjectMapper(); + authenticationRequest.setEmail("invalidEmail"); + String authenticationRequestJson = objectMapper.writeValueAsString(authenticationRequest); + + // WHEN THEN + mockMvc.perform(MockMvcRequestBuilders.post("/auth/authenticate") + .contentType(MediaType.APPLICATION_JSON) + .content(authenticationRequestJson)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("internalCode").value(2002)) + .andExpect(jsonPath("message").value("Invalid email")); + } + + @Test + void authenticate_invalidPassword_fail() throws Exception { + // GIVEN + ObjectMapper objectMapper = new ObjectMapper(); + authenticationRequest.setPassword("123"); + String authenticationRequestJson = objectMapper.writeValueAsString(authenticationRequest); + + // WHEN THEN + mockMvc.perform(MockMvcRequestBuilders.post("/auth/authenticate") + .contentType(MediaType.APPLICATION_JSON) + .content(authenticationRequestJson)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("internalCode").value(2001)) + .andExpect(jsonPath("message").value("Password must be at least 6 characters")); + } + + @Test + void authenticate_nullPassword_fail() throws Exception { + // GIVEN + ObjectMapper objectMapper = new ObjectMapper(); + authenticationRequest.setPassword(null); + String authenticationRequestJson = objectMapper.writeValueAsString(authenticationRequest); + + // WHEN THEN + mockMvc.perform(MockMvcRequestBuilders.post("/auth/authenticate") + .contentType(MediaType.APPLICATION_JSON) + .content(authenticationRequestJson)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("internalCode").value(2004)) + .andExpect(jsonPath("message").value("\"password\" must not be null")); + } + + @Test + void authenticate_nullEmail_fail() throws Exception { + // GIVEN + ObjectMapper objectMapper = new ObjectMapper(); + authenticationRequest.setEmail(null); + String authenticationRequestJson = objectMapper.writeValueAsString(authenticationRequest); + + // WHEN THEN + mockMvc.perform(MockMvcRequestBuilders.post("/auth/authenticate") + .contentType(MediaType.APPLICATION_JSON) + .content(authenticationRequestJson)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("internalCode").value(2004)) + .andExpect(jsonPath("message").value("\"email\" must not be null")); + } + + @Test + void refresh_nullToken_fail() throws Exception { + // GIVEN + ObjectMapper objectMapper = new ObjectMapper(); + refreshRequest.setRefreshToken(null); + String refreshRequestJson = objectMapper.writeValueAsString(refreshRequest); + + // WHEN THEN + mockMvc.perform(MockMvcRequestBuilders.post("/auth/refresh") + .contentType(MediaType.APPLICATION_JSON) + .content(refreshRequestJson)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("internalCode").value(2004)) + .andExpect(jsonPath("message").value("\"refreshToken\" must not be null")); + } + + @Test + void logout_nullToken_fail() throws Exception { + // GIVEN + ObjectMapper objectMapper = new ObjectMapper(); + logoutRequest.setToken(null); + String logoutRequestJson = objectMapper.writeValueAsString(logoutRequest); + + // WHEN THEN + mockMvc.perform(MockMvcRequestBuilders.post("/auth/logout") + .contentType(MediaType.APPLICATION_JSON) + .content(logoutRequestJson)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("internalCode").value(2004)) + .andExpect(jsonPath("message").value("\"token\" must not be null")); + } } - @Test - void refresh_validRequest_success() throws Exception { - // GIVEN - ObjectMapper objectMapper = new ObjectMapper(); - String refreshRequestJson = objectMapper.writeValueAsString(refreshRequest); - - when(authenticationService.refresh(any(RefreshRequest.class))) - .thenReturn(authenticationResponse); - - // WHEN THEN - mockMvc.perform(MockMvcRequestBuilders.post("/auth/refresh") - .contentType(MediaType.APPLICATION_JSON) - .content(refreshRequestJson)) - .andExpect(status().isOk()) - .andExpect(jsonPath("internalCode").value(1000)) - .andExpect(jsonPath("result.tokens.access_token").value("accessToken")) - .andExpect(jsonPath("result.tokens.refresh_token").value("refreshToken")); - - } - - @Test - void refresh_nullToken_fail() throws Exception { - // GIVEN - ObjectMapper objectMapper = new ObjectMapper(); - refreshRequest.setRefreshToken(null); - String refreshRequestJson = objectMapper.writeValueAsString(refreshRequest); - - // WHEN THEN - mockMvc.perform(MockMvcRequestBuilders.post("/auth/refresh") - .contentType(MediaType.APPLICATION_JSON) - .content(refreshRequestJson)) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("internalCode").value(2004)) - .andExpect(jsonPath("message").value("\"refreshToken\" must not be null")); - } - - @Test - void logout_validRequest_success() throws Exception { - // GIVEN - ObjectMapper objectMapper = new ObjectMapper(); - String logoutRequestJson = objectMapper.writeValueAsString(logoutRequest); - - // WHEN THEN - mockMvc.perform(MockMvcRequestBuilders.post("/auth/logout") - .contentType(MediaType.APPLICATION_JSON) - .content(logoutRequestJson)) - .andExpect(status().isOk()) - .andExpect(jsonPath("internalCode").value(1000)) - .andExpect(jsonPath("result").value("Logout successful")); - } - - @Test - void logout_nullToken_fail() throws Exception { - // GIVEN - ObjectMapper objectMapper = new ObjectMapper(); - logoutRequest.setToken(null); - String logoutRequestJson = objectMapper.writeValueAsString(logoutRequest); - - // WHEN THEN - mockMvc.perform(MockMvcRequestBuilders.post("/auth/logout") - .contentType(MediaType.APPLICATION_JSON) - .content(logoutRequestJson)) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("internalCode").value(2004)) - .andExpect(jsonPath("message").value("\"token\" must not be null")); - } } diff --git a/src/test/java/com/learning/yasminishop/controller/CategoryControllerTest.java b/src/test/java/com/learning/yasminishop/controller/CategoryControllerTest.java index 0b1e1bc..cbb1104 100644 --- a/src/test/java/com/learning/yasminishop/controller/CategoryControllerTest.java +++ b/src/test/java/com/learning/yasminishop/controller/CategoryControllerTest.java @@ -60,7 +60,6 @@ void setUp() { } - @Test @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) void createCategory_validRequest_success() throws Exception { diff --git a/src/test/java/com/learning/yasminishop/service/AuthenticationServiceTest.java b/src/test/java/com/learning/yasminishop/service/AuthenticationServiceTest.java index 6ab24b8..6c8477a 100644 --- a/src/test/java/com/learning/yasminishop/service/AuthenticationServiceTest.java +++ b/src/test/java/com/learning/yasminishop/service/AuthenticationServiceTest.java @@ -14,6 +14,7 @@ import com.learning.yasminishop.user.UserRepository; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -83,204 +84,216 @@ void setUp() { .build(); } - @Test - void register_validRequest_success() { + @Nested + class HappyCase { + @Test + void register_validRequest_success() { + + // GIVEN + when(userRepository.existsByEmail(anyString())).thenReturn(false); + when(roleRepository.findById(anyString())) + .thenReturn(Optional.of(Role.builder() + .name("USER") + .description("User role") + .build())); + + when(userRepository.save(any())) + .thenReturn(new User()); + + when(jwtService.generateAccessToken(any())) + .thenReturn("accessToken"); + + when(jwtService.generateRefreshToken(any())) + .thenReturn("refreshToken"); + + // WHEN + var response = authenticationService.register(registerRequest); + + // THEN + verify(userRepository, times(1)).save(any()); + assertThat(response.getTokens().getAccessToken()).isEqualTo("accessToken"); + assertThat(response.getTokens().getRefreshToken()).isEqualTo("refreshToken"); + } + + @Test + void authenticate_validRequest_success() { + // GIVEN + + when(userRepository.findByEmail(anyString())) + .thenReturn(Optional.of(User.builder() + .id("abc-123") + .email("duongminhhieu@gmail.com") + .password("123456") + .isActive(true) + .build())); + when(jwtService.generateAccessToken(any())) + .thenReturn("accessToken"); + when(jwtService.generateRefreshToken(any())) + .thenReturn("refreshToken"); + + when(passwordEncoder.matches(anyString(), anyString())) + .thenReturn(true); + + // WHEN + var response = authenticationService.authenticate(authenticationRequest); + + // THEN + assertThat(response.getTokens().getAccessToken()).isEqualTo("accessToken"); + assertThat(response.getTokens().getRefreshToken()).isEqualTo("refreshToken"); + } + + @Test + void refresh_validRequest_success() { + // GIVEN + when(jwtService.isTokenValid(anyString())).thenReturn(true); + when(jwtService.extractUserEmail(anyString())).thenReturn("duongminhhieu@gmail.com"); + when(userRepository.findByEmail(anyString())) + .thenReturn(Optional.of(User.builder() + .id("abc-123") + .email("duongminhhieu@gmail.com") + .password("123456") + .build())); + when(jwtService.extractIdToken(anyString())).thenReturn("idToken"); + when(jwtService.extractExpiration(anyString())) + .thenReturn(Date.from(new Date().toInstant().plusSeconds(3600))); + when(jwtService.generateAccessToken(any())) + .thenReturn("accessToken"); + when(jwtService.generateRefreshToken(any())) + .thenReturn("refreshToken"); + + // WHEN + var response = authenticationService.refresh(refreshRequest); + + // THEN + assertThat(response.getTokens().getAccessToken()).isEqualTo("accessToken"); + assertThat(response.getTokens().getRefreshToken()).isEqualTo("refreshToken"); + verify(invalidTokenRepository, times(1)).save(any()); + } + + @Test + void logout_validRequest_success() { + // GIVEN + when(jwtService.isTokenValid(anyString())).thenReturn(true); + when(jwtService.extractIdToken(anyString())).thenReturn("idToken"); + + // WHEN + authenticationService.logout(logoutRequest); + // THEN + verify(invalidTokenRepository, times(1)).save(any()); + } - // GIVEN - when(userRepository.existsByEmail(anyString())).thenReturn(false); - when(roleRepository.findById(anyString())) - .thenReturn(Optional.of(Role.builder() - .name("USER") - .description("User role") - .build())); - - when(userRepository.save(any())) - .thenReturn(new User()); - - when(jwtService.generateAccessToken(any())) - .thenReturn("accessToken"); - - when(jwtService.generateRefreshToken(any())) - .thenReturn("refreshToken"); - - // WHEN - var response = authenticationService.register(registerRequest); - - // THEN - verify(userRepository, times(1)).save(any()); - assertThat(response.getTokens().getAccessToken()).isEqualTo("accessToken"); - assertThat(response.getTokens().getRefreshToken()).isEqualTo("refreshToken"); - } - - @Test - void register_emailAlreadyExists_throwException() { - - // GIVEN - when(userRepository.existsByEmail(anyString())).thenReturn(true); - - // WHEN - var exception = assertThrows(AppException.class, () -> authenticationService.register(registerRequest) - ); - - // THEN - assertThat(exception.getErrorCode().getInternalCode()).isEqualTo(1002); - assertThat(exception.getErrorCode().getMessage()).isEqualTo("Email already exists"); } - @Test - void authenticate_validRequest_success() { - // GIVEN - - when(userRepository.findByEmail(anyString())) - .thenReturn(Optional.of(User.builder() - .id("abc-123") - .email("duongminhhieu@gmail.com") - .password("123456") - .isActive(true) - .build())); - when(jwtService.generateAccessToken(any())) - .thenReturn("accessToken"); - when(jwtService.generateRefreshToken(any())) - .thenReturn("refreshToken"); - - when(passwordEncoder.matches(anyString(), anyString())) - .thenReturn(true); - - // WHEN - var response = authenticationService.authenticate(authenticationRequest); - - // THEN - assertThat(response.getTokens().getAccessToken()).isEqualTo("accessToken"); - assertThat(response.getTokens().getRefreshToken()).isEqualTo("refreshToken"); + @Nested + class UnHappyCase { + @Test + void register_emailAlreadyExists_throwException() { + + // GIVEN + when(userRepository.existsByEmail(anyString())).thenReturn(true); + + // WHEN + var exception = assertThrows(AppException.class, () -> authenticationService.register(registerRequest) + ); + + // THEN + assertThat(exception.getErrorCode().getInternalCode()).isEqualTo(1002); + assertThat(exception.getErrorCode().getMessage()).isEqualTo("Email already exists"); + } + + + @Test + void authenticate_invalidPassword_throwException() { + // GIVEN + when(userRepository.findByEmail(anyString())) + .thenReturn(Optional.of(User.builder() + .id("abc-123") + .email("duongminhhieu@gmail.com") + .password("123456") + .build())); + when(passwordEncoder.matches(anyString(), anyString())) + .thenReturn(false); + + // WHEN + var exception = assertThrows(AppException.class, () -> authenticationService.authenticate(authenticationRequest) + ); + + // THEN + assertThat(exception.getErrorCode().getInternalCode()).isEqualTo(1013); + assertThat(exception.getErrorCode().getMessage()).isEqualTo("Email or password is incorrect"); + verify(passwordEncoder, times(1)).matches(anyString(), anyString()); + verify(jwtService, never()).generateAccessToken(any()); + verify(jwtService, never()).generateRefreshToken(any()); + } + + @Test + void authenticate_userNotFound_throwException() { + // GIVEN + when(userRepository.findByEmail(anyString())) + .thenReturn(Optional.empty()); + + // WHEN + var exception = assertThrows(AppException.class, () -> authenticationService.authenticate(authenticationRequest) + ); + + // THEN + assertThat(exception.getErrorCode().getInternalCode()).isEqualTo(1013); + assertThat(exception.getErrorCode().getMessage()).isEqualTo("Email or password is incorrect"); + verify(passwordEncoder, never()).matches(anyString(), anyString()); + verify(jwtService, never()).generateAccessToken(any()); + verify(jwtService, never()).generateRefreshToken(any()); + } + + + @Test + void refresh_invalidToken_throwException() { + // GIVEN + when(jwtService.isTokenValid(anyString())).thenReturn(false); + + // WHEN + var exception = assertThrows(AppException.class, () -> authenticationService.refresh(refreshRequest) + ); + + // THEN + assertThat(exception.getErrorCode().getInternalCode()).isEqualTo(1001); + assertThat(exception.getErrorCode().getMessage()).isEqualTo("Invalid token"); + verify(userRepository, never()).findByEmail(anyString()); + verify(jwtService, never()).generateAccessToken(any()); + verify(jwtService, never()).generateRefreshToken(any()); + verify(invalidTokenRepository, never()).save(any()); + } + + @Test + void refresh_userNotFound_throwException() { + // GIVEN + when(jwtService.isTokenValid(anyString())).thenReturn(true); + when(jwtService.extractUserEmail(anyString())).thenReturn("hehe@gmail.com"); + + // WHEN + var exception = assertThrows(AppException.class, () -> authenticationService.refresh(refreshRequest) + ); + + // THEN + assertThat(exception.getErrorCode().getInternalCode()).isEqualTo(1001); + assertThat(exception.getErrorCode().getMessage()).isEqualTo("Invalid token"); + } + + + @Test + void logout_invalidToken_throwException() { + // GIVEN + when(jwtService.isTokenValid(anyString())).thenReturn(false); + + // WHEN + var exception = assertThrows(AppException.class, () -> authenticationService.logout(logoutRequest) + ); + + // THEN + assertThat(exception.getErrorCode().getInternalCode()).isEqualTo(1001); + assertThat(exception.getErrorCode().getMessage()).isEqualTo("Invalid token"); + verify(invalidTokenRepository, never()).save(any()); + } } - @Test - void authenticate_invalidPassword_throwException() { - // GIVEN - when(userRepository.findByEmail(anyString())) - .thenReturn(Optional.of(User.builder() - .id("abc-123") - .email("duongminhhieu@gmail.com") - .password("123456") - .build())); - when(passwordEncoder.matches(anyString(), anyString())) - .thenReturn(false); - - // WHEN - var exception = assertThrows(AppException.class, () -> authenticationService.authenticate(authenticationRequest) - ); - - // THEN - assertThat(exception.getErrorCode().getInternalCode()).isEqualTo(1013); - assertThat(exception.getErrorCode().getMessage()).isEqualTo("Email or password is incorrect"); - verify(passwordEncoder, times(1)).matches(anyString(), anyString()); - verify(jwtService, never()).generateAccessToken(any()); - verify(jwtService, never()).generateRefreshToken(any()); - } - @Test - void authenticate_userNotFound_throwException() { - // GIVEN - when(userRepository.findByEmail(anyString())) - .thenReturn(Optional.empty()); - - // WHEN - var exception = assertThrows(AppException.class, () -> authenticationService.authenticate(authenticationRequest) - ); - - // THEN - assertThat(exception.getErrorCode().getInternalCode()).isEqualTo(1013); - assertThat(exception.getErrorCode().getMessage()).isEqualTo("Email or password is incorrect"); - verify(passwordEncoder, never()).matches(anyString(), anyString()); - verify(jwtService, never()).generateAccessToken(any()); - verify(jwtService, never()).generateRefreshToken(any()); - } - - @Test - void refresh_validRequest_success() { - // GIVEN - when(jwtService.isTokenValid(anyString())).thenReturn(true); - when(jwtService.extractUserEmail(anyString())).thenReturn("duongminhhieu@gmail.com"); - when(userRepository.findByEmail(anyString())) - .thenReturn(Optional.of(User.builder() - .id("abc-123") - .email("duongminhhieu@gmail.com") - .password("123456") - .build())); - when(jwtService.extractIdToken(anyString())).thenReturn("idToken"); - when(jwtService.extractExpiration(anyString())) - .thenReturn(Date.from(new Date().toInstant().plusSeconds(3600))); - when(jwtService.generateAccessToken(any())) - .thenReturn("accessToken"); - when(jwtService.generateRefreshToken(any())) - .thenReturn("refreshToken"); - - // WHEN - var response = authenticationService.refresh(refreshRequest); - - // THEN - assertThat(response.getTokens().getAccessToken()).isEqualTo("accessToken"); - assertThat(response.getTokens().getRefreshToken()).isEqualTo("refreshToken"); - verify(invalidTokenRepository, times(1)).save(any()); - } - - @Test - void refresh_invalidToken_throwException() { - // GIVEN - when(jwtService.isTokenValid(anyString())).thenReturn(false); - - // WHEN - var exception = assertThrows(AppException.class, () -> authenticationService.refresh(refreshRequest) - ); - - // THEN - assertThat(exception.getErrorCode().getInternalCode()).isEqualTo(1001); - assertThat(exception.getErrorCode().getMessage()).isEqualTo("Invalid token"); - verify(userRepository, never()).findByEmail(anyString()); - verify(jwtService, never()).generateAccessToken(any()); - verify(jwtService, never()).generateRefreshToken(any()); - verify(invalidTokenRepository, never()).save(any()); - } - - @Test - void refresh_userNotFound_throwException() { - // GIVEN - when(jwtService.isTokenValid(anyString())).thenReturn(true); - when(jwtService.extractUserEmail(anyString())).thenReturn("hehe@gmail.com"); - - // WHEN - var exception = assertThrows(AppException.class, () -> authenticationService.refresh(refreshRequest) - ); - - // THEN - assertThat(exception.getErrorCode().getInternalCode()).isEqualTo(1001); - assertThat(exception.getErrorCode().getMessage()).isEqualTo("Invalid token"); - } - - @Test - void logout_validRequest_success() { - // GIVEN - when(jwtService.isTokenValid(anyString())).thenReturn(true); - when(jwtService.extractIdToken(anyString())).thenReturn("idToken"); - - // WHEN - authenticationService.logout(logoutRequest); - // THEN - verify(invalidTokenRepository, times(1)).save(any()); - } - - @Test - void logout_invalidToken_throwException() { - // GIVEN - when(jwtService.isTokenValid(anyString())).thenReturn(false); - - // WHEN - var exception = assertThrows(AppException.class, () -> authenticationService.logout(logoutRequest) - ); - - // THEN - assertThat(exception.getErrorCode().getInternalCode()).isEqualTo(1001); - assertThat(exception.getErrorCode().getMessage()).isEqualTo("Invalid token"); - verify(invalidTokenRepository, never()).save(any()); - } } diff --git a/src/test/java/com/learning/yasminishop/service/CategoryServiceTest.java b/src/test/java/com/learning/yasminishop/service/CategoryServiceTest.java index 6f8b421..fecff2e 100644 --- a/src/test/java/com/learning/yasminishop/service/CategoryServiceTest.java +++ b/src/test/java/com/learning/yasminishop/service/CategoryServiceTest.java @@ -8,6 +8,7 @@ import com.learning.yasminishop.common.entity.Category; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -67,108 +68,115 @@ void setUp() { } - @Test - @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) - void createCategory_validRequest_success() { - // GIVEN - when(categoryRepository.existsBySlug(categoryCreation.getSlug())).thenReturn(false); - when(categoryRepository.save(any(Category.class))).thenReturn(category); - - // WHEN - CategoryResponse response = categoryService.create(categoryCreation); - - // THEN - assertThat(response).isNotNull() - .hasFieldOrPropertyWithValue("name", "Category 1") - .hasFieldOrPropertyWithValue("description", "Category 1 description") - .hasFieldOrPropertyWithValue("slug", "category-1"); + @Nested + class HappyCase { + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void createCategory_validRequest_success() { + // GIVEN + when(categoryRepository.existsBySlug(categoryCreation.getSlug())).thenReturn(false); + when(categoryRepository.save(any(Category.class))).thenReturn(category); + + // WHEN + CategoryResponse response = categoryService.create(categoryCreation); + + // THEN + assertThat(response).isNotNull() + .hasFieldOrPropertyWithValue("name", "Category 1") + .hasFieldOrPropertyWithValue("description", "Category 1 description") + .hasFieldOrPropertyWithValue("slug", "category-1"); + } + + @Test + void getAllCategories_validRequest_success() { + // GIVEN + when(categoryRepository.findAllByIsAvailable(true)).thenReturn(List.of(category)); + + // WHEN + List response = categoryService.getAllCategories(); + + // THEN + assertThat(response).isNotNull() + .hasSize(1) + .first() + .hasFieldOrPropertyWithValue("name", "Category 1") + .hasFieldOrPropertyWithValue("description", "Category 1 description") + .hasFieldOrPropertyWithValue("slug", "category-1"); + } + + @Test + void getBySlug_validRequest_success() { + // GIVEN + when(categoryRepository.findBySlug("category-1")).thenReturn(Optional.of(category)); + + // WHEN + CategoryResponse response = categoryService.getBySlug("category-1"); + + // THEN + assertThat(response).isNotNull() + .hasFieldOrPropertyWithValue("name", "Category 1") + .hasFieldOrPropertyWithValue("description", "Category 1 description") + .hasFieldOrPropertyWithValue("slug", "category-1"); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void getCategory_validRequest_success() { + // GIVEN + when(categoryRepository.findById("cate1")).thenReturn(Optional.of(category)); + + // WHEN + var response = categoryService.getCategory("cate1"); + + // THEN + assertThat(response).isNotNull() + .hasFieldOrPropertyWithValue("name", "Category 1") + .hasFieldOrPropertyWithValue("description", "Category 1 description") + .hasFieldOrPropertyWithValue("slug", "category-1"); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void delete_validRequest_success() { + // GIVEN + when(categoryRepository.findAllById(List.of("cate1"))).thenReturn(List.of(category)); + + // WHEN + categoryService.delete(List.of("cate1")); + + // THEN + verify(categoryRepository).deleteAll(List.of(category)); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void toggleAvailability_validRequest_success() { + // GIVEN + when(categoryRepository.findAllById(List.of("cate1"))).thenReturn(List.of(category)); + + // WHEN + categoryService.toggleAvailability(List.of("cate1")); + + // THEN + verify(categoryRepository).saveAll(List.of(category)); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void update_validRequest_success() { + // GIVEN + when(categoryRepository.findById("cate1")).thenReturn(Optional.of(category)); + when(categoryRepository.save(any(Category.class))).thenReturn(category); + + // WHEN + var response = categoryService.update("cate1", categoryUpdate); + + // THEN + assertThat(response).isNotNull() + .hasFieldOrPropertyWithValue("name", "Category update"); + } } +} - @Test - void getAllCategories_validRequest_success() { - // GIVEN - when(categoryRepository.findAllByIsAvailable(true)).thenReturn(List.of(category)); - - // WHEN - List response = categoryService.getAllCategories(); - - // THEN - assertThat(response).isNotNull() - .hasSize(1) - .first() - .hasFieldOrPropertyWithValue("name", "Category 1") - .hasFieldOrPropertyWithValue("description", "Category 1 description") - .hasFieldOrPropertyWithValue("slug", "category-1"); - } - - @Test - void getBySlug_validRequest_success() { - // GIVEN - when(categoryRepository.findBySlug("category-1")).thenReturn(Optional.of(category)); - - // WHEN - CategoryResponse response = categoryService.getBySlug("category-1"); - - // THEN - assertThat(response).isNotNull() - .hasFieldOrPropertyWithValue("name", "Category 1") - .hasFieldOrPropertyWithValue("description", "Category 1 description") - .hasFieldOrPropertyWithValue("slug", "category-1"); - } - @Test - @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) - void getCategory_validRequest_success() { - // GIVEN - when(categoryRepository.findById("cate1")).thenReturn(Optional.of(category)); - - // WHEN - var response = categoryService.getCategory("cate1"); - - // THEN - assertThat(response).isNotNull() - .hasFieldOrPropertyWithValue("name", "Category 1") - .hasFieldOrPropertyWithValue("description", "Category 1 description") - .hasFieldOrPropertyWithValue("slug", "category-1"); - } - - @Test - @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) - void delete_validRequest_success() { - // GIVEN - when(categoryRepository.findAllById(List.of("cate1"))).thenReturn(List.of(category)); - - // WHEN - categoryService.delete(List.of("cate1")); - - // THEN - verify(categoryRepository).deleteAll(List.of(category)); - } - - @Test - @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) - void toggleAvailability_validRequest_success() { - // GIVEN - when(categoryRepository.findAllById(List.of("cate1"))).thenReturn(List.of(category)); - - // WHEN - categoryService.toggleAvailability(List.of("cate1")); - - // THEN - verify(categoryRepository).saveAll(List.of(category)); - } - - @Test - @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) - void update_validRequest_success() { - // GIVEN - when(categoryRepository.findById("cate1")).thenReturn(Optional.of(category)); - when(categoryRepository.save(any(Category.class))).thenReturn(category); - - // WHEN - var response = categoryService.update("cate1", categoryUpdate); - // THEN - assertThat(response).isNotNull() - .hasFieldOrPropertyWithValue("name", "Category update"); - } -} diff --git a/src/test/java/com/learning/yasminishop/service/PermissionServiceTest.java b/src/test/java/com/learning/yasminishop/service/PermissionServiceTest.java index 70eea8f..fe60af6 100644 --- a/src/test/java/com/learning/yasminishop/service/PermissionServiceTest.java +++ b/src/test/java/com/learning/yasminishop/service/PermissionServiceTest.java @@ -8,6 +8,7 @@ import com.learning.yasminishop.permission.dto.response.PermissionResponse; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -50,50 +51,51 @@ void setUp() { } - @Test - @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) - void createPermission_validRequest_success() { - // given - when(permissionRepository.save(any(Permission.class))).thenReturn(permission); - - // when - PermissionResponse response = permissionService.createPermission(permissionRequest); - - // then - assertThat(response).isNotNull(); - assertThat(response.getName()).isEqualTo("READ"); - assertThat(response.getDescription()).isEqualTo("Read permission"); - } - - @Test - @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) - void getAllPermissions_validRequest_success() { - // given - when(permissionRepository.findAll()).thenReturn(List.of(permission)); - - // when - List response = permissionService.getALlPermissions(); - - // then - assertThat(response).isNotNull() - .isNotEmpty() - .hasSize(1); - + @Nested + class HappyCase { + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void createPermission_validRequest_success() { + // given + when(permissionRepository.save(any(Permission.class))).thenReturn(permission); + + // when + PermissionResponse response = permissionService.createPermission(permissionRequest); + + // then + assertThat(response).isNotNull(); + assertThat(response.getName()).isEqualTo("READ"); + assertThat(response.getDescription()).isEqualTo("Read permission"); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void getAllPermissions_validRequest_success() { + // given + when(permissionRepository.findAll()).thenReturn(List.of(permission)); + + // when + List response = permissionService.getALlPermissions(); + + // then + assertThat(response).isNotNull() + .isNotEmpty() + .hasSize(1); + + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void deletePermission_validRequest_success() { + // given + String permissionName = "READ"; + doNothing().when(permissionRepository).deleteById(permissionName); + + // when + permissionService.deletePermission(permissionName); + + // then + verify(permissionRepository, times(1)).deleteById(permissionName); + } } - - @Test -@WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) -void deletePermission_validRequest_success() { - // given - String permissionName = "READ"; - doNothing().when(permissionRepository).deleteById(permissionName); - - // when - permissionService.deletePermission(permissionName); - - // then - verify(permissionRepository, times(1)).deleteById(permissionName); -} - - } diff --git a/src/test/java/com/learning/yasminishop/service/ProductServiceTest.java b/src/test/java/com/learning/yasminishop/service/ProductServiceTest.java index 0ed514f..f8f08bc 100644 --- a/src/test/java/com/learning/yasminishop/service/ProductServiceTest.java +++ b/src/test/java/com/learning/yasminishop/service/ProductServiceTest.java @@ -16,6 +16,7 @@ import com.learning.yasminishop.storage.dto.response.StorageResponse; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -214,192 +215,195 @@ void setUp() { } + @Nested + class HappyCase { + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void createProduct_validRequest_success() { + // GIVEN + when(productRepository.existsBySlug(any())).thenReturn(false); + when(productRepository.existsBySku(any())).thenReturn(false); + when(categoryRepository.findAllById(Set.of("category1", "category2"))).thenReturn(List.of(category1, category2)); + when(storageRepository.findAllById(Set.of("image1", "image2"))).thenReturn(List.of(image1, image2)); + when(productMapper.toProduct(any())).thenReturn(product); + when(productRepository.save(product)).thenReturn(product); + when(productMapper.toProductAdminResponse(product)).thenReturn(productAdminResponse); + + // WHEN + ProductAdminResponse response = productService.create(productCreation); + + // THEN + assertThat(response).isNotNull() + .hasFieldOrPropertyWithValue("id", "product-1"); + + verify(categoryRepository).findAllById(Set.of("category1", "category2")); + verify(storageRepository).findAllById(Set.of("image1", "image2")); + verify(productRepository).save(product); + } + + @Test + void getBySlug_validSlug_success() { + // GIVEN + when(productRepository.findBySlug("product-1")).thenReturn(Optional.of(product)); + when(productMapper.toProductResponse(any())).thenReturn(productResponse); + + // WHEN + ProductResponse response = productService.getBySlug("product-1"); + + // THEN + assertThat(response).isNotNull() + .hasFieldOrPropertyWithValue("id", "product-1"); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void getById_validId_success() { + // GIVEN + when(productRepository.findById(any())).thenReturn(Optional.of(product)); + when(productMapper.toProductAdminResponse(any())).thenReturn(productAdminResponse); + + // WHEN + ProductAdminResponse productAdminResponse1 = productService.getById("product-1"); + + // THEN + assertThat(productAdminResponse1).isNotNull() + .hasFieldOrPropertyWithValue("id", "product-1"); + + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void getAllProductsForAdmin_validRequest_success() { + // GIVEN + + Pageable pageable = PageRequest.of(0, 5); + Page productPage = new PageImpl<>(List.of(product)); + + + when(categoryRepository.findAllById(anyList())).thenReturn(List.of(category1, category2)); + when(productRepository.findAll(any(Specification.class), eq(pageable))).thenReturn(productPage); + when(productMapper.toProductAdminResponse(any(Product.class))).thenReturn(productAdminResponse); + + // WHEN + PaginationResponse response = productService.getAllProductsForAdmin(productFilter, pageable); + + // THEN + assertThat(response).isNotNull(); + assertThat(response.getData().getFirst().getId()).isEqualTo("product-1"); + + verify(categoryRepository).findAllById(anyList()); + verify(productRepository).findAll(any(Specification.class), eq(pageable)); + verify(productMapper, times(1)).toProductAdminResponse(any(Product.class)); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void toggleAvailability_validRequest_success() { + // GIVEN + List ids = List.of("product-1", "product-2"); + + Product product1 = new Product(); + product1.setId("product-1"); + product1.setIsAvailable(true); + + Product product2 = new Product(); + product2.setId("product-2"); + product2.setIsAvailable(false); + + List products = List.of(product1, product2); + + when(productRepository.findAllById(ids)).thenReturn(products); + + // WHEN + productService.toggleAvailability(ids); + + // THEN + verify(productRepository).findAllById(ids); + verify(productRepository).saveAll(products); + + // Check that the availability of the products has been toggled + assertThat(product1.getIsAvailable()).isFalse(); + assertThat(product2.getIsAvailable()).isTrue(); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void update_validRequest_success() { + // GIVEN + String id = "product-1"; + + when(productRepository.findById(id)).thenReturn(Optional.of(product)); + when(categoryRepository.existsBySlug(productUpdate.getSlug())).thenReturn(false); + when(categoryRepository.existsBySlug(productUpdate.getSku())).thenReturn(false); + when(categoryRepository.findAllById(anySet())).thenReturn(categories); + when(storageRepository.findAllById(anySet())).thenReturn(images); + when(productRepository.save(any(Product.class))).thenReturn(product); + when(productMapper.toProductAdminResponse(any(Product.class))).thenReturn(productAdminResponse); + + // WHEN + ProductAdminResponse response = productService.update(id, productUpdate); + + // THEN + assertThat(response).isNotNull(); + assertThat(response.getId()).isEqualTo("product-1"); + + verify(productRepository).findById(id); + verify(categoryRepository).findAllById(anySet()); + verify(storageRepository).findAllById(anySet()); + verify(productRepository).save(any(Product.class)); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void delete_allProductIdsExistAndNoneInOrderOrCart_success() { + // GIVEN + List ids = List.of("product-1"); + product.setOrderItems(Set.of()); + product.setCartItems(Set.of()); + List products = List.of(product); - @Test - @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) - void createProduct_validRequest_success() { - // GIVEN - when(productRepository.existsBySlug(any())).thenReturn(false); - when(productRepository.existsBySku(any())).thenReturn(false); - when(categoryRepository.findAllById(Set.of("category1", "category2"))).thenReturn(List.of(category1, category2)); - when(storageRepository.findAllById(Set.of("image1", "image2"))).thenReturn(List.of(image1, image2)); - when(productMapper.toProduct(any())).thenReturn(product); - when(productRepository.save(product)).thenReturn(product); - when(productMapper.toProductAdminResponse(product)).thenReturn(productAdminResponse); - - // WHEN - ProductAdminResponse response = productService.create(productCreation); - - // THEN - assertThat(response).isNotNull() - .hasFieldOrPropertyWithValue("id", "product-1"); - - verify(categoryRepository).findAllById(Set.of("category1", "category2")); - verify(storageRepository).findAllById(Set.of("image1", "image2")); - verify(productRepository).save(product); + when(productRepository.findAllById(ids)).thenReturn(products); + + // WHEN + productService.delete(ids); + + // THEN + verify(productRepository).findAllById(ids); + verify(productRepository).deleteAll(products); + } } - @Test - void getBySlug_validSlug_success() { - // GIVEN - when(productRepository.findBySlug("product-1")).thenReturn(Optional.of(product)); - when(productMapper.toProductResponse(any())).thenReturn(productResponse); - - // WHEN - ProductResponse response = productService.getBySlug("product-1"); - - // THEN - assertThat(response).isNotNull() - .hasFieldOrPropertyWithValue("id", "product-1"); + @Nested + class UnhappyCase { + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void delete_someProductIdsDoNotExist_throwsException() { + // GIVEN + List ids = List.of("product-1", "product-2"); + Product product1 = new Product(); + product1.setId("product-1"); + List products = List.of(product1); + + when(productRepository.findAllById(ids)).thenReturn(products); + + // THEN + assertThrows(AppException.class, () -> productService.delete(ids)); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void delete_productInOrderOrCart_throwsException() { + // GIVEN + List ids = List.of("product-1"); + Product product1 = new Product(); + product1.setId("product-1"); + product1.setOrderItems(Set.of(new OrderItem())); // product is in an order + List products = List.of(product1); + + when(productRepository.findAllById(ids)).thenReturn(products); + + // THEN + assertThrows(AppException.class, () -> productService.delete(ids)); + } } - - @Test - @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) - void getById_validId_success() { - // GIVEN - when(productRepository.findById(any())).thenReturn(Optional.of(product)); - when(productMapper.toProductAdminResponse(any())).thenReturn(productAdminResponse); - - // WHEN - ProductAdminResponse productAdminResponse1 = productService.getById("product-1"); - - // THEN - assertThat(productAdminResponse1).isNotNull() - .hasFieldOrPropertyWithValue("id", "product-1"); - - } - - @Test - @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) - void getAllProductsForAdmin_validRequest_success() { - // GIVEN - - Pageable pageable = PageRequest.of(0, 5); - Page productPage = new PageImpl<>(List.of(product)); - - - when(categoryRepository.findAllById(anyList())).thenReturn(List.of(category1, category2)); - when(productRepository.findAll(any(Specification.class), eq(pageable))).thenReturn(productPage); - when(productMapper.toProductAdminResponse(any(Product.class))).thenReturn(productAdminResponse); - - // WHEN - PaginationResponse response = productService.getAllProductsForAdmin(productFilter, pageable); - - // THEN - assertThat(response).isNotNull(); - assertThat(response.getData().getFirst().getId()).isEqualTo("product-1"); - - verify(categoryRepository).findAllById(anyList()); - verify(productRepository).findAll(any(Specification.class), eq(pageable)); - verify(productMapper, times(1)).toProductAdminResponse(any(Product.class)); - } - - @Test - @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) - void toggleAvailability_validRequest_success() { - // GIVEN - List ids = List.of("product-1", "product-2"); - - Product product1 = new Product(); - product1.setId("product-1"); - product1.setIsAvailable(true); - - Product product2 = new Product(); - product2.setId("product-2"); - product2.setIsAvailable(false); - - List products = List.of(product1, product2); - - when(productRepository.findAllById(ids)).thenReturn(products); - - // WHEN - productService.toggleAvailability(ids); - - // THEN - verify(productRepository).findAllById(ids); - verify(productRepository).saveAll(products); - - // Check that the availability of the products has been toggled - assertThat(product1.getIsAvailable()).isFalse(); - assertThat(product2.getIsAvailable()).isTrue(); - } - - @Test - @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) - void update_validRequest_success() { - // GIVEN - String id = "product-1"; - - when(productRepository.findById(id)).thenReturn(Optional.of(product)); - when(categoryRepository.existsBySlug(productUpdate.getSlug())).thenReturn(false); - when(categoryRepository.existsBySlug(productUpdate.getSku())).thenReturn(false); - when(categoryRepository.findAllById(anySet())).thenReturn(categories); - when(storageRepository.findAllById(anySet())).thenReturn(images); - when(productRepository.save(any(Product.class))).thenReturn(product); - when(productMapper.toProductAdminResponse(any(Product.class))).thenReturn(productAdminResponse); - - // WHEN - ProductAdminResponse response = productService.update(id, productUpdate); - - // THEN - assertThat(response).isNotNull(); - assertThat(response.getId()).isEqualTo("product-1"); - - verify(productRepository).findById(id); - verify(categoryRepository).findAllById(anySet()); - verify(storageRepository).findAllById(anySet()); - verify(productRepository).save(any(Product.class)); - } - - @Test - @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) - void delete_allProductIdsExistAndNoneInOrderOrCart_success() { - // GIVEN - List ids = List.of("product-1"); - product.setOrderItems(Set.of()); - product.setCartItems(Set.of()); - List products = List.of(product); - - when(productRepository.findAllById(ids)).thenReturn(products); - - // WHEN - productService.delete(ids); - - // THEN - verify(productRepository).findAllById(ids); - verify(productRepository).deleteAll(products); - } - - @Test - @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) - void delete_someProductIdsDoNotExist_throwsException() { - // GIVEN - List ids = List.of("product-1", "product-2"); - Product product1 = new Product(); - product1.setId("product-1"); - List products = List.of(product1); - - when(productRepository.findAllById(ids)).thenReturn(products); - - // THEN - assertThrows(AppException.class, () -> productService.delete(ids)); - } - - @Test - @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) - void delete_productInOrderOrCart_throwsException() { - // GIVEN - List ids = List.of("product-1"); - Product product1 = new Product(); - product1.setId("product-1"); - product1.setOrderItems(Set.of(new OrderItem())); // product is in an order - List products = List.of(product1); - - when(productRepository.findAllById(ids)).thenReturn(products); - - // THEN - assertThrows(AppException.class, () -> productService.delete(ids)); - } - - } diff --git a/src/test/java/com/learning/yasminishop/service/RoleServiceTest.java b/src/test/java/com/learning/yasminishop/service/RoleServiceTest.java index cc3b7b6..8c17d81 100644 --- a/src/test/java/com/learning/yasminishop/service/RoleServiceTest.java +++ b/src/test/java/com/learning/yasminishop/service/RoleServiceTest.java @@ -10,6 +10,7 @@ import com.learning.yasminishop.role.dto.response.RoleResponse; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -20,6 +21,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; + import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; @@ -69,59 +71,61 @@ void setUp() { .build()); } - @Test - @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) - void create_validRequest_success() { - // GIVEN - when(permissionRepository.findAllById(any())).thenReturn(permissionList); - when(roleRepository.save(any())).thenReturn(Role.builder() - .name("USER") - .description("User role") - .permissions(new HashSet<>(permissionList)) - .build()); - - // WHEN - RoleResponse response = roleService.create(roleRequest); - - // THEN - assertThat(response).isNotNull(); - assertThat(response.getName()).isEqualTo("USER"); - assertThat(response.getDescription()).isEqualTo("User role"); - assertThat(response.getPermissions()).isNotEmpty(); - assertThat(response.getPermissions()).hasSize(1); + @Nested + class HappyCase { + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void create_validRequest_success() { + // GIVEN + when(permissionRepository.findAllById(any())).thenReturn(permissionList); + when(roleRepository.save(any())).thenReturn(Role.builder() + .name("USER") + .description("User role") + .permissions(new HashSet<>(permissionList)) + .build()); + + // WHEN + RoleResponse response = roleService.create(roleRequest); + + // THEN + assertThat(response).isNotNull(); + assertThat(response.getName()).isEqualTo("USER"); + assertThat(response.getDescription()).isEqualTo("User role"); + assertThat(response.getPermissions()).isNotEmpty(); + assertThat(response.getPermissions()).hasSize(1); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void getAll_validRequest_success() { + // GIVEN + when(roleRepository.findAll()).thenReturn(List.of(Role.builder() + .name("USER") + .description("User role") + .permissions(new HashSet<>(permissionList)) + .build())); + + // WHEN + List response = roleService.getAll(); + + // THEN + assertThat(response).isNotNull() + .isNotEmpty() + .hasSize(1); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void delete_validRequest_success() { + // GIVEN + doNothing().when(roleRepository).deleteById(anyString()); + + // WHEN + roleService.delete("USER"); + + // THEN + verify(roleRepository, times(1)).deleteById("USER"); + } } - @Test - @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) - void getAll_validRequest_success() { - // GIVEN - when(roleRepository.findAll()).thenReturn(List.of(Role.builder() - .name("USER") - .description("User role") - .permissions(new HashSet<>(permissionList)) - .build())); - - // WHEN - List response = roleService.getAll(); - - // THEN - assertThat(response).isNotNull() - .isNotEmpty() - .hasSize(1); - } - - @Test - @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) - void delete_validRequest_success() { - // GIVEN - doNothing().when(roleRepository).deleteById(anyString()); - - // WHEN - roleService.delete("USER"); - - // THEN - verify(roleRepository, times(1)).deleteById("USER"); - } - - } diff --git a/src/test/java/com/learning/yasminishop/service/UserServiceTest.java b/src/test/java/com/learning/yasminishop/service/UserServiceTest.java index 00a3e36..5a095c3 100644 --- a/src/test/java/com/learning/yasminishop/service/UserServiceTest.java +++ b/src/test/java/com/learning/yasminishop/service/UserServiceTest.java @@ -15,6 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -48,8 +49,6 @@ class UserServiceTest { private User user; private UserUpdateRequest userUpdateRequest; - - @BeforeEach void setUp() { @@ -78,133 +77,141 @@ void setUp() { .build(); - - } - - @Test - @WithMockUser(username = "duongminhhieu@gmail.com") - void getMyInfo_validRequest_success() { - // Given - - when(userRepository.findByEmail(anyString())) - .thenReturn(Optional.of(user)); - - // When - var userResponse = userService.getMyInfo(); - - // Then - assertThat(userResponse) - .isNotNull() - .hasFieldOrPropertyWithValue("id", "abc-123") - .hasFieldOrPropertyWithValue("email", "duongminhhieu@gmail.com") - .hasFieldOrPropertyWithValue("firstName", "Hieu") - .hasFieldOrPropertyWithValue("lastName", "Duong") - .hasFieldOrPropertyWithValue("dob", LocalDate.of(1999, 1, 1)); - } - - @Test - @WithMockUser(username = "abc@gmail.com") - void getMyInfo_userNotFound_error() { - // Given - when(userRepository.findByEmail(anyString())) - .thenReturn(Optional.empty()); - - // When - var exception = assertThrows(AppException.class, () -> userService.getMyInfo()); - - // Then - assertThat(exception.getErrorCode().getInternalCode()) - .isEqualTo(1006); - assertThat(exception.getErrorCode().getMessage()) - .isEqualTo("User not found"); } - - @Test - @WithMockUser(username = "admin@spring.com", roles = {"ADMIN"}) - void updateUser_validRequest_success() { - // Given - when(userRepository.findById(anyString())) - .thenReturn(Optional.of(user)); - - when(userRepository.save(user)) - .thenReturn(user); - - // When - var userResponse = userService.updateUser("abc-123", userUpdateRequest); - - // Then - assertThat(userResponse) - .isNotNull() - .hasFieldOrPropertyWithValue("id", "abc-123") - .hasFieldOrPropertyWithValue("firstName", "Hieu") - .hasFieldOrPropertyWithValue("lastName", "Duong") - - ; - assertThat(userResponse.getRoles()) - .containsExactlyInAnyOrder( - RoleResponse.builder() - .name("USER") - .description("User role") - .permissions(Set.of()) - .build(), - RoleResponse.builder() - .name("ADMIN") - .description("Admin role") - .permissions(Set.of()) - .build() - ); + @Nested + class HappyCase { + @Test + @WithMockUser(username = "duongminhhieu@gmail.com") + void getMyInfo_validRequest_success() { + // Given + + when(userRepository.findByEmail(anyString())) + .thenReturn(Optional.of(user)); + + // When + var userResponse = userService.getMyInfo(); + + // Then + assertThat(userResponse) + .isNotNull() + .hasFieldOrPropertyWithValue("id", "abc-123") + .hasFieldOrPropertyWithValue("email", "duongminhhieu@gmail.com") + .hasFieldOrPropertyWithValue("firstName", "Hieu") + .hasFieldOrPropertyWithValue("lastName", "Duong") + .hasFieldOrPropertyWithValue("dob", LocalDate.of(1999, 1, 1)); + } + + @Test + @WithMockUser(username = "admin@spring.com", roles = {"ADMIN"}) + void updateUser_validRequest_success() { + // Given + when(userRepository.findById(anyString())) + .thenReturn(Optional.of(user)); + + when(userRepository.save(user)) + .thenReturn(user); + + // When + var userResponse = userService.updateUser("abc-123", userUpdateRequest); + + // Then + assertThat(userResponse) + .isNotNull() + .hasFieldOrPropertyWithValue("id", "abc-123") + .hasFieldOrPropertyWithValue("firstName", "Hieu") + .hasFieldOrPropertyWithValue("lastName", "Duong") + + ; + assertThat(userResponse.getRoles()) + .containsExactlyInAnyOrder( + RoleResponse.builder() + .name("USER") + .description("User role") + .permissions(Set.of()) + .build(), + RoleResponse.builder() + .name("ADMIN") + .description("Admin role") + .permissions(Set.of()) + .build() + ); + + } + + @Test + @WithMockUser(username = "admin@spring.com", roles = {"ADMIN"}) + void deleteUser_valid_success() { + // Given + when(userRepository.existsById(anyString())) + .thenReturn(true); + // When + userService.deleteUser("abc-123"); + // Then + verify(userRepository, times(1)).deleteById("abc-123"); + } + + + @Test + @WithMockUser(username = "admin@spring.com", roles = {"ADMIN"}) + void getUserById_valid_success() { + // Given + when(userRepository.findById(anyString())) + .thenReturn(Optional.of(user)); + // When + var response = userService.getUserById("abc-123"); + // Then + assertThat(response) + .isNotNull() + .hasFieldOrPropertyWithValue("id", "abc-123") + .hasFieldOrPropertyWithValue("email", "duongminhhieu@gmail.com") + .hasFieldOrPropertyWithValue("firstName", "Hieu") + .hasFieldOrPropertyWithValue("lastName", "Duong") + .hasFieldOrPropertyWithValue("dob", LocalDate.of(1999, 1, 1)); + + } } - @Test - @WithMockUser(username = "admin@spring.com", roles = {"ADMIN"}) - void updateUser_notFoundUser_error() { - // Given - when(userRepository.findById(anyString())) - .thenReturn(Optional.empty()); - - // When - var exception = assertThrows(AppException.class, () -> userService.updateUser("abc-123", userUpdateRequest)); - - // Then - assertThat(exception.getErrorCode().getInternalCode()) - .isEqualTo(1006); - - assertThat(exception.getErrorCode().getMessage()) - .isEqualTo("User not found"); - } + @Nested + class UnHappyCase { + @Test + @WithMockUser(username = "abc@gmail.com") + void getMyInfo_userNotFound_error() { + // Given + when(userRepository.findByEmail(anyString())) + .thenReturn(Optional.empty()); + + // When + var exception = assertThrows(AppException.class, () -> userService.getMyInfo()); + + // Then + assertThat(exception.getErrorCode().getInternalCode()) + .isEqualTo(1006); + assertThat(exception.getErrorCode().getMessage()) + .isEqualTo("User not found"); + } + + @Test + @WithMockUser(username = "admin@spring.com", roles = {"ADMIN"}) + void updateUser_notFoundUser_error() { + // Given + when(userRepository.findById(anyString())) + .thenReturn(Optional.empty()); + + // When + var exception = assertThrows(AppException.class, () -> userService.updateUser("abc-123", userUpdateRequest)); + + // Then + assertThat(exception.getErrorCode().getInternalCode()) + .isEqualTo(1006); + + assertThat(exception.getErrorCode().getMessage()) + .isEqualTo("User not found"); + } - @Test - @WithMockUser(username = "admin@spring.com", roles = {"ADMIN"}) - void deleteUser_valid_success() { - // Given - when(userRepository.existsById(anyString())) - .thenReturn(true); - // When - userService.deleteUser("abc-123"); - // Then - verify(userRepository, times(1)).deleteById("abc-123"); } - @Test - @WithMockUser(username = "admin@spring.com", roles = {"ADMIN"}) - void getUserById_valid_success() { - // Given - when(userRepository.findById(anyString())) - .thenReturn(Optional.of(user)); - // When - var response = userService.getUserById("abc-123"); - // Then - assertThat(response) - .isNotNull() - .hasFieldOrPropertyWithValue("id", "abc-123") - .hasFieldOrPropertyWithValue("email", "duongminhhieu@gmail.com") - .hasFieldOrPropertyWithValue("firstName", "Hieu") - .hasFieldOrPropertyWithValue("lastName", "Duong") - .hasFieldOrPropertyWithValue("dob", LocalDate.of(1999, 1, 1)); - - } } From 4ed243efe6cb4ab9a4a1b4c468f7a014593266d3 Mon Sep 17 00:00:00 2001 From: duongminhhieu Date: Thu, 6 Jun 2024 15:56:05 +0700 Subject: [PATCH 19/22] unit-test: write all unit-test for OrderService --- .../yasminishop/service/OrderServiceTest.java | 296 ++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100644 src/test/java/com/learning/yasminishop/service/OrderServiceTest.java diff --git a/src/test/java/com/learning/yasminishop/service/OrderServiceTest.java b/src/test/java/com/learning/yasminishop/service/OrderServiceTest.java new file mode 100644 index 0000000..aba1402 --- /dev/null +++ b/src/test/java/com/learning/yasminishop/service/OrderServiceTest.java @@ -0,0 +1,296 @@ +package com.learning.yasminishop.service; + + +import com.learning.yasminishop.cart.CartItemRepository; +import com.learning.yasminishop.common.dto.PaginationResponse; +import com.learning.yasminishop.common.entity.*; +import com.learning.yasminishop.common.enumeration.EOrderStatus; +import com.learning.yasminishop.common.exception.AppException; +import com.learning.yasminishop.notification.NotificationRepository; +import com.learning.yasminishop.order.OrderRepository; +import com.learning.yasminishop.order.OrderService; +import com.learning.yasminishop.order.dto.filter.OrderFilter; +import com.learning.yasminishop.order.dto.request.OrderAddressRequest; +import com.learning.yasminishop.order.dto.request.OrderRequest; +import com.learning.yasminishop.order.dto.response.OrderAdminResponse; +import com.learning.yasminishop.order.dto.response.OrderResponse; +import com.learning.yasminishop.user.UserRepository; +import lombok.extern.slf4j.Slf4j; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.TestPropertySource; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@SpringBootTest +@Slf4j +@TestPropertySource("/test.properties") +class OrderServiceTest { + + @Autowired + private OrderService orderService; + + @MockBean + private UserRepository userRepository; + + @MockBean + private CartItemRepository cartItemRepository; + + @MockBean + private OrderRepository orderRepository; + + @MockBean + private NotificationRepository notificationRepository; + + private OrderRequest orderRequest; + private User user; + private List cartItems; + private Order order; + + @BeforeEach + void setUp() { + + Set cartItemIds = Set.of("cart-1", "cart-2"); + + OrderAddressRequest orderAddressRequest = OrderAddressRequest.builder() + .contactName("Hieu Duong") + .phone("0933444555") + .addressLine1("addressLine1") + .addressLine2("addressLine2") + .build(); + + orderRequest = OrderRequest.builder() + .cartItemIds(cartItemIds) + .orderAddress(orderAddressRequest) + .build(); + user = User.builder() + .email("user@test.com") + .password("password") + .build(); + Product product = Product.builder() + .name("Product 1") + .description("Product 1 description") + .price(BigDecimal.valueOf(100)) + .quantity(10L) + .slug("product-1") + .sku("sku-1") + .isFeatured(true) + .isAvailable(true) + .build(); + cartItems = List.of(CartItem.builder() + .id("cart-1") + .product(product) + .quantity(2) + .user(user) + .price(BigDecimal.valueOf(200)) + .build(), + CartItem.builder() + .id("cart-2") + .product(product) + .quantity(2) + .user(user) + .price(BigDecimal.valueOf(200)) + .build()); + + OrderItem orderItem = OrderItem.builder() + .product(product) + .quantity(2) + .price(BigDecimal.valueOf(200)) + .build(); + order = Order.builder() + .id("order-1") + .totalQuantity(4) + .totalPrice(BigDecimal.valueOf(400)) + .orderItems(Set.of(orderItem)) + .user(user) + .build(); + } + + + @Nested + class HappyCase { + @Test + @WithMockUser(username = "user@test.com") + void create_validRequest_success() { + // GIVEN + when(userRepository.findByEmail(any())).thenReturn(Optional.of(user)); + when(cartItemRepository.findAllByUserOrderByLastModifiedDateDesc(any())).thenReturn(cartItems); + when(orderRepository.save(any())).thenReturn(Order.builder().id("order-1").build()); + when(cartItemRepository.findAllById(any())).thenReturn(cartItems); + + // WHEN + OrderResponse orderResponse = orderService.create(orderRequest); + + // THEN + assertThat(orderResponse).isNotNull() + .hasFieldOrPropertyWithValue("id", "order-1") + .hasFieldOrPropertyWithValue("totalQuantity", 4) + .hasFieldOrPropertyWithValue("totalPrice", BigDecimal.valueOf(400)); + } + + @Test + @WithMockUser(username = "user@test.com") + void getAllOrderByUser_validRequest_success() { + // GIVEN + when(userRepository.findByEmail(any())).thenReturn(Optional.of(user)); + when(orderRepository.findAllByUserOrderByLastModifiedDateDesc(any())).thenReturn(List.of(Order.builder().id("order-1").build())); + + // WHEN + List orderResponses = orderService.getAllOrderByUser(); + + // THEN + assertThat(orderResponses.getFirst()).isNotNull() + .hasFieldOrPropertyWithValue("id", "order-1"); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void getAllOrders_validRequest_success() { + // GIVEN + OrderFilter orderFilter = new OrderFilter(); + orderFilter.setStatus(EOrderStatus.PENDING); + + Pageable pageable = PageRequest.of(0, 5); + + Order order = new Order(); + order.setId("order-1"); + + Page orders = new PageImpl<>(List.of(order)); + when(orderRepository.findAll(any(Specification.class), eq(pageable))).thenReturn(orders); + + // WHEN + PaginationResponse result = orderService.getAllOrders(orderFilter, pageable); + + // THEN + assertThat(result).isNotNull() + .hasFieldOrPropertyWithValue("page", 1) + .hasFieldOrPropertyWithValue("total", 1L) + .hasFieldOrPropertyWithValue("itemsPerPage", 5) + .hasFieldOrPropertyWithValue("data", List.of(OrderAdminResponse.builder().id("order-1").build())); + } + + @Test + @WithMockUser(username = "user@test.com") + void getOrderById_validRequest_success() { + // GIVEN + when(userRepository.findByEmail(any())).thenReturn(Optional.of(user)); + when(orderRepository.findByIdAndUser(any(), any())).thenReturn(Optional.of(Order.builder().id("order-1").build())); + + // WHEN + OrderResponse result = orderService.getOrderById("order-1"); + + // THEN + assertThat(result).isNotNull() + .hasFieldOrPropertyWithValue("id", "order-1"); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void getOrderByIdForAdmin_validRequest_success() { + // GIVEN + when(orderRepository.findById(any())).thenReturn(Optional.of(Order.builder().id("order-1").build())); + + // WHEN + OrderAdminResponse result = orderService.getOrderByIdForAdmin("order-1"); + + // THEN + assertThat(result).isNotNull() + .hasFieldOrPropertyWithValue("id", "order-1"); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + void updateOrderStatus_validRequest_success() { + // GIVEN + when(orderRepository.findById(any())).thenReturn(Optional.of(order)); + when(notificationRepository.save(any())).thenReturn(Notification.builder().id("notification-1").build()); + + // WHEN + orderService.updateOrderStatus("order-1", EOrderStatus.COMPLETED.name()); + + // THEN + verify(orderRepository, times(1)).save(any(Order.class)); + } + + } + + @Nested + class UnHappyCase { + @Test + @WithMockUser(username = "user@test.com") + void create_userNotFound_throwException() { + // GIVEN + when(userRepository.findByEmail(any())).thenReturn(Optional.empty()); + + // WHEN + var exception = assertThrows(AppException.class, () -> orderService.create(orderRequest)); + + // THEN + assertThat(exception).isInstanceOf(AppException.class); + Assertions.assertThat(exception.getErrorCode().getInternalCode()) + .isEqualTo(1006); + + Assertions.assertThat(exception.getErrorCode().getMessage()) + .isEqualTo("User not found"); + } + + @Test + @WithMockUser(username = "user@test.com") + void create_cartItemNotFound_throwException() { + // GIVEN + when(userRepository.findByEmail(any())).thenReturn(Optional.of(user)); + when(cartItemRepository.findAllByUserOrderByLastModifiedDateDesc(any())).thenReturn(List.of()); + + // WHEN + var exception = assertThrows(AppException.class, () -> orderService.create(orderRequest)); + + // THEN + assertThat(exception).isInstanceOf(AppException.class); + Assertions.assertThat(exception.getErrorCode().getInternalCode()) + .isEqualTo(1022); + + Assertions.assertThat(exception.getErrorCode().getMessage()) + .isEqualTo("Cart item not found"); + } + + @Test + @WithMockUser(username = "user@test.com") + void getOrderById_orderNotFound_throwException() { + // GIVEN + when(userRepository.findByEmail(any())).thenReturn(Optional.of(user)); + when(orderRepository.findByIdAndUser(any(), any())).thenReturn(Optional.empty()); + + // WHEN + var exception = assertThrows(AppException.class, () -> orderService.getOrderById("order-1")); + + // THEN + assertThat(exception).isInstanceOf(AppException.class); + Assertions.assertThat(exception.getErrorCode().getInternalCode()) + .isEqualTo(1023); + + Assertions.assertThat(exception.getErrorCode().getMessage()) + .isEqualTo("Order not found"); + } + } + +} From 98010c362867b0933779ae2f8b9bffe11af97f49 Mon Sep 17 00:00:00 2001 From: duongminhhieu Date: Thu, 6 Jun 2024 16:11:46 +0700 Subject: [PATCH 20/22] unit-test: fix create_validRequest_success --- .../java/com/learning/yasminishop/service/OrderServiceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/learning/yasminishop/service/OrderServiceTest.java b/src/test/java/com/learning/yasminishop/service/OrderServiceTest.java index aba1402..ce44e97 100644 --- a/src/test/java/com/learning/yasminishop/service/OrderServiceTest.java +++ b/src/test/java/com/learning/yasminishop/service/OrderServiceTest.java @@ -70,7 +70,7 @@ class OrderServiceTest { @BeforeEach void setUp() { - Set cartItemIds = Set.of("cart-1", "cart-2"); + Set cartItemIds = Set.of("cart-1"); OrderAddressRequest orderAddressRequest = OrderAddressRequest.builder() .contactName("Hieu Duong") From d5fe585e25d9c081a9975548d98fea9b562fd369 Mon Sep 17 00:00:00 2001 From: duongminhhieu Date: Thu, 6 Jun 2024 16:50:36 +0700 Subject: [PATCH 21/22] unit-test: write unit-test for OrderController --- .../controller/OrderControllerTest.java | 158 ++++++++++++++++++ .../yasminishop/service/OrderServiceTest.java | 33 ---- 2 files changed, 158 insertions(+), 33 deletions(-) create mode 100644 src/test/java/com/learning/yasminishop/controller/OrderControllerTest.java diff --git a/src/test/java/com/learning/yasminishop/controller/OrderControllerTest.java b/src/test/java/com/learning/yasminishop/controller/OrderControllerTest.java new file mode 100644 index 0000000..f49d2fd --- /dev/null +++ b/src/test/java/com/learning/yasminishop/controller/OrderControllerTest.java @@ -0,0 +1,158 @@ +package com.learning.yasminishop.controller; + +import com.learning.yasminishop.common.entity.Order; +import com.learning.yasminishop.common.entity.OrderItem; +import com.learning.yasminishop.common.entity.Product; +import com.learning.yasminishop.common.entity.User; +import com.learning.yasminishop.order.OrderRepository; +import com.learning.yasminishop.product.ProductRepository; +import com.learning.yasminishop.user.UserRepository; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import java.math.BigDecimal; +import java.util.Set; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@Slf4j +@AutoConfigureMockMvc +@TestPropertySource("/test.properties") +class OrderControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private OrderRepository orderRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private ProductRepository productRepository; + + private Order order; + private User user; + private Product product; + + @BeforeEach + void setUp() { + + product = Product.builder() + .name("Product 1") + .description("Product 1 description") + .price(BigDecimal.valueOf(100)) + .quantity(10L) + .slug("product-1") + .sku("sku-1") + .isFeatured(true) + .isAvailable(true) + .build(); + + OrderItem orderItem = OrderItem.builder() + .product(product) + .quantity(2) + .price(BigDecimal.valueOf(200)) + .build(); + + user = User.builder() + .email("user@test.com") + .password("password") + .build(); + order = Order.builder() + .id("order-1") + .totalQuantity(4) + .totalPrice(BigDecimal.valueOf(400)) + .orderItems(Set.of(orderItem)) + .user(user) + .build(); + + } + + @Test + @WithMockUser(username = "user@test.com") + @DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD) + void getAllOrders_validRequest_success() throws Exception { + // GIVEN + User savedUser = userRepository.save(user); + Product savedProduct = productRepository.save(product); + order.setUser(savedUser); + order.getOrderItems().forEach(orderItem -> orderItem.setProduct(savedProduct)); + orderRepository.save(order); + + // WHEN THEN + mockMvc.perform(MockMvcRequestBuilders.get("/orders")) + .andExpect(status().isOk()) + .andExpect(jsonPath("internalCode").value(1000)) + .andExpect(jsonPath("result").isArray()); + } + + @Test + @WithMockUser(username = "user@test.com") + @DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD) + void getOrderById_validRequest_success() throws Exception { + // GIVEN + User savedUser = userRepository.save(user); + Product savedProduct = productRepository.save(product); + order.setUser(savedUser); + order.getOrderItems().forEach(orderItem -> orderItem.setProduct(savedProduct)); + Order savedOrder = orderRepository.save(order); + + // WHEN THEN + mockMvc.perform(MockMvcRequestBuilders.get("/orders/" + savedOrder.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("internalCode").value(1000)) + .andExpect(jsonPath("result.id").value(savedOrder.getId())); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + @DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD) + void getAllOrdersForAdmin_validRequest_success() throws Exception { + // GIVEN + User savedUser = userRepository.save(user); + Product savedProduct = productRepository.save(product); + order.setUser(savedUser); + order.getOrderItems().forEach(orderItem -> orderItem.setProduct(savedProduct)); + orderRepository.save(order); + + // WHEN THEN + mockMvc.perform(MockMvcRequestBuilders.get("/orders/admin")) + .andExpect(status().isOk()) + .andExpect(jsonPath("internalCode").value(1000)) + .andExpect(jsonPath("result.total").value(1)) + .andExpect(jsonPath("result.page").value(1)) + .andExpect(jsonPath("result.data").isArray()); + } + + @Test + @WithMockUser(username = "admin@test.com", roles = {"ADMIN"}) + @DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD) + void getOrderByIdForAdmin_validRequest_success() throws Exception { + // GIVEN + User savedUser = userRepository.save(user); + Product savedProduct = productRepository.save(product); + order.setUser(savedUser); + order.getOrderItems().forEach(orderItem -> orderItem.setProduct(savedProduct)); + Order savedOrder = orderRepository.save(order); + + // WHEN THEN + mockMvc.perform(MockMvcRequestBuilders.get("/orders/" + savedOrder.getId() + "/admin")) + .andExpect(status().isOk()) + .andExpect(jsonPath("internalCode").value(1000)) + .andExpect(jsonPath("result.id").value(savedOrder.getId())); + } + +} diff --git a/src/test/java/com/learning/yasminishop/service/OrderServiceTest.java b/src/test/java/com/learning/yasminishop/service/OrderServiceTest.java index ce44e97..7f068c7 100644 --- a/src/test/java/com/learning/yasminishop/service/OrderServiceTest.java +++ b/src/test/java/com/learning/yasminishop/service/OrderServiceTest.java @@ -64,7 +64,6 @@ class OrderServiceTest { private OrderRequest orderRequest; private User user; - private List cartItems; private Order order; @BeforeEach @@ -97,20 +96,6 @@ void setUp() { .isFeatured(true) .isAvailable(true) .build(); - cartItems = List.of(CartItem.builder() - .id("cart-1") - .product(product) - .quantity(2) - .user(user) - .price(BigDecimal.valueOf(200)) - .build(), - CartItem.builder() - .id("cart-2") - .product(product) - .quantity(2) - .user(user) - .price(BigDecimal.valueOf(200)) - .build()); OrderItem orderItem = OrderItem.builder() .product(product) @@ -129,24 +114,6 @@ void setUp() { @Nested class HappyCase { - @Test - @WithMockUser(username = "user@test.com") - void create_validRequest_success() { - // GIVEN - when(userRepository.findByEmail(any())).thenReturn(Optional.of(user)); - when(cartItemRepository.findAllByUserOrderByLastModifiedDateDesc(any())).thenReturn(cartItems); - when(orderRepository.save(any())).thenReturn(Order.builder().id("order-1").build()); - when(cartItemRepository.findAllById(any())).thenReturn(cartItems); - - // WHEN - OrderResponse orderResponse = orderService.create(orderRequest); - - // THEN - assertThat(orderResponse).isNotNull() - .hasFieldOrPropertyWithValue("id", "order-1") - .hasFieldOrPropertyWithValue("totalQuantity", 4) - .hasFieldOrPropertyWithValue("totalPrice", BigDecimal.valueOf(400)); - } @Test @WithMockUser(username = "user@test.com") From 205fb4a53f62b0f145e58ffb06da46eb6316d45b Mon Sep 17 00:00:00 2001 From: duongminhhieu Date: Thu, 6 Jun 2024 17:50:24 +0700 Subject: [PATCH 22/22] unit-test: write unit-test for RatingService --- .../service/RatingServiceTest.java | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 src/test/java/com/learning/yasminishop/service/RatingServiceTest.java diff --git a/src/test/java/com/learning/yasminishop/service/RatingServiceTest.java b/src/test/java/com/learning/yasminishop/service/RatingServiceTest.java new file mode 100644 index 0000000..6830b6e --- /dev/null +++ b/src/test/java/com/learning/yasminishop/service/RatingServiceTest.java @@ -0,0 +1,179 @@ +package com.learning.yasminishop.service; + +import com.learning.yasminishop.common.dto.PaginationResponse; +import com.learning.yasminishop.common.entity.Product; +import com.learning.yasminishop.common.entity.Rating; +import com.learning.yasminishop.common.entity.User; +import com.learning.yasminishop.common.exception.AppException; +import com.learning.yasminishop.product.ProductRepository; +import com.learning.yasminishop.rating.RatingRepository; +import com.learning.yasminishop.rating.RatingService; +import com.learning.yasminishop.rating.dto.request.RatingRequest; +import com.learning.yasminishop.rating.dto.response.RatingResponse; +import com.learning.yasminishop.user.UserRepository; +import lombok.extern.slf4j.Slf4j; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.TestPropertySource; + +import java.math.BigDecimal; +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +@SpringBootTest +@Slf4j +@TestPropertySource("/test.properties") +class RatingServiceTest { + + @Autowired + private RatingService ratingService; + + @MockBean + private ProductRepository productRepository; + + @MockBean + private RatingRepository ratingRepository; + + @MockBean + private UserRepository userRepository; + + private RatingRequest ratingRequest; + private Product product; + private User user; + private Rating rating; + private Pageable pageable; + + @BeforeEach + void setUp() { + pageable = PageRequest.of(0, 5); + + ratingRequest = RatingRequest.builder() + .productId("product-1") + .star(5) + .comment("Review") + .build(); + + product = Product.builder() + .id("product-1") + .name("Product 1") + .description("Product 1 description") + .price(BigDecimal.valueOf(100)) + .quantity(10L) + .slug("product-1") + .sku("sku-1") + .isFeatured(true) + .isAvailable(true) + .build(); + user = User.builder() + .email("user@test.com") + .password("password") + .build(); + + rating = Rating.builder() + .id("rating-1") + .star(5) + .comment("Review") + .product(product) + .user(user) + .build(); + + } + + @Nested + class HappyCase { + + @Test + @WithMockUser(username = "user@test.com") + void create_validRequest_success() { + // GIVEN + when(productRepository.findById(anyString())).thenReturn(java.util.Optional.of(product)); + when(userRepository.findByEmail(anyString())).thenReturn(java.util.Optional.of(user)); + when(ratingRepository.existsByProductAndUser(any(), any())).thenReturn(false); + when(ratingRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + + // WHEN + var result = ratingService.create(ratingRequest); + + // THEN + assertThat(result).isNotNull(); + assertThat(result.getStar()).isEqualTo(ratingRequest.getStar()); + assertThat(result.getComment()).isEqualTo(ratingRequest.getComment()); + } + + @Test + @WithMockUser(username = "user@test.com") + void getRatings_validRequest_success() { + // GIVEN + List ratings = List.of(rating); + Page ratingPage = new PageImpl<>(ratings, pageable, ratings.size()); + when(ratingRepository.findByProductOrderByCreatedDateDesc(any(), any())).thenReturn(ratingPage); + when(productRepository.findById(anyString())).thenReturn(java.util.Optional.of(product)); + + // WHEN + PaginationResponse result = ratingService.getRatings("product-1", pageable); + + // THEN + Assertions.assertThat(result).isNotNull(); + Assertions.assertThat(result.getData()).isNotEmpty(); + Assertions.assertThat(result.getData().getFirst().getStar()).isEqualTo(rating.getStar()); + Assertions.assertThat(result.getData().getFirst().getComment()).isEqualTo(rating.getComment()); + } + + } + + @Nested + class UnHappyCase { + @Test + @WithMockUser(username = "user@test.com") + void create_ratingAlreadyExists_exception() { + // GIVEN + when(productRepository.findById(anyString())).thenReturn(java.util.Optional.of(product)); + when(userRepository.findByEmail(anyString())).thenReturn(java.util.Optional.of(user)); + when(ratingRepository.existsByProductAndUser(any(), any())).thenReturn(true); + + // WHEN + var exception = assertThrows(AppException.class, () -> ratingService.create(ratingRequest)); + + + // THEN + assertThat(exception).isInstanceOf(AppException.class); + Assertions.assertThat(exception.getErrorCode().getInternalCode()) + .isEqualTo(1017); + + } + + @Test + @WithMockUser(username = "user@test.com") + void create_productNotFound_throwException() { + // GIVEN + when(productRepository.findById(anyString())).thenReturn(java.util.Optional.empty()); + + // WHEN + var exception = assertThrows(AppException.class, () -> ratingService.create(ratingRequest)); + + // THEN + assertThat(exception).isInstanceOf(AppException.class); + Assertions.assertThat(exception.getErrorCode().getInternalCode()) + .isEqualTo(1009); + } + + + } + + +}