From 6bb10fdce6ea3043b1258776c7f37d99f7cf85e2 Mon Sep 17 00:00:00 2001 From: ddingmin Date: Tue, 13 Feb 2024 00:55:31 +0900 Subject: [PATCH 01/11] =?UTF-8?q?fix:=20=EC=B6=A9=EB=8F=8C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0=20(#210)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/net/teumteum/meeting/service/MeetingService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/net/teumteum/meeting/service/MeetingService.java b/src/main/java/net/teumteum/meeting/service/MeetingService.java index 2657f43..99cfc41 100644 --- a/src/main/java/net/teumteum/meeting/service/MeetingService.java +++ b/src/main/java/net/teumteum/meeting/service/MeetingService.java @@ -112,7 +112,6 @@ public PageDto getMeetingsBySpecification(Pageable pageable, T } else if (Boolean.TRUE.equals(isBookmarked)) { spec = MeetingSpecification.withBookmarkedUserId(userId); } - var meetings = meetingRepository.findAll(spec, pageable); return PageDto.of(MeetingsResponse.of(meetings.getContent()), meetings.hasNext()); From 9eac8c543cc919e3ec3dca4e8726d75d97607440 Mon Sep 17 00:00:00 2001 From: ChoiDongKuen Date: Tue, 13 Feb 2024 02:16:48 +0900 Subject: [PATCH 02/11] =?UTF-8?q?fix:=20=EC=86=8C=EC=85=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=97=90=EB=9F=AC=20=EA=B8=B4=EA=B8=89=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#214)=20(#215)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/net/teumteum/alert/app/AlertHandler.java | 4 ++-- .../core/security/filter/JwtAuthenticationFilter.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/teumteum/alert/app/AlertHandler.java b/src/main/java/net/teumteum/alert/app/AlertHandler.java index 86e6bc6..56b6400 100644 --- a/src/main/java/net/teumteum/alert/app/AlertHandler.java +++ b/src/main/java/net/teumteum/alert/app/AlertHandler.java @@ -45,7 +45,7 @@ public void handleBeforeMeetingAlerts(BeforeMeetingAlerted alerted) { @Async(ALERT_EXECUTOR) @EventListener(EndMeetingAlerted.class) - public void handleEndMeetingAlerts(EndMeetingAlerted alerted) { + public void handleStartMeetingAlerts(EndMeetingAlerted alerted) { userAlertService.findAllByUserId(alerted.userIds()) .stream() .map(userAlert -> Pair.of(userAlert.getToken(), @@ -64,7 +64,7 @@ private String toCommaString(List ids) { for (int i = 0; i < ids.size() - 1; i++) { stringBuilder.append(ids.get(i)).append(","); } - stringBuilder.append(ids.get(ids.size() - 1)); + stringBuilder.append(ids.getLast()); return stringBuilder.toString(); } diff --git a/src/main/java/net/teumteum/core/security/filter/JwtAuthenticationFilter.java b/src/main/java/net/teumteum/core/security/filter/JwtAuthenticationFilter.java index 20ef8e3..36e960d 100644 --- a/src/main/java/net/teumteum/core/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/net/teumteum/core/security/filter/JwtAuthenticationFilter.java @@ -72,7 +72,7 @@ private void saveUserAuthentication(User user) { private String resolveTokenFromRequest(HttpServletRequest request) { String token = request.getHeader(jwtProperty.getAccess().getHeader()); - if (token.toLowerCase().startsWith(jwtProperty.getBearer().toLowerCase())) { + if (StringUtils.hasText(token) && token.toLowerCase().startsWith(jwtProperty.getBearer().toLowerCase())) { return token.substring(7); } return null; From 1a7052c14d5cec13ed0fd0ccba918f15e707f84b Mon Sep 17 00:00:00 2001 From: ChoiDongKuen Date: Wed, 14 Feb 2024 04:22:36 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20=EA=B8=B0=EC=A1=B4=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=20=EB=A6=AC=EB=B7=B0=20=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD=20(#218)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: userId 에 해당하는 유저 리뷰 조회 API 로직 변경 (#217) * test: 통합테스트 수정 (#217) * test: 단위테스트 수정 (#217) --- .../user/controller/UserController.java | 6 +++--- .../teumteum/user/domain/UserRepository.java | 9 ++++---- .../domain/response/UserReviewResponse.java | 10 +++++++++ .../domain/response/UserReviewsResponse.java | 8 ++++--- .../teumteum/user/service/UserService.java | 6 ++++-- .../java/net/teumteum/integration/Api.java | 4 ++-- .../integration/UserIntegrationTest.java | 4 +++- .../user/controller/UserControllerTest.java | 19 ++++++++--------- .../unit/user/service/UserServiceTest.java | 21 ++++++++++--------- .../user/domain/UserRepositoryTest.java | 7 ++++--- 10 files changed, 56 insertions(+), 38 deletions(-) create mode 100644 src/main/java/net/teumteum/user/domain/response/UserReviewResponse.java diff --git a/src/main/java/net/teumteum/user/controller/UserController.java b/src/main/java/net/teumteum/user/controller/UserController.java index 6523683..feb37d9 100644 --- a/src/main/java/net/teumteum/user/controller/UserController.java +++ b/src/main/java/net/teumteum/user/controller/UserController.java @@ -116,10 +116,10 @@ public void registerReview( userService.registerReview(meetingId, getCurrentUserId(), request); } - @GetMapping("/reviews") + @GetMapping("/{userId}/reviews") @ResponseStatus(HttpStatus.OK) - public List getUserReviews() { - return userService.getUserReviews(getCurrentUserId()); + public UserReviewsResponse getUserReviews(@PathVariable("userId") Long userId) { + return userService.getUserReviews(userId); } @ResponseStatus(HttpStatus.BAD_REQUEST) diff --git a/src/main/java/net/teumteum/user/domain/UserRepository.java b/src/main/java/net/teumteum/user/domain/UserRepository.java index d0380f2..b611a17 100644 --- a/src/main/java/net/teumteum/user/domain/UserRepository.java +++ b/src/main/java/net/teumteum/user/domain/UserRepository.java @@ -3,7 +3,7 @@ import java.util.List; import java.util.Optional; import net.teumteum.core.security.Authenticated; -import net.teumteum.user.domain.response.UserReviewsResponse; +import net.teumteum.user.domain.response.UserReviewResponse; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -15,7 +15,8 @@ public interface UserRepository extends JpaRepository { Optional findByAuthenticatedAndOAuthId(@Param("authenticated") Authenticated authenticated, @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); + @Query("select new net.teumteum.user.domain.response.UserReviewResponse(r, count(r)) " + + "from users u join u.reviews r where u = :user group by r") + List countUserReviewsByUser(@Param("user") User user); + } diff --git a/src/main/java/net/teumteum/user/domain/response/UserReviewResponse.java b/src/main/java/net/teumteum/user/domain/response/UserReviewResponse.java new file mode 100644 index 0000000..8ec04a8 --- /dev/null +++ b/src/main/java/net/teumteum/user/domain/response/UserReviewResponse.java @@ -0,0 +1,10 @@ +package net.teumteum.user.domain.response; + +import net.teumteum.user.domain.Review; + +public record UserReviewResponse( + Review review, + long count +) { + +} diff --git a/src/main/java/net/teumteum/user/domain/response/UserReviewsResponse.java b/src/main/java/net/teumteum/user/domain/response/UserReviewsResponse.java index 7454f52..bf53ed6 100644 --- a/src/main/java/net/teumteum/user/domain/response/UserReviewsResponse.java +++ b/src/main/java/net/teumteum/user/domain/response/UserReviewsResponse.java @@ -1,10 +1,12 @@ package net.teumteum.user.domain.response; -import net.teumteum.user.domain.Review; +import java.util.List; public record UserReviewsResponse( - Review review, - long count + List reviews ) { + public static UserReviewsResponse of(List reviews) { + return new UserReviewsResponse(reviews); + } } diff --git a/src/main/java/net/teumteum/user/service/UserService.java b/src/main/java/net/teumteum/user/service/UserService.java index 8429d05..723d628 100644 --- a/src/main/java/net/teumteum/user/service/UserService.java +++ b/src/main/java/net/teumteum/user/service/UserService.java @@ -116,8 +116,10 @@ public void registerReview(Long meetingId, Long currentUserId, ReviewRegisterReq }); } - public List getUserReviews(Long userId) { - return userRepository.countUserReviewsByUserId(userId); + public UserReviewsResponse getUserReviews(Long userId) { + var user = getUser(userId); + + return UserReviewsResponse.of(userRepository.countUserReviewsByUser(user)); } public FriendsResponse findFriendsByUserId(Long userId) { diff --git a/src/test/java/net/teumteum/integration/Api.java b/src/test/java/net/teumteum/integration/Api.java index a65e350..0002b52 100644 --- a/src/test/java/net/teumteum/integration/Api.java +++ b/src/test/java/net/teumteum/integration/Api.java @@ -196,10 +196,10 @@ ResponseSpec deleteMeeting(String accessToken, Long meetingId) { .exchange(); } - ResponseSpec getUserReviews(String accessToken) { + ResponseSpec getUserReviews(Long userId, String accessToken) { return webTestClient .get() - .uri("/users/reviews") + .uri("/users/" + userId + "/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 916bb80..bb42461 100644 --- a/src/test/java/net/teumteum/integration/UserIntegrationTest.java +++ b/src/test/java/net/teumteum/integration/UserIntegrationTest.java @@ -346,10 +346,12 @@ void Get_user_reviews() { // given var existUser = repository.saveAndGetUser(); + var userId = existUser.getId(); + securityContextSetting.set(existUser.getId()); // when - var expected = api.getUserReviews(VALID_TOKEN); + var expected = api.getUserReviews(userId, VALID_TOKEN); // then Assertions.assertThat(expected.expectStatus().isOk() 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 f425e05..77afd49 100644 --- a/src/test/java/net/teumteum/unit/user/controller/UserControllerTest.java +++ b/src/test/java/net/teumteum/unit/user/controller/UserControllerTest.java @@ -37,6 +37,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.UserReviewResponse; import net.teumteum.user.domain.response.UserReviewsResponse; import net.teumteum.user.service.UserService; import org.junit.jupiter.api.BeforeEach; @@ -238,27 +239,25 @@ void Register_reviews_with_400_bad_request() throws Exception { class Get_user_reviews_api_unit { @Test - @DisplayName("로그인한 회원 id 에 해당하는 회원 리뷰와 200 OK을 반환한다.") + @DisplayName("user 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))); + .willReturn(UserReviewsResponse.of(List.of(new UserReviewResponse(별로에요, 2L), + new UserReviewResponse(최고에요, 3L)))); // when & then - mockMvc.perform(get("/users/reviews") + mockMvc.perform(get("/users/{userId}/reviews", 1) .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("별로에요")); + .andExpect(jsonPath("$.reviews", notNullValue())) + .andExpect(jsonPath("$.reviews", hasSize(2))) + .andExpect(jsonPath("$.reviews[0].count", is(2))) + .andExpect(jsonPath("$.reviews[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 f682353..66b1668 100644 --- a/src/test/java/net/teumteum/unit/user/service/UserServiceTest.java +++ b/src/test/java/net/teumteum/unit/user/service/UserServiceTest.java @@ -30,7 +30,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.domain.response.UserReviewResponse; import net.teumteum.user.service.UserService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -251,23 +251,24 @@ class Get_user_reviews_api_unit { void Return_user_reviews_with_200_ok() { // given var userId = 1L; + var existUser = UserFixture.getIdUser(); - var response = List.of(new UserReviewsResponse(최고에요, 2L) - , new UserReviewsResponse(별로에요, 3L)); + var response = List.of(new UserReviewResponse(최고에요, 2L) + , new UserReviewResponse(별로에요, 3L)); - given(userRepository.countUserReviewsByUserId(anyLong())).willReturn(response); + given(userRepository.findById(anyLong())).willReturn(Optional.of(existUser)); + given(userRepository.countUserReviewsByUser(any(User.class))).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); + assertThat(result.reviews()).hasSize(2); + assertThat(result.reviews().get(0).review()).isEqualTo(최고에요); + assertThat(result.reviews().get(0).count()).isEqualTo(2L); + assertThat(result.reviews().get(1).review()).isEqualTo(별로에요); - verify(userRepository, times(1)).countUserReviewsByUserId(anyLong()); + verify(userRepository, times(1)).countUserReviewsByUser(any(User.class)); } } } diff --git a/src/test/java/net/teumteum/user/domain/UserRepositoryTest.java b/src/test/java/net/teumteum/user/domain/UserRepositoryTest.java index 59d607b..a046f11 100644 --- a/src/test/java/net/teumteum/user/domain/UserRepositoryTest.java +++ b/src/test/java/net/teumteum/user/domain/UserRepositoryTest.java @@ -7,7 +7,7 @@ import jakarta.persistence.EntityManager; import java.util.Optional; import net.teumteum.core.config.AppConfig; -import net.teumteum.user.domain.response.UserReviewsResponse; +import net.teumteum.user.domain.response.UserReviewResponse; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -85,13 +85,14 @@ void Count_user_reviews_by_user_id() { entityManager.clear(); // when - var result = userRepository.countUserReviewsByUserId(id); + var result = userRepository.countUserReviewsByUser(existUser); // then Assertions.assertThat(result) .isNotEmpty() .hasSize(3) - .extracting(UserReviewsResponse::review, UserReviewsResponse::count) + .extracting(UserReviewResponse::review, + UserReviewResponse::count) .contains( Assertions.tuple(최고에요, 3L), Assertions.tuple(별로에요, 1L), From d7f3a3e44c890ba7731e0d455c12e45646407ca8 Mon Sep 17 00:00:00 2001 From: ddingmin Date: Wed, 14 Feb 2024 22:58:46 +0900 Subject: [PATCH 04/11] =?UTF-8?q?docs:=20readme=20=EC=9E=91=EC=84=B1=20(#2?= =?UTF-8?q?22)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index af97483..7fd4c45 100644 --- a/README.md +++ b/README.md @@ -1 +1,38 @@ -# tmtm-server \ No newline at end of file + + +# Teum-Teum + +> 사람 사이의 **틈**을 이어주는 IT 커리어 네트워킹 서비스 `repo:server` + +[![download](https://img.shields.io/badge/playstore-download-brightgreen?style=for-the-badge&logo=google&logoColor=white&color=36B2FF)](https://play.google.com/store/apps/details?id=com.teumteum.teumteum&pcampaignid=web_share) ![Build](https://img.shields.io/github/actions/workflow/status/depromeet/teum-teum-server/integration-tester.yml?branch=develop&style=for-the-badge&logo=github&logoColor=white&color=36B2FF) ![](https://img.shields.io/sonar/test_success_density/depromeet_teum-teum-server?server=https%3A%2F%2Fsonarcloud.io&style=for-the-badge&logo=sonar&logoColor=white&color=36B2FF) ![](https://img.shields.io/sonar/quality_gate/depromeet_teum-teum-server/develop?server=https%3A%2F%2Fsonarcloud.io&style=for-the-badge&logo=sonar&logoColor=white&color=36B2FF) ![](https://img.shields.io/github/v/release/depromeet/teum-teum-server?include_prereleases&style=for-the-badge&color=36B2FF) + +## 👋 Introduction + +![Behance](https://img.shields.io/badge/Behance-1769ff?style=for-the-badge&logo=behance&logoColor=white) + +## 🌐 Architecture + +## ⚒️ Tech Stack + + ![SpringBoot](https://img.shields.io/badge/springboot-%236DB33F.svg?style=for-the-badge&logo=springboot&logoColor=white)![spring_data_JPA](https://img.shields.io/badge/spring_data_JPA-%236DB33F?style=for-the-badge&logo=databricks&logoColor=white)![SpringSecurity](https://img.shields.io/badge/spring_security-%236DB33F.svg?style=for-the-badge&logo=springsecurity&logoColor=white)![Gradle](https://img.shields.io/badge/Gradle-02303A.svg?style=for-the-badge&logo=Gradle&logoColor=white) + + ![junit5](https://img.shields.io/badge/junit5-25A162?style=for-the-badge&logo=junit5&logoColor=white)![test_containers](https://img.shields.io/badge/test_containers-328ba3?style=for-the-badge&logo=reasonstudios&logoColor=white) + + ![MySQL](https://img.shields.io/badge/mysql-4479A1.svg?style=for-the-badge&logo=mysql&logoColor=white)![Redis](https://img.shields.io/badge/redis-%23DD0031.svg?style=for-the-badge&logo=redis&logoColor=white)![Firebase](https://img.shields.io/badge/Firebase-039BE5?style=for-the-badge&logo=Firebase&logoColor=white)![flyway](https://img.shields.io/badge/flyway-CC0200?style=for-the-badge&logo=flyway&logoColor=white) + + ![Amazon Ec2](https://img.shields.io/badge/amazon_ec2-FF9900.svg?style=for-the-badge&logo=amazonec2&logoColor=white)![Amazon S3](https://img.shields.io/badge/AWS_S3-569A31.svg?style=for-the-badge&logo=amazons3&logoColor=white)![Amazon RDS](https://img.shields.io/badge/amazon_RDS-527FFF.svg?style=for-the-badge&logo=amazonrds&logoColor=white)![Amazon ElastiCache](https://img.shields.io/badge/amazon_elasticache-FF9900.svg?style=for-the-badge&logo=amazondocumentdb&logoColor=white)![Nginx](https://img.shields.io/badge/nginx-%23009639.svg?style=for-the-badge&logo=nginx&logoColor=white) + + ![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)![GitHub Actions](https://img.shields.io/badge/github%20actions-%232671E5.svg?style=for-the-badge&logo=githubactions&logoColor=white)![github container](https://img.shields.io/badge/github_container-181717.svg?style=for-the-badge&logo=github&logoColor=white)![squarespace](https://img.shields.io/badge/squarespace-000000?style=for-the-badge&logo=squarespace&logoColor=white) + + ![SonarCloud](https://img.shields.io/badge/SonarCloud-F3702A?style=for-the-badge&logo=SonarCloud&logoColor=white) + + ![ChatGPT](https://img.shields.io/badge/chatGPT-74aa9c?style=for-the-badge&logo=openai&logoColor=white) + + ![sentry](https://img.shields.io/badge/sentry-362D59?style=for-the-badge&logo=sentry&logoColor=white) + +## 👥 Members + +| Server | Server | Server | +|:----------------------------------------------------------------------------------------------------------------------------------------:|:----------------------------------------------------------------------------------------------------------------------------------------:|:----------------------------------------------------------------------------------------------------------------------------------------:| +| | | | +| [choidongkuen](https://github.com/choidongkuen) | [xb205](https://github.com/devxb) | [ddingmin](https://github.com/ddingmin) | From f5603e0cd6c42bf2f038f46308b982b7add87009 Mon Sep 17 00:00:00 2001 From: ddingmin Date: Wed, 14 Feb 2024 22:59:33 +0900 Subject: [PATCH 05/11] =?UTF-8?q?docs:=20readme=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7fd4c45..d2991fb 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,31 @@ [![download](https://img.shields.io/badge/playstore-download-brightgreen?style=for-the-badge&logo=google&logoColor=white&color=36B2FF)](https://play.google.com/store/apps/details?id=com.teumteum.teumteum&pcampaignid=web_share) ![Build](https://img.shields.io/github/actions/workflow/status/depromeet/teum-teum-server/integration-tester.yml?branch=develop&style=for-the-badge&logo=github&logoColor=white&color=36B2FF) ![](https://img.shields.io/sonar/test_success_density/depromeet_teum-teum-server?server=https%3A%2F%2Fsonarcloud.io&style=for-the-badge&logo=sonar&logoColor=white&color=36B2FF) ![](https://img.shields.io/sonar/quality_gate/depromeet_teum-teum-server/develop?server=https%3A%2F%2Fsonarcloud.io&style=for-the-badge&logo=sonar&logoColor=white&color=36B2FF) ![](https://img.shields.io/github/v/release/depromeet/teum-teum-server?include_prereleases&style=for-the-badge&color=36B2FF) -## 👋 Introduction +--- + +

+ MOBILE_1 + MOBILE_2 +

+

+ MOBILE_3 + MOBILE_4 +

+

+ MOBILE_5 + MOBILE_6 +

+

+ MOBILE_7 + MOBILE_8 +

![Behance](https://img.shields.io/badge/Behance-1769ff?style=for-the-badge&logo=behance&logoColor=white) ## 🌐 Architecture +

+ +

## ⚒️ Tech Stack @@ -30,7 +50,7 @@ ![sentry](https://img.shields.io/badge/sentry-362D59?style=for-the-badge&logo=sentry&logoColor=white) -## 👥 Members +## 👥 Contributors | Server | Server | Server | |:----------------------------------------------------------------------------------------------------------------------------------------:|:----------------------------------------------------------------------------------------------------------------------------------------:|:----------------------------------------------------------------------------------------------------------------------------------------:| From e3552eb54a2b978b76f63b536e177ab253785217 Mon Sep 17 00:00:00 2001 From: ddingmin Date: Wed, 14 Feb 2024 23:49:18 +0900 Subject: [PATCH 06/11] =?UTF-8?q?docs:=20readme=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index d2991fb..31a0898 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ > 사람 사이의 **틈**을 이어주는 IT 커리어 네트워킹 서비스 `repo:server` -[![download](https://img.shields.io/badge/playstore-download-brightgreen?style=for-the-badge&logo=google&logoColor=white&color=36B2FF)](https://play.google.com/store/apps/details?id=com.teumteum.teumteum&pcampaignid=web_share) ![Build](https://img.shields.io/github/actions/workflow/status/depromeet/teum-teum-server/integration-tester.yml?branch=develop&style=for-the-badge&logo=github&logoColor=white&color=36B2FF) ![](https://img.shields.io/sonar/test_success_density/depromeet_teum-teum-server?server=https%3A%2F%2Fsonarcloud.io&style=for-the-badge&logo=sonar&logoColor=white&color=36B2FF) ![](https://img.shields.io/sonar/quality_gate/depromeet_teum-teum-server/develop?server=https%3A%2F%2Fsonarcloud.io&style=for-the-badge&logo=sonar&logoColor=white&color=36B2FF) ![](https://img.shields.io/github/v/release/depromeet/teum-teum-server?include_prereleases&style=for-the-badge&color=36B2FF) +![Build](https://img.shields.io/github/actions/workflow/status/depromeet/teum-teum-server/integration-tester.yml?branch=develop&style=for-the-badge&logo=github&logoColor=white&color=36B2FF) ![](https://img.shields.io/sonar/test_success_density/depromeet_teum-teum-server?server=https%3A%2F%2Fsonarcloud.io&style=for-the-badge&logo=sonar&logoColor=white&color=36B2FF) ![](https://img.shields.io/sonar/quality_gate/depromeet_teum-teum-server/develop?server=https%3A%2F%2Fsonarcloud.io&style=for-the-badge&logo=sonar&logoColor=white&color=36B2FF) ![](https://img.shields.io/github/v/release/depromeet/teum-teum-server?include_prereleases&style=for-the-badge&color=36B2FF) + +[![download](https://img.shields.io/badge/playstore-download-brightgreen?style=social&logo=googleplay&color=36B2FF)](https://play.google.com/store/apps/details?id=com.teumteum.teumteum&pcampaignid=web_share) [![instagram](https://img.shields.io/badge/instagram-click-brightgreen?style=social&logo=instagram&color=36B2FF)](https://www.instagram.com/teumteum_official/) [![behance](https://img.shields.io/badge/behance-click-brightgreen?style=social&logo=behance&color=36B2FF)](https://www.behance.net/gallery/191510163/%08TEUMTEUM-IT-Career-Growth-Networking-Service) --- @@ -25,30 +27,25 @@ MOBILE_8

-![Behance](https://img.shields.io/badge/Behance-1769ff?style=for-the-badge&logo=behance&logoColor=white) - ## 🌐 Architecture +

## ⚒️ Tech Stack - ![SpringBoot](https://img.shields.io/badge/springboot-%236DB33F.svg?style=for-the-badge&logo=springboot&logoColor=white)![spring_data_JPA](https://img.shields.io/badge/spring_data_JPA-%236DB33F?style=for-the-badge&logo=databricks&logoColor=white)![SpringSecurity](https://img.shields.io/badge/spring_security-%236DB33F.svg?style=for-the-badge&logo=springsecurity&logoColor=white)![Gradle](https://img.shields.io/badge/Gradle-02303A.svg?style=for-the-badge&logo=Gradle&logoColor=white) - - ![junit5](https://img.shields.io/badge/junit5-25A162?style=for-the-badge&logo=junit5&logoColor=white)![test_containers](https://img.shields.io/badge/test_containers-328ba3?style=for-the-badge&logo=reasonstudios&logoColor=white) - - ![MySQL](https://img.shields.io/badge/mysql-4479A1.svg?style=for-the-badge&logo=mysql&logoColor=white)![Redis](https://img.shields.io/badge/redis-%23DD0031.svg?style=for-the-badge&logo=redis&logoColor=white)![Firebase](https://img.shields.io/badge/Firebase-039BE5?style=for-the-badge&logo=Firebase&logoColor=white)![flyway](https://img.shields.io/badge/flyway-CC0200?style=for-the-badge&logo=flyway&logoColor=white) +![SpringBoot](https://img.shields.io/badge/springboot-%236DB33F.svg?style=for-the-badge&logo=springboot&logoColor=white)![spring_data_JPA](https://img.shields.io/badge/spring_data_JPA-%236DB33F?style=for-the-badge&logo=databricks&logoColor=white)![SpringSecurity](https://img.shields.io/badge/spring_security-%236DB33F.svg?style=for-the-badge&logo=springsecurity&logoColor=white) ![Gradle](https://img.shields.io/badge/Gradle-02303A.svg?style=for-the-badge&logo=Gradle&logoColor=white) - ![Amazon Ec2](https://img.shields.io/badge/amazon_ec2-FF9900.svg?style=for-the-badge&logo=amazonec2&logoColor=white)![Amazon S3](https://img.shields.io/badge/AWS_S3-569A31.svg?style=for-the-badge&logo=amazons3&logoColor=white)![Amazon RDS](https://img.shields.io/badge/amazon_RDS-527FFF.svg?style=for-the-badge&logo=amazonrds&logoColor=white)![Amazon ElastiCache](https://img.shields.io/badge/amazon_elasticache-FF9900.svg?style=for-the-badge&logo=amazondocumentdb&logoColor=white)![Nginx](https://img.shields.io/badge/nginx-%23009639.svg?style=for-the-badge&logo=nginx&logoColor=white) +![junit5](https://img.shields.io/badge/junit5-25A162?style=for-the-badge&logo=junit5&logoColor=white)![test_containers](https://img.shields.io/badge/test_containers-328ba3?style=for-the-badge&logo=reasonstudios&logoColor=white) ![gatling](https://img.shields.io/badge/gatling-FF9E2A?style=for-the-badge&logo=gatling&logoColor=white) - ![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)![GitHub Actions](https://img.shields.io/badge/github%20actions-%232671E5.svg?style=for-the-badge&logo=githubactions&logoColor=white)![github container](https://img.shields.io/badge/github_container-181717.svg?style=for-the-badge&logo=github&logoColor=white)![squarespace](https://img.shields.io/badge/squarespace-000000?style=for-the-badge&logo=squarespace&logoColor=white) +![MySQL](https://img.shields.io/badge/mysql-4479A1.svg?style=for-the-badge&logo=mysql&logoColor=white)![Redis](https://img.shields.io/badge/redis-%23DD0031.svg?style=for-the-badge&logo=redis&logoColor=white)![Firebase](https://img.shields.io/badge/Firebase-039BE5?style=for-the-badge&logo=Firebase&logoColor=white) ![flyway](https://img.shields.io/badge/flyway-CC0200?style=for-the-badge&logo=flyway&logoColor=white) - ![SonarCloud](https://img.shields.io/badge/SonarCloud-F3702A?style=for-the-badge&logo=SonarCloud&logoColor=white) +![Amazon Ec2](https://img.shields.io/badge/amazon_ec2-FF9900.svg?style=for-the-badge&logo=amazonec2&logoColor=white)![Amazon S3](https://img.shields.io/badge/AWS_S3-569A31.svg?style=for-the-badge&logo=amazons3&logoColor=white)![Amazon RDS](https://img.shields.io/badge/amazon_RDS-527FFF.svg?style=for-the-badge&logo=amazonrds&logoColor=white)![Amazon ElastiCache](https://img.shields.io/badge/amazon_elasticache-FF9900.svg?style=for-the-badge&logo=amazondocumentdb&logoColor=white)![Nginx](https://img.shields.io/badge/nginx-%23009639.svg?style=for-the-badge&logo=nginx&logoColor=white) - ![ChatGPT](https://img.shields.io/badge/chatGPT-74aa9c?style=for-the-badge&logo=openai&logoColor=white) +![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)![GitHub Actions](https://img.shields.io/badge/github%20actions-%232671E5.svg?style=for-the-badge&logo=githubactions&logoColor=white)![github container](https://img.shields.io/badge/github_container-181717.svg?style=for-the-badge&logo=github&logoColor=white) ![squarespace](https://img.shields.io/badge/squarespace-000000?style=for-the-badge&logo=squarespace&logoColor=white) - ![sentry](https://img.shields.io/badge/sentry-362D59?style=for-the-badge&logo=sentry&logoColor=white) +![SonarCloud](https://img.shields.io/badge/SonarCloud-F3702A?style=for-the-badge&logo=SonarCloud&logoColor=white) ![sentry](https://img.shields.io/badge/sentry-362D59?style=for-the-badge&logo=sentry&logoColor=white) ![ChatGPT](https://img.shields.io/badge/chatGPT-74aa9c?style=for-the-badge&logo=openai&logoColor=white) ## 👥 Contributors From 97ee24618c3c0d08e2355e46eba95ab537483bd4 Mon Sep 17 00:00:00 2001 From: devxb Date: Thu, 15 Feb 2024 21:23:51 +0900 Subject: [PATCH 07/11] =?UTF-8?q?test:=20QA=EC=9A=A9=20EndMeetingAlerted?= =?UTF-8?q?=EB=A5=BC=20=EB=B0=9C=ED=96=89=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../meeting/service/MeetingAlertPublisher.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/teumteum/meeting/service/MeetingAlertPublisher.java b/src/main/java/net/teumteum/meeting/service/MeetingAlertPublisher.java index 402c057..858650e 100644 --- a/src/main/java/net/teumteum/meeting/service/MeetingAlertPublisher.java +++ b/src/main/java/net/teumteum/meeting/service/MeetingAlertPublisher.java @@ -2,7 +2,6 @@ import java.time.LocalDateTime; import java.time.ZoneId; -import java.time.ZonedDateTime; import lombok.RequiredArgsConstructor; import net.teumteum.meeting.domain.BeforeMeetingAlerted; import net.teumteum.meeting.domain.EndMeetingAlerted; @@ -49,4 +48,21 @@ public void alertEndMeeting() { new EndMeetingAlerted(meeting.getId(), meeting.getTitle(), meeting.getParticipantUserIds()) )); } + + @Scheduled(cron = EVERY_ONE_MINUTES) + public void alertEndMeetingForQa() { + var today = LocalDateTime.now(ZoneId.of("Asia/Seoul")) + .withNano(0) + .withSecond(0) + .withMinute(0) + .withHour(0); + + var future = today.plusDays(365); + var yesterday = today.minusDays(365); + + var alertTargets = meetingRepository.findAlertMeetings(yesterday, future); + alertTargets.forEach(meeting -> eventPublisher.publishEvent( + new EndMeetingAlerted(meeting.getId(), meeting.getTitle(), meeting.getParticipantUserIds()) + )); + } } From b71556dd3601418ad883ffac94f0a17d15d289d5 Mon Sep 17 00:00:00 2001 From: xb205 <62425964+devxb@users.noreply.github.com> Date: Thu, 15 Feb 2024 23:02:12 +0900 Subject: [PATCH 08/11] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=EA=B0=80=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=EB=90=98=EC=97=88=EC=9D=84=EB=95=8C,=20?= =?UTF-8?q?=EC=95=8C=EB=9E=8C=ED=86=A0=ED=81=B0=EB=8F=84=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#2?= =?UTF-8?q?26)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 유저가 삭제되었을때, 알람토큰도 삭제하도록 수정한다 * test: ApplicationEventPublisher 바인딩 안되는 테스트 삭제 * refactor: UserDeletedEvent 핸들러 이름 변경 * test: 친구조회 테스트 수정 * refactor: code smell을 제거한다 --- .../net/teumteum/alert/app/AlertHandler.java | 6 +++++ .../alert/domain/UserAlertService.java | 8 +++++- .../user/domain/UserDeletedEvent.java | 7 +++++ .../teumteum/user/service/UserService.java | 4 +++ .../integration/UserIntegrationTest.java | 26 ------------------- .../unit/user/service/UserServiceTest.java | 25 ++---------------- 6 files changed, 26 insertions(+), 50 deletions(-) create mode 100644 src/main/java/net/teumteum/user/domain/UserDeletedEvent.java diff --git a/src/main/java/net/teumteum/alert/app/AlertHandler.java b/src/main/java/net/teumteum/alert/app/AlertHandler.java index 56b6400..9cf1e80 100644 --- a/src/main/java/net/teumteum/alert/app/AlertHandler.java +++ b/src/main/java/net/teumteum/alert/app/AlertHandler.java @@ -14,6 +14,7 @@ import net.teumteum.meeting.domain.BeforeMeetingAlerted; import net.teumteum.meeting.domain.EndMeetingAlerted; import net.teumteum.user.UserRecommended; +import net.teumteum.user.domain.UserDeletedEvent; import org.springframework.context.annotation.Profile; import org.springframework.context.event.EventListener; import org.springframework.data.util.Pair; @@ -29,6 +30,11 @@ public class AlertHandler { private final AlertService alertService; private final AlertPublisher alertPublisher; + @EventListener(UserDeletedEvent.class) + public void handleDeleteUserEvent(UserDeletedEvent userDeletedEvent) { + userAlertService.deleteAlertByUserId(userDeletedEvent.id()); + } + @Async(ALERT_EXECUTOR) @EventListener(BeforeMeetingAlerted.class) public void handleBeforeMeetingAlerts(BeforeMeetingAlerted alerted) { diff --git a/src/main/java/net/teumteum/alert/domain/UserAlertService.java b/src/main/java/net/teumteum/alert/domain/UserAlertService.java index 4c6cb18..898754b 100644 --- a/src/main/java/net/teumteum/alert/domain/UserAlertService.java +++ b/src/main/java/net/teumteum/alert/domain/UserAlertService.java @@ -19,13 +19,19 @@ public class UserAlertService { public void registerAlert(Long userId, RegisterAlertRequest registerAlertRequest) { alertRepository.findByUserId(userId) .ifPresentOrElse(userAlert -> { - throw new IllegalArgumentException("이미 토큰이 생성된 user입니다. \"" + userId +"\""); + throw new IllegalArgumentException("이미 토큰이 생성된 user입니다. \"" + userId + "\""); }, () -> { var alert = new UserAlert(null, userId, registerAlertRequest.token()); alertRepository.save(alert); }); } + @Transactional + public void deleteAlertByUserId(Long userId) { + alertRepository.findByUserId(userId).ifPresentOrElse(alertRepository::delete, () -> { + }); + } + @Transactional public void updateAlertToken(Long userId, UpdateAlertTokenRequest updateAlertTokenRequest) { var userAlert = alertRepository.findByUserIdWithLock(userId) diff --git a/src/main/java/net/teumteum/user/domain/UserDeletedEvent.java b/src/main/java/net/teumteum/user/domain/UserDeletedEvent.java new file mode 100644 index 0000000..3cf8d56 --- /dev/null +++ b/src/main/java/net/teumteum/user/domain/UserDeletedEvent.java @@ -0,0 +1,7 @@ +package net.teumteum.user.domain; + +public record UserDeletedEvent( + Long id +) { + +} diff --git a/src/main/java/net/teumteum/user/service/UserService.java b/src/main/java/net/teumteum/user/service/UserService.java index 073117b..76de43d 100644 --- a/src/main/java/net/teumteum/user/service/UserService.java +++ b/src/main/java/net/teumteum/user/service/UserService.java @@ -10,6 +10,7 @@ import net.teumteum.user.domain.BalanceGameType; import net.teumteum.user.domain.InterestQuestion; import net.teumteum.user.domain.User; +import net.teumteum.user.domain.UserDeletedEvent; import net.teumteum.user.domain.UserRepository; import net.teumteum.user.domain.WithdrawReasonRepository; import net.teumteum.user.domain.request.ReviewRegisterRequest; @@ -23,6 +24,7 @@ import net.teumteum.user.domain.response.UserRegisterResponse; import net.teumteum.user.domain.response.UserReviewsResponse; import net.teumteum.user.domain.response.UsersGetByIdResponse; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; @@ -38,6 +40,7 @@ public class UserService { private final RedisService redisService; private final JwtService jwtService; private final MeetingConnector meetingConnector; + private final ApplicationEventPublisher applicationEventPublisher; public UserGetResponse getUserById(Long userId) { var existUser = getUser(userId); @@ -86,6 +89,7 @@ public void withdraw(UserWithdrawRequest request, Long userId) { userRepository.delete(existUser); withdrawReasonRepository.saveAll(request.toEntity()); + applicationEventPublisher.publishEvent(new UserDeletedEvent(existUser.getId())); } @Transactional diff --git a/src/test/java/net/teumteum/integration/UserIntegrationTest.java b/src/test/java/net/teumteum/integration/UserIntegrationTest.java index bb42461..8b1503a 100644 --- a/src/test/java/net/teumteum/integration/UserIntegrationTest.java +++ b/src/test/java/net/teumteum/integration/UserIntegrationTest.java @@ -189,32 +189,6 @@ void Return_200_ok_with_success_make_friends() { @DisplayName("친구 조회 API는") class Find_friends_api { - @Test - @DisplayName("user의 id를 입력받으면, id에 해당하는 user의 친구 목록을 반환한다.") - void Return_friends_when_received_user_id() { - // given - var me = repository.saveAndGetUser(); - var friend1 = repository.saveAndGetUser(); - var friend2 = repository.saveAndGetUser(); - - securityContextSetting.set(me.getId()); - - api.addFriends(VALID_TOKEN, friend1.getId()); - api.addFriends(VALID_TOKEN, friend2.getId()); - - var expected = FriendsResponse.of(List.of(friend1, friend2)); - - // when - var result = api.getFriendsByUserId(VALID_TOKEN, me.getId()); - - // then - Assertions.assertThat(result.expectStatus().isOk() - .expectBody(FriendsResponse.class) - .returnResult() - .getResponseBody()) - .usingRecursiveComparison().isEqualTo(expected); - } - @Test @DisplayName("user의 id를 입력받았을때, 친구가 한명도 없다면, 빈 목록을 반환한다.") void Return_empty_friends_when_received_empty_friends_user_id() { 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 66b1668..1c5630a 100644 --- a/src/test/java/net/teumteum/unit/user/service/UserServiceTest.java +++ b/src/test/java/net/teumteum/unit/user/service/UserServiceTest.java @@ -39,9 +39,9 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.context.junit.jupiter.SpringExtension; -@ExtendWith(MockitoExtension.class) +@ExtendWith(SpringExtension.class) @DisplayName("유저 서비스 단위 테스트의") public class UserServiceTest { @@ -135,27 +135,6 @@ void If_valid_user_logout_request_return_200_OK() { @DisplayName("회원 탈퇴 API는") class Withdraw_user_api_unit { - @Test - @DisplayName("유효한 유저 회원 탈퇴 요청이 들어오는 경우, 회원을 탈퇴하고 탈퇴 사유 데이터를 저장한다.") - void If_valid_user_withdraw_request_withdraw_user() { - // given - UserWithdrawRequest request - = RequestFixture.userWithdrawRequest(List.of("쓰지 않는 앱이에요", "오류가 생겨서 쓸 수 없어요")); - - given(userRepository.findById(anyLong())) - .willReturn(Optional.ofNullable(user)); - - doNothing().when(userRepository).delete(any()); - - doNothing().when(redisService).deleteData(anyString()); - // when - userService.withdraw(request, user.getId()); - // then - verify(userRepository, times(1)).findById(anyLong()); - verify(redisService, times(1)).deleteData(anyString()); - verify(withdrawReasonRepository, times(1)).saveAll(any()); - } - @Test @DisplayName("유저 id에 해당하는 유저가 존재하지 않는 경우, 400 Bad Request 을 반환한다.") void Return_400_bad_request_if_user_is_not_exist() { From f5874bcb1dda8a3f2c47e74906c513145cec7622 Mon Sep 17 00:00:00 2001 From: xb205 <62425964+devxb@users.noreply.github.com> Date: Fri, 16 Feb 2024 12:01:28 +0900 Subject: [PATCH 09/11] =?UTF-8?q?fix:=20=EB=AA=A8=EC=9E=84=20=EC=8B=9C?= =?UTF-8?q?=EC=9E=91,=20=EC=A2=85=EB=A3=8C=20=EC=95=8C=EB=A6=BC=EC=9D=B4?= =?UTF-8?q?=20=EB=AA=A8=EC=9E=84=20=EC=B0=B8=EC=97=AC=EC=9E=90=EA=B0=80=20?= =?UTF-8?q?3=EB=AA=85=20=EC=9D=B4=EC=83=81=EC=9D=B4=20=EC=95=84=EB=8B=88?= =?UTF-8?q?=EB=9D=BC=EB=A9=B4,=20=EB=B0=9C=ED=96=89=EB=90=98=EC=A7=80?= =?UTF-8?q?=EC=95=8A=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#231)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/MeetingAlertPublisher.java | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) 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()) + )); } } From 641ee6ea5e418e2f796dd908f29a02c4b10e2b74 Mon Sep 17 00:00:00 2001 From: ChoiDongKuen Date: Fri, 16 Feb 2024 12:04:36 +0900 Subject: [PATCH 10/11] =?UTF-8?q?feat:=20=EB=A6=AC=EB=B7=B0=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=EC=8B=9C=20=EB=AA=A8=EC=9E=84=20=EC=B0=B8=EC=97=AC=20?= =?UTF-8?q?=EC=9C=A0=EC=A0=80=20=EC=A1=B0=ED=9A=8C=20API=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#229)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: MeetingController getCurrentUserId() 메소드로 통일 (#228) * feat: 리뷰에 자신 포함 확인 로직 추가 (#195) * feat: 모임 참여자 자신 포함 여부 확인 로직 추가(#228) * test: 단위 테스트 추가 (#228) * test: 통합 테스트 수정 및 실패 케이스 추가 (#228) --- .../meeting/controller/MeetingController.java | 31 +++-- ...e.java => MeetingParticipantResponse.java} | 6 +- .../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 +- 9 files changed, 327 insertions(+), 67 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/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/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; From 9cffa3392069f8080193e5b6388a4c5f146f70f4 Mon Sep 17 00:00:00 2001 From: devxb Date: Fri, 16 Feb 2024 12:07:56 +0900 Subject: [PATCH 11/11] =?UTF-8?q?refactor:=20title,=20body=20alert=20data?= =?UTF-8?q?=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/net/teumteum/alert/infra/FcmAlertPublisher.java | 2 ++ 1 file changed, 2 insertions(+) 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())