diff --git a/src/main/java/com/jisungin/api/ApiResponse.java b/src/main/java/com/jisungin/api/ApiResponse.java index 9ac65ed..d47a599 100644 --- a/src/main/java/com/jisungin/api/ApiResponse.java +++ b/src/main/java/com/jisungin/api/ApiResponse.java @@ -24,4 +24,8 @@ public static ApiResponse ok(T data) { return new ApiResponse<>(HttpStatus.OK, HttpStatus.OK.name(), data); } + public static ApiResponse ok() { + return new ApiResponse<>(HttpStatus.OK, HttpStatus.OK.name(), null); + } + } diff --git a/src/main/java/com/jisungin/api/GlobalExceptionHandler.java b/src/main/java/com/jisungin/api/GlobalExceptionHandler.java index 6ca8ef9..157311f 100644 --- a/src/main/java/com/jisungin/api/GlobalExceptionHandler.java +++ b/src/main/java/com/jisungin/api/GlobalExceptionHandler.java @@ -1,6 +1,7 @@ package com.jisungin.api; import com.jisungin.exception.BusinessException; +import com.jisungin.exception.ErrorCode; import com.jisungin.exception.ErrorResponse; import java.util.List; import lombok.extern.slf4j.Slf4j; @@ -9,6 +10,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.validation.BindingResult; import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -31,6 +33,15 @@ public ResponseEntity handleMethodArgumentNotValidException(Metho .body(ErrorResponse.of(HttpStatus.BAD_REQUEST.value(), firstErrorMessage)); } + @ExceptionHandler(MissingServletRequestParameterException.class) + public ResponseEntity handleMissionServletRequestParamException( + MissingServletRequestParameterException e + ) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(ErrorResponse.of(ErrorCode.INVALID_PARAMS_VALUE.getCode(), + ErrorCode.INVALID_PARAMS_VALUE.getMessage())); + } + @ExceptionHandler(BusinessException.class) public ResponseEntity handleBusinessException(BusinessException e) { log.info("BusinessException: {}", e.getMessage()); diff --git a/src/main/java/com/jisungin/api/userlibrary/UserLibraryController.java b/src/main/java/com/jisungin/api/userlibrary/UserLibraryController.java new file mode 100644 index 0000000..4e96e4b --- /dev/null +++ b/src/main/java/com/jisungin/api/userlibrary/UserLibraryController.java @@ -0,0 +1,62 @@ +package com.jisungin.api.userlibrary; + +import com.jisungin.api.ApiResponse; +import com.jisungin.api.oauth.Auth; +import com.jisungin.api.userlibrary.request.UserLibraryCreateRequest; +import com.jisungin.api.userlibrary.request.UserLibraryEditRequest; +import com.jisungin.application.userlibrary.UserLibraryService; +import com.jisungin.application.userlibrary.response.UserLibraryResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/v1") +public class UserLibraryController { + + private final UserLibraryService userLibraryService; + + @GetMapping("/user-libraries") + public ApiResponse getUserLibrary(@RequestParam String isbn, + @Auth Long userId + ) { + return ApiResponse.ok(userLibraryService.getUserLibrary(userId, isbn)); + } + + @PostMapping("/user-libraries") + public ApiResponse createUserLibrary(@Valid @RequestBody UserLibraryCreateRequest request, + @Auth Long userId + ) { + return ApiResponse.ok(userLibraryService.createUserLibrary(request.toServiceRequest(), userId)); + } + + @PatchMapping("/user-libraries/{userLibraryId}") + public ApiResponse editUserLibrary(@PathVariable("userLibraryId") Long userLibraryId, + @Valid @RequestBody UserLibraryEditRequest request, + @Auth Long userId + ) { + userLibraryService.editUserLibrary(userLibraryId, userId, request.toServiceRequest()); + + return ApiResponse.ok(); + } + + @DeleteMapping("/user-libraries/{userLibraryId}") + public ApiResponse deleteUserLibrary(@PathVariable("userLibraryId") Long userLibraryId, + @RequestParam String isbn, + @Auth Long userId + ) { + userLibraryService.deleteUserLibrary(userLibraryId, userId, isbn); + + return ApiResponse.ok(); + } + +} diff --git a/src/main/java/com/jisungin/api/userlibrary/request/UserLibraryCreateRequest.java b/src/main/java/com/jisungin/api/userlibrary/request/UserLibraryCreateRequest.java new file mode 100644 index 0000000..2203b73 --- /dev/null +++ b/src/main/java/com/jisungin/api/userlibrary/request/UserLibraryCreateRequest.java @@ -0,0 +1,32 @@ +package com.jisungin.api.userlibrary.request; + +import com.jisungin.application.userlibrary.request.UserLibraryCreateServiceRequest; +import jakarta.validation.constraints.NotBlank; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class UserLibraryCreateRequest { + + @NotBlank(message = "책 isbn 입력은 필수 입니다.") + private String isbn; + + @NotBlank(message = "독서 상태 정보 입력은 필수 입니다.") + private String readingStatus; + + @Builder + private UserLibraryCreateRequest(String isbn, String readingStatus) { + this.isbn = isbn; + this.readingStatus = readingStatus; + } + + public UserLibraryCreateServiceRequest toServiceRequest() { + return UserLibraryCreateServiceRequest.builder() + .isbn(isbn) + .readingStatus(readingStatus) + .build(); + } + +} diff --git a/src/main/java/com/jisungin/api/userlibrary/request/UserLibraryEditRequest.java b/src/main/java/com/jisungin/api/userlibrary/request/UserLibraryEditRequest.java new file mode 100644 index 0000000..664983e --- /dev/null +++ b/src/main/java/com/jisungin/api/userlibrary/request/UserLibraryEditRequest.java @@ -0,0 +1,32 @@ +package com.jisungin.api.userlibrary.request; + +import com.jisungin.application.userlibrary.request.UserLibraryEditServiceRequest; +import jakarta.validation.constraints.NotBlank; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class UserLibraryEditRequest { + + @NotBlank(message = "책 isbn 입력은 필수 입니다.") + private String isbn; + + @NotBlank(message = "독서 상태 정보 입력은 필수 입니다.") + private String readingStatus; + + @Builder + private UserLibraryEditRequest(String isbn, String readingStatus) { + this.isbn = isbn; + this.readingStatus = readingStatus; + } + + public UserLibraryEditServiceRequest toServiceRequest() { + return UserLibraryEditServiceRequest.builder() + .isbn(isbn) + .readingStatus(readingStatus) + .build(); + } + +} diff --git a/src/main/java/com/jisungin/application/userlibrary/UserLibraryService.java b/src/main/java/com/jisungin/application/userlibrary/UserLibraryService.java new file mode 100644 index 0000000..3cac752 --- /dev/null +++ b/src/main/java/com/jisungin/application/userlibrary/UserLibraryService.java @@ -0,0 +1,103 @@ +package com.jisungin.application.userlibrary; + +import com.jisungin.application.userlibrary.request.UserLibraryCreateServiceRequest; +import com.jisungin.application.userlibrary.request.UserLibraryEditServiceRequest; +import com.jisungin.application.userlibrary.response.UserLibraryResponse; +import com.jisungin.domain.ReadingStatus; +import com.jisungin.domain.book.Book; +import com.jisungin.domain.book.repository.BookRepository; +import com.jisungin.domain.mylibrary.UserLibrary; +import com.jisungin.domain.mylibrary.repository.UserLibraryRepository; +import com.jisungin.domain.user.User; +import com.jisungin.domain.user.repository.UserRepository; +import com.jisungin.exception.BusinessException; +import com.jisungin.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class UserLibraryService { + + private final UserRepository userRepository; + private final BookRepository bookRepository; + private final UserLibraryRepository userLibraryRepository; + + public UserLibraryResponse getUserLibrary(Long userId, String isbn) { + if (userId == null || isbn == null) { + return UserLibraryResponse.empty(); + } + + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + + Book book = bookRepository.findById(isbn) + .orElseThrow(() -> new BusinessException(ErrorCode.BOOK_NOT_FOUND)); + + UserLibrary userLibrary = userLibraryRepository.findByUserAndBook(user, book); + + return UserLibraryResponse.of(userLibrary); + } + + @Transactional + public UserLibraryResponse createUserLibrary(UserLibraryCreateServiceRequest request, Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + + Book book = bookRepository.findById(request.getIsbn()) + .orElseThrow(() -> new BusinessException(ErrorCode.BOOK_NOT_FOUND)); + + UserLibrary savedUserLibrary = userLibraryRepository.save(request.toEntity(user, book)); + + return UserLibraryResponse.of(savedUserLibrary); + } + + @Transactional + public void editUserLibrary(Long userLibraryId, Long userId, UserLibraryEditServiceRequest request) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + + Book book = bookRepository.findById(request.getIsbn()) + .orElseThrow(() -> new BusinessException(ErrorCode.BOOK_NOT_FOUND)); + + UserLibrary userLibrary = userLibraryRepository.findByIdWithBookAndUser(userLibraryId) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_LIBRARY_NOT_FOUND)); + + if (!userLibrary.isUserLibraryOwner(user.getId())) { + throw new BusinessException(ErrorCode.UNAUTHORIZED_REQUEST); + } + + if (!userLibrary.isSameBook(book.getIsbn())) { + throw new BusinessException(ErrorCode.BOOK_INVALID_INFO); + } + + userLibrary.editReadingStatus(ReadingStatus.createReadingStatus(request.getReadingStatus())); + } + + @Transactional + public void deleteUserLibrary(Long userLibraryId, Long userId, String isbn) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + + Book book = bookRepository.findById(isbn) + .orElseThrow(() -> new BusinessException(ErrorCode.BOOK_NOT_FOUND)); + + UserLibrary userLibrary = userLibraryRepository.findByIdWithBookAndUser(userLibraryId) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_LIBRARY_NOT_FOUND)); + + if (!userLibrary.isUserLibraryOwner(user.getId())) { + throw new BusinessException(ErrorCode.UNAUTHORIZED_REQUEST); + } + + if (!userLibrary.isSameBook(book.getIsbn())) { + throw new BusinessException(ErrorCode.BOOK_INVALID_INFO); + } + + userLibraryRepository.deleteById(userLibrary.getId()); + } + +} diff --git a/src/main/java/com/jisungin/application/userlibrary/request/UserLibraryCreateServiceRequest.java b/src/main/java/com/jisungin/application/userlibrary/request/UserLibraryCreateServiceRequest.java new file mode 100644 index 0000000..8208854 --- /dev/null +++ b/src/main/java/com/jisungin/application/userlibrary/request/UserLibraryCreateServiceRequest.java @@ -0,0 +1,32 @@ +package com.jisungin.application.userlibrary.request; + +import com.jisungin.domain.ReadingStatus; +import com.jisungin.domain.book.Book; +import com.jisungin.domain.mylibrary.UserLibrary; +import com.jisungin.domain.user.User; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class UserLibraryCreateServiceRequest { + + private String isbn; + private String readingStatus; + + @Builder + private UserLibraryCreateServiceRequest(String isbn, String readingStatus) { + this.isbn = isbn; + this.readingStatus = readingStatus; + } + + public UserLibrary toEntity(User user, Book book) { + return UserLibrary.builder() + .user(user) + .book(book) + .status(ReadingStatus.createReadingStatus(readingStatus)) + .build(); + } + +} diff --git a/src/main/java/com/jisungin/application/userlibrary/request/UserLibraryEditServiceRequest.java b/src/main/java/com/jisungin/application/userlibrary/request/UserLibraryEditServiceRequest.java new file mode 100644 index 0000000..e0fc085 --- /dev/null +++ b/src/main/java/com/jisungin/application/userlibrary/request/UserLibraryEditServiceRequest.java @@ -0,0 +1,21 @@ +package com.jisungin.application.userlibrary.request; + +import com.jisungin.api.userlibrary.request.UserLibraryCreateRequest; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class UserLibraryEditServiceRequest { + + private String isbn; + private String readingStatus; + + @Builder + private UserLibraryEditServiceRequest(String isbn, String readingStatus) { + this.isbn = isbn; + this.readingStatus = readingStatus; + } + +} diff --git a/src/main/java/com/jisungin/application/userlibrary/response/UserLibraryResponse.java b/src/main/java/com/jisungin/application/userlibrary/response/UserLibraryResponse.java new file mode 100644 index 0000000..6b89e24 --- /dev/null +++ b/src/main/java/com/jisungin/application/userlibrary/response/UserLibraryResponse.java @@ -0,0 +1,42 @@ +package com.jisungin.application.userlibrary.response; + + +import com.jisungin.domain.mylibrary.UserLibrary; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class UserLibraryResponse { + + private Long id; + private String status; + private Boolean hasReadingStatus; + + @Builder + private UserLibraryResponse(Long id, String status, Boolean hasReadingStatus) { + this.id = id; + this.status = status; + this.hasReadingStatus = hasReadingStatus; + } + + public static UserLibraryResponse of(UserLibrary userLibrary) { + if (userLibrary == null) { + return empty(); + } + + return UserLibraryResponse.builder() + .id(userLibrary.getId()) + .status(userLibrary.getStatus().getText()) + .hasReadingStatus(true) + .build(); + } + + public static UserLibraryResponse empty() { + return UserLibraryResponse.builder() + .hasReadingStatus(false) + .build(); + } + +} diff --git a/src/main/java/com/jisungin/domain/ReadingStatus.java b/src/main/java/com/jisungin/domain/ReadingStatus.java index 318d4dd..ec18eef 100644 --- a/src/main/java/com/jisungin/domain/ReadingStatus.java +++ b/src/main/java/com/jisungin/domain/ReadingStatus.java @@ -3,6 +3,7 @@ import com.jisungin.exception.BusinessException; import com.jisungin.exception.ErrorCode; import java.util.List; +import java.util.Locale; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -18,6 +19,10 @@ public enum ReadingStatus { private final String text; + public static ReadingStatus createReadingStatus(String status) { + return ReadingStatus.valueOf(status.toUpperCase(Locale.ENGLISH)); + } + public static List createReadingStatus(List statusList) { if (statusList == null) { throw new BusinessException(ErrorCode.PARTICIPATION_CONDITION_ERROR); diff --git a/src/main/java/com/jisungin/domain/book/Book.java b/src/main/java/com/jisungin/domain/book/Book.java index 0d10a28..a77f06a 100644 --- a/src/main/java/com/jisungin/domain/book/Book.java +++ b/src/main/java/com/jisungin/domain/book/Book.java @@ -56,4 +56,8 @@ private Book(String isbn, String title, String content, String authors, String p this.dateTime = dateTime; } + public boolean isSame(String isbn) { + return this.isbn.equals(isbn); + } + } diff --git a/src/main/java/com/jisungin/domain/mylibrary/UserLibrary.java b/src/main/java/com/jisungin/domain/mylibrary/UserLibrary.java index 7bebbe8..71c6776 100644 --- a/src/main/java/com/jisungin/domain/mylibrary/UserLibrary.java +++ b/src/main/java/com/jisungin/domain/mylibrary/UserLibrary.java @@ -39,4 +39,16 @@ private UserLibrary(User user, Book book, ReadingStatus status) { this.status = status; } + public boolean isUserLibraryOwner(Long userId) { + return user.isMe(userId); + } + + public boolean isSameBook(String isbn) { + return book.isSame(isbn); + } + + public void editReadingStatus(ReadingStatus status) { + this.status = status; + } + } diff --git a/src/main/java/com/jisungin/domain/mylibrary/repository/UserLibraryRepository.java b/src/main/java/com/jisungin/domain/mylibrary/repository/UserLibraryRepository.java index 881ceac..9ba6f96 100644 --- a/src/main/java/com/jisungin/domain/mylibrary/repository/UserLibraryRepository.java +++ b/src/main/java/com/jisungin/domain/mylibrary/repository/UserLibraryRepository.java @@ -1,7 +1,10 @@ package com.jisungin.domain.mylibrary.repository; import com.jisungin.domain.ReadingStatus; +import com.jisungin.domain.book.Book; import com.jisungin.domain.mylibrary.UserLibrary; +import com.jisungin.domain.user.User; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -12,4 +15,15 @@ public interface UserLibraryRepository extends JpaRepository "SELECT ul.status FROM UserLibrary ul JOIN ul.user u WHERE u.id = :id" ) ReadingStatus findByUserId(@Param("id") Long userId); + + @Query( + "SELECT ul FROM UserLibrary ul " + + "JOIN FETCH ul.book " + + "JOIN FETCH ul.user " + + "WHERE ul.id = :id" + ) + Optional findByIdWithBookAndUser(@Param("id") Long id); + + UserLibrary findByUserAndBook(User user, Book book); + } diff --git a/src/main/java/com/jisungin/exception/ErrorCode.java b/src/main/java/com/jisungin/exception/ErrorCode.java index 7b86805..4609689 100644 --- a/src/main/java/com/jisungin/exception/ErrorCode.java +++ b/src/main/java/com/jisungin/exception/ErrorCode.java @@ -26,7 +26,9 @@ public enum ErrorCode { IMAGE_NOT_FOUND(400, "파일이 없습니다."), S3_UPLOAD_FAIL(400, "이미지 업로드가 실패되었습니다."), NOT_IMAGE(400, "이미지 파일이 아닙니다."), - UNABLE_WRITE_COMMENT(400, "의견을 쓸 권한이 없습니다."); + UNABLE_WRITE_COMMENT(400, "의견을 쓸 권한이 없습니다."), + USER_LIBRARY_NOT_FOUND(404, "서재 정보를 찾을 수 없습니다."), + INVALID_PARAMS_VALUE(400, "유효하지 않은 파라미터 입니다."); private final int code; private final String message; diff --git a/src/test/java/com/jisungin/ControllerTestSupport.java b/src/test/java/com/jisungin/ControllerTestSupport.java index 1ea8f4d..4c5b67e 100644 --- a/src/test/java/com/jisungin/ControllerTestSupport.java +++ b/src/test/java/com/jisungin/ControllerTestSupport.java @@ -12,6 +12,7 @@ import com.jisungin.api.talkroom.TalkRoomController; import com.jisungin.api.talkroomlike.TalkRoomLikeController; import com.jisungin.api.user.UserController; +import com.jisungin.api.userlibrary.UserLibraryController; import com.jisungin.application.book.BestSellerService; import com.jisungin.application.book.BookService; import com.jisungin.application.comment.CommentService; @@ -23,6 +24,7 @@ import com.jisungin.application.talkroom.TalkRoomService; import com.jisungin.application.talkroomlike.TalkRoomLikeService; import com.jisungin.application.user.UserService; +import com.jisungin.application.userlibrary.UserLibraryService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; @@ -38,7 +40,8 @@ BookController.class, ReviewLikeController.class, ImageController.class, - SearchController.class + SearchController.class, + UserLibraryController.class }) public abstract class ControllerTestSupport { @@ -84,4 +87,7 @@ public abstract class ControllerTestSupport { @MockBean protected SearchService searchService; + @MockBean + protected UserLibraryService userLibraryService; + } diff --git a/src/test/java/com/jisungin/api/userlibrary/UserLibraryControllerTest.java b/src/test/java/com/jisungin/api/userlibrary/UserLibraryControllerTest.java new file mode 100644 index 0000000..6d7534f --- /dev/null +++ b/src/test/java/com/jisungin/api/userlibrary/UserLibraryControllerTest.java @@ -0,0 +1,197 @@ +package com.jisungin.api.userlibrary; + +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.jisungin.ControllerTestSupport; +import com.jisungin.api.userlibrary.request.UserLibraryCreateRequest; +import com.jisungin.api.userlibrary.request.UserLibraryEditRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class UserLibraryControllerTest extends ControllerTestSupport { + + @Test + @DisplayName("서재 정보를 조회한다.") + public void getUserLibrary() throws Exception { + // given + String isbn = "00001"; + + // when // then + mockMvc.perform(get("/v1/user-libraries") + .param("isbn", isbn) + .accept(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("200")) + .andExpect(jsonPath("$.status").value("OK")) + .andExpect(jsonPath("$.message").value("OK")) + .andDo(print()); + } + + @Test + @DisplayName("서재 정보 조회 시 책 isbn 입력은 필수이다.") + public void getUserLibraryWithoutIsbn() throws Exception { + // when // then + mockMvc.perform(get("/v1/user-libraries") + .accept(APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("400")) + .andExpect(jsonPath("$.message").value("유효하지 않은 파라미터 입니다.")) + .andDo(print()); + } + + @Test + @DisplayName("서재 정보를 생성한다.") + public void createUseLibrary() throws Exception { + // given + UserLibraryCreateRequest request = UserLibraryCreateRequest.builder() + .isbn("00001") + .readingStatus("want") + .build(); + + // when // then + mockMvc.perform(post("/v1/user-libraries") + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("200")) + .andExpect(jsonPath("$.status").value("OK")) + .andExpect(jsonPath("$.message").value("OK")) + .andDo(print()); + } + + @Test + @DisplayName("서재 정보 등록 시 isbn 입력은 필수이다.") + public void createUserLibraryWithoutIsbn() throws Exception { + // given + UserLibraryCreateRequest request = UserLibraryCreateRequest.builder() + .readingStatus("want") + .build(); + + // when // then + mockMvc.perform(post("/v1/user-libraries") + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("400")) + .andExpect(jsonPath("$.message").value("책 isbn 입력은 필수 입니다.")) + .andDo(print()); + } + + @Test + @DisplayName("사재 정보 등록 시 독서 상태 입력은 필수이다.") + public void createUserLibraryWithoutReadingStatus() throws Exception { + // given + UserLibraryCreateRequest request = UserLibraryCreateRequest.builder() + .isbn("00001") + .build(); + + // when // then + mockMvc.perform(post("/v1/user-libraries") + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("400")) + .andExpect(jsonPath("$.message").value("독서 상태 정보 입력은 필수 입니다.")) + .andDo(print()); + } + + @Test + @DisplayName("서재 정보를 수정한다.") + public void editUserLibrary() throws Exception { + // given + Long userLibraryId = 1L; + + UserLibraryEditRequest request = UserLibraryEditRequest.builder() + .isbn("00001") + .readingStatus("want") + .build(); + + // when // then + mockMvc.perform(patch("/v1/user-libraries/{userLibraryId}", userLibraryId) + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("200")) + .andExpect(jsonPath("$.status").value("OK")) + .andExpect(jsonPath("$.message").value("OK")) + .andDo(print()); + } + + @Test + @DisplayName("서재 정보를 수정시 isbn 입력은 필수이다.") + public void editUserLibraryWithoutIsbn() throws Exception { + // given + Long userLibraryId = 1L; + + UserLibraryEditRequest request = UserLibraryEditRequest.builder() + .readingStatus("want") + .build(); + + // when // then + mockMvc.perform(patch("/v1/user-libraries/{userLibraryId}", userLibraryId) + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("400")) + .andExpect(jsonPath("$.message").value("책 isbn 입력은 필수 입니다.")) + .andDo(print()); + } + + @Test + @DisplayName("서재 정보를 수정시 독서 상태 정보 입력은 필수이다.") + public void editUserLibraryWithoutReadingStatus() throws Exception { + // given + Long userLibraryId = 1L; + + UserLibraryEditRequest request = UserLibraryEditRequest.builder() + .isbn("00001") + .build(); + + // when // then + mockMvc.perform(patch("/v1/user-libraries/{userLibraryId}", userLibraryId) + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("400")) + .andExpect(jsonPath("$.message").value("독서 상태 정보 입력은 필수 입니다.")) + .andDo(print()); + } + + @Test + @DisplayName("서재 정보를 삭제한다.") + public void deleteUserLibrary() throws Exception { + // given + Long userLibraryId = 1L; + + // when // then + mockMvc.perform(delete("/v1/user-libraries/{userLibraryId}", userLibraryId) + .param("isbn", "0000X")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("200")) + .andExpect(jsonPath("$.status").value("OK")) + .andExpect(jsonPath("$.message").value("OK")) + .andDo(print()); + } + + @Test + @DisplayName("서재 정보 삭제 시 책 isbn 입력은 필수이다.") + public void deleteUserLibraryWithoutIsbn() throws Exception { + // given + Long userLibraryId = 1L; + + // when // then + mockMvc.perform(delete("/v1/user-libraries/{userLibraryId}", userLibraryId)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("400")) + .andExpect(jsonPath("$.message").value("유효하지 않은 파라미터 입니다.")) + .andDo(print()); + } + +} diff --git a/src/test/java/com/jisungin/application/userlibrary/UserLibraryServiceTest.java b/src/test/java/com/jisungin/application/userlibrary/UserLibraryServiceTest.java new file mode 100644 index 0000000..45c063c --- /dev/null +++ b/src/test/java/com/jisungin/application/userlibrary/UserLibraryServiceTest.java @@ -0,0 +1,476 @@ +package com.jisungin.application.userlibrary; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.jisungin.ServiceTestSupport; +import com.jisungin.application.userlibrary.request.UserLibraryCreateServiceRequest; +import com.jisungin.application.userlibrary.request.UserLibraryEditServiceRequest; +import com.jisungin.application.userlibrary.response.UserLibraryResponse; +import com.jisungin.domain.ReadingStatus; +import com.jisungin.domain.book.Book; +import com.jisungin.domain.book.repository.BookRepository; +import com.jisungin.domain.mylibrary.UserLibrary; +import com.jisungin.domain.mylibrary.repository.UserLibraryRepository; +import com.jisungin.domain.oauth.OauthId; +import com.jisungin.domain.oauth.OauthType; +import com.jisungin.domain.user.User; +import com.jisungin.domain.user.repository.UserRepository; +import com.jisungin.exception.BusinessException; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +public class UserLibraryServiceTest extends ServiceTestSupport { + + @Autowired + private UserRepository userRepository; + + @Autowired + private BookRepository bookRepository; + + @Autowired + private UserLibraryRepository userLibraryRepository; + + @Autowired + private UserLibraryService userLibraryService; + + @BeforeEach + public void tearDown() { + userLibraryRepository.deleteAllInBatch(); + bookRepository.deleteAllInBatch(); + userRepository.deleteAllInBatch(); + } + + @Test + @DisplayName("사용자가 서재 정보를 조회한다.") + public void getUserLibrary() { + // given + User user = userRepository.save(createUser()); + Book book = bookRepository.save(createBook()); + UserLibrary userLibrary = userLibraryRepository.save(create(user, book)); + + // when + UserLibraryResponse response = userLibraryService.getUserLibrary(user.getId(), book.getIsbn()); + + // then + assertThat(response.getId()).isEqualTo(userLibrary.getId()); + assertThat(response.getStatus()).isEqualTo(userLibrary.getStatus().getText()); + assertThat(response.getHasReadingStatus()).isTrue(); + } + + @Test + @DisplayName("비로그인으로 서재 정보 조회시 빈 응답을 받는다.") + public void getUserLibraryForUnAuthenticatedUser() { + // given + String bookIsbn = "0000X"; + + // when + UserLibraryResponse response = userLibraryService.getUserLibrary(null, bookIsbn); + + // then + assertThat(response).isNotNull(); + assertThat(response.getId()).isNull(); + assertThat(response.getStatus()).isNull(); + assertThat(response.getHasReadingStatus()).isFalse(); + } + + @Test + @DisplayName("서재 정보 조회 시 isbn이 없는 경우 빈 응답을 받는다.") + public void getUserLibraryWithNonIsbn() { + // given + User user = userRepository.save(createUser()); + + // when + UserLibraryResponse response = userLibraryService.getUserLibrary(user.getId(), null); + + // then + assertThat(response).isNotNull(); + assertThat(response.getHasReadingStatus()).isFalse(); + } + + @Test + @DisplayName("서재 정보 조회 시 사용자 정보가 존재해야 한다.") + public void getUserLibraryWithoutUser() { + // given + Long invalidUserId = -1L; + Book book = bookRepository.save(createBook()); + + // when // then + assertThatThrownBy(() -> userLibraryService.getUserLibrary(invalidUserId, book.getIsbn())) + .isInstanceOf(BusinessException.class) + .hasMessage("사용자를 찾을 수 없습니다."); + } + + @Test + @DisplayName("서재 정보 조회 시 책 정보가 존재해야 한다.") + public void getUserLibraryWithoutBook() { + // given + String invalidIsbn = "0000X"; + User user = userRepository.save(createUser()); + + // when // then + assertThatThrownBy(() -> userLibraryService.getUserLibrary(user.getId(), invalidIsbn)) + .isInstanceOf(BusinessException.class) + .hasMessage("책을 찾을 수 없습니다."); + } + + @Test + @DisplayName("서재 정보 조회 시 서재 정보가 없을 시 빈 응답을 받는다.") + public void getUserLibraryWhenLibraryEmpty() { + // given + User user = userRepository.save(createUser()); + Book book = bookRepository.save(createBook()); + + // when + UserLibraryResponse response = userLibraryService.getUserLibrary(user.getId(), book.getIsbn()); + + // then + assertThat(response).isNotNull(); + assertThat(response.getId()).isNull(); + assertThat(response.getStatus()).isNull(); + assertThat(response.getHasReadingStatus()).isFalse(); + } + + @Test + @DisplayName("사용자가 서재 정보를 생성한다.") + public void createUserLibrary() { + // given + User user = userRepository.save(createUser()); + Book book = bookRepository.save(createBook()); + + UserLibraryCreateServiceRequest request = UserLibraryCreateServiceRequest.builder() + .isbn(book.getIsbn()) + .readingStatus("want") + .build(); + + // when + UserLibraryResponse response = userLibraryService.createUserLibrary(request, user.getId()); + + // then + UserLibrary savedUserLibrary = userLibraryRepository.findAll().get(0); + + assertThat(response.getId()).isEqualTo(savedUserLibrary.getId()); + assertThat(response.getStatus()).isEqualTo(ReadingStatus.WANT.getText()); + } + + @Test + @DisplayName("서재 등록시 사용자 정보가 존재해야 한다.") + public void createUserLibraryWithoutUser() { + // given + Long invalidUserId = -1L; + Book book = bookRepository.save(createBook()); + + UserLibraryCreateServiceRequest request = UserLibraryCreateServiceRequest.builder() + .isbn(book.getIsbn()) + .readingStatus("want") + .build(); + + // when // then + assertThatThrownBy(() -> userLibraryService.createUserLibrary(request, invalidUserId)) + .isInstanceOf(BusinessException.class) + .hasMessage("사용자를 찾을 수 없습니다."); + } + + @Test + @DisplayName("서재 등록 시 책 정보가 존재해야 한다.") + public void createUserLibraryWithoutBook() { + // given + String invalidIsbn = "XXXXXXXXXXX"; + User user = userRepository.save(createUser()); + + UserLibraryCreateServiceRequest request = UserLibraryCreateServiceRequest.builder() + .isbn(invalidIsbn) + .readingStatus("want") + .build(); + + // when // then + assertThatThrownBy(() -> userLibraryService.createUserLibrary(request, user.getId())) + .isInstanceOf(BusinessException.class) + .hasMessage("책을 찾을 수 없습니다."); + } + + @Test + @DisplayName("서재 정보를 수정한다.") + public void editUserLibrary() { + // given + User user = userRepository.save(createUser()); + Book book = bookRepository.save(createBook()); + UserLibrary userLibrary = userLibraryRepository.save(create(user, book)); + + UserLibraryEditServiceRequest request = UserLibraryEditServiceRequest.builder() + .isbn(book.getIsbn()) + .readingStatus("read") + .build(); + + // when + userLibraryService.editUserLibrary(userLibrary.getId(), user.getId(), request); + + // then + Optional savedLibrary = userLibraryRepository.findById(userLibrary.getId()); + + assertThat(savedLibrary).isNotEmpty(); + assertThat(savedLibrary.get().getStatus()).isEqualTo(ReadingStatus.READ); + } + + @Test + @DisplayName("서재 정보 수정 시 사용자 정보가 존재해야 한다.") + public void editUserLibraryWithoutUser() { + // given + Long userLibraryId = 1L; + Long userId = 1L; + Book book = bookRepository.save(createBook()); + + UserLibraryEditServiceRequest request = UserLibraryEditServiceRequest.builder() + .isbn(book.getIsbn()) + .readingStatus("read") + .build(); + + // when // then + assertThatThrownBy(() -> userLibraryService.editUserLibrary(userLibraryId, userId, request)) + .isInstanceOf(BusinessException.class) + .hasMessage("사용자를 찾을 수 없습니다."); + } + + @Test + @DisplayName("서재 정보 수정 시 책 정보가 존재해야 한다.") + public void editUserLibraryWithoutBook() { + // given + Long userLibraryId = 1L; + String bookIsbn = "0000X"; + User user = userRepository.save(createUser()); + + UserLibraryEditServiceRequest request = UserLibraryEditServiceRequest.builder() + .isbn(bookIsbn) + .readingStatus("read") + .build(); + + // when // then + assertThatThrownBy(() -> userLibraryService.editUserLibrary(userLibraryId, user.getId(), request)) + .isInstanceOf(BusinessException.class) + .hasMessage("책을 찾을 수 없습니다."); + } + + @Test + @DisplayName("서재 정보 수정 시 서재 정보가 존재해야 한다.") + public void editUserLibraryWithoutUserLibrary() { + // given + Long userLibraryId = 1L; + User user = userRepository.save(createUser()); + Book book = bookRepository.save(createBook()); + + UserLibraryEditServiceRequest request = UserLibraryEditServiceRequest.builder() + .isbn(book.getIsbn()) + .readingStatus("read") + .build(); + + // when // then + assertThatThrownBy(() -> userLibraryService.editUserLibrary(userLibraryId, user.getId(), request)) + .isInstanceOf(BusinessException.class) + .hasMessage("서재 정보를 찾을 수 없습니다."); + } + + @Test + @DisplayName("서재 정보 수정 시 서재 정보와 사용자 정보는 일치해야 한다.") + public void editUserLibraryInvalidUser() { + // given + User user = userRepository.save(createUser()); + User anotherUser = userRepository.save(createAnotherUser()); + + Book book = bookRepository.save(createBook()); + + UserLibrary userLibrary = userLibraryRepository.save(create(user, book)); + + UserLibraryEditServiceRequest request = UserLibraryEditServiceRequest.builder() + .isbn(book.getIsbn()) + .readingStatus("read") + .build(); + + // when // then + assertThatThrownBy(() -> userLibraryService.editUserLibrary(userLibrary.getId(), anotherUser.getId(), request)) + .isInstanceOf(BusinessException.class) + .hasMessage("권한이 없는 사용자입니다."); + } + + @Test + @DisplayName("서재 정보 수정 시 서재 정보와 도서 정보는 일치해야 한다.") + public void editUserLibraryInvalidBook() { + // given + User user = userRepository.save(createUser()); + + Book book = bookRepository.save(createBookWithIsbn("00001")); + Book anotherBook = bookRepository.save(createBookWithIsbn("00002")); + + UserLibrary userLibrary = userLibraryRepository.save(create(user, book)); + + UserLibraryEditServiceRequest request = UserLibraryEditServiceRequest.builder() + .isbn(anotherBook.getIsbn()) + .readingStatus("read") + .build(); + + // when // then + assertThatThrownBy(() -> userLibraryService.editUserLibrary(userLibrary.getId(), user.getId(), request)) + .isInstanceOf(BusinessException.class) + .hasMessage("올바르지 않은 책 정보 입니다."); + } + + @Test + @DisplayName("서재 정보를 삭제한다.") + public void deleteUserLibrary() { + // given + User user = userRepository.save(createUser()); + Book book = bookRepository.save(createBook()); + UserLibrary userLibrary = userLibraryRepository.save(create(user, book)); + + // when + userLibraryService.deleteUserLibrary(userLibrary.getId(), user.getId(), book.getIsbn()); + + // then + List response = userLibraryRepository.findAll(); + + assertThat(response).isEmpty(); + } + + @Test + @DisplayName("서재 정보 삭제 시 사용자 정보가 존재해야 한다.") + public void deleteUserLibraryWithoutUser() { + // given + Long userLibraryId = 1L; + Long userId = 1L; + Book book = bookRepository.save(createBook()); + + // when // then + assertThatThrownBy(() -> userLibraryService.deleteUserLibrary(userLibraryId, userId, book.getIsbn())) + .isInstanceOf(BusinessException.class) + .hasMessage("사용자를 찾을 수 없습니다."); + } + + @Test + @DisplayName("서재 정보 삭제 시 책 정보가 존재해야 한다.") + public void deleteUserLibraryWithoutBook() { + // given + Long userLibraryId = 1L; + String bookIsbn = "0000X"; + User user = userRepository.save(createUser()); + + // when // then + assertThatThrownBy(() -> userLibraryService.deleteUserLibrary(userLibraryId, user.getId(), bookIsbn)) + .isInstanceOf(BusinessException.class) + .hasMessage("책을 찾을 수 없습니다."); + } + + @Test + @DisplayName("서재 정보 삭제 시 서재 정보가 존재해야 한다.") + public void deleteUserLibraryWithoutUserLibrary() { + // given + Long userLibraryId = 1L; + User user = userRepository.save(createUser()); + Book book = bookRepository.save(createBook()); + + // when // then + assertThatThrownBy(() -> userLibraryService.deleteUserLibrary(userLibraryId, user.getId(), book.getIsbn())) + .isInstanceOf(BusinessException.class) + .hasMessage("서재 정보를 찾을 수 없습니다."); + } + + @Test + @DisplayName("서재 정보 삭제 시 서재 정보와 사용자 정보는 일치해야 한다.") + public void deleteUserLibraryInvalidUser() { + // given + User user = userRepository.save(createUser()); + User anotherUser = userRepository.save(createAnotherUser()); + + Book book = bookRepository.save(createBook()); + + UserLibrary userLibrary = userLibraryRepository.save(create(user, book)); + + // when // then + assertThatThrownBy( + () -> userLibraryService.deleteUserLibrary(userLibrary.getId(), anotherUser.getId(), book.getIsbn())) + .isInstanceOf(BusinessException.class) + .hasMessage("권한이 없는 사용자입니다."); + } + + @Test + @DisplayName("서재 정보 삭제 시 서재 정보와 도서 정보는 일치해야 한다.") + public void deleteUserLibraryInvalidBook() { + // given + User user = userRepository.save(createUser()); + + Book book = bookRepository.save(createBookWithIsbn("00001")); + Book anotherBook = bookRepository.save(createBookWithIsbn("00002")); + + UserLibrary userLibrary = userLibraryRepository.save(create(user, book)); + + // when // then + assertThatThrownBy( + () -> userLibraryService.deleteUserLibrary(userLibrary.getId(), user.getId(), anotherBook.getIsbn())) + .isInstanceOf(BusinessException.class) + .hasMessage("올바르지 않은 책 정보 입니다."); + } + + private static User createUser() { + return User.builder() + .name("user@gmail.com") + .profileImage("image") + .oauthId( + OauthId.builder() + .oauthId("oauthId") + .oauthType(OauthType.KAKAO) + .build() + ) + .build(); + } + + private static User createAnotherUser() { + return User.builder() + .name("another@gmail.com") + .profileImage("image") + .oauthId( + OauthId.builder() + .oauthId("anotherOauthId") + .oauthType(OauthType.KAKAO) + .build() + ) + .build(); + } + + private static Book createBook() { + return Book.builder() + .title("제목") + .content("내용") + .authors("작가") + .isbn("11111") + .publisher("publisher") + .dateTime(LocalDateTime.now()) + .imageUrl("www") + .thumbnail("이미지") + .build(); + } + + private static Book createBookWithIsbn(String isbn) { + return Book.builder() + .title("제목") + .content("내용") + .authors("작가") + .isbn(isbn) + .publisher("publisher") + .dateTime(LocalDateTime.now()) + .imageUrl("www") + .thumbnail("이미지") + .build(); + } + + public static UserLibrary create(User user, Book book) { + return UserLibrary.builder() + .user(user) + .book(book) + .status(ReadingStatus.WANT) + .build(); + } + +}