From b81a3960e365aaac91bebdee7c1d26c2ff01bd9a Mon Sep 17 00:00:00 2001 From: xb205 <62425964+devxb@users.noreply.github.com> Date: Fri, 16 Feb 2024 12:08:48 +0900 Subject: [PATCH] release: 0.5.6 (#232) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 충돌 해결 (#210) * fix: 소셜 로그인 에러 긴급 수정 (#214) (#215) * feat: 기존 회원 리뷰 조회 API 로직 변경 (#218) * feat: userId 에 해당하는 유저 리뷰 조회 API 로직 변경 (#217) * test: 통합테스트 수정 (#217) * test: 단위테스트 수정 (#217) * docs: readme 작성 (#222) * docs: readme 작성 * docs: readme 수정 * test: QA용 EndMeetingAlerted를 발행한다 * feat: 유저가 삭제되었을때, 알람토큰도 삭제하도록 수정 (#226) * feat: 유저가 삭제되었을때, 알람토큰도 삭제하도록 수정한다 * test: ApplicationEventPublisher 바인딩 안되는 테스트 삭제 * refactor: UserDeletedEvent 핸들러 이름 변경 * test: 친구조회 테스트 수정 * refactor: code smell을 제거한다 * fix: 모임 시작, 종료 알림이 모임 참여자가 3명 이상이 아니라면, 발행되지않도록 수정 (#231) * feat: 리뷰 등록시 모임 참여 유저 조회 API 수정 (#229) * refactor: MeetingController getCurrentUserId() 메소드로 통일 (#228) * feat: 리뷰에 자신 포함 확인 로직 추가 (#195) * feat: 모임 참여자 자신 포함 여부 확인 로직 추가(#228) * test: 단위 테스트 추가 (#228) * test: 통합 테스트 수정 및 실패 케이스 추가 (#228) * refactor: title, body alert data에 추가 --------- Co-authored-by: ddingmin Co-authored-by: ChoiDongKuen --- .../alert/infra/FcmAlertPublisher.java | 2 + .../meeting/controller/MeetingController.java | 31 +++-- ...e.java => MeetingParticipantResponse.java} | 6 +- .../service/MeetingAlertPublisher.java | 33 +++-- .../meeting/service/MeetingService.java | 18 ++- .../integration/MeetingIntegrationTest.java | 118 +++++++++++------- .../net/teumteum/integration/Repository.java | 7 +- .../meeting/domain/MeetingFixture.java | 8 ++ .../controller/MeetingControllerTest.java | 112 +++++++++++++++++ .../meeting/service/MeetingServiceTest.java | 91 ++++++++++++++ .../user/controller/UserControllerTest.java | 3 +- 11 files changed, 349 insertions(+), 80 deletions(-) rename src/main/java/net/teumteum/meeting/domain/response/{MeetingParticipantsResponse.java => MeetingParticipantResponse.java} (70%) create mode 100644 src/test/java/net/teumteum/unit/meeting/controller/MeetingControllerTest.java create mode 100644 src/test/java/net/teumteum/unit/meeting/service/MeetingServiceTest.java diff --git a/src/main/java/net/teumteum/alert/infra/FcmAlertPublisher.java b/src/main/java/net/teumteum/alert/infra/FcmAlertPublisher.java index 7b9c830..68ae10d 100644 --- a/src/main/java/net/teumteum/alert/infra/FcmAlertPublisher.java +++ b/src/main/java/net/teumteum/alert/infra/FcmAlertPublisher.java @@ -44,6 +44,8 @@ private Message buildMessage(String token, Alert alert, Map data .setToken(token) .setNotification(buildNotification(alert)) .setAndroidConfig(buildAndroidConfig(alert)) + .putData("title", alert.getTitle()) + .putData("body", alert.getBody()) .putData("publishedAt", alert.getCreatedAt().toString()) .putData("userId", alert.getUserId().toString()) .putData("type", alert.getType().toString()) diff --git a/src/main/java/net/teumteum/meeting/controller/MeetingController.java b/src/main/java/net/teumteum/meeting/controller/MeetingController.java index d36a633..ca559ad 100644 --- a/src/main/java/net/teumteum/meeting/controller/MeetingController.java +++ b/src/main/java/net/teumteum/meeting/controller/MeetingController.java @@ -9,7 +9,7 @@ import net.teumteum.meeting.domain.Topic; import net.teumteum.meeting.domain.request.CreateMeetingRequest; import net.teumteum.meeting.domain.request.UpdateMeetingRequest; -import net.teumteum.meeting.domain.response.MeetingParticipantsResponse; +import net.teumteum.meeting.domain.response.MeetingParticipantResponse; import net.teumteum.meeting.domain.response.MeetingResponse; import net.teumteum.meeting.domain.response.MeetingsResponse; import net.teumteum.meeting.model.PageDto; @@ -43,14 +43,14 @@ public class MeetingController { public MeetingResponse createMeeting( @RequestPart @Valid CreateMeetingRequest meetingRequest, @RequestPart List images) { - Long userId = securityService.getCurrentUserId(); + Long userId = getCurrentUserId(); return meetingService.createMeeting(images, meetingRequest, userId); } @GetMapping("/{meetingId}") @ResponseStatus(HttpStatus.OK) public MeetingResponse getMeetingById(@PathVariable("meetingId") Long meetingId) { - Long userId = securityService.getCurrentUserId(); + Long userId = getCurrentUserId(); return meetingService.getMeetingById(meetingId, userId); } @@ -64,7 +64,7 @@ public PageDto getMeetingsByCondition( @RequestParam(value = "participantUserId", required = false) Long participantUserId, @RequestParam(value = "isBookmarked", required = false) Boolean isBookmarked, @RequestParam(value = "searchWord", required = false) String searchWord) { - Long userId = securityService.getCurrentUserId(); + Long userId = getCurrentUserId(); return meetingService.getMeetingsBySpecification(pageable, topic, meetingAreaStreet, participantUserId, searchWord, isBookmarked, isOpen, userId); } @@ -74,55 +74,56 @@ public PageDto getMeetingsByCondition( public MeetingResponse updateMeeting(@PathVariable Long meetingId, @RequestPart @Valid UpdateMeetingRequest request, @RequestPart List images) { - Long userId = securityService.getCurrentUserId(); + Long userId = getCurrentUserId(); return meetingService.updateMeeting(meetingId, images, request, userId); } @DeleteMapping("/{meetingId}") @ResponseStatus(HttpStatus.OK) public void deleteMeeting(@PathVariable("meetingId") Long meetingId) { - Long userId = securityService.getCurrentUserId(); + Long userId = getCurrentUserId(); meetingService.deleteMeeting(meetingId, userId); } @PostMapping("/{meetingId}/participants") @ResponseStatus(HttpStatus.CREATED) public MeetingResponse addParticipant(@PathVariable("meetingId") Long meetingId) { - Long userId = securityService.getCurrentUserId(); + Long userId = getCurrentUserId(); return meetingService.addParticipant(meetingId, userId); } @DeleteMapping("/{meetingId}/participants") @ResponseStatus(HttpStatus.OK) public void deleteParticipant(@PathVariable("meetingId") Long meetingId) { - Long userId = securityService.getCurrentUserId(); + Long userId = getCurrentUserId(); meetingService.cancelParticipant(meetingId, userId); } @GetMapping("/{meetingId}/participants") @ResponseStatus(HttpStatus.OK) - public List getParticipants(@PathVariable("meetingId") Long meetingId) { - return meetingService.getParticipants(meetingId); + public List getParticipants(@PathVariable("meetingId") Long meetingId) { + Long userId = getCurrentUserId(); + return meetingService.getParticipants(meetingId, userId); } @PostMapping("/{meetingId}/reports") @ResponseStatus(HttpStatus.CREATED) public void reportMeeting(@PathVariable("meetingId") Long meetingId) { - Long userId = securityService.getCurrentUserId(); + Long userId = getCurrentUserId(); meetingService.reportMeeting(meetingId, userId); } @PostMapping("/{meetingId}/bookmarks") @ResponseStatus(HttpStatus.CREATED) public void addBookmark(@PathVariable("meetingId") Long meetingId) { - Long userId = securityService.getCurrentUserId(); + Long userId = getCurrentUserId(); meetingService.addBookmark(meetingId, userId); } @DeleteMapping("/{meetingId}/bookmarks") @ResponseStatus(HttpStatus.OK) public void deleteBookmark(@PathVariable("meetingId") Long meetingId) { - Long userId = securityService.getCurrentUserId(); + Long userId = getCurrentUserId(); meetingService.cancelBookmark(meetingId, userId); } @@ -132,4 +133,8 @@ public ErrorResponse handleIllegalArgumentException(IllegalArgumentException ill Sentry.captureException(illegalArgumentException); return ErrorResponse.of(illegalArgumentException); } + + private Long getCurrentUserId() { + return securityService.getCurrentUserId(); + } } diff --git a/src/main/java/net/teumteum/meeting/domain/response/MeetingParticipantsResponse.java b/src/main/java/net/teumteum/meeting/domain/response/MeetingParticipantResponse.java similarity index 70% rename from src/main/java/net/teumteum/meeting/domain/response/MeetingParticipantsResponse.java rename to src/main/java/net/teumteum/meeting/domain/response/MeetingParticipantResponse.java index 10d9df9..055ee11 100644 --- a/src/main/java/net/teumteum/meeting/domain/response/MeetingParticipantsResponse.java +++ b/src/main/java/net/teumteum/meeting/domain/response/MeetingParticipantResponse.java @@ -2,17 +2,17 @@ import net.teumteum.user.domain.User; -public record MeetingParticipantsResponse( +public record MeetingParticipantResponse( Long id, Long characterId, String name, String job ) { - public static MeetingParticipantsResponse of( + public static MeetingParticipantResponse of( User user ) { - return new MeetingParticipantsResponse( + return new MeetingParticipantResponse( user.getId(), user.getCharacterId(), user.getName(), diff --git a/src/main/java/net/teumteum/meeting/service/MeetingAlertPublisher.java b/src/main/java/net/teumteum/meeting/service/MeetingAlertPublisher.java index 858650e..bcc2bd8 100644 --- a/src/main/java/net/teumteum/meeting/service/MeetingAlertPublisher.java +++ b/src/main/java/net/teumteum/meeting/service/MeetingAlertPublisher.java @@ -16,6 +16,7 @@ @Transactional(readOnly = true) public class MeetingAlertPublisher { + private static final String KR_TIME_ZONE = "Asia/Seoul"; private static final String EVERY_ONE_MINUTES = "0 * * * * *"; private static final String EVERY_12PM = "0 0 12 * * *"; @@ -24,18 +25,20 @@ public class MeetingAlertPublisher { @Scheduled(cron = EVERY_ONE_MINUTES) public void alertBeforeMeeting() { - var alertStart = LocalDateTime.now(ZoneId.of("Asia/Seoul")).plusMinutes(5).withNano(0).withSecond(0); + var alertStart = LocalDateTime.now(ZoneId.of(KR_TIME_ZONE)).plusMinutes(5).withNano(0).withSecond(0); var alertEnd = alertStart.plusMinutes(1).withNano(0).withSecond(0); var alertTargets = meetingRepository.findAlertMeetings(alertStart, alertEnd); - alertTargets.forEach(meeting -> eventPublisher.publishEvent( - new BeforeMeetingAlerted(meeting.getParticipantUserIds()) - ) - ); + alertTargets.stream() + .filter(alertTarget -> alertTarget.getParticipantUserIds().size() > 2) + .forEach(meeting -> eventPublisher.publishEvent( + new BeforeMeetingAlerted(meeting.getParticipantUserIds()) + ) + ); } @Scheduled(cron = EVERY_12PM) public void alertEndMeeting() { - var today = LocalDateTime.now(ZoneId.of("Asia/Seoul")) + var today = LocalDateTime.now(ZoneId.of(KR_TIME_ZONE)) .withNano(0) .withSecond(0) .withMinute(0) @@ -44,14 +47,16 @@ public void alertEndMeeting() { var yesterday = today.minusDays(1); var alertTargets = meetingRepository.findAlertMeetings(yesterday, today); - alertTargets.forEach(meeting -> eventPublisher.publishEvent( - new EndMeetingAlerted(meeting.getId(), meeting.getTitle(), meeting.getParticipantUserIds()) - )); + alertTargets.stream() + .filter(alertTarget -> alertTarget.getParticipantUserIds().size() > 2) + .forEach(meeting -> eventPublisher.publishEvent( + new EndMeetingAlerted(meeting.getId(), meeting.getTitle(), meeting.getParticipantUserIds()) + )); } @Scheduled(cron = EVERY_ONE_MINUTES) public void alertEndMeetingForQa() { - var today = LocalDateTime.now(ZoneId.of("Asia/Seoul")) + var today = LocalDateTime.now(ZoneId.of(KR_TIME_ZONE)) .withNano(0) .withSecond(0) .withMinute(0) @@ -61,8 +66,10 @@ public void alertEndMeetingForQa() { var yesterday = today.minusDays(365); var alertTargets = meetingRepository.findAlertMeetings(yesterday, future); - alertTargets.forEach(meeting -> eventPublisher.publishEvent( - new EndMeetingAlerted(meeting.getId(), meeting.getTitle(), meeting.getParticipantUserIds()) - )); + alertTargets.stream() + .filter(alertTarget -> alertTarget.getParticipantUserIds().size() > 2) + .forEach(meeting -> eventPublisher.publishEvent( + new EndMeetingAlerted(meeting.getId(), meeting.getTitle(), meeting.getParticipantUserIds()) + )); } } diff --git a/src/main/java/net/teumteum/meeting/service/MeetingService.java b/src/main/java/net/teumteum/meeting/service/MeetingService.java index 99cfc41..5003080 100644 --- a/src/main/java/net/teumteum/meeting/service/MeetingService.java +++ b/src/main/java/net/teumteum/meeting/service/MeetingService.java @@ -12,7 +12,7 @@ import net.teumteum.meeting.domain.Topic; import net.teumteum.meeting.domain.request.CreateMeetingRequest; import net.teumteum.meeting.domain.request.UpdateMeetingRequest; -import net.teumteum.meeting.domain.response.MeetingParticipantsResponse; +import net.teumteum.meeting.domain.response.MeetingParticipantResponse; import net.teumteum.meeting.domain.response.MeetingResponse; import net.teumteum.meeting.domain.response.MeetingsResponse; import net.teumteum.meeting.model.PageDto; @@ -94,7 +94,8 @@ public void deleteMeeting(Long meetingId, Long userId) { } @Transactional(readOnly = true) - public PageDto getMeetingsBySpecification(Pageable pageable, Topic topic, String meetingAreaStreet, + public PageDto getMeetingsBySpecification(Pageable pageable, Topic topic, + String meetingAreaStreet, Long participantUserId, String searchWord, Boolean isBookmarked, Boolean isOpen, Long userId) { Specification spec = MeetingSpecification.withIsOpen(isOpen); @@ -153,13 +154,16 @@ public void cancelParticipant(Long meetingId, Long userId) { } @Transactional(readOnly = true) - public List getParticipants(Long meetingId) { + public List getParticipants(Long meetingId, Long userId) { var existMeeting = getMeeting(meetingId); + checkMeetingContainUser(existMeeting, userId); + return existMeeting.getParticipantUserIds().stream() + .filter(id -> !id.equals(userId)) .map(userConnector::findUserById) .flatMap(Optional::stream) - .map(MeetingParticipantsResponse::of) + .map(MeetingParticipantResponse::of) .toList(); } @@ -207,4 +211,10 @@ public void reportMeeting(Long meetingId, Long userId) { throw new IllegalArgumentException("모임 개설자는 모임을 신고할 수 없습니다."); } } + + private void checkMeetingContainUser(Meeting meeting, Long userId) { + if (!meeting.getParticipantUserIds().contains(userId)) { + throw new IllegalArgumentException("모임에 참여하지 않은 회원입니다."); + } + } } diff --git a/src/test/java/net/teumteum/integration/MeetingIntegrationTest.java b/src/test/java/net/teumteum/integration/MeetingIntegrationTest.java index dc45f2c..27d76d2 100644 --- a/src/test/java/net/teumteum/integration/MeetingIntegrationTest.java +++ b/src/test/java/net/teumteum/integration/MeetingIntegrationTest.java @@ -1,7 +1,10 @@ package net.teumteum.integration; +import static org.assertj.core.api.Assertions.assertThat; + import java.util.Collection; import java.util.Comparator; +import java.util.List; import java.util.stream.Stream; import net.teumteum.core.error.ErrorResponse; import net.teumteum.meeting.domain.Meeting; @@ -9,7 +12,6 @@ import net.teumteum.meeting.domain.response.MeetingResponse; import net.teumteum.meeting.domain.response.MeetingsResponse; import net.teumteum.meeting.model.PageDto; -import org.assertj.core.api.Assertions; import org.assertj.core.api.Condition; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -42,10 +44,10 @@ void Return_meeting_info_if_exist_meeting_id_received() { // when var result = api.getMeetingById(VALID_TOKEN, meeting.getId()); // then - Assertions.assertThat( - result.expectStatus().isOk() - .expectBody(MeetingResponse.class) - .returnResult().getResponseBody()) + assertThat( + result.expectStatus().isOk() + .expectBody(MeetingResponse.class) + .returnResult().getResponseBody()) .usingRecursiveComparison() .isEqualTo(expected); } @@ -75,10 +77,10 @@ void Return_is_bookmarked_true_if_user_bookmarked_meeting() { // when var result = api.getMeetingById(VALID_TOKEN, meeting.getId()); // then - Assertions.assertThat( - result.expectStatus().isOk() - .expectBody(MeetingResponse.class) - .returnResult().getResponseBody()) + assertThat( + result.expectStatus().isOk() + .expectBody(MeetingResponse.class) + .returnResult().getResponseBody()) .extracting(MeetingResponse::isBookmarked) .isEqualTo(true); } @@ -158,11 +160,11 @@ void Return_meeting_list_if_topic_and_page_nation_received() { // when var result = api.getMeetingsByTopic(VALID_TOKEN, FIRST_PAGE_NATION, true, Topic.스터디); // then - Assertions.assertThat( - result.expectStatus().isOk() - .expectBody(new ParameterizedTypeReference>() { - }) - .returnResult().getResponseBody()) + assertThat( + result.expectStatus().isOk() + .expectBody(new ParameterizedTypeReference>() { + }) + .returnResult().getResponseBody()) .usingRecursiveComparison() .isEqualTo(expected); } @@ -192,11 +194,11 @@ void Return_meeting_list_if_search_word_and_page_nation_received() { // when var result = api.getMeetingsByTopic(VALID_TOKEN, FIRST_PAGE_NATION, true, Topic.스터디); // then - Assertions.assertThat( - result.expectStatus().isOk() - .expectBody(new ParameterizedTypeReference>() { - }) - .returnResult().getResponseBody()) + assertThat( + result.expectStatus().isOk() + .expectBody(new ParameterizedTypeReference>() { + }) + .returnResult().getResponseBody()) .usingRecursiveComparison() .isEqualTo(expected); } @@ -222,11 +224,11 @@ void Return_meeting_list_if_participant_user_id_and_page_nation_received() { // when var result = api.getMeetingsByTopic(VALID_TOKEN, FIRST_PAGE_NATION, true, Topic.스터디); // then - Assertions.assertThat( - result.expectStatus().isOk() - .expectBody(new ParameterizedTypeReference>() { - }) - .returnResult().getResponseBody()) + assertThat( + result.expectStatus().isOk() + .expectBody(new ParameterizedTypeReference>() { + }) + .returnResult().getResponseBody()) .usingRecursiveComparison() .isEqualTo(expected); } @@ -252,11 +254,11 @@ void Return_has_next_true_if_more_data_exists_than_requested_size_and_page() { // when var result = api.getMeetingsByTopic(VALID_TOKEN, FIRST_PAGE_NATION, true, Topic.스터디); // then - Assertions.assertThat( - result.expectStatus().isOk() - .expectBody(new ParameterizedTypeReference>() { - }) - .returnResult().getResponseBody()) + assertThat( + result.expectStatus().isOk() + .expectBody(new ParameterizedTypeReference>() { + }) + .returnResult().getResponseBody()) .usingRecursiveComparison() .isEqualTo(expected); } @@ -277,11 +279,11 @@ void Join_meeting_if_exist_meeting_id_received() { // when var result = api.joinMeeting(VALID_TOKEN, existMeeting.getId()); // then - Assertions.assertThat( - result.expectStatus().isCreated() - .expectBody(MeetingResponse.class) - .returnResult() - .getResponseBody()) + assertThat( + result.expectStatus().isCreated() + .expectBody(MeetingResponse.class) + .returnResult() + .getResponseBody()) .extracting(MeetingResponse::participantIds) .has(new Condition<>(ids -> ids.contains(me.getId()), "참여자 목록에 나를 포함한다.") ); @@ -380,11 +382,11 @@ void Return_400_bad_request_if_not_joined_meeting_id_received() { // when var result = api.cancelMeeting(VALID_TOKEN, meeting.getId()); // then - Assertions.assertThat(result.expectStatus().isBadRequest() - .expectBody(ErrorResponse.class) - .returnResult() - .getResponseBody() - ) + assertThat(result.expectStatus().isBadRequest() + .expectBody(ErrorResponse.class) + .returnResult() + .getResponseBody() + ) .extracting(ErrorResponse::getMessage) .isEqualTo("참여하지 않은 모임입니다."); } @@ -400,11 +402,11 @@ void Return_400_bad_request_if_closed_meeting_id_received() { // when var result = api.cancelMeeting(VALID_TOKEN, meeting.getId()); // then - Assertions.assertThat(result.expectStatus().isBadRequest() - .expectBody(ErrorResponse.class) - .returnResult() - .getResponseBody() - ) + assertThat(result.expectStatus().isBadRequest() + .expectBody(ErrorResponse.class) + .returnResult() + .getResponseBody() + ) .extracting(ErrorResponse::getMessage) .isEqualTo("종료된 모임에서 참여를 취소할 수 없습니다."); } @@ -488,11 +490,37 @@ class Get_meeting_participants_api { @DisplayName("참여한 meeting id 가 주어지면, 참여한 참가자들의 정보가 주어진다.") void Get_participants_if_exist_meeting_id_received() { // given - var meeting = repository.saveAndGetOpenMeeting(); + var me = repository.saveAndGetUser(); + var meeting = repository.saveAndGetClosedMetingWithParticipantUserIds(List.of(me.getId(), 2L)); + + securityContextSetting.set(me.getId()); + // when var result = api.getMeetingParticipants(VALID_TOKEN, meeting.getId()); + // then result.expectStatus().isOk(); } + + @Test + @DisplayName("API 호출한 회원이 모임에 참여하지 않았다면, 400 bad request 을 응답한다.") + void Return_400_bad_request_if_meeting_not_contain_user() { + // given + var me = repository.saveAndGetUser(); + var meeting = repository.saveAndGetClosedMetingWithParticipantUserIds(List.of(100L, 101L)); + + securityContextSetting.set(me.getId()); + + // when + var result = api.getMeetingParticipants(VALID_TOKEN, meeting.getId()); + + // then + assertThat(result.expectStatus().isBadRequest() + .expectBody(ErrorResponse.class) + .returnResult() + .getResponseBody()) + .extracting(ErrorResponse::getMessage) + .isEqualTo("모임에 참여하지 않은 회원입니다."); + } } } diff --git a/src/test/java/net/teumteum/integration/Repository.java b/src/test/java/net/teumteum/integration/Repository.java index 7d829ee..894c036 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; @@ -82,6 +81,11 @@ Meeting saveAndGetOpenFullMeeting() { return meetingRepository.saveAndFlush(meeting); } + Meeting saveAndGetClosedMetingWithParticipantUserIds(List participantUserIds) { + var meeting = MeetingFixture.getCloseMeetingWithParticipantUserIds(participantUserIds); + return meetingRepository.saveAndFlush(meeting); + } + List saveAndGetOpenMeetingsByTopic(int size, Topic topic) { var meetings = Stream.generate(() -> MeetingFixture.getOpenMeetingWithTopic(topic)) .limit(size) @@ -146,6 +150,7 @@ List saveAndGetOpenMeetings(int size) { return meetingRepository.saveAllAndFlush(meetings); } + void clear() { userRepository.deleteAll(); meetingRepository.deleteAll(); diff --git a/src/test/java/net/teumteum/meeting/domain/MeetingFixture.java b/src/test/java/net/teumteum/meeting/domain/MeetingFixture.java index 93f1da2..09b850e 100644 --- a/src/test/java/net/teumteum/meeting/domain/MeetingFixture.java +++ b/src/test/java/net/teumteum/meeting/domain/MeetingFixture.java @@ -82,6 +82,14 @@ public static Meeting getCloseMeetingWithParticipantUserId(Long participantUserI ); } + public static Meeting getCloseMeetingWithParticipantUserIds(List participantUserIds) { + return newMeetingByBuilder(MeetingBuilder.builder() + .promiseDateTime(LocalDateTime.of(2000, 1, 1, 0, 0)) + .participantUserIds(new HashSet<>(participantUserIds)) + .build() + ); + } + public static Meeting getOpenMeetingWithTitle(String title) { return newMeetingByBuilder(MeetingBuilder.builder() .promiseDateTime(LocalDateTime.of(4000, 1, 1, 0, 0)) diff --git a/src/test/java/net/teumteum/unit/meeting/controller/MeetingControllerTest.java b/src/test/java/net/teumteum/unit/meeting/controller/MeetingControllerTest.java new file mode 100644 index 0000000..da166b3 --- /dev/null +++ b/src/test/java/net/teumteum/unit/meeting/controller/MeetingControllerTest.java @@ -0,0 +1,112 @@ +package net.teumteum.unit.meeting.controller; + +import static net.teumteum.unit.common.SecurityValue.VALID_ACCESS_TOKEN; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +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.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.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import net.teumteum.core.security.SecurityConfig; +import net.teumteum.core.security.filter.JwtAuthenticationFilter; +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.controller.MeetingController; +import net.teumteum.meeting.domain.response.MeetingParticipantResponse; +import net.teumteum.meeting.service.MeetingService; +import net.teumteum.user.domain.UserFixture; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(value = MeetingController.class, + excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = SecurityConfig.class), + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = JwtAuthenticationFilter.class), + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = RedisService.class), + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = JwtService.class)} +) +@WithMockUser +@DisplayName("모임 컨트롤러 단위 테스트의") +public class MeetingControllerTest { + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private MockMvc mockMvc; + + @MockBean + private MeetingService meetingService; + + @MockBean + private SecurityService securityService; + + @Nested + @DisplayName("모임 참여자 조회 API는") + class Get_meeting_participants_api_unit { + + @Test + @DisplayName("API 호출 회원을 제외한, meetingId 해당하는 모임의 참여자 정보를 반환한다.") + void Return_meeting_participants_with_200_ok() throws Exception { + // given + var existUser1 = UserFixture.getUserWithId(1L); + var existUser2 = UserFixture.getUserWithId(2L); + var existUser3 = UserFixture.getUserWithId(3L); + + List response + = List.of(MeetingParticipantResponse.of(existUser2), MeetingParticipantResponse.of(existUser3)); + + given(securityService.getCurrentUserId()) + .willReturn(existUser1.getId()); + + given(meetingService.getParticipants(anyLong(), anyLong())) + .willReturn(response); + + // when && then + mockMvc.perform(get("/meetings/{meetingId}/participants", 2L) + .with(csrf()) + .header(AUTHORIZATION, VALID_ACCESS_TOKEN)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isNotEmpty()) + .andExpect(jsonPath("$.length()").value(2)) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$[0].id").value(2)) + .andExpect(jsonPath("$[0].characterId").value(1)); + } + + @Test + @DisplayName("모임에 API 호출 회원이 존재하지 않는 경우, 400 bad request를 응답한다.") + void Return_400_bad_request_if_meeting_not_contain_user() throws Exception { + // given + var existUser1 = UserFixture.getUserWithId(1L); + + given(securityService.getCurrentUserId()) + .willReturn(existUser1.getId()); + + given(meetingService.getParticipants(anyLong(), anyLong())).willThrow( + new IllegalArgumentException("모임에 참여하지 않은 회원입니다.")); + + // when & then + mockMvc.perform(get("/meetings/{meetingId}/participants", 2L) + .with(csrf()) + .header(AUTHORIZATION, VALID_ACCESS_TOKEN)) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("모임에 참여하지 않은 회원입니다.")); + } + } +} diff --git a/src/test/java/net/teumteum/unit/meeting/service/MeetingServiceTest.java b/src/test/java/net/teumteum/unit/meeting/service/MeetingServiceTest.java new file mode 100644 index 0000000..c6cd397 --- /dev/null +++ b/src/test/java/net/teumteum/unit/meeting/service/MeetingServiceTest.java @@ -0,0 +1,91 @@ +package net.teumteum.unit.meeting.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; + +import java.util.List; +import java.util.Optional; +import net.teumteum.meeting.domain.ImageUpload; +import net.teumteum.meeting.domain.MeetingFixture; +import net.teumteum.meeting.domain.MeetingRepository; +import net.teumteum.meeting.domain.response.MeetingParticipantResponse; +import net.teumteum.meeting.service.MeetingService; +import net.teumteum.user.domain.UserConnector; +import net.teumteum.user.domain.UserFixture; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +@DisplayName("모임 서비스 단위 테스트의") +public class MeetingServiceTest { + + @InjectMocks + MeetingService meetingService; + + @Mock + MeetingRepository meetingRepository; + + @Mock + UserConnector userConnector; + + @Mock + ImageUpload imageUpload; + + @Nested + @DisplayName("모임 참여자 조회 API는") + class Return_meeting_participants_api_unit { + + @Test + @DisplayName("API 호출 회원을 제외한, meetingId 해당하는 모임의 참여자 정보를 반환한다.") + void Return_meeting_participants_with_200_ok() { + // given + var userId = 1L; + var meetingId = 1L; + + var existMeeting + = MeetingFixture.getCloseMeetingWithParticipantUserIds(List.of(userId, 2L, 3L)); + + var existUser2 = UserFixture.getUserWithId(2L); + var existUser3 = UserFixture.getUserWithId(3L); + + given(meetingRepository.findById(anyLong())) + .willReturn(Optional.of(existMeeting)); + + given(userConnector.findUserById(existUser2.getId())).willReturn(Optional.of(existUser2)); + given(userConnector.findUserById(existUser3.getId())).willReturn(Optional.of(existUser3)); + + // when + List participants = meetingService.getParticipants(meetingId, userId); + // then + assertThat(participants).hasSize(2); + } + + @Test + @DisplayName("모임에 API 호출 회원이 존재하지 않는 경우, 400 bad request를 응답한다.") + void Return_400_bad_request_if_meeting_not_contain_user() { + // given + var userId = 1L; + var notContainedUserId = 4L; + + var meetingId = 1L; + + var existMeeting + = MeetingFixture.getCloseMeetingWithParticipantUserIds(List.of(userId, 2L, 3L)); + + given(meetingRepository.findById(anyLong())) + .willReturn(Optional.of(existMeeting)); + + // when + assertThatThrownBy(() -> meetingService.getParticipants(meetingId, notContainedUserId)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("모임에 참여하지 않은 회원입니다."); + } + } +} 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..2b1d877 100644 --- a/src/test/java/net/teumteum/unit/user/controller/UserControllerTest.java +++ b/src/test/java/net/teumteum/unit/user/controller/UserControllerTest.java @@ -63,10 +63,11 @@ public class UserControllerTest { @Autowired - ObjectMapper objectMapper; + private ObjectMapper objectMapper; @Autowired private MockMvc mockMvc; + @MockBean private UserService userService;