diff --git a/src/main/java/net/teumteum/core/security/SecurityConfig.java b/src/main/java/net/teumteum/core/security/SecurityConfig.java index d903204..066c5da 100644 --- a/src/main/java/net/teumteum/core/security/SecurityConfig.java +++ b/src/main/java/net/teumteum/core/security/SecurityConfig.java @@ -66,7 +66,7 @@ CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); - config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "TRACE")); config.addExposedHeader("Authorization"); config.addExposedHeader("Authorization-refresh"); config.setAllowCredentials(true); 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 b5cc4bb..e94d89c 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 = 2, max = 5) + @Size(min = 1, 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 76de43d..557daad 100644 --- a/src/main/java/net/teumteum/user/service/UserService.java +++ b/src/main/java/net/teumteum/user/service/UserService.java @@ -1,11 +1,13 @@ package net.teumteum.user.service; +import java.time.LocalDateTime; import java.util.List; import lombok.RequiredArgsConstructor; import net.teumteum.core.security.Authenticated; import net.teumteum.core.security.service.JwtService; import net.teumteum.core.security.service.RedisService; import net.teumteum.core.security.service.SecurityService; +import net.teumteum.meeting.domain.Meeting; import net.teumteum.meeting.domain.MeetingConnector; import net.teumteum.user.domain.BalanceGameType; import net.teumteum.user.domain.InterestQuestion; @@ -109,7 +111,10 @@ public void logout(Long userId) { @Transactional public void registerReview(Long meetingId, Long currentUserId, ReviewRegisterRequest request) { - checkMeetingExistence(meetingId); + var meeting = getMeeting(meetingId); + + checkMeetingIsClosed(meeting); + checkUserParticipationInMeeting(meeting, currentUserId); checkUserNotRegisterSelfReview(request, currentUserId); request.reviews() @@ -157,12 +162,9 @@ private void checkUserExistence(Authenticated authenticated, String oauthId) { ); } - private void checkMeetingExistence(Long meetingId) { - Assert.isTrue(meetingConnector.existById(meetingId), - () -> { - throw new IllegalArgumentException("meetingId에 해당하는 meeting을 찾을 수 없습니다. \"" + meetingId + "\""); - } - ); + private Meeting getMeeting(Long meetingId) { + return meetingConnector.findById(meetingId) + .orElseThrow(() -> new IllegalArgumentException("meetingId에 해당하는 모임을 찾을 수 없습니다. \"" + meetingId + "\"")); } private void checkUserNotRegisterSelfReview(ReviewRegisterRequest request, Long currentUserId) { @@ -172,4 +174,16 @@ private void checkUserNotRegisterSelfReview(ReviewRegisterRequest request, Long } ); } + + private void checkUserParticipationInMeeting(Meeting meeting, Long userId) { + if (!meeting.getParticipantUserIds().contains(userId)) { + throw new IllegalArgumentException("모임에 참여하지 않은 회원입니다."); + } + } + + private void checkMeetingIsClosed(Meeting meeting) { + if (!LocalDateTime.now().isAfter(meeting.getPromiseDateTime())) { + throw new IllegalArgumentException("해당 모임은 아직 종료되지 않았습니다."); + } + } } diff --git a/src/test/java/net/teumteum/integration/Repository.java b/src/test/java/net/teumteum/integration/Repository.java index 7d829ee..780398d 100644 --- a/src/test/java/net/teumteum/integration/Repository.java +++ b/src/test/java/net/teumteum/integration/Repository.java @@ -2,7 +2,6 @@ import java.util.List; -import java.util.stream.IntStream; import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import net.teumteum.core.config.AppConfig; @@ -139,6 +138,11 @@ List saveAndGetCloseMeetingsByParticipantUserId(int size, Long particip return meetingRepository.saveAllAndFlush(meetings); } + Meeting saveAndGetCloseMeetingByParticipantUserIds(List participantUserIds) { + var meeting = MeetingFixture.getCloseMeetingWithParticipantIds(participantUserIds); + return meetingRepository.save(meeting); + } + List saveAndGetOpenMeetings(int size) { var meetings = Stream.generate(MeetingFixture::getOpenMeeting) .limit(size) diff --git a/src/test/java/net/teumteum/integration/UserIntegrationTest.java b/src/test/java/net/teumteum/integration/UserIntegrationTest.java index 8b1503a..40ddd08 100644 --- a/src/test/java/net/teumteum/integration/UserIntegrationTest.java +++ b/src/test/java/net/teumteum/integration/UserIntegrationTest.java @@ -4,10 +4,8 @@ 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; @@ -15,7 +13,6 @@ 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; @@ -341,48 +338,101 @@ void Get_user_reviews() { @DisplayName("회원 리뷰 등록 API는") class Register_user_review_api { - User existUser; + @Test + @DisplayName("정상적인 요청이 오는 경우, 해당 회원의 리뷰 등록과 함께 200 OK 을 반환한다.") + void Return_200_OK_and_register_review_if_request_is_valid() { + // given + var user = repository.saveAndGetUser(); + var participant1 = repository.saveAndGetUser(); + var participant2 = repository.saveAndGetUser(); - List users; + var closedMeeting = repository.saveAndGetCloseMeetingByParticipantUserIds( + List.of(user.getId(), participant1.getId(), participant2.getId())); - ReviewRegisterRequest request; + var request = RequestFixture.reviewRegisterRequest(List.of(participant1, participant2)); - Meeting meeting; + securityContextSetting.set(user.getId()); - @BeforeEach - void setUp() { - existUser = repository.saveAndGetUser(); - users = repository.saveAndGetUsers(3); - request = RequestFixture.reviewRegisterRequest(users); - meeting = repository.saveAndGetOpenMeetings(1).get(0); + // when + var expected = api.registerUserReview(VALID_TOKEN, closedMeeting.getId(), request); + + // then + Assertions.assertThat(expected.expectStatus().isOk()); } @Test - @DisplayName("회원 리뷰 등록 요청이 들어오면 리뷰를 등록하고, 200 OK 을 반환한다.") - void Return_200_OK_with_success_register_user_review() { + @DisplayName("meeting id 에 해당하는 meeting 이 아직 종료되지 않았다면, 400 Bad Request 와 함께 리뷰 등록을 실패한다.") + void Return_400_bad_request_if_meeting_is_not_closed() { // given - securityContextSetting.set(existUser.getId()); + var user = repository.saveAndGetUser(); + var participant = repository.saveAndGetUser(); + + var openMeeting = repository.saveAndGetOpenMeeting(); + var request = RequestFixture.reviewRegisterRequest(List.of(participant)); + + securityContextSetting.set(user.getId()); // when - var expected = api.registerUserReview(VALID_TOKEN, meeting.getId(), request); + var expected = api.registerUserReview(VALID_TOKEN, openMeeting.getId(), request); // then - Assertions.assertThat(expected.expectStatus().isOk()); + Assertions.assertThat(expected.expectStatus().isBadRequest() + .expectBody(ErrorResponse.class) + .returnResult().getResponseBody()) + .extracting(ErrorResponse::getMessage) + .isEqualTo("해당 모임은 아직 종료되지 않았습니다."); } @Test @DisplayName("현재 로그인한 회원의 id 가 리뷰 등록 요청에 포함된다면, 회원 리뷰 등록을 실패하고 400 bad request 을 반환한다.") void Return_400_bad_request_if_current_user_id_in_request() { // given - securityContextSetting.set(users.get(0).getId()); + var user = repository.saveAndGetUser(); + var participant = repository.saveAndGetUser(); + + var closedMeeting = repository.saveAndGetCloseMeetingByParticipantUserIds( + List.of(user.getId(), participant.getId())); + + var request = RequestFixture.reviewRegisterRequest(List.of(user, participant)); + + securityContextSetting.set(user.getId()); + + // when + var expected = api.registerUserReview(VALID_TOKEN, closedMeeting.getId(), request); + + // then + Assertions.assertThat(expected.expectStatus().isBadRequest() + .expectBody(ErrorResponse.class) + .returnResult().getResponseBody()) + .extracting(ErrorResponse::getMessage) + .isEqualTo("나의 리뷰에 대한 리뷰를 작성할 수 없습니다."); + } + + @Test + @DisplayName("현재 로그인한 회원의 id 가 모임 참여자에 포함되지 않는다면, 회원 리뷰 등록을 실패하고 400 bad request 을 반환한다.") + void Return_400_bad_request_if_meeting_not_contain_current_user_id_() { + // given + var user = repository.saveAndGetUser(); + var participant1 = repository.saveAndGetUser(); + var participant2 = repository.saveAndGetUser(); + + var closedMeeting = repository.saveAndGetCloseMeetingByParticipantUserIds( + List.of(participant1.getId(), participant2.getId())); + + var request = RequestFixture.reviewRegisterRequest(List.of(participant1, participant2)); + + securityContextSetting.set(user.getId()); // when - var expected = api.registerUserReview(VALID_TOKEN, meeting.getId(), request); + var expected = api.registerUserReview(VALID_TOKEN, closedMeeting.getId(), request); // then Assertions.assertThat(expected.expectStatus().isBadRequest() - .expectBody(ErrorResponse.class) - .returnResult().getResponseBody()); + .expectBody(ErrorResponse.class) + .returnResult().getResponseBody()) + .extracting(ErrorResponse::getMessage) + .isEqualTo("모임에 참여하지 않은 회원입니다."); } } } + diff --git a/src/test/java/net/teumteum/meeting/domain/MeetingFixture.java b/src/test/java/net/teumteum/meeting/domain/MeetingFixture.java index 93f1da2..0c22148 100644 --- a/src/test/java/net/teumteum/meeting/domain/MeetingFixture.java +++ b/src/test/java/net/teumteum/meeting/domain/MeetingFixture.java @@ -26,6 +26,14 @@ public static Meeting getOpenMeeting() { ); } + public static Meeting getOpenMeetingWithId(Long meetingId) { + return newMeetingByBuilder(MeetingBuilder.builder() + .id(meetingId) + .promiseDateTime(LocalDateTime.of(4000, 1, 1, 0, 0)) + .build()); + + } + public static Meeting getCloseMeeting() { return newMeetingByBuilder(MeetingBuilder.builder() .promiseDateTime(LocalDateTime.of(2000, 1, 1, 0, 0)) @@ -33,6 +41,29 @@ public static Meeting getCloseMeeting() { ); } + public static Meeting getCloseMeetingWithId(Long meetingId) { + return newMeetingByBuilder(MeetingBuilder.builder() + .id(meetingId) + .promiseDateTime(LocalDateTime.of(2000, 1, 1, 0, 0)) + .build()); + } + + public static Meeting getCloseMeetingWithIdAndParticipantIds(Long meetingId, List participantIds) { + return newMeetingByBuilder(MeetingBuilder.builder() + .id(meetingId) + .participantUserIds(new HashSet<>(participantIds)) + .promiseDateTime(LocalDateTime.of(2000, 1, 1, 0, 0)) + .build() + ); + } + + public static Meeting getCloseMeetingWithParticipantIds(List participantIds) { + return newMeetingByBuilder(MeetingBuilder.builder() + .participantUserIds(new HashSet<>(participantIds)) + .promiseDateTime(LocalDateTime.of(2000, 1, 1, 0, 0)) + .build()); + } + public static Meeting getOpenFullMeeting() { return newMeetingByBuilder(MeetingBuilder.builder() .promiseDateTime(LocalDateTime.of(4000, 1, 1, 0, 0)) 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 77afd49..4d13b92 100644 --- a/src/test/java/net/teumteum/unit/user/controller/UserControllerTest.java +++ b/src/test/java/net/teumteum/unit/user/controller/UserControllerTest.java @@ -67,6 +67,7 @@ public class UserControllerTest { @Autowired private MockMvc mockMvc; + @MockBean private UserService userService; @@ -88,9 +89,9 @@ class Register_user_card_api_unit { @DisplayName("유효한 사용자의 등록 요청값이 주어지면, 201 Created 상태값을 반환한다.") void Register_user_card_with_201_created() throws Exception { // given - UserRegisterRequest request = RequestFixture.userRegisterRequest(user); + var request = RequestFixture.userRegisterRequest(user); - UserRegisterResponse response = new UserRegisterResponse(1L, VALID_ACCESS_TOKEN, VALID_REFRESH_TOKEN); + var response = new UserRegisterResponse(1L, VALID_ACCESS_TOKEN, VALID_REFRESH_TOKEN); given(userService.register(any(UserRegisterRequest.class))).willReturn(response); @@ -111,7 +112,7 @@ void Register_user_card_with_201_created() throws Exception { @DisplayName("이미 카드 등록한 사용자의 등록 요청값이 주어지면, 400 Bad Request을 반환한다.") void Return_400_bad_request_if_user_already_exist() throws Exception { // given - UserRegisterRequest request = RequestFixture.userRegisterRequest(user); + var request = RequestFixture.userRegisterRequest(user); given(userService.register(any(UserRegisterRequest.class))) .willThrow(new IllegalArgumentException("일치하는 user 가 이미 존재합니다.")); @@ -131,7 +132,7 @@ void Return_400_bad_request_if_user_already_exist() throws Exception { @DisplayName("유효하지 않은 사용자의 등록 요청값이 주어지면, 400 Bad Request 상태값을 반환한다.") void Register_user_card_with_400_bad_request() throws Exception { // given - UserRegisterRequest request = RequestFixture.userRegisterRequestWithNoValid(user); + var request = RequestFixture.userRegisterRequestWithNoValid(user); // when // then mockMvc.perform(post("/users") @@ -153,7 +154,7 @@ class Withdraw_user_api_unit { @DisplayName("회원 탈퇴 사유와 회원 탈퇴 요청이 들어오면, 탈퇴를 진행하고 200 OK을 반환한다.") void Withdraw_user_with_200_ok() throws Exception { // given - UserWithdrawRequest request + var request = RequestFixture.userWithdrawRequest(List.of("쓰지 않는 앱이에요", "오류가 생겨서 쓸 수 없어요")); // when & then @@ -170,7 +171,7 @@ void Withdraw_user_with_200_ok() throws Exception { @DisplayName("회원 탈퇴 하고자 하는 회원이 존재하지 않으면, 400 Bad Request을 반환한다.") void Return_400_bad_request_if_user_is_not_exist() throws Exception { // given - UserWithdrawRequest request + var request = RequestFixture.userWithdrawRequest(List.of("쓰지 않는 앱이에요", "오류가 생겨서 쓸 수 없어요")); doThrow(new IllegalArgumentException("일치하는 user가 이미 존재합니다.")).when(userService).withdraw(any( @@ -195,7 +196,7 @@ class Register_user_review_api_unit { @DisplayName("회원 id 와 리뷰 정보 요청이 들어오면, 회원 리뷰를 등록하고 200 OK을 반환한다.") void Register_user_review_with_200_ok() throws Exception { // given - ReviewRegisterRequest reviewRegisterRequest = RequestFixture.reviewRegisterRequest(); + var reviewRegisterRequest = RequestFixture.reviewRegisterRequest(); // when & then mockMvc.perform(post("/users/reviews") @@ -210,11 +211,11 @@ void Register_user_review_with_200_ok() throws Exception { @Test @DisplayName("현재 로그인한 회원의 id 가 리뷰 등록 요청에 포함된다면, 회원 리뷰 등록을 실패하고 400 bad request을 반환한다.") - void Register_reviews_with_400_bad_request() throws Exception { + void Return_400_bad_request_if_request_contains_current_user_id() throws Exception { // given - ReviewRegisterRequest reviewRegisterRequest = RequestFixture.reviewRegisterRequest(); + var reviewRegisterRequest = RequestFixture.reviewRegisterRequest(); - String errorMessage = "나의 리뷰에 대한 리뷰를 작성할 수 없습니다."; + var errorMessage = "나의 리뷰에 대한 리뷰를 작성할 수 없습니다."; doThrow(new IllegalArgumentException(errorMessage)) .when(userService) 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 1c5630a..f3afd61 100644 --- a/src/test/java/net/teumteum/unit/user/service/UserServiceTest.java +++ b/src/test/java/net/teumteum/unit/user/service/UserServiceTest.java @@ -22,6 +22,7 @@ import net.teumteum.core.security.service.RedisService; import net.teumteum.integration.RequestFixture; import net.teumteum.meeting.domain.MeetingConnector; +import net.teumteum.meeting.domain.MeetingFixture; import net.teumteum.user.domain.User; import net.teumteum.user.domain.UserFixture; import net.teumteum.user.domain.UserRepository; @@ -164,60 +165,56 @@ void Register_user_review_with_200_ok() { // given ReviewRegisterRequest reviewRegisterRequest = RequestFixture.reviewRegisterRequest(); - Long meetingId = 1L; + var meeting = MeetingFixture.getCloseMeetingWithIdAndParticipantIds(1L, List.of(1L, 2L, 10L)); + var userId = 10L; - Long userId = 10L; - - Long currentUserId = 20L; - - given(meetingConnector.existById(anyLong())) - .willReturn(true); + given(meetingConnector.findById(anyLong())) + .willReturn(Optional.of(meeting)); given(userRepository.findById(anyLong())) - .willReturn(Optional.of(UserFixture.getUserWithId(userId++))); + .willReturn(Optional.of(UserFixture.getUserWithId(userId))); // when - userService.registerReview(meetingId, currentUserId, reviewRegisterRequest); + userService.registerReview(meeting.getId(), userId, reviewRegisterRequest); // then - verify(meetingConnector, times(1)).existById(anyLong()); + verify(meetingConnector, times(1)).findById(anyLong()); verify(userRepository, times(3)).findById(anyLong()); } @Test - @DisplayName("회원 id 가 리뷰 정보 요청에 포함되면, 400 Bad Request 와 함께 리뷰 등록을 실패한다.") - void Return_400_bad_request_if_current_user_id_in_request() { + @DisplayName("meeting id 에 해당하는 meeting 이 존재하지 않는 경우, 400 Bad Request 와 함께 리뷰 등록을 실패한다.") + void Return_400_bad_request_if_meeting_is_not_exist() { // given - ReviewRegisterRequest reviewRegisterRequest = RequestFixture.reviewRegisterRequest(); - - Long meetingId = 1L; + var reviewRegisterRequest = RequestFixture.reviewRegisterRequest(); - Long currentUserId = reviewRegisterRequest.reviews().get(0).id(); - - given(meetingConnector.existById(anyLong())) - .willReturn(true); + var meeting = MeetingFixture.getCloseMeetingWithId(1L); + var currentUserId = 1L; + given(meetingConnector.findById(anyLong())) + .willReturn(Optional.empty()); // when & then - assertThatThrownBy(() -> userService.registerReview(meetingId, currentUserId, reviewRegisterRequest)) + assertThatThrownBy(() -> userService.registerReview(meeting.getId(), currentUserId, reviewRegisterRequest)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("나의 리뷰에 대한 리뷰를 작성할 수 없습니다."); + .hasMessage("meetingId에 해당하는 모임을 찾을 수 없습니다. \"" + meeting.getId() + "\""); } @Test - @DisplayName("meeting id 에 해당하는 meeting 이 존재하지 않는 경우, 400 Bad Request 와 함께 리뷰 등록을 실패한다.") - void Return_400_bad_request_if_meeting_is_not_exist() { + @DisplayName("meeting id 에 해당하는 meeting 이 아직 종료되지 않았다면, 400 Bad Request 와 함께 리뷰 등록을 실패한다.") + void Return_400_bad_request_if_meeting_is_not_closed() { // given - ReviewRegisterRequest reviewRegisterRequest = RequestFixture.reviewRegisterRequest(); + var reviewRegisterRequest = RequestFixture.reviewRegisterRequest(); + + var meeting = MeetingFixture.getOpenMeetingWithId(1L); + var currentUserId = 1L; - Long meetingId = 1L; - Long currentUserId = 1L; + given(meetingConnector.findById(anyLong())) + .willReturn(Optional.of(meeting)); - given(meetingConnector.existById(anyLong())) - .willReturn(false); // when & then - assertThatThrownBy(() -> userService.registerReview(meetingId, currentUserId, reviewRegisterRequest)) + assertThatThrownBy(() -> userService.registerReview(meeting.getId(), currentUserId, reviewRegisterRequest)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("meetingId에 해당하는 meeting을 찾을 수 없습니다. \"" + meetingId + "\""); + .hasMessage("해당 모임은 아직 종료되지 않았습니다."); } }