From bbb5e73c1f7414d8550f620f380cb1e936eb7420 Mon Sep 17 00:00:00 2001 From: ChoiDongKuen Date: Tue, 30 Jan 2024 01:22:55 +0900 Subject: [PATCH] release: 0.2.5 (#164) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 리뷰 등록시 회원 매너 온도 반영 기능 추가 및 리뷰 등록 리팩토링 (#158) * refactor: 회원 리뷰 등록 API 리팩토링 (#157) * feat: 회원 리뷰 등록시, 온도 반영 및 리뷰 피드백 반영 (#157) * feat: User 엔티티 메소드 추가 (#157) * feat: Review enum 필드 추가 (#157) * test: 리뷰 등록시 온도 업데이트에 대한 단위 테스트 (#157) * test: 리뷰 등록 기능 통합 테스트 (#157) * refactor: 회원 리뷰 등록 메소드 수정 (#157) * test: 불필요한 테스트 제거 및 CI 오류 수정 (#157) * test: MeetingRepository 테스트에서 시간과 id비교 비활서화 * refactor: 회원 탈퇴 URI 변경 (#162) * refactor: 회원 탈퇴 URI 수정 (#161) * test: 회원 탈퇴 URI 수정에 대한 테스트 수정 (#161) * fix: CI 에러 수정 (#161) * fix: 필드 값 비교를 위해 deprecated 된 메소드을 대체 (#161) * fix: 필드 값 비교를 위해 deprecated 된 메소드을 대체 (#161) --------- Co-authored-by: devxb --- .../user/controller/UserController.java | 4 +- .../java/net/teumteum/user/domain/Review.java | 13 +++- .../java/net/teumteum/user/domain/User.java | 10 ++- .../domain/request/ReviewRegisterRequest.java | 2 +- .../teumteum/user/service/UserService.java | 22 ++++-- .../java/net/teumteum/integration/Api.java | 13 +++- .../net/teumteum/integration/Repository.java | 8 ++ .../teumteum/integration/RequestFixture.java | 15 ++++ .../integration/UserIntegrationTest.java | 78 ++++++++++++++----- .../meeting/domain/MeetingRepositoryTest.java | 1 + .../user/controller/UserControllerTest.java | 28 ++++++- .../unit/user/service/UserServiceTest.java | 28 ++++++- .../net/teumteum/user/domain/UserFixture.java | 4 +- 13 files changed, 184 insertions(+), 42 deletions(-) diff --git a/src/main/java/net/teumteum/user/controller/UserController.java b/src/main/java/net/teumteum/user/controller/UserController.java index 49bbfb8f..6523683f 100644 --- a/src/main/java/net/teumteum/user/controller/UserController.java +++ b/src/main/java/net/teumteum/user/controller/UserController.java @@ -89,7 +89,7 @@ public InterestQuestionResponse getInterestQuestion(@RequestParam("user-id") Lis return userService.getInterestQuestionByUserIds(userIds, balance); } - @PostMapping("/withdraw") + @PostMapping("/withdraws") @ResponseStatus(HttpStatus.OK) public void withdraw(@Valid @RequestBody UserWithdrawRequest request) { userService.withdraw(request, getCurrentUserId()); @@ -113,7 +113,7 @@ public void registerReview( @RequestParam Long meetingId, @Valid @RequestBody ReviewRegisterRequest request ) { - userService.registerReview(meetingId, request); + userService.registerReview(meetingId, getCurrentUserId(), request); } @GetMapping("/reviews") diff --git a/src/main/java/net/teumteum/user/domain/Review.java b/src/main/java/net/teumteum/user/domain/Review.java index cb01b81e..a04445aa 100644 --- a/src/main/java/net/teumteum/user/domain/Review.java +++ b/src/main/java/net/teumteum/user/domain/Review.java @@ -1,7 +1,14 @@ package net.teumteum.user.domain; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor public enum Review { - 별로에요, - 좋아요, - 최고에요 + 별로에요(-1), + 좋아요(1), + 최고에요(2); + + private final int score; } diff --git a/src/main/java/net/teumteum/user/domain/User.java b/src/main/java/net/teumteum/user/domain/User.java index bdec964c..c491a028 100644 --- a/src/main/java/net/teumteum/user/domain/User.java +++ b/src/main/java/net/teumteum/user/domain/User.java @@ -122,9 +122,11 @@ public void addFriend(User user) { friends.add(user.id); } - public void addReview(Review review) { - List newReviews = new ArrayList<>(reviews); - newReviews.add(review); - reviews = newReviews; + public void registerReview(Review review) { + reviews.add(review); + } + + public void updateMannerTemperature(Review review) { + mannerTemperature += review.getScore(); } } diff --git a/src/main/java/net/teumteum/user/domain/request/ReviewRegisterRequest.java b/src/main/java/net/teumteum/user/domain/request/ReviewRegisterRequest.java index 60603575..b5cc4bbb 100644 --- a/src/main/java/net/teumteum/user/domain/request/ReviewRegisterRequest.java +++ b/src/main/java/net/teumteum/user/domain/request/ReviewRegisterRequest.java @@ -8,7 +8,7 @@ public record ReviewRegisterRequest( @Valid - @Size(min = 3, max = 6) + @Size(min = 2, max = 5) List reviews ) { diff --git a/src/main/java/net/teumteum/user/service/UserService.java b/src/main/java/net/teumteum/user/service/UserService.java index f690b304..38f9ca02 100644 --- a/src/main/java/net/teumteum/user/service/UserService.java +++ b/src/main/java/net/teumteum/user/service/UserService.java @@ -103,13 +103,15 @@ public void logout(Long userId) { @Transactional - public void registerReview(Long meetingId, ReviewRegisterRequest request) { + public void registerReview(Long meetingId, Long currentUserId, ReviewRegisterRequest request) { checkMeetingExistence(meetingId); + checkUserNotRegisterSelfReview(request, currentUserId); request.reviews() .forEach(userReview -> { User user = getUser(userReview.id()); - user.addReview(userReview.review()); + user.registerReview(userReview.review()); + user.updateMannerTemperature(userReview.review()); }); } @@ -148,8 +150,18 @@ private void checkUserExistence(Authenticated authenticated, String oauthId) { } private void checkMeetingExistence(Long meetingId) { - if (!meetingConnector.existById(meetingId)) { - throw new IllegalArgumentException("meetingId에 해당하는 meeting을 찾을 수 없습니다. \"" + meetingId + "\""); - } + Assert.isTrue(meetingConnector.existById(meetingId), + () -> { + throw new IllegalArgumentException("meetingId에 해당하는 meeting을 찾을 수 없습니다. \"" + meetingId + "\""); + } + ); + } + + private void checkUserNotRegisterSelfReview(ReviewRegisterRequest request, Long currentUserId) { + Assert.isTrue(request.reviews().stream().noneMatch(review -> review.id().equals(currentUserId)), + () -> { + throw new IllegalArgumentException("나의 리뷰에 대한 리뷰를 작성할 수 없습니다."); + } + ); } } diff --git a/src/test/java/net/teumteum/integration/Api.java b/src/test/java/net/teumteum/integration/Api.java index d679e895..1fd41c22 100644 --- a/src/test/java/net/teumteum/integration/Api.java +++ b/src/test/java/net/teumteum/integration/Api.java @@ -4,6 +4,7 @@ import net.teumteum.meeting.config.PageableHandlerMethodArgumentResolver; import net.teumteum.meeting.domain.Topic; import net.teumteum.teum_teum.domain.request.UserLocationRequest; +import net.teumteum.user.domain.request.ReviewRegisterRequest; import net.teumteum.user.domain.request.UserRegisterRequest; import net.teumteum.user.domain.request.UserUpdateRequest; import net.teumteum.user.domain.request.UserWithdrawRequest; @@ -141,7 +142,7 @@ ResponseSpec reissueJwt(String accessToken, String refreshToken) { ResponseSpec withdrawUser(String accessToken, UserWithdrawRequest request) { return webTestClient .post() - .uri("/users/withdraw") + .uri("/users/withdraws") .header(HttpHeaders.AUTHORIZATION, accessToken) .bodyValue(request) .exchange(); @@ -188,4 +189,14 @@ ResponseSpec getUserReviews(String accessToken) { .header(HttpHeaders.AUTHORIZATION, accessToken) .exchange(); } + + ResponseSpec registerUserReview(String accessToken, Long meetingId, ReviewRegisterRequest request) { + String uri = "/users/reviews?meetingId=" + meetingId; + return webTestClient + .post() + .uri(uri) + .header(HttpHeaders.AUTHORIZATION, accessToken) + .bodyValue(request) + .exchange(); + } } diff --git a/src/test/java/net/teumteum/integration/Repository.java b/src/test/java/net/teumteum/integration/Repository.java index 87f43e8c..7d829ee9 100644 --- a/src/test/java/net/teumteum/integration/Repository.java +++ b/src/test/java/net/teumteum/integration/Repository.java @@ -2,6 +2,7 @@ import java.util.List; +import java.util.stream.IntStream; import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import net.teumteum.core.config.AppConfig; @@ -40,6 +41,13 @@ public User saveAndGetUser(Long id) { return userRepository.saveAndFlush(user); } + public List saveAndGetUsers(int size) { + return Stream.generate(UserFixture::getNullIdUser) + .limit(size) + .map(userRepository::saveAndFlush) + .toList(); + } + List getAllUser() { return userRepository.findAll(); } diff --git a/src/test/java/net/teumteum/integration/RequestFixture.java b/src/test/java/net/teumteum/integration/RequestFixture.java index 442560bd..7c6e5dc7 100644 --- a/src/test/java/net/teumteum/integration/RequestFixture.java +++ b/src/test/java/net/teumteum/integration/RequestFixture.java @@ -5,8 +5,10 @@ import static net.teumteum.user.domain.Review.최고에요; import java.util.List; +import java.util.Random; import java.util.UUID; import net.teumteum.core.security.Authenticated; +import net.teumteum.user.domain.Review; import net.teumteum.user.domain.User; import net.teumteum.user.domain.request.ReviewRegisterRequest; import net.teumteum.user.domain.request.ReviewRegisterRequest.UserReviewRegisterRequest; @@ -62,11 +64,24 @@ public static ReviewRegisterRequest reviewRegisterRequest() { return new ReviewRegisterRequest(userReviewRegisterRequests()); } + public static ReviewRegisterRequest reviewRegisterRequest(List users) { + return new ReviewRegisterRequest(userReviewRegisterRequests(users)); + } + private static List userReviewRegisterRequests() { return List.of(new UserReviewRegisterRequest(1L, 별로에요), new UserReviewRegisterRequest(2L, 최고에요), new UserReviewRegisterRequest(3L, 좋아요)); } + private static List userReviewRegisterRequests(List users) { + Review[] reviews = Review.values(); + int length = reviews.length; + + return users.stream() + .map(user -> new UserReviewRegisterRequest(user.getId(), reviews[new Random().nextInt(length)])) + .toList(); + } + private static Job job(User user) { return new Job(user.getJob().getName(), user.getJob().getJobClass(), diff --git a/src/test/java/net/teumteum/integration/UserIntegrationTest.java b/src/test/java/net/teumteum/integration/UserIntegrationTest.java index 67695200..916bb80d 100644 --- a/src/test/java/net/teumteum/integration/UserIntegrationTest.java +++ b/src/test/java/net/teumteum/integration/UserIntegrationTest.java @@ -4,8 +4,10 @@ import java.util.List; import net.teumteum.core.error.ErrorResponse; +import net.teumteum.meeting.domain.Meeting; import net.teumteum.user.domain.User; import net.teumteum.user.domain.UserFixture; +import net.teumteum.user.domain.request.ReviewRegisterRequest; import net.teumteum.user.domain.response.FriendsResponse; import net.teumteum.user.domain.response.UserGetResponse; import net.teumteum.user.domain.response.UserMeGetResponse; @@ -13,6 +15,7 @@ import net.teumteum.user.domain.response.UserReviewsResponse; import net.teumteum.user.domain.response.UsersGetByIdResponse; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -253,25 +256,6 @@ void Withdraw_user_info_api() { assertThatCode(() -> api.withdrawUser(VALID_TOKEN, request)) .doesNotThrowAnyException(); } - - @Test - @DisplayName("해당 회원이 존재하지 않으면, 500 에러를 반환한다.") - void Return_500_error_if_user_not_exist() { - // given - repository.clearUserRepository(); - - var request = RequestFixture.userWithdrawRequest(List.of("쓰지 않는 앱이에요", "오류가 생겨서 쓸 수 없어요")); - - // when - var result = api.withdrawUser(VALID_TOKEN, request); - - // then - Assertions.assertThat(result.expectStatus().is5xxServerError() - .expectBody(ErrorResponse.class) - .returnResult() - .getResponseBody()) - .usingRecursiveComparison().isNull(); - } } @Nested @@ -341,6 +325,9 @@ class Logout_user_api { void Logout_user() { // given var existUser = repository.saveAndGetUser(); + + securityContextSetting.set(existUser.getId()); + redisRepository.saveRedisDataWithExpiration(String.valueOf(existUser.getId()), VALID_TOKEN, DURATION); // when & then @@ -351,11 +338,11 @@ void Logout_user() { @Nested @DisplayName("회원 리뷰 조회 API는") - class Get_user_review_api { + class Get_user_reviews_api { @Test @DisplayName("userId 유저의 리뷰 정보를 가져온다.") - void Get_user_review() { + void Get_user_reviews() { // given var existUser = repository.saveAndGetUser(); @@ -373,4 +360,53 @@ void Get_user_review() { .isNotNull(); } } + + @Nested + @DisplayName("회원 리뷰 등록 API는") + class Register_user_review_api { + + User existUser; + + List users; + + ReviewRegisterRequest request; + + Meeting meeting; + + @BeforeEach + void setUp() { + existUser = repository.saveAndGetUser(); + users = repository.saveAndGetUsers(3); + request = RequestFixture.reviewRegisterRequest(users); + meeting = repository.saveAndGetOpenMeetings(1).get(0); + } + + @Test + @DisplayName("회원 리뷰 등록 요청이 들어오면 리뷰를 등록하고, 200 OK 을 반환한다.") + void Return_200_OK_with_success_register_user_review() { + // given + securityContextSetting.set(existUser.getId()); + + // when + var expected = api.registerUserReview(VALID_TOKEN, meeting.getId(), request); + + // then + Assertions.assertThat(expected.expectStatus().isOk()); + } + + @Test + @DisplayName("현재 로그인한 회원의 id 가 리뷰 등록 요청에 포함된다면, 회원 리뷰 등록을 실패하고 400 bad request 을 반환한다.") + void Return_400_bad_request_if_current_user_id_in_request() { + // given + securityContextSetting.set(users.get(0).getId()); + + // when + var expected = api.registerUserReview(VALID_TOKEN, meeting.getId(), request); + + // then + Assertions.assertThat(expected.expectStatus().isBadRequest() + .expectBody(ErrorResponse.class) + .returnResult().getResponseBody()); + } + } } diff --git a/src/test/java/net/teumteum/meeting/domain/MeetingRepositoryTest.java b/src/test/java/net/teumteum/meeting/domain/MeetingRepositoryTest.java index 541f52f3..18d30411 100644 --- a/src/test/java/net/teumteum/meeting/domain/MeetingRepositoryTest.java +++ b/src/test/java/net/teumteum/meeting/domain/MeetingRepositoryTest.java @@ -268,6 +268,7 @@ void Return_meetings_between_start_time_and_end_time() { // then Assertions.assertThat(result) .usingRecursiveComparison() + .ignoringFields("createdAt", "updatedAt", "id", "promiseDateTime") .isEqualTo(expected); } } diff --git a/src/test/java/net/teumteum/unit/user/controller/UserControllerTest.java b/src/test/java/net/teumteum/unit/user/controller/UserControllerTest.java index b7b47bd0..5870936e 100644 --- a/src/test/java/net/teumteum/unit/user/controller/UserControllerTest.java +++ b/src/test/java/net/teumteum/unit/user/controller/UserControllerTest.java @@ -7,9 +7,11 @@ import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doThrow; import static org.springframework.http.HttpHeaders.AUTHORIZATION; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; @@ -133,7 +135,7 @@ void Withdraw_user_with_200_ok() throws Exception { = RequestFixture.userWithdrawRequest(List.of("쓰지 않는 앱이에요", "오류가 생겨서 쓸 수 없어요")); // when & then - mockMvc.perform(post("/users/withdraw") + mockMvc.perform(post("/users/withdraws") .content(objectMapper.writeValueAsString(request)) .contentType(APPLICATION_JSON) .with(csrf()) @@ -163,6 +165,30 @@ void Register_user_review_with_200_ok() throws Exception { .andDo(print()) .andExpect(status().isOk()); } + + @Test + @DisplayName("현재 로그인한 회원의 id 가 리뷰 등록 요청에 포함된다면, 회원 리뷰 등록을 실패하고 400 bad request을 반환한다.") + void Register_reviews_with_400_bad_request() throws Exception { + // given + ReviewRegisterRequest reviewRegisterRequest = RequestFixture.reviewRegisterRequest(); + + String errorMessage = "나의 리뷰에 대한 리뷰를 작성할 수 없습니다."; + + doThrow(new IllegalArgumentException(errorMessage)) + .when(userService) + .registerReview(anyLong(), anyLong(), any(ReviewRegisterRequest.class)); + + // when & then + mockMvc.perform(post("/users/reviews") + .param("meetingId", String.valueOf(1L)) + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(reviewRegisterRequest)) + .with(csrf()) + .header(AUTHORIZATION, VALID_ACCESS_TOKEN)) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(result -> assertEquals(errorMessage, result.getResolvedException().getMessage())); + } } diff --git a/src/test/java/net/teumteum/unit/user/service/UserServiceTest.java b/src/test/java/net/teumteum/unit/user/service/UserServiceTest.java index 5733b2de..7a7525b0 100644 --- a/src/test/java/net/teumteum/unit/user/service/UserServiceTest.java +++ b/src/test/java/net/teumteum/unit/user/service/UserServiceTest.java @@ -148,7 +148,9 @@ void Register_user_review_with_200_ok() { Long meetingId = 1L; - Long userId = 1L; + Long userId = 10L; + + Long currentUserId = 20L; given(meetingConnector.existById(anyLong())) .willReturn(true); @@ -157,13 +159,32 @@ void Register_user_review_with_200_ok() { .willReturn(Optional.of(UserFixture.getUserWithId(userId++))); // when - userService.registerReview(meetingId, reviewRegisterRequest); + userService.registerReview(meetingId, currentUserId, reviewRegisterRequest); // then verify(meetingConnector, times(1)).existById(anyLong()); verify(userRepository, times(3)).findById(anyLong()); } + @Test + @DisplayName("회원 id 가 리뷰 정보 요청에 포함되면, 400 Bad Request 와 함께 리뷰 등록을 실패한다.") + void Return_400_bad_request_if_current_user_id_in_request() { + // given + ReviewRegisterRequest reviewRegisterRequest = RequestFixture.reviewRegisterRequest(); + + Long meetingId = 1L; + + Long currentUserId = reviewRegisterRequest.reviews().get(0).id(); + + given(meetingConnector.existById(anyLong())) + .willReturn(true); + + // when & then + assertThatThrownBy(() -> userService.registerReview(meetingId, currentUserId, reviewRegisterRequest)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("나의 리뷰에 대한 리뷰를 작성할 수 없습니다."); + } + @Test @DisplayName("meeting id 에 해당하는 meeting 이 존재하지 않는 경우, 400 Bad Request 와 함께 리뷰 등록을 실패한다.") void Return_400_bad_request_if_meeting_is_not_exist() { @@ -171,11 +192,12 @@ void Return_400_bad_request_if_meeting_is_not_exist() { ReviewRegisterRequest reviewRegisterRequest = RequestFixture.reviewRegisterRequest(); Long meetingId = 1L; + Long currentUserId = 1L; given(meetingConnector.existById(anyLong())) .willReturn(false); // when & then - assertThatThrownBy(() -> userService.registerReview(meetingId, reviewRegisterRequest)) + assertThatThrownBy(() -> userService.registerReview(meetingId, currentUserId, reviewRegisterRequest)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("meetingId에 해당하는 meeting을 찾을 수 없습니다. \"" + meetingId + "\""); } diff --git a/src/test/java/net/teumteum/user/domain/UserFixture.java b/src/test/java/net/teumteum/user/domain/UserFixture.java index 43616597..2b52ff27 100644 --- a/src/test/java/net/teumteum/user/domain/UserFixture.java +++ b/src/test/java/net/teumteum/user/domain/UserFixture.java @@ -5,6 +5,8 @@ import static net.teumteum.user.domain.Review.좋아요; import static net.teumteum.user.domain.Review.최고에요; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Set; import java.util.UUID; @@ -89,7 +91,7 @@ public static class UserBuilder { @Builder.Default private Terms terms = new Terms(true, true); @Builder.Default - private List reviews = List.of(최고에요, 최고에요, 최고에요, 별로에요, 좋아요, 좋아요); + private List reviews = new ArrayList<>(Arrays.asList(최고에요, 최고에요, 최고에요, 별로에요, 좋아요, 좋아요)); } }