diff --git a/.github/workflows/cd_workflow.yml b/.github/workflows/cd_workflow.yml index 0916e5c..491276b 100644 --- a/.github/workflows/cd_workflow.yml +++ b/.github/workflows/cd_workflow.yml @@ -64,5 +64,5 @@ jobs: script: | echo "Starting new application..." source ~/.bashrc - nohup java -jar ~/target/BOOK-STORE-COUPON.jar --spring.profiles.active=prod --server.port=${{ secrets.COUPON_PORT }} --spring.datasource.password=${{ secrets.MYSQL_PASSWORD }} --spring.redis.password=${{ secrets.REDIS_PASSWORD }} > coupon.log 2>&1 & + nohup java -Xmx256m -jar ~/target/BOOK-STORE-COUPON.jar --spring.profiles.active=prod --server.port=${{ secrets.COUPON_PORT }} --spring.datasource.password=${{ secrets.MYSQL_PASSWORD }} --spring.redis.password=${{ secrets.REDIS_PASSWORD }} > coupon.log 2>&1 & echo "New application started. Check app.log for details." diff --git a/pom.xml b/pom.xml index 1ea5816..2ff00d2 100644 --- a/pom.xml +++ b/pom.xml @@ -32,15 +32,6 @@ - - - - - - - - - @@ -61,6 +52,7 @@ spring-boot-starter-web + org.springframework.security spring-security-test @@ -88,7 +80,11 @@ runtime - + + org.junit.jupiter + junit-jupiter-engine + test + org.springframework.boot @@ -186,6 +182,20 @@ spring-boot-starter-amqp + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.3.0 + + + + + org.assertj + assertj-core + test + + + diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/actuator/ApplicationStatus.java b/src/main/java/com/nhnacademy/bookstorecoupon/actuator/ApplicationStatus.java deleted file mode 100644 index 051c750..0000000 --- a/src/main/java/com/nhnacademy/bookstorecoupon/actuator/ApplicationStatus.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.nhnacademy.bookstorecoupon.actuator; - -import org.springframework.stereotype.Component; - -@Component -public final class ApplicationStatus { - private boolean status = true; - - public void stopService() { - this.status = false; - } - - public boolean getStatus() { - return status; - } -} diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/actuator/CustomHealthIndicator.java b/src/main/java/com/nhnacademy/bookstorecoupon/actuator/CustomHealthIndicator.java deleted file mode 100644 index f9c30b3..0000000 --- a/src/main/java/com/nhnacademy/bookstorecoupon/actuator/CustomHealthIndicator.java +++ /dev/null @@ -1,24 +0,0 @@ - - -package com.nhnacademy.bookstorecoupon.actuator; - -import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.HealthIndicator; -import org.springframework.stereotype.Component; - -@Component -public class CustomHealthIndicator implements HealthIndicator { - private final ApplicationStatus applicationStatus; - - public CustomHealthIndicator(ApplicationStatus applicationStatus) { - this.applicationStatus = applicationStatus; - } - - @Override - public Health health() { - if (!applicationStatus.getStatus()) { - return Health.down().build(); - } - return Health.up().withDetail("service", "start").build(); - } -} diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/bookcoupon/exception/BookNotFoundException.java b/src/main/java/com/nhnacademy/bookstorecoupon/bookcoupon/exception/BookNotFoundException.java deleted file mode 100644 index e4ca4d6..0000000 --- a/src/main/java/com/nhnacademy/bookstorecoupon/bookcoupon/exception/BookNotFoundException.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.nhnacademy.bookstorecoupon.bookcoupon.exception; - - -import com.nhnacademy.bookstorecoupon.global.exception.GlobalException; -import com.nhnacademy.bookstorecoupon.global.exception.payload.ErrorStatus; - -import lombok.Getter; - -@Getter -public class BookNotFoundException extends GlobalException { - public BookNotFoundException(ErrorStatus errorStatus) { - super(errorStatus); - } -} diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/categorycoupon/exception/CategoryNotFoundException.java b/src/main/java/com/nhnacademy/bookstorecoupon/categorycoupon/exception/CategoryNotFoundException.java deleted file mode 100644 index b4919b9..0000000 --- a/src/main/java/com/nhnacademy/bookstorecoupon/categorycoupon/exception/CategoryNotFoundException.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.nhnacademy.bookstorecoupon.categorycoupon.exception; - - -import com.nhnacademy.bookstorecoupon.global.exception.GlobalException; -import com.nhnacademy.bookstorecoupon.global.exception.payload.ErrorStatus; - -import lombok.Getter; - -@Getter -public class CategoryNotFoundException extends GlobalException { - public CategoryNotFoundException(ErrorStatus errorStatus) { - super(errorStatus); - } -} diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/controller/CouponPolicyController.java b/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/controller/CouponPolicyController.java index 42a84b7..a6b71bb 100644 --- a/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/controller/CouponPolicyController.java +++ b/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/controller/CouponPolicyController.java @@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import com.nhnacademy.bookstorecoupon.auth.annotation.AuthorizeRole; import com.nhnacademy.bookstorecoupon.couponpolicy.domain.dto.request.CouponPolicyRequestDTO; import com.nhnacademy.bookstorecoupon.couponpolicy.domain.dto.request.CouponPolicyUpdateRequestDTO; import com.nhnacademy.bookstorecoupon.couponpolicy.domain.dto.response.CouponPolicyResponseDTO; @@ -22,8 +23,13 @@ import com.nhnacademy.bookstorecoupon.couponpolicy.service.CouponPolicyService; import com.nhnacademy.bookstorecoupon.global.exception.payload.ErrorStatus; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; - +@Tag(name = "CouponPolicy", description = "쿠폰 정책관련 API") @RestController @RequestMapping("/coupons/policies") public class CouponPolicyController { @@ -33,24 +39,51 @@ public CouponPolicyController(CouponPolicyService couponPolicyService) { this.couponPolicyService = couponPolicyService; } + @Operation( + summary = "웰컴쿠폰 생성", + description = "웰컴쿠폰을 생성합니다" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "웰컴쿠폰이 성공적으로 발행되었습니다."), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.") + }) + @AuthorizeRole({"COUPON_ADMIN", "HEAD_ADMIN"}) @PostMapping("/welcome") - public ResponseEntity issueWelcomeCoupon(@Valid @RequestBody CouponPolicyRequestDTO couponPolicyRequestDTO) { + public ResponseEntity issueWelcomeCoupon( + @Parameter(description = "웰컴쿠폰정책 발행 요청 데이터", required = true) @Valid @RequestBody CouponPolicyRequestDTO couponPolicyRequestDTO) { validateSaleFields(couponPolicyRequestDTO); couponPolicyService.issueWelcomeCoupon(couponPolicyRequestDTO); return ResponseEntity.status(HttpStatus.CREATED).build(); } - + @Operation( + summary = "생일쿠폰 생성", + description = "웰컴쿠폰을 생성합니다" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "생일쿠폰이 성공적으로 발행되었습니다."), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.") + }) + @AuthorizeRole({"COUPON_ADMIN", "HEAD_ADMIN"}) @PostMapping("/birthday") - public ResponseEntity issueBirthdayCoupon(@Valid @RequestBody CouponPolicyRequestDTO couponPolicyRequestDTO) { + public ResponseEntity issueBirthdayCoupon( + @Parameter(description = "생일쿠폰정책 발행 요청 데이터", required = true) @Valid @RequestBody CouponPolicyRequestDTO couponPolicyRequestDTO) { validateSaleFields(couponPolicyRequestDTO); couponPolicyService.issueBirthdayCoupon(couponPolicyRequestDTO); return ResponseEntity.status(HttpStatus.CREATED).build(); } + + @Operation(summary = "도서 쿠폰 발행", description = "특정 도서에 대한 도서쿠폰을 발행합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "도서쿠폰이 성공적으로 발행되었습니다."), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.") + }) + @AuthorizeRole({"COUPON_ADMIN", "HEAD_ADMIN"}) @PostMapping("/books") - public ResponseEntity issueSpecificBookCoupon(@Valid @RequestBody CouponPolicyRequestDTO couponPolicyRequestDTO) { + public ResponseEntity issueSpecificBookCoupon( + @Parameter(description = "도서쿠폰정책 발행 요청 데이터", required = true) @Valid @RequestBody CouponPolicyRequestDTO couponPolicyRequestDTO) { validateSaleFields(couponPolicyRequestDTO); - if (couponPolicyRequestDTO.bookId()==null && couponPolicyRequestDTO.bookTitle() == null){ + if (couponPolicyRequestDTO.bookId()==null || couponPolicyRequestDTO.bookTitle() == null){ ErrorStatus errorStatus = ErrorStatus.from("북 쿠폰 발행시 책 아이디와 책 제목이 필요합니다.", HttpStatus.BAD_REQUEST, LocalDateTime.now()); throw new CouponPolicyValidationException(errorStatus); } @@ -58,10 +91,17 @@ public ResponseEntity issueSpecificBookCoupon(@Valid @RequestBody CouponPo return ResponseEntity.status(HttpStatus.CREATED).build(); } + @Operation(summary = "카테고리 쿠폰 발행", description = "특정 카테고리에 대한 카테고리쿠폰을 발행합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "카테고리쿠폰이 성공적으로 발행되었습니다."), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.") + }) + @AuthorizeRole({"COUPON_ADMIN", "HEAD_ADMIN"}) @PostMapping("/categories") - public ResponseEntity issueSpecificCategoryCoupon(@Valid @RequestBody CouponPolicyRequestDTO couponPolicyRequestDTO) { + public ResponseEntity issueSpecificCategoryCoupon( + @Parameter(description = "카테고리쿠폰정책 발행 요청 데이터", required = true) @Valid @RequestBody CouponPolicyRequestDTO couponPolicyRequestDTO) { validateSaleFields(couponPolicyRequestDTO); - if (couponPolicyRequestDTO.categoryId()==null && couponPolicyRequestDTO.categoryName() == null){ + if (couponPolicyRequestDTO.categoryId()==null || couponPolicyRequestDTO.categoryName() == null){ ErrorStatus errorStatus = ErrorStatus.from("카테고리 쿠폰 발행시 카테고리 아이디와 카테고리 이름이 필요합니다.", HttpStatus.BAD_REQUEST, LocalDateTime.now()); throw new CouponPolicyValidationException(errorStatus); } @@ -69,25 +109,47 @@ public ResponseEntity issueSpecificCategoryCoupon(@Valid @RequestBody Coup return ResponseEntity.status(HttpStatus.CREATED).build(); } + + @Operation(summary = "할인 쿠폰 발행", description = "할인 쿠폰을 발행합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "할인쿠폰이 성공적으로 발행되었습니다."), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.") + }) + @AuthorizeRole({"COUPON_ADMIN", "HEAD_ADMIN"}) @PostMapping("/sale") - public ResponseEntity issueDiscountCoupon(@Valid @RequestBody CouponPolicyRequestDTO couponPolicyRequestDTO) { + public ResponseEntity issueDiscountCoupon( + @Parameter(description = "할인쿠폰정책 발행 요청 데이터", required = true) @Valid @RequestBody CouponPolicyRequestDTO couponPolicyRequestDTO) { validateSaleFields(couponPolicyRequestDTO); couponPolicyService.issueDiscountCoupon(couponPolicyRequestDTO); return ResponseEntity.status(HttpStatus.CREATED).build(); } + @Operation(summary = "모든 쿠폰 정책 조회", description = "페이징을 통해 모든 쿠폰 정책을 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공적으로 조회되었습니다.") + }) + @AuthorizeRole({"COUPON_ADMIN", "HEAD_ADMIN"}) @GetMapping - public ResponseEntity> getAllCouponPolicies(@PageableDefault(page = 1, size = 3) Pageable pageable) { + public ResponseEntity> getAllCouponPolicies(@Parameter(description = "페이지 수, 페이지 사이즈", required = false) @PageableDefault(page = 1, size = 3) Pageable pageable) { Page policies = couponPolicyService.getAllCouponPolicies(pageable); return ResponseEntity.status(HttpStatus.OK).body(policies); } + @Operation(summary = "쿠폰 정책 업데이트", description = "쿠폰 정책을 업데이트합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공적으로 업데이트되었습니다."), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.") + }) + @AuthorizeRole({"COUPON_ADMIN", "HEAD_ADMIN"}) @PatchMapping("/{couponPolicyId}") - public ResponseEntity updateCouponPolicy(@PathVariable Long couponPolicyId, @Valid @RequestBody CouponPolicyUpdateRequestDTO requestDTO) { + public ResponseEntity updateCouponPolicy( + @Parameter(description = "쿠폰 정책 아이디", required = true) @PathVariable Long couponPolicyId, @Parameter(description = "쿠폰정책 변경 요청 데이터", required = true) @Valid @RequestBody CouponPolicyUpdateRequestDTO requestDTO) { if ((requestDTO.salePrice() == null && requestDTO.saleRate() == null && requestDTO.maxSalePrice() == null) || - (requestDTO.salePrice() != null && requestDTO.saleRate() != null && requestDTO.maxSalePrice() != null)) { + (requestDTO.salePrice() != null && requestDTO.saleRate() != null && requestDTO.maxSalePrice() != null)|| + (requestDTO.salePrice() != null && requestDTO.saleRate() != null && requestDTO.maxSalePrice() == null)|| + (requestDTO.salePrice() != null && requestDTO.saleRate() == null && requestDTO.maxSalePrice() != null)) { ErrorStatus errorStatus = ErrorStatus.from("해당 할인률, 최대가격과 할인가격은 동시에 작성할 수 없습니다", HttpStatus.BAD_REQUEST, LocalDateTime.now()); throw new CouponPolicyValidationException(errorStatus); @@ -100,7 +162,9 @@ public ResponseEntity updateCouponPolicy(@PathVariable Long couponPolicyId // SalePrice와 SaleRate 유효성 검사 메서드 추가 private void validateSaleFields(CouponPolicyRequestDTO requestDTO) { if ((requestDTO.salePrice() == null && requestDTO.saleRate() == null && requestDTO.maxSalePrice() == null) || - (requestDTO.salePrice() != null && requestDTO.saleRate() != null && requestDTO.maxSalePrice() != null)) { + (requestDTO.salePrice() != null && requestDTO.saleRate() != null && requestDTO.maxSalePrice() != null)|| + (requestDTO.salePrice() != null && requestDTO.saleRate() != null && requestDTO.maxSalePrice() == null)|| + (requestDTO.salePrice() != null && requestDTO.saleRate() == null && requestDTO.maxSalePrice() != null)) { ErrorStatus errorStatus = ErrorStatus.from("해당 할인률, 최대가격과 할인가격은 동시에 작성할 수 없습니다", HttpStatus.BAD_REQUEST, LocalDateTime.now()); throw new CouponPolicyValidationException(errorStatus); } diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/domain/dto/request/CouponPolicyRequestDTO.java b/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/domain/dto/request/CouponPolicyRequestDTO.java index 312bb95..669d945 100644 --- a/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/domain/dto/request/CouponPolicyRequestDTO.java +++ b/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/domain/dto/request/CouponPolicyRequestDTO.java @@ -2,12 +2,16 @@ import java.math.BigDecimal; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.Digits; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; -public record CouponPolicyRequestDTO(@NotNull BigDecimal minOrderPrice, - BigDecimal salePrice, - BigDecimal saleRate, - BigDecimal maxSalePrice, - @NotBlank String type, Long bookId, String bookTitle, Long categoryId, String categoryName) { -} \ No newline at end of file + +public record CouponPolicyRequestDTO(@NotNull @Positive @Digits(integer = 8, fraction = 2) BigDecimal minOrderPrice, + @Nullable @Positive @Digits(integer = 8, fraction = 2) BigDecimal salePrice, + @Nullable @Positive @Digits(integer = 0, fraction = 2) BigDecimal saleRate, + @Nullable @Positive @Digits(integer = 8, fraction = 2) BigDecimal maxSalePrice, + @NotBlank String type, @Nullable Long bookId, @Nullable String bookTitle, @Nullable Long categoryId, @Nullable String categoryName) { +} diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/domain/dto/request/CouponPolicyUpdateRequestDTO.java b/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/domain/dto/request/CouponPolicyUpdateRequestDTO.java index 2c409c2..d7b4e49 100644 --- a/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/domain/dto/request/CouponPolicyUpdateRequestDTO.java +++ b/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/domain/dto/request/CouponPolicyUpdateRequestDTO.java @@ -2,8 +2,12 @@ import java.math.BigDecimal; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.Digits; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; -public record CouponPolicyUpdateRequestDTO(@NotNull BigDecimal minOrderPrice, BigDecimal salePrice, BigDecimal saleRate, - BigDecimal maxSalePrice, @NotNull Boolean isUsed) { +public record CouponPolicyUpdateRequestDTO(@NotNull @Positive @Digits(integer = 8, fraction = 2) BigDecimal minOrderPrice, @Nullable @Positive @Digits(integer = 8, fraction = 2) BigDecimal salePrice, + @Nullable @Positive @Digits(integer = 0, fraction = 2) BigDecimal saleRate, + @Nullable @Positive @Digits(integer = 8, fraction = 2) BigDecimal maxSalePrice, @NotNull Boolean isUsed) { } \ No newline at end of file diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/service/impl/CouponPolicyServiceImpl.java b/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/service/impl/CouponPolicyServiceImpl.java index 5b74518..dff17f1 100644 --- a/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/service/impl/CouponPolicyServiceImpl.java +++ b/src/main/java/com/nhnacademy/bookstorecoupon/couponpolicy/service/impl/CouponPolicyServiceImpl.java @@ -20,7 +20,6 @@ import com.nhnacademy.bookstorecoupon.couponpolicy.domain.dto.request.CouponPolicyUpdateRequestDTO; import com.nhnacademy.bookstorecoupon.couponpolicy.domain.dto.response.CouponPolicyResponseDTO; import com.nhnacademy.bookstorecoupon.couponpolicy.domain.entity.CouponPolicy; -import com.nhnacademy.bookstorecoupon.couponpolicy.exception.CouponPolicyBanUpdateException; import com.nhnacademy.bookstorecoupon.couponpolicy.exception.CouponPolicyNotFoundException; import com.nhnacademy.bookstorecoupon.couponpolicy.repository.CouponPolicyRepository; import com.nhnacademy.bookstorecoupon.couponpolicy.service.CouponPolicyService; @@ -108,10 +107,6 @@ public Page getAllCouponPolicies(Pageable pageable) { public void updateCouponPolicy(Long id, CouponPolicyUpdateRequestDTO requestDTO) { Optional optionalPolicy = couponPolicyRepository.findById(id); - if (Boolean.TRUE.equals(requestDTO.isUsed())) { - ErrorStatus errorStatus = ErrorStatus.from("이미 폐기된 쿠폰정책은 변경할 수 없습니다", HttpStatus.BAD_REQUEST, LocalDateTime.now()); - throw new CouponPolicyBanUpdateException(errorStatus); - } if (optionalPolicy.isPresent()) { CouponPolicy policy = optionalPolicy.get(); @@ -137,6 +132,7 @@ public void updateCouponPolicy(Long id, CouponPolicyUpdateRequestDTO requestDTO) } } + } diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/coupontemplate/controller/CouponTemplateController.java b/src/main/java/com/nhnacademy/bookstorecoupon/coupontemplate/controller/CouponTemplateController.java index 1ec59a4..a78c479 100644 --- a/src/main/java/com/nhnacademy/bookstorecoupon/coupontemplate/controller/CouponTemplateController.java +++ b/src/main/java/com/nhnacademy/bookstorecoupon/coupontemplate/controller/CouponTemplateController.java @@ -12,12 +12,18 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import com.nhnacademy.bookstorecoupon.auth.annotation.AuthorizeRole; import com.nhnacademy.bookstorecoupon.coupontemplate.domain.dto.request.CouponTemplateRequestDTO; import com.nhnacademy.bookstorecoupon.coupontemplate.domain.dto.response.CouponTemplateResponseDTO; import com.nhnacademy.bookstorecoupon.coupontemplate.service.CouponTemplateService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; - +@Tag(name = "CouponTemplate", description = "쿠폰템플릿관련 API") @RestController @RequestMapping("/coupons") public class CouponTemplateController { @@ -29,16 +35,27 @@ public CouponTemplateController(CouponTemplateService couponTemplateService) { this.couponTemplateService = couponTemplateService; } + + @Operation(summary = "쿠폰 템플릿 생성", description = "새로운 쿠폰 템플릿을 생성합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "쿠폰 템플릿이 성공적으로 생성되었습니다."), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.") + }) + @AuthorizeRole({"COUPON_ADMIN", "HEAD_ADMIN"}) @PostMapping - public ResponseEntity createCouponTemplate(@RequestBody @Valid CouponTemplateRequestDTO requestDTO) { + public ResponseEntity createCouponTemplate(@Parameter(description = "쿠폰 템플릿 발행시 요청데이터", required = true) @Valid @RequestBody CouponTemplateRequestDTO requestDTO) { couponTemplateService.createCouponTemplate(requestDTO); return ResponseEntity.status(HttpStatus.CREATED).build(); } - + @Operation(summary = "유저 페이징으로 모든 쿠폰 템플릿 조회", description = "유저 기준으로 페이징된 모든 쿠폰 템플릿을 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "쿠폰 템플릿 조회 성공"), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.") + }) @GetMapping("/issue") - public ResponseEntity> getAllCouponTemplatesByUserPaging(@PageableDefault(page = 1, size = 3)Pageable pageable) { + public ResponseEntity> getAllCouponTemplatesByUserPaging(@Parameter(description = "페이지 수, 페이지 사이즈", required = false) @PageableDefault(page = 1, size = 3)Pageable pageable) { Page coupons = couponTemplateService.getAllCouponTemplatesByUserPaging(pageable); return ResponseEntity.status(HttpStatus.OK).body(coupons); @@ -46,8 +63,14 @@ public ResponseEntity> getAllCouponTemplatesByUs } + @Operation(summary = "관리자 페이징으로 모든 쿠폰 템플릿 조회", description = "관리자 기준으로 페이징된 모든 쿠폰 템플릿을 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "쿠폰 템플릿 조회 성공"), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.") + }) + @AuthorizeRole({"COUPON_ADMIN", "HEAD_ADMIN"}) @GetMapping - public ResponseEntity> getAllCouponTemplatesByManagerPaging(@PageableDefault(page = 1, size = 3) Pageable pageable + public ResponseEntity> getAllCouponTemplatesByManagerPaging(@Parameter(description = "페이지 수, 페이지 사이즈", required = false) @PageableDefault(page = 1, size = 3) Pageable pageable ) { Page coupons = couponTemplateService.getAllCouponTemplatesByManagerPaging(pageable); diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/coupontemplate/domain/dto/request/CouponTemplateRequestDTO.java b/src/main/java/com/nhnacademy/bookstorecoupon/coupontemplate/domain/dto/request/CouponTemplateRequestDTO.java index a26905f..1e3eb90 100644 --- a/src/main/java/com/nhnacademy/bookstorecoupon/coupontemplate/domain/dto/request/CouponTemplateRequestDTO.java +++ b/src/main/java/com/nhnacademy/bookstorecoupon/coupontemplate/domain/dto/request/CouponTemplateRequestDTO.java @@ -7,6 +7,7 @@ public record CouponTemplateRequestDTO( @NotNull + @Positive Long couponPolicyId, @NotNull diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/global/config/QuerydslConfig.java b/src/main/java/com/nhnacademy/bookstorecoupon/global/config/QuerydslConfig.java index e9db841..bd0ae8e 100644 --- a/src/main/java/com/nhnacademy/bookstorecoupon/global/config/QuerydslConfig.java +++ b/src/main/java/com/nhnacademy/bookstorecoupon/global/config/QuerydslConfig.java @@ -19,4 +19,5 @@ public JPAQueryFactory jpaQueryFactory() { return new JPAQueryFactory(entityManager); } -} \ No newline at end of file +} + diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/global/config/SecurityConfig.java b/src/main/java/com/nhnacademy/bookstorecoupon/global/config/SecurityConfig.java index be594f7..492df3b 100644 --- a/src/main/java/com/nhnacademy/bookstorecoupon/global/config/SecurityConfig.java +++ b/src/main/java/com/nhnacademy/bookstorecoupon/global/config/SecurityConfig.java @@ -40,10 +40,6 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .formLogin(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) .authorizeHttpRequests((requests) -> requests - // .requestMatchers("/api/sign-up").permitAll() - // .requestMatchers("/api/auth/info").authenticated() - // .requestMatchers("/api/internal/users/info").permitAll() - // .requestMatchers("/api/admin").hasRole("ADMIN") .anyRequest().permitAll() ) .addFilterBefore(ipAddressFilter, UsernamePasswordAuthenticationFilter.class) diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/global/config/SwaggerConfig.java b/src/main/java/com/nhnacademy/bookstorecoupon/global/config/SwaggerConfig.java new file mode 100644 index 0000000..902919f --- /dev/null +++ b/src/main/java/com/nhnacademy/bookstorecoupon/global/config/SwaggerConfig.java @@ -0,0 +1,31 @@ +package com.nhnacademy.bookstorecoupon.global.config; + +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .info(new Info().title("Coupon API").version("1.0").description("쿠폰 API 명세서")); + } + + @Bean + public GroupedOpenApi api() { + String[] paths = {"/coupons/**"}; + String[] packagesToScan = {"com.nhnacademy.bookstorecoupon"}; + return GroupedOpenApi.builder() + .group("coupon-api") + .pathsToMatch(paths) + .packagesToScan(packagesToScan) + .build(); + } +} + + diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/global/controller/SwaggerController.java b/src/main/java/com/nhnacademy/bookstorecoupon/global/controller/SwaggerController.java new file mode 100644 index 0000000..f62c24a --- /dev/null +++ b/src/main/java/com/nhnacademy/bookstorecoupon/global/controller/SwaggerController.java @@ -0,0 +1,40 @@ +package com.nhnacademy.bookstorecoupon.global.controller; + +import java.nio.charset.StandardCharsets; +import java.util.Locale; + +import org.springdoc.webmvc.api.OpenApiResource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import jakarta.servlet.http.HttpServletRequest; + +@RestController +@RequestMapping("/coupons/api") +public class SwaggerController { + + @Autowired + private OpenApiResource openApiResource; + + @GetMapping() + public ResponseEntity getSwaggerJson(HttpServletRequest request) throws JsonProcessingException { + String apiDocsUrl = "/coupon-docs/coupon-api"; // Swagger 문서 URL + Locale locale = request.getLocale(); // 현재 요청의 Locale + + byte[] swaggerJsonBytes = openApiResource.openapiJson(request, apiDocsUrl, locale); + if (swaggerJsonBytes == null) { + return ResponseEntity.ok("{}"); // 빈 JSON 반환 + } + + String swaggerJson = new String(swaggerJsonBytes, StandardCharsets.UTF_8); + return ResponseEntity.ok(swaggerJson); + } +} + + + diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/global/controller/SwaggerControllerTest.java b/src/main/java/com/nhnacademy/bookstorecoupon/global/controller/SwaggerControllerTest.java new file mode 100644 index 0000000..c442d6f --- /dev/null +++ b/src/main/java/com/nhnacademy/bookstorecoupon/global/controller/SwaggerControllerTest.java @@ -0,0 +1,41 @@ +package com.nhnacademy.bookstorecoupon.global.controller; + +import java.nio.charset.StandardCharsets; +import java.util.Locale; + +import org.springdoc.webmvc.api.OpenApiResource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; + +@Controller +@RequestMapping("/coupons/api-test") +@Slf4j +public class SwaggerControllerTest { + + @Autowired + private OpenApiResource openApiResource; + + @GetMapping() + public String getSwaggerJson(HttpServletRequest request, Model model) throws JsonProcessingException { + String apiDocsUrl = "/coupon-docs/coupon-api"; // Swagger 문서 URL + Locale locale = request.getLocale(); // 현재 요청의 Locale + + byte[] swaggerJsonBytes = openApiResource.openapiJson(request, apiDocsUrl, locale); + if (swaggerJsonBytes == null) { + model.addAttribute("swaggerJson", "{}"); + } else { + String swaggerJson = new String(swaggerJsonBytes, StandardCharsets.UTF_8); + model.addAttribute("swaggerJson", swaggerJson); + } + + return "api/coupon-api"; + } +} \ No newline at end of file diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/userandcoupon/controller/UserAndCouponController.java b/src/main/java/com/nhnacademy/bookstorecoupon/userandcoupon/controller/UserAndCouponController.java index 49a97fb..9a1ab67 100644 --- a/src/main/java/com/nhnacademy/bookstorecoupon/userandcoupon/controller/UserAndCouponController.java +++ b/src/main/java/com/nhnacademy/bookstorecoupon/userandcoupon/controller/UserAndCouponController.java @@ -18,6 +18,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import com.nhnacademy.bookstorecoupon.auth.annotation.AuthorizeRole; import com.nhnacademy.bookstorecoupon.auth.annotation.CurrentUser; import com.nhnacademy.bookstorecoupon.auth.jwt.dto.CurrentUserDetails; import com.nhnacademy.bookstorecoupon.global.exception.payload.ErrorStatus; @@ -28,8 +29,15 @@ import com.nhnacademy.bookstorecoupon.userandcoupon.service.UserAndCouponService; import com.nhnacademy.bookstorecoupon.userandcoupon.service.impl.RabbitMQUserAndCouponService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; + @RestController @RequestMapping("/coupons") +@Tag(name = "UserAndCoupon", description = "사용자 쿠폰관련 API") public class UserAndCouponController { private final UserAndCouponService userAndCouponService; @@ -40,17 +48,27 @@ public UserAndCouponController(UserAndCouponService userAndCouponService, Rabbit this.rabbitMQUserAndCouponService = rabbitMQUserAndCouponService; } + @Operation(summary = "사용자와 쿠폰 생성", description = "특정 쿠폰 ID로 사용자에게 쿠폰을 발행합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "쿠폰이 성공적으로 발행되었습니다."), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.") + }) + @AuthorizeRole({"COUPON_ADMIN", "MEMBER", "HEAD_ADMIN"}) @PostMapping("/{couponId}") - public ResponseEntity createUserAndCoupon(@PathVariable("couponId") Long couponId, @CurrentUser CurrentUserDetails currentUser) { + public ResponseEntity createUserAndCoupon(@Parameter(description = "쿠폰아이디", required = true) @PathVariable("couponId") Long couponId, @Parameter(description = "유저 아이디 가져오는 용도", required = true) @CurrentUser CurrentUserDetails currentUser) { Long userId= currentUser.getUserId(); rabbitMQUserAndCouponService.createUserAndCoupon(couponId, userId); return ResponseEntity.status(HttpStatus.CREATED).build(); } - + @Operation(summary = "웰컴 쿠폰 생성", description = "특정 사용자에게 웰컴 쿠폰을 발행합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "웰컴 쿠폰이 성공적으로 발행되었습니다."), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.") + }) @PostMapping("/coupon/welcome") - public ResponseEntity createUserWelcomeCouponIssue(Long userId) { + public ResponseEntity createUserWelcomeCouponIssue(@Parameter(description = "사용자 ID", required = true) Long userId) { if (userId == null) { ErrorStatus errorStatus = ErrorStatus.from("유저 아이디가 필요합니다.", HttpStatus.BAD_REQUEST, LocalDateTime.now()); throw new UserCouponValidationException(errorStatus); @@ -62,9 +80,14 @@ public ResponseEntity createUserWelcomeCouponIssue(Long userId) { - + @Operation(summary = "마이페이지 쿠폰 조회", description = "특정 사용자 기준으로 페이징된 모든 쿠폰을 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "쿠폰 조회 성공"), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.") + }) + @AuthorizeRole({"COUPON_ADMIN", "MEMBER", "HEAD_ADMIN"}) @GetMapping("/users/user") - public ResponseEntity> getAllUserAndCouponsByUserPaging(@CurrentUser CurrentUserDetails currentUser,@PageableDefault(page = 1, size = 3) Pageable pageable) { + public ResponseEntity> getAllUserAndCouponsByUserPaging(@Parameter(description = "유저 아이디 가져오는 용도", required = true) @CurrentUser CurrentUserDetails currentUser, @Parameter(description = "페이지 수, 페이지 사이즈", required = false) @PageableDefault(page = 1, size = 4) Pageable pageable) { Long userId= currentUser.getUserId(); if (userId == null) { ErrorStatus errorStatus = ErrorStatus.from( "유저 아이디가 필요합니다.", HttpStatus.BAD_REQUEST, LocalDateTime.now()); @@ -76,11 +99,17 @@ public ResponseEntity> getAllUserAndCouponsByUser } + @Operation(summary = "관리자 기준 페이징된 사용자 쿠폰 조회", description = "관리자 기준으로 페이징된 모든 사용자 쿠폰을 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "쿠폰 조회 성공"), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.") + }) + @AuthorizeRole({"COUPON_ADMIN", "HEAD_ADMIN"}) @GetMapping("/users") public ResponseEntity> getAllUsersAndCouponsByManagerPaging( - @PageableDefault(page = 1, size = 3) Pageable pageable, - @RequestParam(required = false) String type, - @RequestParam(required = false) Long userId + @Parameter(description = "페이지 수, 페이지 사이즈", required = false) @PageableDefault(page = 1, size = 4) Pageable pageable, + @Parameter(description = "정책타입", required = false) @RequestParam(required = false) String type, + @Parameter(description = "유저 아이디", required = false) @RequestParam(required = false) Long userId ) { Page coupons = userAndCouponService.getAllUsersAndCouponsByManagerPaging(pageable, type, userId); @@ -88,13 +117,18 @@ public ResponseEntity> getAllUsersAndCouponsByMan } - + @Operation(summary = "단건 주문에 대한 쿠폰 조회", description = "특정 주문에 대한 쿠폰을 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "쿠폰 조회 성공"), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.") + }) + @AuthorizeRole({"COUPON_ADMIN", "MEMBER", "HEAD_ADMIN"}) @GetMapping("/users/order") public ResponseEntity> findCouponByOrder( - @CurrentUser CurrentUserDetails currentUserDetails, - @RequestParam(required = false) List bookIds, - @RequestParam(required = false) List categoryIds, - @RequestParam BigDecimal bookPrice) { + @Parameter(description = "유저 아이디 가져오는 용도", required = true) @CurrentUser CurrentUserDetails currentUserDetails, + @Parameter(description = "도서 아이디", required = false) @RequestParam(required = false) List bookIds, + @Parameter(description = "카테고리 아이디", required = false) @RequestParam(required = false) List categoryIds, + @Parameter(description = "도서 가격", required = true) @RequestParam BigDecimal bookPrice) { if (currentUserDetails.getUserId() == null) { ErrorStatus errorStatus = ErrorStatus.from("유저 아이디가 필요합니다.", HttpStatus.BAD_REQUEST, LocalDateTime.now()); @@ -108,11 +142,16 @@ public ResponseEntity> findCouponByOrder( - + @Operation(summary = "장바구니 주문에 대한 쿠폰 조회", description = "특정 장바구니 주문에 대한 쿠폰을 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "쿠폰 조회 성공"), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.") + }) + @AuthorizeRole({"COUPON_ADMIN", "MEMBER", "HEAD_ADMIN"}) @PostMapping("/users/order/carts") public ResponseEntity> findCouponByCartOrder( - @CurrentUser CurrentUserDetails currentUserDetails, - @RequestBody(required = false) List bookDetails + @Parameter(description = "유저 아이디 가져오는 용도", required = true) @CurrentUser CurrentUserDetails currentUserDetails, + @Parameter(description = "도서 주문 정보", required = false) @RequestBody(required = false) List bookDetails ) { if (currentUserDetails.getUserId() == null) { @@ -125,9 +164,16 @@ public ResponseEntity> findCouponByCartOrder( return ResponseEntity.status(HttpStatus.OK).body(coupons); } + + @Operation(summary = "결제 후 쿠폰 사용됨처리", description = "특정 사용자 쿠폰 ID로 쿠폰을 결제 후 사용됨 처리합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "쿠폰이 성공적으로 업데이트되었습니다."), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.") + }) + @AuthorizeRole({"COUPON_ADMIN", "MEMBER", "HEAD_ADMIN"}) @PatchMapping("/users/payment/{userAndCouponId}") public ResponseEntity updateCouponAfterPayment( - @PathVariable("userAndCouponId") Long userAndCouponId) { + @Parameter(description = "사용자 쿠폰아이디", required = true) @PathVariable("userAndCouponId") Long userAndCouponId) { if (userAndCouponId == null) { ErrorStatus errorStatus = ErrorStatus.from("사용자 쿠폰 아이디가 필요합니다.", HttpStatus.BAD_REQUEST, LocalDateTime.now()); @@ -143,10 +189,15 @@ public ResponseEntity updateCouponAfterPayment( - + @Operation(summary = "선택된 쿠폰 조회", description = "특정 쿠폰 ID로 선택된 쿠폰을 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "쿠폰 조회 성공"), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다.") + }) @GetMapping("/users/order/coupon") + @AuthorizeRole({"COUPON_ADMIN", "MEMBER", "HEAD_ADMIN"}) public ResponseEntity getSelectedCoupon( - @RequestParam(value = "couponId") Long couponId) { + @Parameter(description = "쿠폰아이디", required = true) @RequestParam(value = "couponId") Long couponId) { if (couponId == null) { ErrorStatus errorStatus = ErrorStatus.from("사용자 쿠폰 아이디가 필요합니다.", HttpStatus.BAD_REQUEST, LocalDateTime.now()); throw new UserCouponValidationException(errorStatus); @@ -158,9 +209,13 @@ public ResponseEntity getSelectedCoupon( + @Operation(summary = "회원인지 아닌지 검사", description = "비회원인지 회원인지 검사합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "검사결과를 true or false로 반환"), + }) @GetMapping("/users/auth") public ResponseEntity isRealUserCheck( - @CurrentUser CurrentUserDetails currentUserDetails) { + @Parameter(description = "유저 아이디 가져오는 용도", required = true) @CurrentUser CurrentUserDetails currentUserDetails) { if(currentUserDetails==null){ return ResponseEntity.status(HttpStatus.OK).body(false); } else { diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/web/ApplicationStatusController.java b/src/main/java/com/nhnacademy/bookstorecoupon/web/ApplicationStatusController.java deleted file mode 100644 index c3c53de..0000000 --- a/src/main/java/com/nhnacademy/bookstorecoupon/web/ApplicationStatusController.java +++ /dev/null @@ -1,33 +0,0 @@ -// package com.nhnacademy.bookstorecoupon.web; -// -// import org.springframework.http.HttpStatus; -// import org.springframework.web.bind.annotation.PostMapping; -// import org.springframework.web.bind.annotation.RequestMapping; -// import org.springframework.web.bind.annotation.ResponseStatus; -// import org.springframework.web.bind.annotation.RestController; -// -// import com.netflix.appinfo.ApplicationInfoManager; -// import com.netflix.appinfo.InstanceInfo; -// import com.nhnacademy.bookstorecoupon.actuator.ApplicationStatus; -// -// @RestController -// @RequestMapping("/actuator/status") -// public class ApplicationStatusController { -// -// private final ApplicationInfoManager applicationInfoManager; -// private final ApplicationStatus applicationStatus; -// -// public ApplicationStatusController(ApplicationInfoManager applicationInfoManager, -// ApplicationStatus applicationStatus) { -// this.applicationInfoManager = applicationInfoManager; -// this.applicationStatus = applicationStatus; -// } -// -// @PostMapping -// @ResponseStatus(value = HttpStatus.OK) -// public void stopStatus() { -// applicationInfoManager.setInstanceStatus(InstanceInfo.InstanceStatus.DOWN); -// applicationStatus.stopService(); -// } -// -// } diff --git a/src/main/java/com/nhnacademy/bookstorecoupon/web/HelloController.java b/src/main/java/com/nhnacademy/bookstorecoupon/web/HelloController.java deleted file mode 100644 index c015ed7..0000000 --- a/src/main/java/com/nhnacademy/bookstorecoupon/web/HelloController.java +++ /dev/null @@ -1,22 +0,0 @@ - -package com.nhnacademy.bookstorecoupon.web; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - - -// @RestController -// @RequestMapping("/coupon") -// public class HelloController { -// -// @Value("${server.port}") -// private String port; -// -// @GetMapping -// public String getHello() { -// return port + ":hello"; -// } -// -// } diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 7e7e180..6cc660f 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -36,45 +36,3 @@ logging: org.springframework.transaction.interceptor: info remote-addr: 133.186.134.30 - -# shutdown: graceful -# -# eureka: -# instance: -# instance-id: coupon -# hostname: localhost -# prefer-ip-address: true -# client: -# fetch-registry: true -# register-with-eureka: true -# service-url: -# defaultZone: http://admin:1234@localhost:8761/eureka -# -# spring: -# lifecycle: -# timeout-per-shutdown-phase: 30s -# -# management: -# health: -# status: -# order: DOWN, UP -# endpoint: -# jolokia: -# enabled: true -# metrics: -# enabled: true -# pause: -# enabled: true -# resume: -# enabled: true -# restart: -# enabled: true -# shutdown: -# enabled: true -# endpoints: -# web: -# exposure: -# include: '*' -# info: -# env: -# enabled: true \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5d6d778..3116d92 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -31,6 +31,23 @@ spring: test-on-borrow: true # 풀에서 연결을 빌릴 때마다 해당 연결이 유효한지 검증할지 여부를 설정합니다. validation-query: SELECT 1 # 연결 유효성을 검사하기 위해 실행할 SQL 쿼리를 설정합니다. + + +springdoc: + api-docs: + path: /coupon-api + default-consumes-media-type: application/json + default-produces-media-type: application/json + swagger-ui: + operations-sorter: alpha + tags-sorter: alpha + path: /coupon-api.html + disable-swagger-default-url: true + logging: file: - path: ./logs \ No newline at end of file + path: ./logs + + + + diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index b5196b5..5bed036 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -9,8 +9,8 @@ - - + + diff --git a/src/main/resources/templates/api/coupon-api.html b/src/main/resources/templates/api/coupon-api.html new file mode 100644 index 0000000..a76190a --- /dev/null +++ b/src/main/resources/templates/api/coupon-api.html @@ -0,0 +1,41 @@ + + + + + Swagger API Documentation + + + + +
+ + + + + diff --git a/src/test/java/com/nhnacademy/bookstorecoupon/bookcoupon/domain/entity/BookCouponTest.java b/src/test/java/com/nhnacademy/bookstorecoupon/bookcoupon/domain/entity/BookCouponTest.java new file mode 100644 index 0000000..a267a3c --- /dev/null +++ b/src/test/java/com/nhnacademy/bookstorecoupon/bookcoupon/domain/entity/BookCouponTest.java @@ -0,0 +1,61 @@ +package com.nhnacademy.bookstorecoupon.bookcoupon.domain.entity; + +import static org.assertj.core.api.Assertions.*; + +import java.math.BigDecimal; + +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.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import com.nhnacademy.bookstorecoupon.bookcoupon.repository.BookCouponRepository; +import com.nhnacademy.bookstorecoupon.couponpolicy.domain.entity.CouponPolicy; +import com.nhnacademy.bookstorecoupon.couponpolicy.repository.CouponPolicyRepository; +import com.nhnacademy.bookstorecoupon.global.config.QuerydslConfig; + +@DataJpaTest +@Import(QuerydslConfig.class) +public class BookCouponTest { + + @Autowired + private BookCouponRepository bookCouponRepository; + + @Autowired + private CouponPolicyRepository couponPolicyRepository; + + private CouponPolicy couponPolicy; + + @BeforeEach + public void setUp() { + couponPolicy = CouponPolicy.builder() + .minOrderPrice(BigDecimal.valueOf(5000)) + .salePrice(BigDecimal.valueOf(10000)) + .saleRate(null) + .maxSalePrice(null) + .type("book") + .build(); + couponPolicyRepository.save(couponPolicy); + } + + @Test + public void testCreateBookCoupon() { + // Given + BookCoupon bookCoupon = BookCoupon.builder() + .couponPolicy(couponPolicy) + .bookId(123L) + .bookTitle("Test Book") + .build(); + + // When + BookCoupon savedBookCoupon = bookCouponRepository.save(bookCoupon); + + // Then + assertThat(savedBookCoupon).isNotNull(); + assertThat(savedBookCoupon.getId()).isNotNull(); + assertThat(savedBookCoupon.getBookId()).isEqualTo(123L); + assertThat(savedBookCoupon.getBookTitle()).isEqualTo("Test Book"); + assertThat(savedBookCoupon.getCouponPolicy()).isEqualTo(couponPolicy); + } +} \ No newline at end of file diff --git a/src/test/java/com/nhnacademy/bookstorecoupon/categorycoupon/domain/entity/CategoryCouponTest.java b/src/test/java/com/nhnacademy/bookstorecoupon/categorycoupon/domain/entity/CategoryCouponTest.java new file mode 100644 index 0000000..e28a86f --- /dev/null +++ b/src/test/java/com/nhnacademy/bookstorecoupon/categorycoupon/domain/entity/CategoryCouponTest.java @@ -0,0 +1,61 @@ +package com.nhnacademy.bookstorecoupon.categorycoupon.domain.entity; + +import static org.assertj.core.api.AssertionsForClassTypes.*; + +import java.math.BigDecimal; + +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.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import com.nhnacademy.bookstorecoupon.categorycoupon.repository.CategoryCouponRepository; +import com.nhnacademy.bookstorecoupon.couponpolicy.domain.entity.CouponPolicy; +import com.nhnacademy.bookstorecoupon.couponpolicy.repository.CouponPolicyRepository; +import com.nhnacademy.bookstorecoupon.global.config.QuerydslConfig; + +@DataJpaTest +@Import(QuerydslConfig.class) +public class CategoryCouponTest { + + @Autowired + private CategoryCouponRepository categoryCouponRepository; + + @Autowired + private CouponPolicyRepository couponPolicyRepository; + + private CouponPolicy couponPolicy; + + @BeforeEach + public void setUp() { + couponPolicy = CouponPolicy.builder() + .minOrderPrice(BigDecimal.valueOf(5000)) + .salePrice(BigDecimal.valueOf(10000)) + .saleRate(null) + .maxSalePrice(null) + .type("category") + .build(); + couponPolicyRepository.save(couponPolicy); + } + + @Test + public void testCreateCategoryCoupon() { + // Given + CategoryCoupon categoryCoupon = CategoryCoupon.builder() + .couponPolicy(couponPolicy) + .categoryId(456L) + .categoryName("Test Category") + .build(); + + // When + CategoryCoupon savedCategoryCoupon = categoryCouponRepository.save(categoryCoupon); + + // Then + assertThat(savedCategoryCoupon).isNotNull(); + assertThat(savedCategoryCoupon.getId()).isNotNull(); + assertThat(savedCategoryCoupon.getCategoryId()).isEqualTo(456L); + assertThat(savedCategoryCoupon.getCategoryName()).isEqualTo("Test Category"); + assertThat(savedCategoryCoupon.getCouponPolicy()).isEqualTo(couponPolicy); + } +} \ No newline at end of file diff --git a/src/test/java/com/nhnacademy/bookstorecoupon/couponpolicy/controller/CouponPolicyControllerTest.java b/src/test/java/com/nhnacademy/bookstorecoupon/couponpolicy/controller/CouponPolicyControllerTest.java new file mode 100644 index 0000000..45184cf --- /dev/null +++ b/src/test/java/com/nhnacademy/bookstorecoupon/couponpolicy/controller/CouponPolicyControllerTest.java @@ -0,0 +1,439 @@ +package com.nhnacademy.bookstorecoupon.couponpolicy.controller; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*; +import static org.springframework.test.util.AssertionErrors.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.math.BigDecimal; +import java.util.Collections; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +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.Pageable; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nhnacademy.bookstorecoupon.couponpolicy.domain.dto.request.CouponPolicyRequestDTO; +import com.nhnacademy.bookstorecoupon.couponpolicy.domain.dto.request.CouponPolicyUpdateRequestDTO; +import com.nhnacademy.bookstorecoupon.couponpolicy.domain.dto.response.CouponPolicyResponseDTO; +import com.nhnacademy.bookstorecoupon.couponpolicy.exception.CouponPolicyValidationException; +import com.nhnacademy.bookstorecoupon.couponpolicy.service.CouponPolicyService; + +/** + * @author 이기훈 + * couponPolicyController 단위테스트 + */ +@WebMvcTest(CouponPolicyController.class) +class CouponPolicyControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private CouponPolicyService couponPolicyService; + + + + @Autowired + private ObjectMapper objectMapper; + + + + @Test + @WithMockUser(roles = {"COUPON_ADMIN"}) + void testIssueWelcomeCoupon_Success() throws Exception { + CouponPolicyRequestDTO requestDTO = new CouponPolicyRequestDTO( + BigDecimal.valueOf(1000), // minOrderPrice + BigDecimal.valueOf(500), // salePrice + null, // saleRate + null, // maxSalePrice + "WELCOME", // type + null, // bookId + null, // bookTitle + null, // categoryId + null // categoryName + ); + + mockMvc.perform(post("/coupons/policies/welcome") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDTO)).with(csrf())) + + .andExpect(status().isCreated()); + + verify(couponPolicyService, times(1)).issueWelcomeCoupon(any(CouponPolicyRequestDTO.class)); + } + + @Test + @WithMockUser(roles = {"COUPON_ADMIN"}) + void testIssueWelcomeCoupon_InvalidRequest() throws Exception { + CouponPolicyRequestDTO requestDTO = new CouponPolicyRequestDTO( + null, // minOrderPrice + BigDecimal.valueOf(500), // salePrice + null, // saleRate + null, // maxSalePrice + "WELCOME", // type + null, // bookId + null, // bookTitle + null, // categoryId + null // categoryName + ); + + mockMvc.perform(post("/coupons/policies/welcome") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDTO)).with(csrf())) + .andExpect(status().isBadRequest()); + + verify(couponPolicyService, times(0)).issueWelcomeCoupon(any(CouponPolicyRequestDTO.class)); + } + + @Test + @WithMockUser(roles = {"COUPON_ADMIN"}) + void testIssueBirthdayCoupon_Success() throws Exception { + CouponPolicyRequestDTO requestDTO = new CouponPolicyRequestDTO( + BigDecimal.valueOf(1000), // minOrderPrice + BigDecimal.valueOf(2000), // salePrice + null, // saleRate + null, // maxSalePrice + "BIRTHDAY", // type + null, // bookId + null, // bookTitle + null, // categoryId + null // categoryName + ); + + mockMvc.perform(post("/coupons/policies/birthday") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDTO)).with(csrf())) + .andExpect(status().isCreated()); + + verify(couponPolicyService, times(1)).issueBirthdayCoupon(any(CouponPolicyRequestDTO.class)); + } + + @Test + @WithMockUser(roles = {"COUPON_ADMIN"}) + void testIssueSpecificBookCoupon_Success() throws Exception { + CouponPolicyRequestDTO requestDTO = new CouponPolicyRequestDTO( + BigDecimal.valueOf(1000), // minOrderPrice + BigDecimal.valueOf(500), // salePrice + null, // saleRate + null, // maxSalePrice + "BOOK", // type + 1L, // bookId + "Book Title", // bookTitle + null, // categoryId + null // categoryName + ); + + mockMvc.perform(post("/coupons/policies/books") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDTO)).with(csrf())) + .andExpect(status().isCreated()); + + verify(couponPolicyService, times(1)).issueSpecificBookCoupon(any(CouponPolicyRequestDTO.class)); + } + + @Test + @WithMockUser(roles = {"COUPON_ADMIN"}) + void testIssueSpecificBookCoupon_BookIdOrTitleMissing() throws Exception { + CouponPolicyRequestDTO requestDTO = new CouponPolicyRequestDTO( + BigDecimal.valueOf(1000), // minOrderPrice + BigDecimal.valueOf(500), // salePrice + null, // saleRate + null, // maxSalePrice + "BOOK", // type + 1L, // bookId + null, // bookTitle + null, // categoryId + null // categoryName + ); + + mockMvc.perform(post("/coupons/policies/books") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDTO)).with(csrf())) + .andExpect(status().isBadRequest()); + verify(couponPolicyService, times(0)).issueSpecificBookCoupon(any(CouponPolicyRequestDTO.class)); + } + + @Test + @WithMockUser(roles = {"COUPON_ADMIN"}) + void testIssueSpecificCategoryCoupon_Success() throws Exception { + CouponPolicyRequestDTO requestDTO = new CouponPolicyRequestDTO( + BigDecimal.valueOf(1000), // minOrderPrice + BigDecimal.valueOf(500), // salePrice + null, // saleRate + null, // maxSalePrice + "CATEGORY", // type + null, // bookId + null, // bookTitle + 1L, // categoryId + "Category Name" // categoryName + ); + + mockMvc.perform(post("/coupons/policies/categories") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDTO)).with(csrf())) + .andExpect(status().isCreated()); + + verify(couponPolicyService, times(1)).issueSpecificCategoryCoupon(any(CouponPolicyRequestDTO.class)); + } + + @Test + @WithMockUser(roles = {"COUPON_ADMIN"}) + void testIssueSpecificCategoryCoupon_CategoryIdOrNameMissing() throws Exception { + CouponPolicyRequestDTO requestDTO = new CouponPolicyRequestDTO( + BigDecimal.valueOf(1000), // minOrderPrice + BigDecimal.valueOf(500), // salePrice + null, // saleRate + null, // maxSalePrice + "CATEGORY", // type + null, // bookId + null, // bookTitle + 1L, // categoryId + null // categoryName + ); + + mockMvc.perform(post("/coupons/policies/categories") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDTO)).with(csrf())) + .andExpect(status().isBadRequest()); + + + verify(couponPolicyService, times(0)).issueSpecificCategoryCoupon(any(CouponPolicyRequestDTO.class)); + } + + @Test + @WithMockUser(roles = {"COUPON_ADMIN"}) + void testIssueDiscountCoupon_Success() throws Exception { + CouponPolicyRequestDTO requestDTO = new CouponPolicyRequestDTO( + BigDecimal.valueOf(1000), // minOrderPrice + BigDecimal.valueOf(5000), // salePrice + null, // saleRate + null, // maxSalePrice + "sale", // type + null, // bookId + null, // bookTitle + null, // categoryId + null // categoryName + ); + + mockMvc.perform(post("/coupons/policies/sale") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDTO)).with(csrf())) + .andExpect(status().isCreated()); + + verify(couponPolicyService, times(1)).issueDiscountCoupon(any(CouponPolicyRequestDTO.class)); + } + + @Test + @WithMockUser(roles = {"COUPON_ADMIN"}) + void testGetAllCouponPolicies_Success() throws Exception { + CouponPolicyResponseDTO responseDTO = new CouponPolicyResponseDTO( + 1L, + BigDecimal.valueOf(1000), // minOrderPrice + BigDecimal.valueOf(5000), // salePrice + null, // saleRate + null, // maxSalePrice + "sale",// type + true, + null, // bookId + null, // bookTitle + null, // categoryId + null + ); + Page page = new PageImpl<>(Collections.singletonList(responseDTO)); + + when(couponPolicyService.getAllCouponPolicies(any(Pageable.class))).thenReturn(page); + + mockMvc.perform(get("/coupons/policies") + .contentType(MediaType.APPLICATION_JSON).with(csrf())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content[0]").isNotEmpty()); + + verify(couponPolicyService, times(1)).getAllCouponPolicies(any(Pageable.class)); + } + + @Test + @WithMockUser(roles = {"COUPON_ADMIN"}) + void testUpdateCouponPolicy_Success() throws Exception { + CouponPolicyUpdateRequestDTO requestDTO = new CouponPolicyUpdateRequestDTO( + BigDecimal.valueOf(3000), + BigDecimal.valueOf(2000), + null, + null, + true + ); + + mockMvc.perform(patch("/coupons/policies/1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDTO)).with(csrf())) + .andExpect(status().isOk()); + + verify(couponPolicyService, times(1)).updateCouponPolicy(anyLong(), any(CouponPolicyUpdateRequestDTO.class)); + } + + @Test + @WithMockUser(roles = {"COUPON_ADMIN"}) + void testUpdateCouponPolicy_InvalidRequest() throws Exception { + CouponPolicyUpdateRequestDTO requestDTO = new CouponPolicyUpdateRequestDTO( + null, + BigDecimal.valueOf(2000), + null, + null, + true + ); + + + mockMvc.perform(patch("/coupons/policies/1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDTO)).with(csrf())) + .andExpect(status().isBadRequest()); + + verify(couponPolicyService, times(0)).updateCouponPolicy(anyLong(), any(CouponPolicyUpdateRequestDTO.class)); + } + + @Test + @WithMockUser(roles = {"COUPON_ADMIN"}) + void testIssueCoupon_SalePriceAndSaleRateInvalid() throws Exception { + CouponPolicyRequestDTO requestDTO = new CouponPolicyRequestDTO( + BigDecimal.valueOf(1000), // minOrderPrice + BigDecimal.valueOf(500), // salePrice + BigDecimal.valueOf(0.22), // saleRate + null, // maxSalePrice + "welcome", // type + null, // bookId + null, // bookTitle + null, // categoryId + null // categoryName + ); + + mockMvc.perform(post("/coupons/policies/welcome") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDTO)) + .with(csrf())) + .andExpect(status().isBadRequest()) + .andExpect(result -> { + Throwable exception = result.getResolvedException(); + assertTrue("Expected CouponPolicyValidationException but got: " + exception, + exception instanceof CouponPolicyValidationException); + + CouponPolicyValidationException ex = null; + if (exception instanceof CouponPolicyValidationException) { + ex = (CouponPolicyValidationException) exception; + } + + String actualMessage = null; + if (ex != null) { + actualMessage = ex.getErrorStatus().getMessage(); + } + String expectedMessage = "해당 할인률, 최대가격과 할인가격은 동시에 작성할 수 없습니다"; + + System.out.println("Actual message: " + actualMessage); // 로그로 확인 + + assertTrue("메시지 체크", actualMessage.equals(expectedMessage)); + }); + + + verify(couponPolicyService, times(0)).issueWelcomeCoupon( any(CouponPolicyRequestDTO.class)); + } + + @Test + @WithMockUser(roles = {"COUPON_ADMIN"}) + void testIssueCoupon_SalePriceAndMaxSalePriceInvalid() throws Exception { + CouponPolicyRequestDTO requestDTO = new CouponPolicyRequestDTO( + BigDecimal.valueOf(1000), // minOrderPrice + BigDecimal.valueOf(500), // salePrice + null, // saleRate + BigDecimal.valueOf(1000), // maxSalePrice + "welcome", // type + null, // bookId + null, // bookTitle + null, // categoryId + null // categoryName + ); + + mockMvc.perform(post("/coupons/policies/welcome") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDTO)) + .with(csrf())) + .andExpect(status().isBadRequest()) + .andExpect(result -> { + Throwable exception = result.getResolvedException(); + assertTrue("Expected CouponPolicyValidationException but got: " + exception, + exception instanceof CouponPolicyValidationException); + + CouponPolicyValidationException ex = null; + if (exception instanceof CouponPolicyValidationException) { + ex = (CouponPolicyValidationException) exception; + } + + String actualMessage = null; + if (ex != null) { + actualMessage = ex.getErrorStatus().getMessage(); + } + String expectedMessage = "해당 할인률, 최대가격과 할인가격은 동시에 작성할 수 없습니다"; + + System.out.println("Actual message: " + actualMessage); // 로그로 확인 + + if (actualMessage != null) { + assertTrue("메시지 체크", actualMessage.equals(expectedMessage)); + } + }); + verify(couponPolicyService, times(0)).issueWelcomeCoupon( any(CouponPolicyRequestDTO.class)); + } + + + @Test + @WithMockUser(roles = {"COUPON_ADMIN"}) + void testUpdateCoupon_SalePriceAndMaxSalePriceSet() throws Exception { + CouponPolicyUpdateRequestDTO requestDTO = new CouponPolicyUpdateRequestDTO( + BigDecimal.valueOf(1000), // minOrderPrice + BigDecimal.valueOf(500), // salePrice + null, // saleRate + BigDecimal.valueOf(1000), + true + ); + + mockMvc.perform(patch("/coupons/policies/1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDTO)) + .with(csrf())) + .andExpect(status().isBadRequest()) + .andExpect(result -> { + Throwable exception = result.getResolvedException(); + assertTrue("Expected CouponPolicyValidationException but got: " + exception, + exception instanceof CouponPolicyValidationException); + + CouponPolicyValidationException ex = null; + if (exception instanceof CouponPolicyValidationException) { + ex = (CouponPolicyValidationException) exception; + } + + String actualMessage = null; + if (ex != null) { + actualMessage = ex.getErrorStatus().getMessage(); + } + String expectedMessage = "해당 할인률, 최대가격과 할인가격은 동시에 작성할 수 없습니다"; + + System.out.println("Actual message: " + actualMessage); // 로그로 확인 + + if (actualMessage != null) { + assertTrue("메시지 체크", actualMessage.equals(expectedMessage)); + } + }); + + verify(couponPolicyService, times(0)).updateCouponPolicy(anyLong(), any(CouponPolicyUpdateRequestDTO.class)); + } + + + + +} diff --git a/src/test/java/com/nhnacademy/bookstorecoupon/couponpolicy/domain/entity/CouponPolicyTest.java b/src/test/java/com/nhnacademy/bookstorecoupon/couponpolicy/domain/entity/CouponPolicyTest.java new file mode 100644 index 0000000..a0bd566 --- /dev/null +++ b/src/test/java/com/nhnacademy/bookstorecoupon/couponpolicy/domain/entity/CouponPolicyTest.java @@ -0,0 +1,109 @@ +package com.nhnacademy.bookstorecoupon.couponpolicy.domain.entity; + +import static org.assertj.core.api.AssertionsForClassTypes.*; + +import java.math.BigDecimal; + +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.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import com.nhnacademy.bookstorecoupon.couponpolicy.domain.dto.request.CouponPolicyRequestDTO; +import com.nhnacademy.bookstorecoupon.couponpolicy.repository.CouponPolicyRepository; +import com.nhnacademy.bookstorecoupon.global.config.QuerydslConfig; + +@DataJpaTest +@Import(QuerydslConfig.class) +public class CouponPolicyTest { + + @Autowired + private CouponPolicyRepository couponPolicyRepository; + + private CouponPolicy couponPolicy; + + @BeforeEach + public void setUp() { + couponPolicy = CouponPolicy.builder() + .minOrderPrice(BigDecimal.valueOf(5000)) + .salePrice(null) + .saleRate(BigDecimal.valueOf(0.12)) + .maxSalePrice(BigDecimal.valueOf(20000)) + .type("sale") + .build(); + couponPolicyRepository.save(couponPolicy); + } + + @Test + public void testCreateCouponPolicy() { + // Given + CouponPolicy couponPolicy = CouponPolicy.builder() + .minOrderPrice(BigDecimal.valueOf(5000)) + .salePrice(BigDecimal.valueOf(10000)) + .saleRate(null) + .maxSalePrice(null) + .type("welcome") + .build(); + + // When + CouponPolicy savedCouponPolicy = couponPolicyRepository.save(couponPolicy); + + // Then + assertThat(savedCouponPolicy).isNotNull(); + assertThat(savedCouponPolicy.getId()).isNotNull(); + assertThat(savedCouponPolicy.getMinOrderPrice()).isEqualTo(BigDecimal.valueOf(5000)); + assertThat(savedCouponPolicy.getSalePrice()).isEqualTo(BigDecimal.valueOf(10000)); + assertThat(savedCouponPolicy.getSaleRate()).isNull(); + assertThat(savedCouponPolicy.getMaxSalePrice()).isNull(); + assertThat(savedCouponPolicy.getType()).isEqualTo("welcome"); + } + + @Test + public void testUpdateCouponPolicy() { + // Given + BigDecimal newMinOrderPrice = BigDecimal.valueOf(15000); + BigDecimal newSaleRate = BigDecimal.valueOf(0.2); + BigDecimal newMaxSalePrice = BigDecimal.valueOf(22000); + Boolean newIsUsed = false; + + // When + couponPolicy.update(newMinOrderPrice, null, newSaleRate, newMaxSalePrice, newIsUsed); + CouponPolicy updatedCouponPolicy = couponPolicyRepository.save(couponPolicy); + + // Then + assertThat(updatedCouponPolicy).isNotNull(); + assertThat(updatedCouponPolicy.getMinOrderPrice()).isEqualTo(newMinOrderPrice); + assertThat(updatedCouponPolicy.getSalePrice()).isNull(); + assertThat(updatedCouponPolicy.getSaleRate()).isEqualTo(newSaleRate); + assertThat(updatedCouponPolicy.getMaxSalePrice()).isEqualTo(newMaxSalePrice); + assertThat(updatedCouponPolicy.getIsUsed()).isEqualTo(newIsUsed); + } + + @Test + public void testCreateFromRequestDTO() { + // Given + CouponPolicyRequestDTO requestDTO = new CouponPolicyRequestDTO( + BigDecimal.valueOf(5000), + BigDecimal.valueOf(10000), + null, + null, + "welcome", + null, // Optional field, so it can be null + null, + null, + null + ); + + // When + CouponPolicy createdCouponPolicy = CouponPolicy.createFromRequestDTO(requestDTO); + + // Then + assertThat(createdCouponPolicy).isNotNull(); + assertThat(createdCouponPolicy.getMinOrderPrice()).isEqualTo(requestDTO.minOrderPrice()); + assertThat(createdCouponPolicy.getSalePrice()).isEqualTo(requestDTO.salePrice()); + assertThat(createdCouponPolicy.getSaleRate()).isEqualTo(requestDTO.saleRate()); + assertThat(createdCouponPolicy.getMaxSalePrice()).isEqualTo(requestDTO.maxSalePrice()); + assertThat(createdCouponPolicy.getType()).isEqualTo(requestDTO.type()); + } +} \ No newline at end of file diff --git a/src/test/java/com/nhnacademy/bookstorecoupon/coupontemplate/controller/CouponTemplateControllerTest.java b/src/test/java/com/nhnacademy/bookstorecoupon/coupontemplate/controller/CouponTemplateControllerTest.java new file mode 100644 index 0000000..e3dd976 --- /dev/null +++ b/src/test/java/com/nhnacademy/bookstorecoupon/coupontemplate/controller/CouponTemplateControllerTest.java @@ -0,0 +1,255 @@ +package com.nhnacademy.bookstorecoupon.coupontemplate.controller; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +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.Pageable; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nhnacademy.bookstorecoupon.coupontemplate.domain.dto.request.CouponTemplateRequestDTO; +import com.nhnacademy.bookstorecoupon.coupontemplate.domain.dto.response.CouponTemplateResponseDTO; +import com.nhnacademy.bookstorecoupon.coupontemplate.service.CouponTemplateService; + +@WebMvcTest(CouponTemplateController.class) +class CouponTemplateControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private CouponTemplateService couponTemplateService; + + @Test + @WithMockUser(roles = {"COUPON_ADMIN"}) + void testCreateCouponTemplate_Valid() throws Exception { + CouponTemplateRequestDTO validRequest = new CouponTemplateRequestDTO( + 1L, + LocalDateTime.now().plusDays(365), + LocalDateTime.now(), + 100L + ); + + mockMvc.perform(post("/coupons") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(validRequest)).with(csrf())) + .andExpect(status().isCreated()); + + verify(couponTemplateService, times(1)).createCouponTemplate(any(CouponTemplateRequestDTO.class)); + } + + @Test + @WithMockUser(roles = {"COUPON_ADMIN"}) + void testCreateCouponTemplate_InvalidCouponPolicyId() throws Exception { + CouponTemplateRequestDTO invalidRequest = new CouponTemplateRequestDTO( + null, + LocalDateTime.now().plusDays(1), + LocalDateTime.now(), + 100L + ); + + mockMvc.perform(post("/coupons") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest)).with(csrf())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("couponPolicyId 에러: must not be null, 입력된 값: null; ")) + .andExpect(jsonPath("$.status").value("BAD_REQUEST")); + verify(couponTemplateService, times(0)).createCouponTemplate(any(CouponTemplateRequestDTO.class)); + + } + + @Test + @WithMockUser(roles = {"COUPON_ADMIN"}) + void testCreateCouponTemplate_InvalidExpiredDate() throws Exception { + CouponTemplateRequestDTO invalidRequest = new CouponTemplateRequestDTO( + 1L, + null, + LocalDateTime.now(), + 100L + ); + + mockMvc.perform(post("/coupons") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest)).with(csrf())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("expiredDate 에러: must not be null, 입력된 값: null; ")) + .andExpect(jsonPath("$.status").value("BAD_REQUEST")); + + verify(couponTemplateService, times(0)).createCouponTemplate(any(CouponTemplateRequestDTO.class)); + } + + @Test + @WithMockUser(roles = {"COUPON_ADMIN"}) + void testCreateCouponTemplate_InvalidIssueDate() throws Exception { + CouponTemplateRequestDTO invalidRequest = new CouponTemplateRequestDTO( + 1L, + LocalDateTime.now().plusDays(1), + null, + 100L + ); + + mockMvc.perform(post("/coupons") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest)).with(csrf())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("issueDate 에러: must not be null, 입력된 값: null; ")) + .andExpect(jsonPath("$.status").value("BAD_REQUEST")); + + verify(couponTemplateService, times(0)).createCouponTemplate(any(CouponTemplateRequestDTO.class)); + } + + @Test + @WithMockUser(roles = {"COUPON_ADMIN"}) + void testCreateCouponTemplate_InvalidQuantity() throws Exception { + CouponTemplateRequestDTO invalidRequest = new CouponTemplateRequestDTO( + 1L, + LocalDateTime.now().plusDays(1), + LocalDateTime.now(), + null + ); + + mockMvc.perform(post("/coupons") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest)).with(csrf())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("quantity 에러: must not be null, 입력된 값: null; ")) + .andExpect(jsonPath("$.status").value("BAD_REQUEST")); + + verify(couponTemplateService, times(0)).createCouponTemplate(any(CouponTemplateRequestDTO.class)); + } + + @Test + @WithMockUser(roles = {"COUPON_ADMIN"}) + void testCreateCouponTemplate_NegativeQuantity() throws Exception { + CouponTemplateRequestDTO invalidRequest = new CouponTemplateRequestDTO( + 1L, + LocalDateTime.now().plusDays(1), + LocalDateTime.now(), + -100L + ); + + mockMvc.perform(post("/coupons") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest)).with(csrf())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("quantity 에러: must be greater than 0, 입력된 값: -100; ")) + .andExpect(jsonPath("$.status").value("BAD_REQUEST")); + + verify(couponTemplateService, times(0)).createCouponTemplate(any(CouponTemplateRequestDTO.class)); + } + + @Test + @WithMockUser(roles = {"MEMBER"}) + void testGetAllCouponTemplatesByUserPaging() throws Exception { + // CouponTemplateResponseDTO 객체 생성 + CouponTemplateResponseDTO coupon1 = new CouponTemplateResponseDTO( + 1L, 1L, new BigDecimal("100.00"), new BigDecimal("10.00"), + null, null, "sale", + true, null, null, null, null, + LocalDateTime.now().plusDays(30), LocalDateTime.now(), 500L + ); + + CouponTemplateResponseDTO coupon2 = new CouponTemplateResponseDTO( + 2L, 2L, new BigDecimal("200.00"), null, + new BigDecimal("0.15"), null, "book", + true, 2L, "Another Great Book", null, null, + LocalDateTime.now().plusDays(60), LocalDateTime.now().minusDays(5), 300L + ); + + CouponTemplateResponseDTO coupon3 = new CouponTemplateResponseDTO( + 3L, 3L, new BigDecimal("300.00"), new BigDecimal("500.00"), + null, null, "category", + true, null, null, 3L, "Science", + LocalDateTime.now().plusDays(90), LocalDateTime.now().minusDays(10), 200L + ); + + Page page = new PageImpl<>(List.of(coupon1, coupon2, coupon3)); + + Mockito.when(couponTemplateService.getAllCouponTemplatesByUserPaging(Mockito.any(Pageable.class))) + .thenReturn(page); + + mockMvc.perform(get("/coupons/issue") + .param("page", "0") // 페이지는 0부터 시작하므로 0으로 설정 + .param("size", "3") + .contentType(MediaType.APPLICATION_JSON) + .with(csrf())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.content[0].id").value(1)) + .andExpect(jsonPath("$.content[0].couponPolicyId").value(1)) + .andExpect(jsonPath("$.content[0].minOrderPrice").value(100.00)) + .andExpect(jsonPath("$.content[1].id").value(2)) + .andExpect(jsonPath("$.content[1].couponPolicyId").value(2)) + .andExpect(jsonPath("$.content[1].minOrderPrice").value(200.00)) + .andExpect(jsonPath("$.content[2].id").value(3)) + .andExpect(jsonPath("$.content[2].couponPolicyId").value(3)) + .andExpect(jsonPath("$.content[2].minOrderPrice").value(300.00)); + } + + @Test + @WithMockUser(roles = {"COUPON_ADMIN"}) // 올바른 역할로 설정 + void testGetAllCouponTemplatesByManagerPaging() throws Exception { + // CouponTemplateResponseDTO 객체 생성 + CouponTemplateResponseDTO coupon1 = new CouponTemplateResponseDTO( + 1L, 1L, new BigDecimal("100.00"), new BigDecimal("10.00"), + null, null, "sale", + true, null, null, null, null, + LocalDateTime.now().plusDays(30), LocalDateTime.now(), 500L + ); + + CouponTemplateResponseDTO coupon2 = new CouponTemplateResponseDTO( + 2L, 2L, new BigDecimal("200.00"), null, + new BigDecimal("0.15"), null, "book", + true, 2L, "Another Great Book", null, null, + LocalDateTime.now().plusDays(60), LocalDateTime.now().minusDays(5), 300L + ); + + CouponTemplateResponseDTO coupon3 = new CouponTemplateResponseDTO( + 3L, 3L, new BigDecimal("300.00"), new BigDecimal("500.00"), + null, null, "category", + true, null, null, 3L, "Science", + LocalDateTime.now().plusDays(90), LocalDateTime.now().minusDays(10), 200L + ); + + Page page = new PageImpl<>(List.of(coupon1, coupon2, coupon3)); + + Mockito.when(couponTemplateService.getAllCouponTemplatesByManagerPaging(Mockito.any(Pageable.class))) + .thenReturn(page); + + mockMvc.perform(get("/coupons") + .param("page", "0") // 페이지는 0부터 시작하므로 0으로 설정 + .param("size", "2") + .contentType(MediaType.APPLICATION_JSON) + .with(csrf())) // CSRF 토큰을 포함 + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.content[0].id").value(1)) + .andExpect(jsonPath("$.content[0].couponPolicyId").value(1)) + .andExpect(jsonPath("$.content[0].minOrderPrice").value(100.00)) + .andExpect(jsonPath("$.content[1].id").value(2)) + .andExpect(jsonPath("$.content[1].couponPolicyId").value(2)) + .andExpect(jsonPath("$.content[1].minOrderPrice").value(200.00)) + .andExpect(jsonPath("$.content[2].id").value(3)) + .andExpect(jsonPath("$.content[2].couponPolicyId").value(3)) + .andExpect(jsonPath("$.content[2].minOrderPrice").value(300.00)); + } +} \ No newline at end of file diff --git a/src/test/java/com/nhnacademy/bookstorecoupon/coupontemplate/domain/entity/CouponTemplateTest.java b/src/test/java/com/nhnacademy/bookstorecoupon/coupontemplate/domain/entity/CouponTemplateTest.java new file mode 100644 index 0000000..673a909 --- /dev/null +++ b/src/test/java/com/nhnacademy/bookstorecoupon/coupontemplate/domain/entity/CouponTemplateTest.java @@ -0,0 +1,85 @@ +package com.nhnacademy.bookstorecoupon.coupontemplate.domain.entity; + +import static org.assertj.core.api.AssertionsForClassTypes.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +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.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import com.nhnacademy.bookstorecoupon.couponpolicy.domain.entity.CouponPolicy; +import com.nhnacademy.bookstorecoupon.couponpolicy.repository.CouponPolicyRepository; +import com.nhnacademy.bookstorecoupon.coupontemplate.repository.CouponTemplateRepository; +import com.nhnacademy.bookstorecoupon.global.config.QuerydslConfig; + +@DataJpaTest +@Import(QuerydslConfig.class) +public class CouponTemplateTest { + + @Autowired + private CouponTemplateRepository couponTemplateRepository; + + @Autowired + private CouponPolicyRepository couponPolicyRepository; + + private CouponPolicy couponPolicy; + + @BeforeEach + public void setUp() { + couponPolicy = CouponPolicy.builder() + .minOrderPrice(BigDecimal.valueOf(5000)) + .salePrice(BigDecimal.valueOf(10000)) + .saleRate(null) + .maxSalePrice(null) + .type("welcome") + .build(); + couponPolicyRepository.save(couponPolicy); + } + + @Test + public void testCreateCouponTemplate() { + // Given + LocalDateTime now = LocalDateTime.now(); + CouponTemplate couponTemplate = CouponTemplate.builder() + .couponPolicy(couponPolicy) + .expiredDate(now.plusDays(30)) + .issueDate(now) + .quantity(100L) + .build(); + + // When + CouponTemplate savedCouponTemplate = couponTemplateRepository.save(couponTemplate); + + // Then + assertThat(savedCouponTemplate).isNotNull(); + assertThat(savedCouponTemplate.getId()).isNotNull(); + assertThat(savedCouponTemplate.getCouponPolicy()).isEqualTo(couponPolicy); + assertThat(savedCouponTemplate.getExpiredDate()).isEqualTo(now.plusDays(30)); + assertThat(savedCouponTemplate.getIssueDate()).isEqualTo(now); + assertThat(savedCouponTemplate.getQuantity()).isEqualTo(100L); + } + + @Test + public void testUpdateCouponTemplate() { + // Given + LocalDateTime now = LocalDateTime.now(); + CouponTemplate couponTemplate = CouponTemplate.builder() + .couponPolicy(couponPolicy) + .expiredDate(now.plusDays(30)) + .issueDate(now) + .quantity(100L) + .build(); + CouponTemplate savedCouponTemplate = couponTemplateRepository.save(couponTemplate); + + // When + savedCouponTemplate.update(200L); + + // Then + assertThat(savedCouponTemplate).isNotNull(); + assertThat(savedCouponTemplate.getQuantity()).isEqualTo(200L); + } +} \ No newline at end of file diff --git a/src/test/java/com/nhnacademy/bookstorecoupon/userandcoupon/controller/UserAndCouponControllerTest.java b/src/test/java/com/nhnacademy/bookstorecoupon/userandcoupon/controller/UserAndCouponControllerTest.java new file mode 100644 index 0000000..65b4256 --- /dev/null +++ b/src/test/java/com/nhnacademy/bookstorecoupon/userandcoupon/controller/UserAndCouponControllerTest.java @@ -0,0 +1,214 @@ +// package com.nhnacademy.bookstorecoupon.userandcoupon.controller; +// +// import static org.mockito.Mockito.*; +// import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*; +// +// import org.junit.jupiter.api.Test; +// import org.springframework.beans.factory.annotation.Autowired; +// import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +// import org.springframework.boot.test.mock.mockito.MockBean; +// import org.springframework.http.MediaType; +// import org.springframework.security.test.context.support.WithMockUser; +// import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors; +// import org.springframework.test.web.servlet.MockMvc; +// import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +// import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +// +// import com.fasterxml.jackson.databind.ObjectMapper; +// import com.nhnacademy.bookstorecoupon.auth.jwt.dto.CurrentUserDetails; +// import com.nhnacademy.bookstorecoupon.userandcoupon.service.UserAndCouponService; +// import com.nhnacademy.bookstorecoupon.userandcoupon.service.impl.RabbitMQUserAndCouponService; +// +// @WebMvcTest(UserAndCouponController.class) +// class UserAndCouponControllerTest { +// +// +// @Autowired +// private MockMvc mockMvc; +// +// +// @MockBean +// private UserAndCouponService userAndCouponService; +// +// @MockBean +// private RabbitMQUserAndCouponService rabbitMQUserAndCouponService; +// +// @MockBean +// private CurrentUserDetails currentUserDetails; +// +// @Autowired +// private ObjectMapper objectMapper; +// +// +// @Test +// @WithMockUser(roles = {"MEMBER"}) +// void createUserAndCoupon_ShouldReturnCreated_WhenValidRequest() throws Exception { +// Long couponId = 1L; +// Long userId = 1L; +// +// +// // Mock the currentUserDetails to return the userId +// when(currentUserDetails.getUserId()).thenReturn(userId); +// +// // Perform the request +// mockMvc.perform(MockMvcRequestBuilders.post("/coupons/{couponId}", couponId) +// .with(SecurityMockMvcRequestPostProcessors.user("user").roles("MEMBER")) +// .contentType(MediaType.APPLICATION_JSON) +// .with(csrf())) // csrf token이 필요한 경우 +// .andExpect(MockMvcResultMatchers.status().isCreated()); +// +// // Verify that the RabbitMQUserAndCouponService was called exactly once +// verify(rabbitMQUserAndCouponService, times(1)).createUserAndCoupon(couponId, userId); +// } +// } +// +// // @Test +// // void createUserWelcomeCouponIssue_ShouldReturnCreated_WhenValidRequest() { +// // Long userId = 1L; +// // +// // ResponseEntity response = userAndCouponController.createUserWelcomeCouponIssue(userId); +// // +// // verify(userAndCouponService).createUserWelcomeCouponIssue(userId); +// // assertEquals(HttpStatus.CREATED, response.getStatusCode()); +// // } +// // +// // @Test +// // void createUserWelcomeCouponIssue_ShouldThrowException_WhenUserIdIsNull() { +// // Long userId = null; +// // +// // UserCouponValidationException thrown = assertThrows(UserCouponValidationException.class, () -> +// // userAndCouponController.createUserWelcomeCouponIssue(userId)); +// // +// // assertEquals("유저 아이디가 필요합니다.", thrown.getMessage()); +// // } +// // +// // @Test +// // void getAllUserAndCouponsByUserPaging_ShouldReturnPageOfCoupons() { +// // Long userId = 1L; +// // Pageable pageable = PageRequest.of(0, 10); +// // Page page = new PageImpl<>(List.of(new UserAndCouponResponseDTO())); +// // +// // when(currentUserDetails.getUserId()).thenReturn(userId); +// // when(userAndCouponService.getAllUsersAndCouponsByUserPaging(userId, pageable)).thenReturn(page); +// // +// // ResponseEntity> response = userAndCouponController.getAllUserAndCouponsByUserPaging(currentUserDetails, pageable); +// // +// // assertEquals(HttpStatus.OK, response.getStatusCode()); +// // assertEquals(1, response.getBody().getTotalElements()); +// // } +// // +// // @Test +// // void getAllUserAndCouponsByUserPaging_ShouldThrowException_WhenUserIdIsNull() { +// // when(currentUserDetails.getUserId()).thenReturn(null); +// // +// // UserCouponValidationException thrown = assertThrows(UserCouponValidationException.class, () -> +// // userAndCouponController.getAllUserAndCouponsByUserPaging(currentUserDetails, PageRequest.of(0, 10))); +// // +// // assertEquals("유저 아이디가 필요합니다.", thrown.getMessage()); +// // } +// // +// // @Test +// // void getAllUsersAndCouponsByManagerPaging_ShouldReturnPageOfCoupons() { +// // Pageable pageable = PageRequest.of(0, 10); +// // Page page = new PageImpl<>(List.of(new UserAndCouponResponseDTO())); +// // +// // when(userAndCouponService.getAllUsersAndCouponsByManagerPaging(pageable, null, null)).thenReturn(page); +// // +// // ResponseEntity> response = userAndCouponController.getAllUsersAndCouponsByManagerPaging(pageable, null, null); +// // +// // assertEquals(HttpStatus.OK, response.getStatusCode()); +// // assertEquals(1, response.getBody().getTotalElements()); +// // } +// // +// // @Test +// // void findCouponByOrder_ShouldReturnListOfCoupons() { +// // Long userId = 1L; +// // BigDecimal bookPrice = BigDecimal.valueOf(100); +// // List coupons = List.of(new UserAndCouponResponseDTO()); +// // +// // when(currentUserDetails.getUserId()).thenReturn(userId); +// // when(userAndCouponService.findCouponByOrder(userId, null, null, bookPrice)).thenReturn(coupons); +// // +// // ResponseEntity> response = userAndCouponController.findCouponByOrder(currentUserDetails, null, null, bookPrice); +// // +// // assertEquals(HttpStatus.OK, response.getStatusCode()); +// // assertEquals(coupons, response.getBody()); +// // } +// // +// // @Test +// // void findCouponByCartOrder_ShouldReturnListOfCoupons() { +// // Long userId = 1L; +// // List bookDetails = List.of(new GetBookByOrderCouponResponse()); +// // List coupons = List.of(new UserAndCouponResponseDTO()); +// // +// // when(currentUserDetails.getUserId()).thenReturn(userId); +// // when(userAndCouponService.findCouponByCartOrder(userId, bookDetails)).thenReturn(coupons); +// // +// // ResponseEntity> response = userAndCouponController.findCouponByCartOrder(currentUserDetails, bookDetails); +// // +// // assertEquals(HttpStatus.OK, response.getStatusCode()); +// // assertEquals(coupons, response.getBody()); +// // } +// // +// // @Test +// // void updateCouponAfterPayment_ShouldReturnOk_WhenValidRequest() { +// // Long userAndCouponId = 1L; +// // +// // ResponseEntity response = userAndCouponController.updateCouponAfterPayment(userAndCouponId); +// // +// // verify(userAndCouponService).updateCouponAfterPayment(userAndCouponId); +// // assertEquals(HttpStatus.OK, response.getStatusCode()); +// // } +// // +// // @Test +// // void updateCouponAfterPayment_ShouldThrowException_WhenUserAndCouponIdIsNull() { +// // Long userAndCouponId = null; +// // +// // UserCouponValidationException thrown = assertThrows(UserCouponValidationException.class, () -> +// // userAndCouponController.updateCouponAfterPayment(userAndCouponId)); +// // +// // assertEquals("사용자 쿠폰 아이디가 필요합니다.", thrown.getMessage()); +// // } +// // +// // @Test +// // void getSelectedCoupon_ShouldReturnCoupon_WhenValidRequest() { +// // Long couponId = 1L; +// // UserAndCouponOrderResponseDTO coupon = new UserAndCouponOrderResponseDTO(); +// // +// // when(userAndCouponService.findUserAndCouponsById(couponId)).thenReturn(coupon); +// // +// // ResponseEntity response = userAndCouponController.getSelectedCoupon(couponId); +// // +// // assertEquals(HttpStatus.OK, response.getStatusCode()); +// // assertEquals(coupon, response.getBody()); +// // } +// // +// // @Test +// // void getSelectedCoupon_ShouldThrowException_WhenCouponIdIsNull() { +// // Long couponId = null; +// // +// // UserCouponValidationException thrown = assertThrows(UserCouponValidationException.class, () -> +// // userAndCouponController.getSelectedCoupon(couponId)); +// // +// // assertEquals("사용자 쿠폰 아이디가 필요합니다.", thrown.getMessage()); +// // } +// +// // @Test +// // void isRealUserCheck_ShouldReturnTrue_WhenUserIsReal() { +// // when(currentUserDetails).thenReturn(new CurrentUserDetails()); // Non-null CurrentUserDetails +// // +// // ResponseEntity response = userAndCouponController.isRealUserCheck(currentUserDetails); +// // +// // assertEquals(HttpStatus.OK, response.getStatusCode()); +// // assertTrue(response.getBody()); +// // } +// // +// // @Test +// // void isRealUserCheck_ShouldReturnFalse_WhenUserIsNull() { +// // when(currentUserDetails).thenReturn(null); +// // +// // ResponseEntity response = userAndCouponController.isRealUserCheck(null); +// // +// // assertEquals(HttpStatus.OK, response.getStatusCode()); +// // assertFalse(response.getBody()); +// // } diff --git a/src/test/java/com/nhnacademy/bookstorecoupon/userandcoupon/domain/entity/UserAndCouponTest.java b/src/test/java/com/nhnacademy/bookstorecoupon/userandcoupon/domain/entity/UserAndCouponTest.java new file mode 100644 index 0000000..fa2e73a --- /dev/null +++ b/src/test/java/com/nhnacademy/bookstorecoupon/userandcoupon/domain/entity/UserAndCouponTest.java @@ -0,0 +1,93 @@ +package com.nhnacademy.bookstorecoupon.userandcoupon.domain.entity; + +import static org.assertj.core.api.AssertionsForClassTypes.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +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.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import com.nhnacademy.bookstorecoupon.couponpolicy.domain.entity.CouponPolicy; +import com.nhnacademy.bookstorecoupon.couponpolicy.repository.CouponPolicyRepository; +import com.nhnacademy.bookstorecoupon.global.config.QuerydslConfig; +import com.nhnacademy.bookstorecoupon.userandcoupon.repository.UserAndCouponRepository; + +@DataJpaTest +@Import(QuerydslConfig.class) +public class UserAndCouponTest { + + @Autowired + private UserAndCouponRepository userAndCouponRepository; + + @Autowired + private CouponPolicyRepository couponPolicyRepository; + + private CouponPolicy couponPolicy; + + @BeforeEach + public void setUp() { + couponPolicy = CouponPolicy.builder() + .minOrderPrice(BigDecimal.valueOf(5000)) + .salePrice(BigDecimal.valueOf(10000)) + .saleRate(null) + .maxSalePrice(null) + .type("welcome") + .build(); + couponPolicyRepository.save(couponPolicy); + } + + @Test + public void testCreateUserAndCoupon() { + // Given + UserAndCoupon userAndCoupon = UserAndCoupon.builder() + .couponPolicy(couponPolicy) + .userId(123L) + .usedDate(null) + .isUsed(false) + .expiredDate(LocalDateTime.of(2024, 12, 31, 23, 59, 59)) + .issueDate(LocalDateTime.now()) + .build(); + + // When + UserAndCoupon savedUserAndCoupon = userAndCouponRepository.save(userAndCoupon); + + // Then + assertThat(savedUserAndCoupon).isNotNull(); + assertThat(savedUserAndCoupon.getId()).isNotNull(); + assertThat(savedUserAndCoupon.getCouponPolicy()).isEqualTo(couponPolicy); + assertThat(savedUserAndCoupon.getUserId()).isEqualTo(123L); + assertThat(savedUserAndCoupon.getUsedDate()).isNull(); + assertThat(savedUserAndCoupon.getIsUsed()).isFalse(); + assertThat(savedUserAndCoupon.getExpiredDate()).isEqualTo(LocalDateTime.of(2024, 12, 31, 23, 59, 59)); + assertThat(savedUserAndCoupon.getIssueDate()).isNotNull(); + } + + @Test + public void testUpdateUserAndCoupon() { + // Given + UserAndCoupon userAndCoupon = UserAndCoupon.builder() + .couponPolicy(couponPolicy) + .userId(123L) + .usedDate(null) + .isUsed(false) + .expiredDate(LocalDateTime.of(2024, 12, 31, 23, 59, 59)) + .issueDate(LocalDateTime.now()) + .build(); + UserAndCoupon savedUserAndCoupon = userAndCouponRepository.save(userAndCoupon); + + LocalDateTime newUsedDate = LocalDateTime.now(); + Boolean newIsUsed = true; + + // When + savedUserAndCoupon.update(newUsedDate, newIsUsed); + + // Then + assertThat(savedUserAndCoupon).isNotNull(); + assertThat(savedUserAndCoupon.getUsedDate()).isEqualTo(newUsedDate); + assertThat(savedUserAndCoupon.getIsUsed()).isTrue(); + } +} \ No newline at end of file diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 9580ce1..d93dfe7 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -13,10 +13,18 @@ spring: refresh-token: expires-in: 86400000 datasource: - url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1 + url: jdbc:h2:mem:testdb;MODE=MYSQL; driver-class-name: org.h2.Driver username: sa - password: + type: org.apache.commons.dbcp2.BasicDataSource + dbcp2: + initial-size: 50 # 초기로 생성할 연결의 수를 설정합니다. + max-total: 50 # 풀에서 유지할 수 있는 최대 연결 수를 설정합니다. + max-idle: 50 # 유휴 상태로 유지할 최대 연결 수를 설정합니다. + min-idle: 50 # 유휴 상태로 유지할 최소 연결 수를 설정합니다. + test-on-borrow: true # 풀에서 연결을 빌릴 때마다 해당 연결이 유효한지 검증할지 여부를 설정합니다. + validation-query: SELECT 1 # 연결 유효성을 검사하기 위해 실행할 SQL 쿼리를 설정합니다. + jpa: hibernate: ddl-auto: update @@ -27,3 +35,5 @@ spring: remote-addr: 127.0.0.1 + +