From d24c6a7a8e9ea37b22551bcfb289d3d9f7152abb Mon Sep 17 00:00:00 2001 From: ChoiDongKuen Date: Sun, 28 Jan 2024 03:12:48 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20(#152)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 회원 리뷰 조회 기능 구현 (#147) * feat: 회원 리뷰 조회 기능 구현 (#147) * test: 회원 리뷰 조회 기능 단위 테스트 (#147) * test: 회원 리뷰 조회 기능 단위 테스트 (service) (#147) * test: 회원 리뷰 조회 기능 통합 테스트 (#147) --- .../user/controller/UserController.java | 7 +++ .../teumteum/user/domain/UserRepository.java | 13 +++-- .../domain/response/UserReviewsResponse.java | 10 ++++ .../teumteum/user/service/UserService.java | 4 ++ .../java/net/teumteum/integration/Api.java | 9 +++- .../integration/UserIntegrationTest.java | 28 ++++++++++- .../user/controller/UserControllerTest.java | 38 ++++++++++++++ .../unit/user/service/UserServiceTest.java | 40 +++++++++++++-- .../net/teumteum/user/domain/UserFixture.java | 6 ++- .../user/domain/UserRepositoryTest.java | 50 +++++++++++++++---- 10 files changed, 182 insertions(+), 23 deletions(-) create mode 100644 src/main/java/net/teumteum/user/domain/response/UserReviewsResponse.java diff --git a/src/main/java/net/teumteum/user/controller/UserController.java b/src/main/java/net/teumteum/user/controller/UserController.java index 76f36738..49bbfb8f 100644 --- a/src/main/java/net/teumteum/user/controller/UserController.java +++ b/src/main/java/net/teumteum/user/controller/UserController.java @@ -16,6 +16,7 @@ import net.teumteum.user.domain.response.UserGetResponse; import net.teumteum.user.domain.response.UserMeGetResponse; import net.teumteum.user.domain.response.UserRegisterResponse; +import net.teumteum.user.domain.response.UserReviewsResponse; import net.teumteum.user.domain.response.UsersGetByIdResponse; import net.teumteum.user.service.UserService; import org.springframework.http.HttpStatus; @@ -115,6 +116,12 @@ public void registerReview( userService.registerReview(meetingId, request); } + @GetMapping("/reviews") + @ResponseStatus(HttpStatus.OK) + public List getUserReviews() { + return userService.getUserReviews(getCurrentUserId()); + } + @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(IllegalArgumentException.class) public ErrorResponse handleIllegalArgumentException(IllegalArgumentException illegalArgumentException) { diff --git a/src/main/java/net/teumteum/user/domain/UserRepository.java b/src/main/java/net/teumteum/user/domain/UserRepository.java index 6257b9a4..d0380f29 100644 --- a/src/main/java/net/teumteum/user/domain/UserRepository.java +++ b/src/main/java/net/teumteum/user/domain/UserRepository.java @@ -1,18 +1,21 @@ package net.teumteum.user.domain; +import java.util.List; +import java.util.Optional; import net.teumteum.core.security.Authenticated; +import net.teumteum.user.domain.response.UserReviewsResponse; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import java.util.Optional; - public interface UserRepository extends JpaRepository { @Query("select u from users u " + - "where u.oauth.authenticated = :authenticated and u.oauth.oauthId = :oAuthId") + "where u.oauth.authenticated = :authenticated and u.oauth.oauthId = :oAuthId") Optional findByAuthenticatedAndOAuthId(@Param("authenticated") Authenticated authenticated, - @Param("oAuthId") String oAuthId); - + @Param("oAuthId") String oAuthId); + @Query("select new net.teumteum.user.domain.response.UserReviewsResponse(r,count(r)) " + + "from users u join u.reviews r where u.id = :userId group by r") + List countUserReviewsByUserId(@Param("userId") Long userId); } diff --git a/src/main/java/net/teumteum/user/domain/response/UserReviewsResponse.java b/src/main/java/net/teumteum/user/domain/response/UserReviewsResponse.java new file mode 100644 index 00000000..7454f524 --- /dev/null +++ b/src/main/java/net/teumteum/user/domain/response/UserReviewsResponse.java @@ -0,0 +1,10 @@ +package net.teumteum.user.domain.response; + +import net.teumteum.user.domain.Review; + +public record UserReviewsResponse( + Review review, + long count +) { + +} diff --git a/src/main/java/net/teumteum/user/service/UserService.java b/src/main/java/net/teumteum/user/service/UserService.java index 9c60ff1a..f690b304 100644 --- a/src/main/java/net/teumteum/user/service/UserService.java +++ b/src/main/java/net/teumteum/user/service/UserService.java @@ -21,6 +21,7 @@ import net.teumteum.user.domain.response.UserGetResponse; import net.teumteum.user.domain.response.UserMeGetResponse; import net.teumteum.user.domain.response.UserRegisterResponse; +import net.teumteum.user.domain.response.UserReviewsResponse; import net.teumteum.user.domain.response.UsersGetByIdResponse; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -112,6 +113,9 @@ public void registerReview(Long meetingId, ReviewRegisterRequest request) { }); } + public List getUserReviews(Long userId) { + return userRepository.countUserReviewsByUserId(userId); + } public FriendsResponse findFriendsByUserId(Long userId) { var user = getUser(userId); diff --git a/src/test/java/net/teumteum/integration/Api.java b/src/test/java/net/teumteum/integration/Api.java index aaa988c2..d679e895 100644 --- a/src/test/java/net/teumteum/integration/Api.java +++ b/src/test/java/net/teumteum/integration/Api.java @@ -4,7 +4,6 @@ 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; @@ -181,4 +180,12 @@ ResponseSpec deleteMeeting(String accessToken, Long meetingId) { .header(HttpHeaders.AUTHORIZATION, accessToken) .exchange(); } + + ResponseSpec getUserReviews(String accessToken) { + return webTestClient + .get() + .uri("/users/reviews") + .header(HttpHeaders.AUTHORIZATION, accessToken) + .exchange(); + } } diff --git a/src/test/java/net/teumteum/integration/UserIntegrationTest.java b/src/test/java/net/teumteum/integration/UserIntegrationTest.java index 04fea7e2..67695200 100644 --- a/src/test/java/net/teumteum/integration/UserIntegrationTest.java +++ b/src/test/java/net/teumteum/integration/UserIntegrationTest.java @@ -6,17 +6,16 @@ import net.teumteum.core.error.ErrorResponse; 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; import net.teumteum.user.domain.response.UserRegisterResponse; +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.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec; @DisplayName("유저 통합테스트의") class UserIntegrationTest extends IntegrationTest { @@ -349,4 +348,29 @@ void Logout_user() { .doesNotThrowAnyException(); } } + + @Nested + @DisplayName("회원 리뷰 조회 API는") + class Get_user_review_api { + + @Test + @DisplayName("userId 유저의 리뷰 정보를 가져온다.") + void Get_user_review() { + // given + var existUser = repository.saveAndGetUser(); + + securityContextSetting.set(existUser.getId()); + + // when + var expected = api.getUserReviews(VALID_TOKEN); + + // then + Assertions.assertThat(expected.expectStatus().isOk() + .expectBodyList(UserReviewsResponse.class) + .returnResult() + .getResponseBody()) + .usingRecursiveComparison() + .isNotNull(); + } + } } 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 71730118..b7b47bd0 100644 --- a/src/test/java/net/teumteum/unit/user/controller/UserControllerTest.java +++ b/src/test/java/net/teumteum/unit/user/controller/UserControllerTest.java @@ -2,11 +2,18 @@ import static net.teumteum.unit.auth.common.SecurityValue.VALID_ACCESS_TOKEN; import static net.teumteum.unit.auth.common.SecurityValue.VALID_REFRESH_TOKEN; +import static net.teumteum.user.domain.Review.별로에요; +import static net.teumteum.user.domain.Review.최고에요; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; 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; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 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; @@ -27,6 +34,7 @@ import net.teumteum.user.domain.request.UserRegisterRequest; import net.teumteum.user.domain.request.UserWithdrawRequest; import net.teumteum.user.domain.response.UserRegisterResponse; +import net.teumteum.user.domain.response.UserReviewsResponse; import net.teumteum.user.service.UserService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -156,4 +164,34 @@ void Register_user_review_with_200_ok() throws Exception { .andExpect(status().isOk()); } } + + + @Nested + @DisplayName("회원 리뷰 조회 API는") + class Get_user_reviews_api_unit { + + @Test + @DisplayName("로그인한 회원 id 에 해당하는 회원 리뷰와 200 OK을 반환한다.") + void Get_user_reviews_with_200_ok() throws Exception { + // given + var userId = 1L; + + given(securityService.getCurrentUserId()).willReturn(userId); + + given(userService.getUserReviews(anyLong())) + .willReturn(List.of(new UserReviewsResponse(별로에요, 2L), + new UserReviewsResponse(최고에요, 3L))); + + // when & then + mockMvc.perform(get("/users/reviews") + .with(csrf()) + .header(AUTHORIZATION, VALID_ACCESS_TOKEN)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", notNullValue())) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[0].count", is(2))) + .andExpect(jsonPath("$[0].review").value("별로에요")); + } + } } 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 f3b75579..5733b2de 100644 --- a/src/test/java/net/teumteum/unit/user/service/UserServiceTest.java +++ b/src/test/java/net/teumteum/unit/user/service/UserServiceTest.java @@ -2,6 +2,8 @@ import static net.teumteum.unit.auth.common.SecurityValue.VALID_ACCESS_TOKEN; import static net.teumteum.unit.auth.common.SecurityValue.VALID_REFRESH_TOKEN; +import static net.teumteum.user.domain.Review.별로에요; +import static net.teumteum.user.domain.Review.최고에요; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; @@ -27,6 +29,7 @@ import net.teumteum.user.domain.request.UserRegisterRequest; import net.teumteum.user.domain.request.UserWithdrawRequest; import net.teumteum.user.domain.response.UserRegisterResponse; +import net.teumteum.user.domain.response.UserReviewsResponse; import net.teumteum.user.service.UserService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -157,13 +160,13 @@ void Register_user_review_with_200_ok() { userService.registerReview(meetingId, reviewRegisterRequest); // then - verify(meetingConnector,times(1)).existById(anyLong()); - verify(userRepository,times(3)).findById(anyLong()); + verify(meetingConnector, times(1)).existById(anyLong()); + verify(userRepository, times(3)).findById(anyLong()); } @Test @DisplayName("meeting id 에 해당하는 meeting 이 존재하지 않는 경우, 400 Bad Request 와 함께 리뷰 등록을 실패한다.") - void Return_400_bad_request_if_meeting_is_not_exist(){ + void Return_400_bad_request_if_meeting_is_not_exist() { // given ReviewRegisterRequest reviewRegisterRequest = RequestFixture.reviewRegisterRequest(); @@ -172,9 +175,38 @@ void Return_400_bad_request_if_meeting_is_not_exist(){ given(meetingConnector.existById(anyLong())) .willReturn(false); // when & then - assertThatThrownBy( () -> userService.registerReview(meetingId,reviewRegisterRequest)) + assertThatThrownBy(() -> userService.registerReview(meetingId, reviewRegisterRequest)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("meetingId에 해당하는 meeting을 찾을 수 없습니다. \"" + meetingId + "\""); } } + + @Nested + @DisplayName("회원 리뷰 조회 API는") + class Get_user_reviews_api_unit { + + @Test + @DisplayName("로그인한 회원의 리뷰 리스트로 200 OK 응답한다.") + void Return_user_reviews_with_200_ok() { + // given + var userId = 1L; + + var response = List.of(new UserReviewsResponse(최고에요, 2L) + , new UserReviewsResponse(별로에요, 3L)); + + given(userRepository.countUserReviewsByUserId(anyLong())).willReturn(response); + + // when + var result = userService.getUserReviews(userId); + + // then + assertThat(result).hasSize(2); + assertThat(result.get(0).review()).isEqualTo(최고에요); + assertThat(result.get(0).count()).isEqualTo(2L); + assertThat(result.get(1).review()).isEqualTo(별로에요); + assertThat(result.get(1).count()).isEqualTo(3L); + + verify(userRepository, times(1)).countUserReviewsByUserId(anyLong()); + } + } } diff --git a/src/test/java/net/teumteum/user/domain/UserFixture.java b/src/test/java/net/teumteum/user/domain/UserFixture.java index da0e9278..43616597 100644 --- a/src/test/java/net/teumteum/user/domain/UserFixture.java +++ b/src/test/java/net/teumteum/user/domain/UserFixture.java @@ -1,7 +1,9 @@ package net.teumteum.user.domain; import static net.teumteum.core.security.Authenticated.네이버; -import static net.teumteum.user.domain.Review.*; +import static net.teumteum.user.domain.Review.별로에요; +import static net.teumteum.user.domain.Review.좋아요; +import static net.teumteum.user.domain.Review.최고에요; import java.util.List; import java.util.Set; @@ -87,7 +89,7 @@ public static class UserBuilder { @Builder.Default private Terms terms = new Terms(true, true); @Builder.Default - private List reviews = List.of(최고에요); + private List reviews = List.of(최고에요, 최고에요, 최고에요, 별로에요, 좋아요, 좋아요); } } diff --git a/src/test/java/net/teumteum/user/domain/UserRepositoryTest.java b/src/test/java/net/teumteum/user/domain/UserRepositoryTest.java index f786240b..59d607b6 100644 --- a/src/test/java/net/teumteum/user/domain/UserRepositoryTest.java +++ b/src/test/java/net/teumteum/user/domain/UserRepositoryTest.java @@ -1,7 +1,13 @@ package net.teumteum.user.domain; +import static net.teumteum.user.domain.Review.별로에요; +import static net.teumteum.user.domain.Review.좋아요; +import static net.teumteum.user.domain.Review.최고에요; + import jakarta.persistence.EntityManager; +import java.util.Optional; import net.teumteum.core.config.AppConfig; +import net.teumteum.user.domain.response.UserReviewsResponse; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -10,8 +16,6 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; -import java.util.Optional; - @DataJpaTest @Import(AppConfig.class) @DisplayName("UserRepository 클래스의") @@ -49,22 +53,50 @@ class FindById_method { @DisplayName("저장된 유저의 id로 조회하면, 유저를 반환한다.") void Find_success_if_exists_user_id_input() { // given - var id = 1L; - var existsUser = UserFixture.getUserWithId(id); + var existsUser = UserFixture.getNullIdUser(); userRepository.saveAndFlush(existsUser); entityManager.clear(); // when - var result = userRepository.findById(id); + var result = userRepository.findById(existsUser.getId()); // then Assertions.assertThat(result) - .isPresent() - .usingRecursiveComparison() - .ignoringFields("value.createdAt", "value.updatedAt") - .isEqualTo(Optional.of(existsUser)); + .usingRecursiveComparison() + .ignoringFields("value.createdAt", "value.updatedAt") + .isEqualTo(Optional.ofNullable(existsUser)); } } + + @Nested + @DisplayName("countUserReviewsByUserId 메소드는") + class CountUserReviewsByUserId_method { + + @Test + @DisplayName("저장된 유저의 id 을 이용해서 유저의 리뷰 갯수를 조회하면, UserReviewResponse 을 반환한다.") + void Count_user_reviews_by_user_id() { + // given + var id = 1L; + var existUser = UserFixture.getUserWithId(id); + + userRepository.saveAndFlush(existUser); + entityManager.clear(); + + // when + var result = userRepository.countUserReviewsByUserId(id); + + // then + Assertions.assertThat(result) + .isNotEmpty() + .hasSize(3) + .extracting(UserReviewsResponse::review, UserReviewsResponse::count) + .contains( + Assertions.tuple(최고에요, 3L), + Assertions.tuple(별로에요, 1L), + Assertions.tuple(좋아요, 2L)); + + } + } }