diff --git a/build.gradle b/build.gradle index 2640278..e3e0a39 100644 --- a/build.gradle +++ b/build.gradle @@ -35,6 +35,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation "org.springframework.restdocs:spring-restdocs-mockmvc:3.0.0" // Lombok compileOnly 'org.projectlombok:lombok' diff --git a/src/main/java/com/codiary/backend/domain/member/controller/MemberController.java b/src/main/java/com/codiary/backend/domain/member/controller/MemberController.java index f81f846..a09c2bf 100644 --- a/src/main/java/com/codiary/backend/domain/member/controller/MemberController.java +++ b/src/main/java/com/codiary/backend/domain/member/controller/MemberController.java @@ -55,7 +55,7 @@ public ApiResponse getProfileImage(@PathVariab @Operation(summary = "사용자 프로필 기본 정보 조회", description = "마이페이지 사용자 정보 조회 기능") public ApiResponse getUserProfile(@AuthenticationPrincipal CustomMemberDetails memberDetails, @PathVariable(value = "member_id") Long memberId){ Member currentMember = memberQueryService.getUserInfo(memberDetails.getId()); - Member user = memberQueryService.getUserProfile(memberId); + Member user = memberQueryService.getUserInfo(memberId); return ApiResponse.onSuccess(SuccessStatus.MEMBER_OK, MemberConverter.toSimpleMemberResponseDto(currentMember, user)); } diff --git a/src/main/java/com/codiary/backend/domain/member/entity/Member.java b/src/main/java/com/codiary/backend/domain/member/entity/Member.java index e4caa8a..dbc1187 100644 --- a/src/main/java/com/codiary/backend/domain/member/entity/Member.java +++ b/src/main/java/com/codiary/backend/domain/member/entity/Member.java @@ -143,4 +143,8 @@ public void setImage(MemberImage image) { public void setTeamMemberList(List teamMembers) { this.teamMemberList = teamMembers; } + + public void setMemberId(Long id) { this.memberId = id; } + + public void setEmail(String mail) { this.email = mail; } } diff --git a/src/main/java/com/codiary/backend/domain/member/service/MemberQueryService.java b/src/main/java/com/codiary/backend/domain/member/service/MemberQueryService.java index ef39f54..4c34b1c 100644 --- a/src/main/java/com/codiary/backend/domain/member/service/MemberQueryService.java +++ b/src/main/java/com/codiary/backend/domain/member/service/MemberQueryService.java @@ -32,12 +32,6 @@ public ApiResponse getProfileImage(Long member return ApiResponse.onSuccess(SuccessStatus.MEMBER_OK, memberRepository.findProfileImageUrl(memberId)); } - public Member getUserProfile(Long memberId) { - Member user = memberRepository.findMemberWithTechStacksAndProjectsAndTeam(memberId) - .orElseThrow(() -> new GeneralException(ErrorStatus.MEMBER_NOT_FOUND)); - return user; - } - public Member getUserInfo(Long memberId) { Member user = memberRepository.findMemberWithTechStacksAndProjectsAndTeam(memberId) .orElseThrow(() -> new GeneralException(ErrorStatus.MEMBER_NOT_FOUND)); diff --git a/src/main/java/com/codiary/backend/global/apiPayload/exception/GeneralException.java b/src/main/java/com/codiary/backend/global/apiPayload/exception/GeneralException.java index 752dba1..49714a5 100644 --- a/src/main/java/com/codiary/backend/global/apiPayload/exception/GeneralException.java +++ b/src/main/java/com/codiary/backend/global/apiPayload/exception/GeneralException.java @@ -6,7 +6,6 @@ import lombok.Getter; @Getter -@AllArgsConstructor public class GeneralException extends RuntimeException { private BaseErrorCode code; @@ -16,5 +15,9 @@ public ErrorReasonDTO getErrorReason() { public ErrorReasonDTO getErrorReasonHttpStatus(){ return this.code.getReasonHttpStatus(); } + public GeneralException(BaseErrorCode code) { + super(code.getReason().getMessage()); + this.code = code; + } } diff --git a/src/test/java/com/codiary/backend/domain/member/controller/MemberControllerTest.java b/src/test/java/com/codiary/backend/domain/member/controller/MemberControllerTest.java new file mode 100644 index 0000000..ccaf41e --- /dev/null +++ b/src/test/java/com/codiary/backend/domain/member/controller/MemberControllerTest.java @@ -0,0 +1,84 @@ +package com.codiary.backend.domain.member.controller; + +import com.codiary.backend.domain.member.entity.Member; +import com.codiary.backend.domain.member.util.MemberUtilTest; +import com.codiary.backend.global.apiPayload.code.status.ErrorStatus; +import com.codiary.backend.global.util.ControllerTest; +import org.junit.jupiter.api.BeforeEach; +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.junit.jupiter.MockitoExtension; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.restdocs.RestDocumentationExtension; + +import java.util.Optional; + +import static org.mockito.BDDMockito.given; +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.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; + +@SpringBootTest +@ExtendWith({MockitoExtension.class, RestDocumentationExtension.class}) +@AutoConfigureMockMvc +@DisplayName("MemberController 테스트") +public class MemberControllerTest extends ControllerTest { + private Member member2; + + @BeforeEach + void setUp() { + member2 = MemberUtilTest.createMember2(); + member2.setMemberId(2L); + } + + @Nested + @DisplayName("사용자 기본 프로필 조회 메서드 테스트") + class GetUserProfileTest { + + @Test + @DisplayName("✅ member_id가 주어지면 해당하는 사용자 정보를 조회한다.") + void getUserProfile_ShouldReturnUserProfile() throws Exception { + // given: 조회하는 사용자 정보와 accessToken, 조회 대상 사용자의 정보 Mocking + String accessToken = jwtTokenProvider.generateToken(member1.getEmail()).getAccessToken(); + given(memberRepository.findMemberWithTechStacksAndProjectsAndTeam(member1.getMemberId())) + .willReturn(Optional.of(member1)); + given(memberRepository.findMemberWithTechStacksAndProjectsAndTeam(member2.getMemberId())) + .willReturn(Optional.of(member2)); + + // when & then: 사용자 정보 조회 API 호출 시 조회 대상 사용자의 정보 반환 + mockMvc.perform(get("/api/v2/member/profile/{member_id}", member2.getMemberId()) + .header("Authorization", "Bearer " + accessToken)) + .andDo(print()) + .andExpect(status().isOk()) // HTTP status 200 OK 응답 검증 + .andExpect(jsonPath("$.isSuccess").value(true)) // isSuccess 값 검증 + .andExpect(jsonPath("$.code").value("MEMBER_1000")) // code 값 검증 + .andExpect(jsonPath("$.message").value("성공입니다.")) // message 값 검증 + .andExpect(jsonPath("$.result.current_member_id").value(member1.getMemberId())) // current_member_id 값 검증 + .andExpect(jsonPath("$.result.user_id").value(member2.getMemberId())) // user_id 값 검증 + .andExpect(jsonPath("$.result.user_name").value(member2.getNickname())); // user_name 값 검증 + } + + @Test + @DisplayName("❌ 존재하지 않는 사용자 프로필 조회 시 예외가 발생한다.") + void getUserProfile_ShouldReturnError_WhenUserNotFound() throws Exception { + // given: 존재하지 않는 사용자 ID를 조회하는 경우 + Long nonExistentMemberId = 999L; + String accessToken = jwtTokenProvider.generateToken(member1.getEmail()).getAccessToken(); + given(memberRepository.findMemberWithTechStacksAndProjectsAndTeam(nonExistentMemberId)) + .willReturn(Optional.empty()); + + // when & then: 존재하지 않는 사용자 ID로 사용자 정보 조회 시 오류 발생 + mockMvc.perform(get("/api/v2/member/profile/{member_id}", nonExistentMemberId) + .header("Authorization", "Bearer " + accessToken)) + .andDo(print()) + .andExpect(status().isBadRequest()) // HTTP status 400 Bad Request + .andExpect(jsonPath("$.isSuccess").value(false)) // 실패 여부 확인 + .andExpect(jsonPath("$.code").value(ErrorStatus.MEMBER_NOT_FOUND.getCode())) // MEMBER_1001 코드 확인 + .andExpect(jsonPath("$.message").value(ErrorStatus.MEMBER_NOT_FOUND.getMessage())); // 실패 메시지 확인 + } + } +} \ No newline at end of file diff --git a/src/test/java/com/codiary/backend/domain/MemberTest.java b/src/test/java/com/codiary/backend/domain/member/entity/MemberTest.java similarity index 97% rename from src/test/java/com/codiary/backend/domain/MemberTest.java rename to src/test/java/com/codiary/backend/domain/member/entity/MemberTest.java index db5a7be..ef85d4b 100644 --- a/src/test/java/com/codiary/backend/domain/MemberTest.java +++ b/src/test/java/com/codiary/backend/domain/member/entity/MemberTest.java @@ -1,4 +1,4 @@ -package com.codiary.backend.domain; +package com.codiary.backend.domain.member.entity; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; diff --git a/src/test/java/com/codiary/backend/domain/member/service/MemberQueryServiceTest.java b/src/test/java/com/codiary/backend/domain/member/service/MemberQueryServiceTest.java new file mode 100644 index 0000000..1bbb198 --- /dev/null +++ b/src/test/java/com/codiary/backend/domain/member/service/MemberQueryServiceTest.java @@ -0,0 +1,74 @@ +package com.codiary.backend.domain.member.service; + +import com.codiary.backend.domain.member.entity.Member; +import com.codiary.backend.domain.member.repository.MemberRepository; +import com.codiary.backend.domain.member.util.MemberUtilTest; +import com.codiary.backend.global.apiPayload.code.status.ErrorStatus; +import com.codiary.backend.global.apiPayload.exception.GeneralException; +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; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@DisplayName("MemberQueryService 유닛테스트") +@ExtendWith(MockitoExtension.class) +public class MemberQueryServiceTest { + @InjectMocks + private MemberQueryService memberQueryService; + + @Mock + private MemberRepository memberRepository; + + private final Member member1 = MemberUtilTest.createMember1(); + private final Member member2 = MemberUtilTest.createMember2(); + + @Nested + @DisplayName("사용자 기본 정보 조회 메서드 테스트") + class GetUserInfoTest { + + @Test + @DisplayName("✅ memberId가 주어지면 해당 사용자를 반환한다.") + void shouldReturnMemberWhenValidIdGiven() { + // given + Long memberId = member1.getMemberId(); + given(memberRepository.findMemberWithTechStacksAndProjectsAndTeam(memberId)) + .willReturn(Optional.of(member1)); // Member1 반환하도록 Mocking + + // when + Member result = memberQueryService.getUserInfo(memberId); + + // then + assertThat(result).isNotNull(); + assertThat(result.getMemberId()).isEqualTo(memberId); // 반환된 Member의 memberId가 주어진 memberId와 같은지 확인 + verify(memberRepository, times(1)) + .findMemberWithTechStacksAndProjectsAndTeam(memberId); // memberRepository의 메서드가 1번 호출되었는지 확인 + } + + @Test + @DisplayName("❌ 존재하지 않는 memberId를 조회하면 예외가 발생한다.") + void shouldThrowExceptionWhenMemberNotFound() { + // given + Long invalidMemberId = 999L; + given(memberRepository.findMemberWithTechStacksAndProjectsAndTeam(invalidMemberId)) + .willReturn(Optional.empty()); // 빈 Optional 반환하도록 Mocking + + // when & then + assertThatThrownBy(() -> memberQueryService.getUserInfo(invalidMemberId)) // 예외 발생 여부 확인 + .isInstanceOf(GeneralException.class) + .hasMessageContaining(ErrorStatus.MEMBER_NOT_FOUND.getMessage()); + + verify(memberRepository, times(1)) + .findMemberWithTechStacksAndProjectsAndTeam(invalidMemberId); // memberRepository의 메서드가 1번 호출되었는지 확인 + } + } +} \ No newline at end of file diff --git a/src/test/java/com/codiary/backend/domain/member/util/MemberUtilTest.java b/src/test/java/com/codiary/backend/domain/member/util/MemberUtilTest.java new file mode 100644 index 0000000..43533b5 --- /dev/null +++ b/src/test/java/com/codiary/backend/domain/member/util/MemberUtilTest.java @@ -0,0 +1,29 @@ +package com.codiary.backend.domain.member.util; + +import com.codiary.backend.domain.member.entity.Member; + +public class MemberUtilTest { + public static Member createMember1() { + return Member.builder() + .email("codiary123@gmail.com") + .nickname("codiary1") + .password("codiary1234") + .birth("1996-01-01") + .discord("codiary#1234") + .github("codiary#1234") + .linkedin("codiary#1234") + .build(); + } + + public static Member createMember2() { + return Member.builder() + .email("codiary321@gmail.com") + .nickname("codiary2") + .password("codiary1234") + .birth("1996-01-01") + .discord("codiary#1234") + .github("codiary#1234") + .linkedin("codiary#1234") + .build(); + } +} diff --git a/src/test/java/com/codiary/backend/global/util/ControllerTest.java b/src/test/java/com/codiary/backend/global/util/ControllerTest.java new file mode 100644 index 0000000..b75bc26 --- /dev/null +++ b/src/test/java/com/codiary/backend/global/util/ControllerTest.java @@ -0,0 +1,83 @@ +package com.codiary.backend.global.util; + +import com.codiary.backend.domain.member.entity.Member; +import com.codiary.backend.domain.member.repository.MemberRepository; +import com.codiary.backend.domain.member.security.CustomMemberDetails; +import com.codiary.backend.domain.member.security.CustomMemberDetailsService; +import com.codiary.backend.domain.member.service.MemberQueryService; +import com.codiary.backend.domain.member.util.MemberUtilTest; +import com.codiary.backend.global.jwt.JwtTokenProvider; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.BDDMockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Configuration; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.filter.CharacterEncodingFilter; + +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; + +@ExtendWith({MockitoExtension.class, RestDocumentationExtension.class}) +@AutoConfigureMockMvc +public abstract class ControllerTest { + @Autowired + protected MockMvc mockMvc; + + @Autowired + WebApplicationContext wac; + + @Autowired + public MemberQueryService memberQueryService; + + @Autowired + public JwtTokenProvider jwtTokenProvider; + + @MockBean + protected MemberRepository memberRepository; + + @MockBean + protected CustomMemberDetailsService customMemberDetailsService; + + public Member member1; + + @BeforeEach + void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) { + this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) + .addFilter(new CharacterEncodingFilter("UTF-8", true)) + .apply(documentationConfiguration(restDocumentation)) + .build(); + + // member1 생성 및 설정 + member1 = MemberUtilTest.createMember1(); + member1.setMemberId(1L); + + // `MemberRepository` 모킹하여 member1의 이메일에 해당하는 사용자를 반환하도록 설정 + given(memberRepository.findByEmail(member1.getEmail())).willReturn(Optional.of(member1)); + + // `CustomMemberDetails` 생성 및 모킹 + CustomMemberDetails customMemberDetails = new CustomMemberDetails( + member1.getEmail(), member1.getMemberId(), member1.getPassword(), null); + given(customMemberDetailsService.loadUserByUsername(anyString())) + .willReturn(customMemberDetails); + + // SecurityContext에 CustomMemberDetails 설정 + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(new UsernamePasswordAuthenticationToken( + customMemberDetails, null, customMemberDetails.getAuthorities())); + SecurityContextHolder.setContext(securityContext); + } +} \ No newline at end of file