Skip to content

Commit

Permalink
Merge pull request #76 from dnd-side-project/dev
Browse files Browse the repository at this point in the history
Dev -> Main
  • Loading branch information
FacerAin authored Mar 17, 2024
2 parents a13cdca + b38b4fb commit 1af9337
Show file tree
Hide file tree
Showing 5 changed files with 329 additions and 1 deletion.
33 changes: 32 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -65,4 +66,34 @@ sonar {
checkstyle {
toolVersion '10.13.0'
configFile file("${project.rootDir}/config/checkstyle-config.xml")
}
}

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% 이상 커버리지가 되어야 빌드 성공
}
}
}
}
12 changes: 12 additions & 0 deletions src/main/java/org/dnd/timeet/meeting/dto/MeetingCreateRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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";


}


Original file line number Diff line number Diff line change
@@ -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<WithMockCustomUser> {

@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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
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());
// }
}

0 comments on commit 1af9337

Please sign in to comment.