From 229a6570ca5b205997bdd6edd836bcf21b0564c9 Mon Sep 17 00:00:00 2001 From: Mint Date: Thu, 22 Feb 2024 21:49:14 +0900 Subject: [PATCH 1/4] [#73] feat: Implement Meeting Integration tests --- .../meeting/dto/MeetingCreateRequest.java | 12 + .../annotation/WithMockCustomUser.java | 18 ++ ...hMockCustomUserSecurityContextFactory.java | 37 +++ .../integration/MeetingIntegrationTest.java | 231 ++++++++++++++++++ 4 files changed, 298 insertions(+) create mode 100644 src/test/java/org/dnd/timeet/common/security/annotation/WithMockCustomUser.java create mode 100644 src/test/java/org/dnd/timeet/common/security/annotation/WithMockCustomUserSecurityContextFactory.java create mode 100644 src/test/java/org/dnd/timeet/meeting/integration/MeetingIntegrationTest.java diff --git a/src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateRequest.java b/src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateRequest.java index 375a778..33af638 100644 --- a/src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateRequest.java +++ b/src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateRequest.java @@ -4,6 +4,7 @@ import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; import java.time.LocalTime; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -43,6 +44,17 @@ public class MeetingCreateRequest { @Schema(description = "썸네일 이미지 번호", example = "1") private Integer imageNum; + @Builder + public MeetingCreateRequest(String title, String location, LocalDateTime startTime, String description, + LocalTime estimatedTotalDuration, Integer imageNum) { + this.title = title; + this.location = location; + this.startTime = startTime; + this.description = description; + this.estimatedTotalDuration = estimatedTotalDuration; + this.imageNum = imageNum; + } + public Meeting toEntity(Member member) { return Meeting.builder() .hostMember(member) diff --git a/src/test/java/org/dnd/timeet/common/security/annotation/WithMockCustomUser.java b/src/test/java/org/dnd/timeet/common/security/annotation/WithMockCustomUser.java new file mode 100644 index 0000000..d827a79 --- /dev/null +++ b/src/test/java/org/dnd/timeet/common/security/annotation/WithMockCustomUser.java @@ -0,0 +1,18 @@ +package org.dnd.timeet.common.security.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import org.springframework.security.test.context.support.WithSecurityContext; + +@Retention(RetentionPolicy.RUNTIME) +@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class) +public @interface WithMockCustomUser { + + String username() default "1"; + + String role() default "USER"; + + +} + + diff --git a/src/test/java/org/dnd/timeet/common/security/annotation/WithMockCustomUserSecurityContextFactory.java b/src/test/java/org/dnd/timeet/common/security/annotation/WithMockCustomUserSecurityContextFactory.java new file mode 100644 index 0000000..c51d6d3 --- /dev/null +++ b/src/test/java/org/dnd/timeet/common/security/annotation/WithMockCustomUserSecurityContextFactory.java @@ -0,0 +1,37 @@ +package org.dnd.timeet.common.security.annotation; + +import java.util.HashSet; +import org.dnd.timeet.common.security.CustomUserDetails; +import org.dnd.timeet.member.domain.Member; +import org.dnd.timeet.member.domain.MemberRole; +import org.dnd.timeet.oauth.OAuth2Provider; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.context.support.WithSecurityContextFactory; + +public class WithMockCustomUserSecurityContextFactory implements WithSecurityContextFactory { + + @Override + public SecurityContext createSecurityContext(WithMockCustomUser customUser) { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + + Member member = Member.builder() + .role(MemberRole.ROLE_USER) + .name("Test User") + .imageUrl("http://example.com/image.jpg") + .oauthId("oauth123") + .provider(OAuth2Provider.KAKAO) + .fcmToken("fcmToken123") + .imageNum(5) + .participations(new HashSet<>()) + .build(); + + CustomUserDetails userDetails = new CustomUserDetails(member); + Authentication auth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + context.setAuthentication(auth); + + return context; + } +} diff --git a/src/test/java/org/dnd/timeet/meeting/integration/MeetingIntegrationTest.java b/src/test/java/org/dnd/timeet/meeting/integration/MeetingIntegrationTest.java new file mode 100644 index 0000000..9b750db --- /dev/null +++ b/src/test/java/org/dnd/timeet/meeting/integration/MeetingIntegrationTest.java @@ -0,0 +1,231 @@ +package org.dnd.timeet.meeting.integration; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.time.LocalDateTime; +import java.time.LocalTime; +import org.dnd.timeet.common.security.annotation.WithMockCustomUser; +import org.dnd.timeet.meeting.application.MeetingService; +import org.dnd.timeet.meeting.dto.MeetingCreateRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) +@AutoConfigureMockMvc +@ActiveProfiles("test") +@DisplayName("[API][Integration] 회의 API 테스트") +class MeetingIntegrationTest { + + @Autowired + MockMvc mvc; + + @Autowired + private ObjectMapper om; + + @Autowired + MeetingService meetingService; + + @Test + @WithMockUser(username = "1", roles = "USER") + @DisplayName("[GET] 타이머 조회 API 테스트") + void getTimers() throws Exception { + + + ResultActions perform = mvc.perform( + get("/api/v1/timers") + .contentType(MediaType.APPLICATION_JSON) + ); + perform + .andExpect(status().isOk()) // 201 Created 상태 코드가 반환되는지 확인 + .andDo(print()); // 요청/응답 로그를 출력합니다. + + } + + @Test + @WithMockCustomUser + @DisplayName("[POST] 회의 생성 API 테스트") + void createMeeting() throws Exception { + // given + MeetingCreateRequest meetingCreateRequest = MeetingCreateRequest.builder() + .title("Test Meeting") + .location("Test Location") + .startTime(LocalDateTime.now()) + .description("Test Description") + .estimatedTotalDuration(LocalTime.of(3, 20, 0)) + .imageNum(5) + .build(); + String requestBody = om.writeValueAsString(meetingCreateRequest); + + // when + ResultActions perform = mvc.perform( + post("/api/meetings") + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody) + ); + + // then + perform + .andExpect(status().isOk()) + .andDo(print()); + } + + @Test + @WithMockCustomUser + @DisplayName("[POST] 회의 참가 API 테스트") + void attendMeeting() throws Exception { + // given + + // when + ResultActions perform = mvc.perform( + post("/api/meetings/2/attend") + .contentType(MediaType.APPLICATION_JSON) + ); + + // then + perform + .andExpect(status().isOk()) + .andDo(print()); + } + + @Test + @WithMockCustomUser + @DisplayName("[PATCH] 회의 종료 API 테스트 : 실패 - 방장이 아닐 경우") + void closeMeeting() throws Exception { + // given + + // when + ResultActions perform = mvc.perform( + patch("/api/meetings/2/end") + .contentType(MediaType.APPLICATION_JSON) + ); + + // then + perform + .andExpect(status().isForbidden()) + .andDo(print()); + } + + @Test + @WithMockCustomUser + @DisplayName("[GET] 단일 회의 조회 API 테스트") + void getTimerById() throws Exception { + // given + + // when + ResultActions perform = mvc.perform( + get("/api/meetings/2") + .contentType(MediaType.APPLICATION_JSON) + ); + + // then + perform + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(jsonPath("$.success").value(true)); + // MEMO : 테스트 DB에 따라 반환되는 값이 달라질 수 있음 +// .andExpect(jsonPath("$.response.description").value("2개의 사안 모두 해결하기")) +// .andExpect(jsonPath("$.response.meetingStatus").value("COMPLETED")) +// .andExpect(jsonPath("$.response.hostMemberId").value(1)) +// .andExpect(jsonPath("$.response.startTime").value("2024-02-28T07:33:01")) +// .andExpect(jsonPath("$.response.totalEstimatedDuration").value("02:00:00")) +// .andExpect(jsonPath("$.response.imgNum").value(1)); + } + + // TODO : 웹소켓 테스트 + @Test + void getCurrentDuration() { + + } + + @Test + @WithMockCustomUser + @DisplayName("[GET] 리포트 조회 API 테스트") + void getMeetingReport() throws Exception { + // given + + // when + ResultActions perform = mvc.perform( + get("/api/meetings/2/report") + .contentType(MediaType.APPLICATION_JSON) + ); + + // then + perform + .andExpect(status().isOk()) + .andDo(print()); + } + + @Test + @WithMockCustomUser + @DisplayName("[DELETE] 회의 삭제 API 테스트") + void deleteMeeting() throws Exception { + // given + + // when + ResultActions perform = mvc.perform( + delete("/api/meetings/2") + .contentType(MediaType.APPLICATION_JSON) + ); + + // then + perform + .andExpect(status().isNoContent()) // 204 No Content + .andDo(print()); + } + + // MEMO: JWT 토큰 정보값 문제 때문에 테스트 불가 +// @Test +// @WithMockCustomUser +// @DisplayName("[GET] 회의 참가자 조회 API 테스트") +// void getMeetingMembers() throws Exception { +// // given +// +// // when +// ResultActions perform = mvc.perform( +// get("/api/meetings/4/users") +// .contentType(MediaType.APPLICATION_JSON) +// ); +// +// // then +// perform +// .andExpect(status().isOk()) +// .andDo(print()); +// } + + // MEMO: 테스트 환경에서 JWT 토큰을 통한 인증을 시뮬레이션하기 위해 Member 객체를 생성하고 있다. + // 이 과정에서 Member 객체는 ID 없이 생성되는데, 테스트 중에 Member 객체의 ID가 필요한 경우가 있어 에러가 발생한다. +// @Test +// @WithMockCustomUser +// @DisplayName("[DELETE] 회의 나가기 API 테스트") +// void leaveMeeting() throws Exception { +// // given +// +// // when +// ResultActions perform = mvc.perform( +// delete("/api/meetings/2/leave") +// .contentType(MediaType.APPLICATION_JSON) +// ); +// +// // then +// perform +// .andExpect(status().isNoContent()) // 204 No Content +// .andDo(print()); +// } +} \ No newline at end of file From 54b7a3bee7982e6474b939bbfd2fb82c60e98ac7 Mon Sep 17 00:00:00 2001 From: Mint Date: Thu, 22 Feb 2024 22:13:18 +0900 Subject: [PATCH 2/4] [#-] style: Apply code format --- .../dnd/timeet/meeting/integration/MeetingIntegrationTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/org/dnd/timeet/meeting/integration/MeetingIntegrationTest.java b/src/test/java/org/dnd/timeet/meeting/integration/MeetingIntegrationTest.java index 9b750db..32ede34 100644 --- a/src/test/java/org/dnd/timeet/meeting/integration/MeetingIntegrationTest.java +++ b/src/test/java/org/dnd/timeet/meeting/integration/MeetingIntegrationTest.java @@ -47,7 +47,6 @@ class MeetingIntegrationTest { @DisplayName("[GET] 타이머 조회 API 테스트") void getTimers() throws Exception { - ResultActions perform = mvc.perform( get("/api/v1/timers") .contentType(MediaType.APPLICATION_JSON) From 0f9f944375dfec06cbbb7e1f55b0f60a4d10ba71 Mon Sep 17 00:00:00 2001 From: Mint Date: Fri, 23 Feb 2024 21:37:15 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[#-]=20chore:=20Jacoco=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=BB=A4=EB=B2=84=EB=A6=AC=EC=A7=80=20=ED=94=8C=EB=9F=AC?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e212ffc..7c39e8b 100644 --- a/build.gradle +++ b/build.gradle @@ -4,6 +4,7 @@ plugins { id 'io.spring.dependency-management' version '1.1.4' id "org.sonarqube" version "4.0.0.2929" id 'checkstyle' + id 'jacoco' } group = 'org.dnd' @@ -65,4 +66,36 @@ sonar { checkstyle { toolVersion '10.13.0' configFile file("${project.rootDir}/config/checkstyle-config.xml") -} \ No newline at end of file +} + +jacoco { + // JaCoCo 버전 + toolVersion = '0.8.11' + +} + +jacocoTestReport { + reports { + html { + required.set(true) + outputLocation.set(file("build/reports/jacocoHtml")) + } + xml.required.set(false) + csv.required.set(false) + } +} + +jacocoTestCoverageVerification { + + violationRules { // 커버리지의 범위와 퍼센테이지를 설정 + rule { + element = 'CLASS' + + limit { + counter = 'BRANCH' + value = 'COVEREDRATIO' + minimum = 0.30 + } + } + } +} From 1780b39b7efeabdeb8b9dbc55700fe639dab955b Mon Sep 17 00:00:00 2001 From: Mint Date: Fri, 23 Feb 2024 21:46:53 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[#-]=20chore:=20Jacoco=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=BB=A4=EB=B2=84=EB=A6=AC=EC=A7=80=20=ED=94=8C=EB=9F=AC?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e212ffc..845b851 100644 --- a/build.gradle +++ b/build.gradle @@ -4,6 +4,7 @@ plugins { id 'io.spring.dependency-management' version '1.1.4' id "org.sonarqube" version "4.0.0.2929" id 'checkstyle' + id 'jacoco' } group = 'org.dnd' @@ -65,4 +66,34 @@ sonar { checkstyle { toolVersion '10.13.0' configFile file("${project.rootDir}/config/checkstyle-config.xml") -} \ No newline at end of file +} + +jacoco { + toolVersion = '0.8.11' +} + +jacocoTestReport { + reports { + html { + required.set(true) + outputLocation.set(file("build/reports/jacocoHtml")) + } + xml.required.set(false) + csv.required.set(false) + } +} + +jacocoTestCoverageVerification { + + violationRules { + rule { + element = 'CLASS' + + limit { + counter = 'BRANCH' + value = 'COVEREDRATIO' + minimum = 0.30 // 30% 이상 커버리지가 되어야 빌드 성공 + } + } + } +}