From ba34a6e8b1293444c3449602ec3c2376b77a319c Mon Sep 17 00:00:00 2001 From: Watcharapong Suksrisawangwong Date: Sun, 25 Feb 2024 22:21:23 +0700 Subject: [PATCH] add unit test for purchase lottery method add validation level at Controller add ConstrainViolationException in ControllerHandleException --- .../exception/ControllerExceptionHandler.java | 44 +++++++++++++ .../exception/LotteryRunOutException.java | 7 ++ .../posttest/lottery/LotteryController.java | 3 +- .../lottery/LotteryPurchaseReponse.java | 2 +- .../posttest/lottery/LotteryService.java | 44 +++++++++++-- .../bootcamp/posttest/user/UserTicket.java | 8 +++ .../lottery/LotteryControllerTest.java | 1 - .../posttest/lottery/LotteryServiceTest.java | 65 ++++++++++++++++--- 8 files changed, 159 insertions(+), 15 deletions(-) create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/exception/LotteryRunOutException.java diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/exception/ControllerExceptionHandler.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/exception/ControllerExceptionHandler.java index 056cf2c1..ba74530e 100644 --- a/posttest/src/main/java/com/kbtg/bootcamp/posttest/exception/ControllerExceptionHandler.java +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/exception/ControllerExceptionHandler.java @@ -1,5 +1,6 @@ package com.kbtg.bootcamp.posttest.exception; +import jakarta.validation.ConstraintViolationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -46,4 +47,47 @@ public ApiErrorResponse handleValidationExceptions(MethodArgumentNotValidExcepti request.getDescription(false) ); } + + @ExceptionHandler(value = {NotFoundException.class}) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ApiErrorResponse handleNotFoundException(NotFoundException exception,WebRequest request){ + return new ApiErrorResponse( + LocalDateTime.now(), + HttpStatus.NOT_FOUND.value(), + HttpStatus.NOT_FOUND.getReasonPhrase(), + exception.getMessage(), + request.getDescription(false) + ); + } + + @ExceptionHandler(value = {LotteryRunOutException.class}) + @ResponseStatus(HttpStatus.CONFLICT) + public ApiErrorResponse handleLotteryRunOutException(LotteryRunOutException exception,WebRequest request){ + return new ApiErrorResponse( + LocalDateTime.now(), + HttpStatus.CONFLICT.value(), + HttpStatus.CONFLICT.getReasonPhrase(), + exception.getMessage(), + request.getDescription(false) + ); + } + + @ExceptionHandler(value = {ConstraintViolationException.class}) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiErrorResponse handleConstraintViolationException(ConstraintViolationException exception, WebRequest request) { + List error = exception.getConstraintViolations() + .stream() + .map(f -> f.getMessage()) + .toList(); + + return new ApiErrorResponse( + LocalDateTime.now(), + HttpStatus.BAD_REQUEST.value(), + HttpStatus.BAD_REQUEST.getReasonPhrase(), + String.join(",",error), + request.getDescription(false) + ); + } + + } diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/exception/LotteryRunOutException.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/exception/LotteryRunOutException.java new file mode 100644 index 00000000..0c4ef33a --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/exception/LotteryRunOutException.java @@ -0,0 +1,7 @@ +package com.kbtg.bootcamp.posttest.exception; + +public class LotteryRunOutException extends RuntimeException{ + public LotteryRunOutException(String message){ + super(message); + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/LotteryController.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/LotteryController.java index fb9457b1..629cba67 100644 --- a/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/LotteryController.java +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/LotteryController.java @@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.*; @RestController +@Validated public class LotteryController { private LotteryService lotteryService; @@ -37,6 +38,6 @@ public LotteryPurchaseReponse purchaseTicket( @PathVariable(value = "userId") @TenDigitUser String userId, @PathVariable(value = "ticketId") @SixDigitTicket String ticketId){ - return null; + return lotteryService.purchaseLottery(userId,ticketId); } } diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/LotteryPurchaseReponse.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/LotteryPurchaseReponse.java index 884df3ed..42f7f2c1 100644 --- a/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/LotteryPurchaseReponse.java +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/LotteryPurchaseReponse.java @@ -1,4 +1,4 @@ package com.kbtg.bootcamp.posttest.lottery; -public record LotteryPurchaseReponse(int id) { +public record LotteryPurchaseReponse(String id) { } diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/LotteryService.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/LotteryService.java index cf699c23..7b57089a 100644 --- a/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/LotteryService.java +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/LotteryService.java @@ -2,8 +2,11 @@ import com.kbtg.bootcamp.posttest.exception.InternalServerException; +import com.kbtg.bootcamp.posttest.exception.LotteryRunOutException; +import com.kbtg.bootcamp.posttest.exception.NotFoundException; import com.kbtg.bootcamp.posttest.user.User; import com.kbtg.bootcamp.posttest.user.UserRepository; +import com.kbtg.bootcamp.posttest.user.UserTicket; import com.kbtg.bootcamp.posttest.user.UserTicketRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -27,9 +30,6 @@ public LotteryService(LotteryRepository lotteryRepository, UserTicketRepository this.userRepository = userRepository; } - public Boolean checkValidTicket(String ticket){ - return true; - } public LotteryListResponse getAvailableTicketIds() { try{ List tickets = lotteryRepository.findByAmountMoreThanZero(); @@ -63,6 +63,42 @@ public LotteryResponse createLottery(LotteryDto request){ @Transactional public LotteryPurchaseReponse purchaseLottery(String userId,String ticketId){ - return null; + Optional optionalLottery = lotteryRepository.findById(ticketId); + Lottery lottery; + if(optionalLottery.isEmpty()){ + throw new NotFoundException("Ticket "+ticketId+" not found"); + } + + lottery = optionalLottery.get(); + + if(lottery.getAmount() == 0){ + throw new LotteryRunOutException("Ticket "+ticketId+" has already been purchased"); + } + + UserTicket userTicket; + try{ + User user; + Optional optionalUser = userRepository.findById(userId); + if(optionalUser.isEmpty()){ + user = new User(userId); + userRepository.save(user); + }else{ + user = optionalUser.get(); + } + + // create purchase record + userTicket = new UserTicket(lottery,user); + userTicket = userTicketRepository.save(userTicket); + + // discount from storage + lottery.setAmount(lottery.getAmount()-1); + lotteryRepository.save(lottery); + + }catch (Exception ex){ + throw new InternalServerException("Failed to purchase lottery"); + } + + String recordId = Integer.toString(userTicket.getId()); + return new LotteryPurchaseReponse(recordId); } } diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/UserTicket.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/UserTicket.java index b1db84a7..7ebcb5df 100644 --- a/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/UserTicket.java +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/UserTicket.java @@ -19,6 +19,14 @@ public class UserTicket { @JoinColumn(name = "user_id") private User user; + public UserTicket() { + } + + public UserTicket(Lottery lottery, User user) { + this.lottery = lottery; + this.user = user; + } + public int getId() { return id; } diff --git a/posttest/src/test/java/com/kbtg/bootcamp/posttest/lottery/LotteryControllerTest.java b/posttest/src/test/java/com/kbtg/bootcamp/posttest/lottery/LotteryControllerTest.java index f92bc7d2..b5dd752b 100644 --- a/posttest/src/test/java/com/kbtg/bootcamp/posttest/lottery/LotteryControllerTest.java +++ b/posttest/src/test/java/com/kbtg/bootcamp/posttest/lottery/LotteryControllerTest.java @@ -104,5 +104,4 @@ public void createTicket_withInvalidData_ShouldReturn400() throws Exception { } - } diff --git a/posttest/src/test/java/com/kbtg/bootcamp/posttest/lottery/LotteryServiceTest.java b/posttest/src/test/java/com/kbtg/bootcamp/posttest/lottery/LotteryServiceTest.java index 80dab319..bbdd7eca 100644 --- a/posttest/src/test/java/com/kbtg/bootcamp/posttest/lottery/LotteryServiceTest.java +++ b/posttest/src/test/java/com/kbtg/bootcamp/posttest/lottery/LotteryServiceTest.java @@ -1,6 +1,8 @@ package com.kbtg.bootcamp.posttest.lottery; import com.kbtg.bootcamp.posttest.exception.InternalServerException; +import com.kbtg.bootcamp.posttest.exception.LotteryRunOutException; +import com.kbtg.bootcamp.posttest.exception.NotFoundException; import com.kbtg.bootcamp.posttest.user.User; import com.kbtg.bootcamp.posttest.user.UserRepository; import com.kbtg.bootcamp.posttest.user.UserTicket; @@ -17,6 +19,7 @@ import java.util.Collections; import java.util.List; +import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; @@ -53,7 +56,7 @@ void whenGetAvailableTicket_withNoData_ShouldReturnResponseWithEmptyList() { @Test @DisplayName("Get Available Ticket IDs With Data Should Return Correctly List Of Ticket Id") - void gwhenGetAvailableTicket_withData_ShouldReturnResponseWithListStringTicketId() { + void whenGetAvailableTicket_withData_ShouldReturnResponseWithListStringTicketId() { // Arrange Lottery lottery1 = new Lottery(); lottery1.setTicketId("000001"); @@ -115,26 +118,72 @@ void whenCreateLotteryFails_ShouldThrowInternalServerException() { } @Test - @DisplayName("Purchase lottery should be return id from User_ticket") - void whenPurchaseLottery_ShouldReturnLotteryPurchaseResponseWithUserTicketId(){ + @DisplayName("Purchase lottery throws NotFoundException if ticket is not found") + void whenPurchaseLottery_withNoTicket_ShouldReturnNotFound(){ // Arrange + String ticket = "000001"; String userId = "0123456789"; - User user = new User(userId); + Lottery lottery = new Lottery(ticket,80,1); + + when(lotteryRepository.findById(ticket)).thenReturn(Optional.empty()); + + + // Act & Assert + Exception exception = assertThrows(NotFoundException.class, () -> { + lotteryService.purchaseLottery(userId,ticket); + }); + + String actualMessage = exception.getMessage(); + String expectedMessage = "Ticket "+ticket+" not found"; + + assertEquals(expectedMessage,actualMessage); + } + + @Test + @DisplayName("Purchase lottery throws LotteryRunOutException if ticket is already purchased") + void whenPurchaseLottery_withTicketRunOut_ShouldReturnRunOutException(){ + + // Arrange String ticket = "000001"; + String userId = "0123456789"; + Lottery lottery = new Lottery(ticket,80,0); + + when(lotteryRepository.findById(ticket)).thenReturn(Optional.of(lottery)); + + + // Act & Assert + Exception exception = assertThrows(LotteryRunOutException.class, () -> { + lotteryService.purchaseLottery(userId,ticket); + }); + + String actualMessage = exception.getMessage(); + String expectedMessage = "Ticket "+ticket+" has already been purchased"; + + assertEquals(expectedMessage,actualMessage); + } + + @Test + @DisplayName("Purchase lottery return LotteryPurchaseResponse with UserTicketId") + void whenPurchaseLottery_ShouldReturnResponseWithUserTicketId(){ + + // Arrange + String ticket = "000001"; + String userId = "0123456789"; Lottery lottery = new Lottery(ticket,80,1); - UserTicket userTicket = new UserTicket(); + User user = new User(userId); + UserTicket userTicket = new UserTicket(lottery,user); userTicket.setId(1); - userTicket.setUser(user); - userTicket.setLottery(lottery); + when(lotteryRepository.findById(ticket)).thenReturn(Optional.of(lottery)); + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); when(userTicketRepository.save(any(UserTicket.class))).thenReturn(userTicket); // Act LotteryPurchaseReponse actual = lotteryService.purchaseLottery(userId,ticket); // Assert - int expected = 1; + String expected = "1"; assertEquals(expected,actual.id()); } } \ No newline at end of file