From 9b16776b9cc729eb127ca18fff823270e657b5a6 Mon Sep 17 00:00:00 2001 From: Jeong Wonju Date: Wed, 4 Dec 2024 15:14:00 +0900 Subject: [PATCH] =?UTF-8?q?MATE-86=20:=20[FEAT]=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=ED=9A=8C=EC=9B=90=20URI=20=EC=88=98=EC=A0=95=20(#7?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * MATE-86 : [CHORE] application-common.yml 네이버 키 수정 * MATE-86 : [REFACTOR] 네이버 로그인 관련 URI 수정 * MATE-86 : [FEAT] 회원 권한 에러 처리 추가 * MATE-86 : [FEAT] 네이버 state 설정 추가 * MATE-86 : [FEAT] 테스트에서 회원 인증 AuthMember 설정 * MATE-86 : [FEAT] 프로필 컨트롤러 URI 변경 * MATE-86 : [REFACTOR] 회원 컨트롤러 URI 수정 및 로그인 후 토큰 발급 기능 추가 * MATE-86 : [REFACTOR] 팔로우 컨트롤러 URI 변경 * MATE-86 : [CHORE] 크롤링 매 분 실행 자동화 테스트 주석 해제 --- .../example/mate/common/error/ErrorCode.java | 1 + .../auth/controller/AuthController.java | 6 ++- .../domain/auth/service/NaverAuthService.java | 4 +- .../crawler/scheduler/CrawlingScheduler.java | 30 +++++------ .../member/controller/FollowController.java | 23 +++----- .../member/controller/MemberController.java | 39 ++++---------- .../member/controller/ProfileController.java | 18 ++++--- .../dto/request/MemberInfoUpdateRequest.java | 4 +- .../dto/response/MemberLoginResponse.java | 10 ++-- .../domain/member/service/MemberService.java | 8 +-- src/main/resources/application-common.yml | 7 ++- .../example/mate/config/WithAuthMember.java | 15 ++++++ .../WithAuthMemberSecurityContextFactory.java | 28 ++++++++++ .../auth/controller/AuthControllerTest.java | 5 +- .../integration/NaverAuthIntegrationTest.java | 3 +- .../auth/service/NaverAuthServiceTest.java | 2 +- .../controller/FollowControllerTest.java | 20 +++---- .../controller/MemberControllerTest.java | 33 +++--------- .../controller/ProfileControllerTest.java | 32 +++--------- .../integration/FollowIntegrationTest.java | 48 ++++++++++------- .../integration/MemberIntegrationTest.java | 52 +++++++------------ .../integration/ProfileIntegrationTest.java | 38 ++++++-------- 22 files changed, 196 insertions(+), 230 deletions(-) create mode 100644 src/test/java/com/example/mate/config/WithAuthMember.java create mode 100644 src/test/java/com/example/mate/config/WithAuthMemberSecurityContextFactory.java diff --git a/src/main/java/com/example/mate/common/error/ErrorCode.java b/src/main/java/com/example/mate/common/error/ErrorCode.java index b63dd794..7639fdbd 100644 --- a/src/main/java/com/example/mate/common/error/ErrorCode.java +++ b/src/main/java/com/example/mate/common/error/ErrorCode.java @@ -31,6 +31,7 @@ public enum ErrorCode { UNSUPPORTED_RESPONSE_TYPE(HttpStatus.BAD_REQUEST, "M002", "회원 프로필 조회에서 지원하지 않는 응답 타입입니다."), ALREADY_USED_NICKNAME(HttpStatus.BAD_REQUEST, "M003", "이미 사용 중인 닉네임입니다."), MEMBER_NOT_FOUND_BY_EMAIL(HttpStatus.NOT_FOUND, "M004", "해당 이메일의 회원 정보를 찾을 수 없습니다."), + MEMBER_UNAUTHORIZED_ACCESS(HttpStatus.FORBIDDEN, "M005", "해당 회원의 접근 권한이 없습니다."), // Follow ALREADY_FOLLOWED_MEMBER(HttpStatus.BAD_REQUEST, "F001", "이미 팔로우한 회원입니다."), diff --git a/src/main/java/com/example/mate/domain/auth/controller/AuthController.java b/src/main/java/com/example/mate/domain/auth/controller/AuthController.java index 38c0cfce..3fb3f3e6 100644 --- a/src/main/java/com/example/mate/domain/auth/controller/AuthController.java +++ b/src/main/java/com/example/mate/domain/auth/controller/AuthController.java @@ -4,6 +4,7 @@ import com.example.mate.domain.auth.dto.response.LoginResponse; import com.example.mate.domain.auth.service.NaverAuthService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.constraints.NotEmpty; import lombok.RequiredArgsConstructor; @@ -30,8 +31,9 @@ public class AuthController { */ @Operation(summary = "네이버 소셜 인증 페이지 리다이렉트") @GetMapping("/connect/naver") - public RedirectView connectNaver() { - return new RedirectView(naverAuthService.getAuthUrl()); + public RedirectView connectNaver( + @Parameter(description = "네이버 로그인 요청 state") @RequestParam String state) { + return new RedirectView(naverAuthService.getAuthUrl(state)); } /** diff --git a/src/main/java/com/example/mate/domain/auth/service/NaverAuthService.java b/src/main/java/com/example/mate/domain/auth/service/NaverAuthService.java index d26675de..ac5d5c96 100644 --- a/src/main/java/com/example/mate/domain/auth/service/NaverAuthService.java +++ b/src/main/java/com/example/mate/domain/auth/service/NaverAuthService.java @@ -32,12 +32,12 @@ public class NaverAuthService { * * @return 네이버 로그인 URL */ - public String getAuthUrl() { + public String getAuthUrl(String state) { return "https://nid.naver.com/oauth2.0/authorize" + "?client_id=" + oAuthConfig.getNaverClientId() + "&redirect_uri=" + oAuthConfig.getNaverRedirectUri() + "&response_type=code" - + "&state=STATE_STRING"; + + "&state=" + state; } /** diff --git a/src/main/java/com/example/mate/domain/crawler/scheduler/CrawlingScheduler.java b/src/main/java/com/example/mate/domain/crawler/scheduler/CrawlingScheduler.java index de8cf982..7f444179 100644 --- a/src/main/java/com/example/mate/domain/crawler/scheduler/CrawlingScheduler.java +++ b/src/main/java/com/example/mate/domain/crawler/scheduler/CrawlingScheduler.java @@ -43,19 +43,19 @@ public void eveningUpdate() { } } -// // 매 분 실행 자동화 테스트 (현재 off-season ) -// @Scheduled(cron = "0 * * * * *", zone = "Asia/Seoul") -// public void testScheduler() { -// log.info("Test Scheduler is running!"); -// try { -// // 현재 경기 크롤링 -// crawlingService.crawlAllCurrentMatches(); -// // 팀 순위 크롤링 -// crawlingService.crawlTeamRankings(); -// } catch (CustomException e) { -// log.error("Evening update failed: {}", e.getErrorCode().getMessage(), e); -// } catch (Exception e) { -// log.error("Evening update failed with unexpected error", e); -// } -// } + // 매 분 실행 자동화 테스트 (현재 off-season ) + @Scheduled(cron = "0 * * * * *", zone = "Asia/Seoul") + public void testScheduler() { + log.info("Test Scheduler is running!"); + try { + // 현재 경기 크롤링 + crawlingService.crawlAllCurrentMatches(); + // 팀 순위 크롤링 + crawlingService.crawlTeamRankings(); + } catch (CustomException e) { + log.error("Evening update failed: {}", e.getErrorCode().getMessage(), e); + } catch (Exception e) { + log.error("Evening update failed with unexpected error", e); + } + } } diff --git a/src/main/java/com/example/mate/domain/member/controller/FollowController.java b/src/main/java/com/example/mate/domain/member/controller/FollowController.java index 5bd44e8e..191e7a29 100644 --- a/src/main/java/com/example/mate/domain/member/controller/FollowController.java +++ b/src/main/java/com/example/mate/domain/member/controller/FollowController.java @@ -4,6 +4,7 @@ import com.example.mate.common.response.ApiResponse; import com.example.mate.common.response.PageResponse; +import com.example.mate.common.security.auth.AuthMember; import com.example.mate.domain.member.dto.response.MemberSummaryResponse; import com.example.mate.domain.member.service.FollowService; import io.swagger.v3.oas.annotations.Operation; @@ -13,12 +14,12 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -29,29 +30,21 @@ public class FollowController { private final FollowService followService; - /* - TODO : 2024/11/29 - 회원 팔로우 기능 - 1. JwtToken 을 통해 사용자 정보 조회 - 현재는 임시로 @RequestParam 사용 - */ @Operation(summary = "회원 팔로우 기능") @PostMapping("/follow/{memberId}") public ResponseEntity> followMember( @Parameter(description = "팔로우할 회원 ID") @PathVariable Long memberId, - @Parameter(description = "팔로우하는 회원 ID") @RequestParam Long followerId) { - followService.follow(followerId, memberId); + @Parameter(description = "회원 로그인 정보") @AuthenticationPrincipal AuthMember authMember) { + followService.follow(authMember.getMemberId(), memberId); return ResponseEntity.ok(ApiResponse.success(null)); } - /* - TODO : 2024/11/29 - 회원 언팔로우 기능 - 1. JwtToken 을 통해 사용자 정보 조회 - 현재는 임시로 @RequestParam 사용 - */ @Operation(summary = "회원 언팔로우 기능") @DeleteMapping("/follow/{memberId}") public ResponseEntity unfollowMember( @Parameter(description = "언팔로우할 회원 ID") @PathVariable Long memberId, - @Parameter(description = "언팔로우하는 회원 ID") @RequestParam Long unfollowerId) { - followService.unfollow(unfollowerId, memberId); + @Parameter(description = "회원 로그인 정보") @AuthenticationPrincipal AuthMember authMember) { + followService.unfollow(authMember.getMemberId(), memberId); return ResponseEntity.noContent().build(); } @@ -59,7 +52,7 @@ public ResponseEntity unfollowMember( @GetMapping("{memberId}/followings") public ResponseEntity>> getFollowings( @Parameter(description = "특정 회원 ID") @PathVariable Long memberId, - @PageableDefault Pageable pageable + @Parameter(description = "페이지 요청 정보") @PageableDefault Pageable pageable ) { pageable = validatePageable(pageable); PageResponse response = followService.getFollowingsPage(memberId, pageable); @@ -70,7 +63,7 @@ public ResponseEntity>> getFollo @GetMapping("{memberId}/followers") public ResponseEntity>> getFollowers( @Parameter(description = "특정 회원 ID") @PathVariable Long memberId, - @PageableDefault Pageable pageable + @Parameter(description = "페이지 요청 정보") @PageableDefault Pageable pageable ) { pageable = validatePageable(pageable); PageResponse response = followService.getFollowersPage(memberId, pageable); diff --git a/src/main/java/com/example/mate/domain/member/controller/MemberController.java b/src/main/java/com/example/mate/domain/member/controller/MemberController.java index 76806446..0551495c 100644 --- a/src/main/java/com/example/mate/domain/member/controller/MemberController.java +++ b/src/main/java/com/example/mate/domain/member/controller/MemberController.java @@ -24,7 +24,6 @@ import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -37,11 +36,6 @@ public class MemberController { private final MemberService memberService; - /* - TODO : 2024/11/29 - 소셜 회원가입 후, 자체 회원가입 기능 - 1. 소셜 로그인 후 사용자 정보가 바로 넘어오도록 처리 - 2. nickname, myTeam 정보 저장 - */ @Operation(summary = "자체 회원가입 기능") @PostMapping("/join") public ResponseEntity> join( @@ -50,10 +44,6 @@ public ResponseEntity> join( return ResponseEntity.ok(ApiResponse.success(memberService.join(joinRequest))); } - /* - CATCH Mi 서비스 로그인 - 소셜 로그인 후, 받아온 이메일을 통해 로그인 처리 - */ @Operation(summary = "CATCH Mi 서비스 로그인", description = "캐치미 서비스에 로그인합니다.") @PostMapping("/login") public ResponseEntity> catchMiLogin( @@ -63,11 +53,11 @@ public ResponseEntity> catchMiLogin( return ResponseEntity.ok(ApiResponse.success(response)); } - // TODO : 2024/11/29 - 내 프로필 조회 : 추후 @AuthenticationPrincipal Long memberId 받음 @Operation(summary = "내 프로필 조회") @GetMapping("/me") - public ResponseEntity> findMyInfo(@RequestParam Long memberId) { - return ResponseEntity.ok(ApiResponse.success(memberService.getMyProfile(memberId))); + public ResponseEntity> findMyInfo( + @Parameter(description = "회원 로그인 정보") @AuthenticationPrincipal AuthMember authMember) { + return ResponseEntity.ok(ApiResponse.success(memberService.getMyProfile(authMember.getMemberId()))); } @Operation(summary = "다른 회원 프로필 조회") @@ -77,32 +67,21 @@ public ResponseEntity> findMemberInfo( return ResponseEntity.ok(ApiResponse.success(memberService.getMemberProfile(memberId))); } - /* - TODO : 회원 정보 수정 : - 1. JwtToken 을 통해 사용자 정보 조회 -> 본인만 수정 가능하도록 - */ @Operation(summary = "회원 내 정보 수정") @PutMapping(value = "/me") public ResponseEntity> updateMemberInfo( @Parameter(description = "프로필 사진") @RequestPart(value = "image", required = false) MultipartFile image, - @Parameter(description = "수정할 회원 정보") @Valid @RequestPart(value = "data") MemberInfoUpdateRequest updateRequest) { + @Parameter(description = "수정할 회원 정보") @Valid @RequestPart(value = "data") MemberInfoUpdateRequest updateRequest, + @Parameter(description = "회원 로그인 정보") @AuthenticationPrincipal AuthMember authMember) { + updateRequest.setMemberId(authMember.getMemberId()); return ResponseEntity.ok(ApiResponse.success(memberService.updateMyProfile(image, updateRequest))); } - /* - TODO : 회원 삭제 : 임시로 @RequestParam Long memberId - 1. JwtToken 을 통해 사용자 정보 조회 -> 본인만 수정 가능하도록 - */ @Operation(summary = "회원 탈퇴") @DeleteMapping("/me") - public ResponseEntity deleteMember(@RequestParam Long memberId) { - memberService.deleteMember(memberId); + public ResponseEntity deleteMember( + @Parameter(description = "회원 로그인 정보") @AuthenticationPrincipal AuthMember authMember) { + memberService.deleteMember(authMember.getMemberId()); return ResponseEntity.noContent().build(); } - - @GetMapping("/test") - public String test(@AuthenticationPrincipal AuthMember authMember) { - return "principal getName == " + authMember.getName() + " || " + "principal getMemberId == " - + authMember.getMemberId(); - } } diff --git a/src/main/java/com/example/mate/domain/member/controller/ProfileController.java b/src/main/java/com/example/mate/domain/member/controller/ProfileController.java index b6084581..6de3c51c 100644 --- a/src/main/java/com/example/mate/domain/member/controller/ProfileController.java +++ b/src/main/java/com/example/mate/domain/member/controller/ProfileController.java @@ -2,8 +2,11 @@ import static com.example.mate.common.response.PageResponse.validatePageable; +import com.example.mate.common.error.CustomException; +import com.example.mate.common.error.ErrorCode; import com.example.mate.common.response.ApiResponse; import com.example.mate.common.response.PageResponse; +import com.example.mate.common.security.auth.AuthMember; import com.example.mate.domain.member.dto.response.MyGoodsRecordResponse; import com.example.mate.domain.member.dto.response.MyReviewResponse; import com.example.mate.domain.member.dto.response.MyVisitResponse; @@ -15,6 +18,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -50,15 +54,14 @@ public ResponseEntity>> getMateReview return ResponseEntity.ok(ApiResponse.success(response)); } - // TODO : 본인만 접근할 수 있도록 @AuthenticationPrincipal Long memberId @Operation(summary = "직관 타임라인 페이징 조회") - @GetMapping("/timeline/{memberId}") + @GetMapping("/timeline") public ResponseEntity>> getMyVisits( - @Parameter(description = "회원 ID") @PathVariable Long memberId, + @Parameter(description = "회원 로그인 정보") @AuthenticationPrincipal AuthMember authMember, @Parameter(description = "페이지 요청 정보") @PageableDefault Pageable pageable ) { validatePageable(pageable); - PageResponse response = profileService.getMyVisitPage(memberId, pageable); + PageResponse response = profileService.getMyVisitPage(authMember.getMemberId(), pageable); return ResponseEntity.ok(ApiResponse.success(response)); } @@ -73,13 +76,16 @@ public ResponseEntity>> getSoldG return ResponseEntity.ok(ApiResponse.success(response)); } - // TODO : 본인만 접근할 수 있도록 @AuthenticationPrincipal Long memberId @Operation(summary = "굿즈 구매기록 페이징 조회") @GetMapping("/{memberId}/goods/bought") public ResponseEntity>> getBoughtGoods( @Parameter(description = "회원 ID") @PathVariable Long memberId, - @Parameter(description = "페이지 요청 정보") @PageableDefault Pageable pageable + @Parameter(description = "페이지 요청 정보") @PageableDefault Pageable pageable, + @Parameter(description = "회원 로그인 정보") @AuthenticationPrincipal AuthMember authMember ) { + if (!authMember.getMemberId().equals(memberId)) { + throw new CustomException(ErrorCode.MEMBER_UNAUTHORIZED_ACCESS); + } pageable = validatePageable(pageable); PageResponse response = profileService.getBoughtGoodsPage(memberId, pageable); return ResponseEntity.ok(ApiResponse.success(response)); diff --git a/src/main/java/com/example/mate/domain/member/dto/request/MemberInfoUpdateRequest.java b/src/main/java/com/example/mate/domain/member/dto/request/MemberInfoUpdateRequest.java index 1a7f0e8e..9794a144 100644 --- a/src/main/java/com/example/mate/domain/member/dto/request/MemberInfoUpdateRequest.java +++ b/src/main/java/com/example/mate/domain/member/dto/request/MemberInfoUpdateRequest.java @@ -6,9 +6,9 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import lombok.Builder; -import lombok.Getter; +import lombok.Data; -@Getter +@Data @Builder public class MemberInfoUpdateRequest { diff --git a/src/main/java/com/example/mate/domain/member/dto/response/MemberLoginResponse.java b/src/main/java/com/example/mate/domain/member/dto/response/MemberLoginResponse.java index 6104fac3..93ca9dae 100644 --- a/src/main/java/com/example/mate/domain/member/dto/response/MemberLoginResponse.java +++ b/src/main/java/com/example/mate/domain/member/dto/response/MemberLoginResponse.java @@ -1,5 +1,6 @@ package com.example.mate.domain.member.dto.response; +import com.example.mate.common.jwt.JwtToken; import com.example.mate.domain.member.entity.Member; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -16,13 +17,12 @@ public class MemberLoginResponse { private final String accessToken; private final String refreshToken; - // TODO : 파라미터로 JwtToken 추가 및 토큰 매핑 - public static MemberLoginResponse from(Member member) { + public static MemberLoginResponse from(Member member, JwtToken jwtToken) { return MemberLoginResponse.builder() .memberId(member.getId()) - .grantType("Bearer") - .accessToken("accessToken") - .refreshToken("refreshToken") + .grantType(jwtToken.getGrantType()) + .accessToken(jwtToken.getAccessToken()) + .refreshToken(jwtToken.getRefreshToken()) .build(); } } diff --git a/src/main/java/com/example/mate/domain/member/service/MemberService.java b/src/main/java/com/example/mate/domain/member/service/MemberService.java index a1f4c8c3..0f91f4c2 100644 --- a/src/main/java/com/example/mate/domain/member/service/MemberService.java +++ b/src/main/java/com/example/mate/domain/member/service/MemberService.java @@ -49,13 +49,10 @@ public JoinResponse join(JoinRequest request) { return JoinResponse.from(savedMember); } - // TODO : JWT 토큰 발급 // 자체 로그인 기능 public MemberLoginResponse loginByEmail(MemberLoginRequest request) { Member member = findByEmail(request.getEmail()); - // 토큰 발급한 뒤 member와 함께 넘기기 - JwtToken jwtToken = makeToken(member); - return MemberLoginResponse.from(member); + return MemberLoginResponse.from(member, makeToken(member)); } // JWT 토큰 생성 @@ -75,7 +72,6 @@ private Member findByEmail(String email) { .orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND_BY_EMAIL)); } - // TODO : JWT 도입 이후 본인만 접근할 수 있도록 수정 // 내 프로필 조회 public MyProfileResponse getMyProfile(Long memberId) { return getProfile(memberId, MyProfileResponse.class); @@ -86,7 +82,6 @@ public MemberProfileResponse getMemberProfile(Long memberId) { return getProfile(memberId, MemberProfileResponse.class); } - // TODO : JWT 도입 이후 본인만 접근할 수 있도록 수정 // 회원 정보 수정 public MyProfileResponse updateMyProfile(MultipartFile image, MemberInfoUpdateRequest request) { Member member = findByMemberId(request.getMemberId()); @@ -104,7 +99,6 @@ public MyProfileResponse updateMyProfile(MultipartFile image, MemberInfoUpdateRe return MyProfileResponse.from(memberRepository.save(member)); } - // TODO : JWT 도입 이후 본인만 접근할 수 있도록 수정 // 회원 탈퇴 public void deleteMember(Long memberId) { findByMemberId(memberId); diff --git a/src/main/resources/application-common.yml b/src/main/resources/application-common.yml index cdbdfc04..728797a4 100644 --- a/src/main/resources/application-common.yml +++ b/src/main/resources/application-common.yml @@ -25,10 +25,9 @@ springdoc: oauth: - naver: - client-id: ${NAVER_CLIENT_ID} - redirect-uri: ${NAVER_REDIRECT_URI} - client-secret: ${NAVER_CLIENT_SECRET} + naver_client_id: ${NAVER_CLIENT_ID} + naver_redirect_uri: ${NAVER_REDIRECT_URI} + naver_client_secret: ${NAVER_CLIENT_SECRET} jwt: secret_key: ${JWT_SECRET_KEY} diff --git a/src/test/java/com/example/mate/config/WithAuthMember.java b/src/test/java/com/example/mate/config/WithAuthMember.java new file mode 100644 index 00000000..00db320f --- /dev/null +++ b/src/test/java/com/example/mate/config/WithAuthMember.java @@ -0,0 +1,15 @@ +package com.example.mate.config; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import org.springframework.security.test.context.support.WithSecurityContext; + +@Retention(RetentionPolicy.RUNTIME) +@WithSecurityContext(factory = WithAuthMemberSecurityContextFactory.class) +public @interface WithAuthMember { + String userId() default "testUser"; + + long memberId() default 1L; + + String[] roles() default {"USER"}; +} diff --git a/src/test/java/com/example/mate/config/WithAuthMemberSecurityContextFactory.java b/src/test/java/com/example/mate/config/WithAuthMemberSecurityContextFactory.java new file mode 100644 index 00000000..8a6deb6d --- /dev/null +++ b/src/test/java/com/example/mate/config/WithAuthMemberSecurityContextFactory.java @@ -0,0 +1,28 @@ +package com.example.mate.config; + +import com.example.mate.common.security.auth.AuthMember; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.context.support.WithSecurityContextFactory; + +public class WithAuthMemberSecurityContextFactory implements WithSecurityContextFactory { + @Override + public SecurityContext createSecurityContext(WithAuthMember annotation) { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + + AuthMember authMember = new AuthMember(annotation.userId(), annotation.memberId()); + List authorities = Arrays.stream(annotation.roles()) + .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) + .collect(Collectors.toList()); + + Authentication auth = new UsernamePasswordAuthenticationToken(authMember, null, authorities); + context.setAuthentication(auth); + return context; + } +} diff --git a/src/test/java/com/example/mate/domain/auth/controller/AuthControllerTest.java b/src/test/java/com/example/mate/domain/auth/controller/AuthControllerTest.java index 86ddd369..1cfc4b76 100644 --- a/src/test/java/com/example/mate/domain/auth/controller/AuthControllerTest.java +++ b/src/test/java/com/example/mate/domain/auth/controller/AuthControllerTest.java @@ -39,10 +39,11 @@ public class AuthControllerTest { void connectNaver_Success() throws Exception { // given String expectedAuthUrl = "https://nid.naver.com/oauth2.0/authorize?client_id=testClientId&redirect_uri=testRedirectUri&response_type=code&state=STATE_STRING"; - when(naverAuthService.getAuthUrl()).thenReturn(expectedAuthUrl); + when(naverAuthService.getAuthUrl("state")).thenReturn(expectedAuthUrl); // when & then - mockMvc.perform(get("/api/auth/connect/naver")) + mockMvc.perform(get("/api/auth/connect/naver") + .param("state", "state")) .andExpect(status().is3xxRedirection()) // 3xx 리다이렉션 응답 상태 .andExpect(redirectedUrl(expectedAuthUrl)); // 리다이렉트 URL 확인 } diff --git a/src/test/java/com/example/mate/domain/auth/integration/NaverAuthIntegrationTest.java b/src/test/java/com/example/mate/domain/auth/integration/NaverAuthIntegrationTest.java index aec259e4..0865a7d8 100644 --- a/src/test/java/com/example/mate/domain/auth/integration/NaverAuthIntegrationTest.java +++ b/src/test/java/com/example/mate/domain/auth/integration/NaverAuthIntegrationTest.java @@ -77,7 +77,8 @@ void connectNaver_Success() throws Exception { + "&state=STATE_STRING"; // when & then - mockMvc.perform(MockMvcRequestBuilders.get("/api/auth/connect/naver")) + mockMvc.perform(MockMvcRequestBuilders.get("/api/auth/connect/naver") + .param("state", "STATE_STRING")) .andExpect(status().is3xxRedirection()) .andExpect(header().string("Location", is(expectedUrl))); } diff --git a/src/test/java/com/example/mate/domain/auth/service/NaverAuthServiceTest.java b/src/test/java/com/example/mate/domain/auth/service/NaverAuthServiceTest.java index d47f5b6d..f7c120e4 100644 --- a/src/test/java/com/example/mate/domain/auth/service/NaverAuthServiceTest.java +++ b/src/test/java/com/example/mate/domain/auth/service/NaverAuthServiceTest.java @@ -42,7 +42,7 @@ public class NaverAuthServiceTest { @DisplayName("네이버 로그인 연결 URL 생성") public void getAuthUrl_Success() { // when - String authUrl = naverAuthService.getAuthUrl(); + String authUrl = naverAuthService.getAuthUrl("state"); // then assertTrue(authUrl.startsWith("https://nid.naver.com/oauth2.0/authorize")); diff --git a/src/test/java/com/example/mate/domain/member/controller/FollowControllerTest.java b/src/test/java/com/example/mate/domain/member/controller/FollowControllerTest.java index 736e1df2..e02423bd 100644 --- a/src/test/java/com/example/mate/domain/member/controller/FollowControllerTest.java +++ b/src/test/java/com/example/mate/domain/member/controller/FollowControllerTest.java @@ -19,6 +19,7 @@ import com.example.mate.common.error.ErrorCode; import com.example.mate.common.response.PageResponse; import com.example.mate.common.security.filter.JwtCheckFilter; +import com.example.mate.config.WithAuthMember; import com.example.mate.domain.constant.Gender; import com.example.mate.domain.member.dto.response.MemberSummaryResponse; import com.example.mate.domain.member.entity.Follow; @@ -41,6 +42,7 @@ @WebMvcTest(FollowController.class) @MockBean(JpaMetamodelMappingContext.class) @AutoConfigureMockMvc(addFilters = false) +@WithAuthMember(userId = "customUser", memberId = 1L) class FollowControllerTest { @Autowired @@ -96,8 +98,7 @@ void follow_member_success() throws Exception { willDoNothing().given(followService).follow(followerId, followingId); // when & then - mockMvc.perform(post("/api/profile/follow/{memberId}", followingId) - .param("followerId", String.valueOf(followerId))) + mockMvc.perform(post("/api/profile/follow/{memberId}", followingId)) .andExpect(status().isOk()) .andDo(print()); @@ -116,8 +117,7 @@ void follow_member_already_followed() throws Exception { .given(followService).follow(followerId, followingId); // when & then - mockMvc.perform(post("/api/profile/follow/{memberId}", followingId) - .param("followerId", String.valueOf(followerId))) + mockMvc.perform(post("/api/profile/follow/{memberId}", followingId)) .andExpect(status().isBadRequest()) // 400 Bad Request .andDo(print()); @@ -136,8 +136,7 @@ void follow_member_not_found() throws Exception { .given(followService).follow(followerId, followingId); // when & then - mockMvc.perform(post("/api/profile/follow/{memberId}", followingId) - .param("followerId", String.valueOf(followerId))) + mockMvc.perform(post("/api/profile/follow/{memberId}", followingId)) .andExpect(status().isNotFound()) // 404 Not Found .andDo(print()); @@ -159,8 +158,7 @@ void unfollow_member_success() throws Exception { willDoNothing().given(followService).unfollow(unfollowerId, unfollowingId); // when & then - mockMvc.perform(delete("/api/profile/follow/{memberId}", unfollowingId) - .param("unfollowerId", String.valueOf(unfollowerId))) + mockMvc.perform(delete("/api/profile/follow/{memberId}", unfollowingId)) .andExpect(status().isNoContent()) .andDo(print()); @@ -179,8 +177,7 @@ void unfollow_member_already_unfollowed() throws Exception { .given(followService).unfollow(unfollowerId, unfollowingId); // when & then - mockMvc.perform(delete("/api/profile/follow/{memberId}", unfollowingId) - .param("unfollowerId", String.valueOf(unfollowerId))) + mockMvc.perform(delete("/api/profile/follow/{memberId}", unfollowingId)) .andExpect(status().isBadRequest()) .andDo(print()); @@ -199,8 +196,7 @@ void unfollow_member_not_found() throws Exception { .given(followService).unfollow(unfollowerId, unfollowingId); // when & then - mockMvc.perform(delete("/api/profile/follow/{memberId}", unfollowingId) - .param("unfollowerId", String.valueOf(unfollowerId))) + mockMvc.perform(delete("/api/profile/follow/{memberId}", unfollowingId)) .andExpect(status().isNotFound()) .andDo(print()); diff --git a/src/test/java/com/example/mate/domain/member/controller/MemberControllerTest.java b/src/test/java/com/example/mate/domain/member/controller/MemberControllerTest.java index d61f7626..698937bc 100644 --- a/src/test/java/com/example/mate/domain/member/controller/MemberControllerTest.java +++ b/src/test/java/com/example/mate/domain/member/controller/MemberControllerTest.java @@ -16,6 +16,7 @@ import com.example.mate.common.error.CustomException; import com.example.mate.common.error.ErrorCode; import com.example.mate.common.security.filter.JwtCheckFilter; +import com.example.mate.config.WithAuthMember; import com.example.mate.domain.member.dto.request.JoinRequest; import com.example.mate.domain.member.dto.request.MemberInfoUpdateRequest; import com.example.mate.domain.member.dto.response.JoinResponse; @@ -39,6 +40,7 @@ @WebMvcTest(MemberController.class) @MockBean(JpaMetamodelMappingContext.class) @AutoConfigureMockMvc(addFilters = false) +@WithAuthMember(userId = "customUser", memberId = 1L) class MemberControllerTest { @Autowired @@ -207,8 +209,7 @@ void get_my_profile_success() throws Exception { given(memberService.getMyProfile(memberId)).willReturn(response); // when & then - mockMvc.perform(get("/api/members/me") - .param("memberId", memberId.toString())) + mockMvc.perform(get("/api/members/me")) .andExpect(status().isOk()) .andExpect(jsonPath("$.status").value("SUCCESS")) .andExpect(jsonPath("$.data.nickname").value("tester")) @@ -229,34 +230,18 @@ void get_my_profile_success() throws Exception { @DisplayName("내 프로필 조회 실패 - 회원이 존재하지 않는 경우") void get_my_profile_member_not_found() throws Exception { // given - Long memberId = 999L; + Long memberId = 1L; given(memberService.getMyProfile(memberId)).willThrow( new CustomException(ErrorCode.MEMBER_NOT_FOUND_BY_ID)); // when & then - mockMvc.perform(get("/api/members/me") - .param("memberId", memberId.toString())) + mockMvc.perform(get("/api/members/me")) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.status").value("ERROR")) .andExpect(jsonPath("$.message").value("해당 ID의 회원 정보를 찾을 수 없습니다")) .andDo(print()); } - - @Test - @DisplayName("내 프로필 조회 실패 - 잘못된 memberId 형식") - void get_my_profile_invalid_member_id() throws Exception { - // given - String invalidMemberId = "invalid"; // 숫자가 아닌 잘못된 ID - - // when & then - mockMvc.perform(get("/api/members/me") - .param("memberId", invalidMemberId)) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.status").value("ERROR")) - .andExpect(jsonPath("$.message").value("잘못된 입력 형식입니다. 'invalid' 타입이 'Long'로 변환될 수 없습니다.")) - .andDo(print()); - } } @Test @@ -371,8 +356,7 @@ void delete_member_success() throws Exception { willDoNothing().given(memberService).deleteMember(memberId); // when & then - mockMvc.perform(delete("/api/members/me") - .param("memberId", memberId.toString())) + mockMvc.perform(delete("/api/members/me")) .andDo(print()) .andExpect(status().isNoContent()); @@ -383,14 +367,13 @@ void delete_member_success() throws Exception { @DisplayName("회원 탈퇴 실패 - 존재하지 않는 회원") void delete_member_fail_not_exists_member() throws Exception { // given - Long memberId = 999L; + Long memberId = 1L; willThrow(new CustomException(ErrorCode.MEMBER_NOT_FOUND_BY_ID)) .given(memberService).deleteMember(memberId); // when & then - mockMvc.perform(delete("/api/members/me") - .param("memberId", memberId.toString())) + mockMvc.perform(delete("/api/members/me")) .andDo(print()) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.status").value("ERROR")) diff --git a/src/test/java/com/example/mate/domain/member/controller/ProfileControllerTest.java b/src/test/java/com/example/mate/domain/member/controller/ProfileControllerTest.java index 519bd22a..b674faaa 100644 --- a/src/test/java/com/example/mate/domain/member/controller/ProfileControllerTest.java +++ b/src/test/java/com/example/mate/domain/member/controller/ProfileControllerTest.java @@ -13,6 +13,7 @@ import com.example.mate.common.error.ErrorCode; import com.example.mate.common.response.PageResponse; import com.example.mate.common.security.filter.JwtCheckFilter; +import com.example.mate.config.WithAuthMember; import com.example.mate.domain.member.dto.response.MyGoodsRecordResponse; import com.example.mate.domain.member.dto.response.MyReviewResponse; import com.example.mate.domain.member.dto.response.MyVisitResponse; @@ -37,6 +38,7 @@ @WebMvcTest(ProfileController.class) @MockBean(JpaMetamodelMappingContext.class) @AutoConfigureMockMvc(addFilters = false) +@WithAuthMember(userId = "customUser", memberId = 1L) class ProfileControllerTest { @Autowired @@ -134,6 +136,7 @@ class ProfileBoughtGoodsPage { @Test @DisplayName("회원 프로필 굿즈 구매기록 페이징 조회 성공") + @WithAuthMember(userId = "customUser", memberId = 2L) void get_bought_goods_page_success() throws Exception { // given Long memberId = 2L; @@ -169,29 +172,6 @@ void get_bought_goods_page_success() throws Exception { .andExpect(jsonPath("$.data.pageSize").value(response.getPageSize())) .andExpect(jsonPath("$.code").value(200)); } - - @Test - @DisplayName("회원 프로필 굿즈 구매기록 페이징 조회 실패 - 유효하지 않은 회원 아이디로 조회") - void get_bought_goods_page_invalid_member_id() throws Exception { - // given - Long memberId = 999L; // 존재 하지 않는 아이디 - Pageable pageable = PageRequest.of(0, 10); - - willThrow(new CustomException(ErrorCode.MEMBER_NOT_FOUND_BY_ID)) - .given(profileService).getBoughtGoodsPage(memberId, pageable); - - // when & then - mockMvc.perform(get("/api/profile/{memberId}/goods/bought", memberId) - .param("page", String.valueOf(pageable.getPageNumber())) - .param("size", String.valueOf(pageable.getPageSize()))) - .andDo(print()) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.status").value("ERROR")) - .andExpect(jsonPath("$.code").value(404)) - .andExpect(jsonPath("$.message").value(ErrorCode.MEMBER_NOT_FOUND_BY_ID.getMessage())); - - verify(profileService, times(1)).getBoughtGoodsPage(memberId, pageable); - } } @Nested @@ -382,7 +362,7 @@ void get_my_visit_page_success() throws Exception { given(profileService.getMyVisitPage(memberId, pageable)).willReturn(response); // when & then - mockMvc.perform(get("/api/profile/timeline/{memberId}", memberId) + mockMvc.perform(get("/api/profile/timeline") .param("page", String.valueOf(pageable.getPageNumber())) .param("size", String.valueOf(pageable.getPageSize()))) .andDo(print()) @@ -405,14 +385,14 @@ void get_my_visit_page_success() throws Exception { @DisplayName("회원 타임라인 페이징 조회 실패 - 유효하지 않은 회원 아이디로 조회") void get_my_visit_page_fail_invalid_member_id() throws Exception { // given - Long memberId = 999L; // 존재 하지 않는 아이디 + Long memberId = 1L; // 존재 하지 않는 아이디 Pageable pageable = PageRequest.of(0, 10); willThrow(new CustomException(ErrorCode.MEMBER_NOT_FOUND_BY_ID)) .given(profileService).getMyVisitPage(memberId, pageable); // when & then - mockMvc.perform(get("/api/profile/timeline/{memberId}", memberId) + mockMvc.perform(get("/api/profile/timeline") .param("page", String.valueOf(pageable.getPageNumber())) .param("size", String.valueOf(pageable.getPageSize()))) .andDo(print()) diff --git a/src/test/java/com/example/mate/domain/member/integration/FollowIntegrationTest.java b/src/test/java/com/example/mate/domain/member/integration/FollowIntegrationTest.java index c761bf92..ac78a1a3 100644 --- a/src/test/java/com/example/mate/domain/member/integration/FollowIntegrationTest.java +++ b/src/test/java/com/example/mate/domain/member/integration/FollowIntegrationTest.java @@ -9,13 +9,13 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.example.mate.common.error.ErrorCode; +import com.example.mate.config.WithAuthMember; import com.example.mate.common.security.util.JwtUtil; import com.example.mate.domain.constant.Gender; import com.example.mate.domain.member.entity.Follow; import com.example.mate.domain.member.entity.Member; import com.example.mate.domain.member.repository.FollowRepository; import com.example.mate.domain.member.repository.MemberRepository; -import jakarta.transaction.Transactional; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -26,7 +26,9 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; @SpringBootTest @AutoConfigureMockMvc(addFilters = false) @@ -45,12 +47,19 @@ public class FollowIntegrationTest { @MockBean private JwtUtil jwtUtil; + @Autowired + private JdbcTemplate jdbcTemplate; + private Member member1; private Member member2; private Follow follow; @BeforeEach void setUp() { + jdbcTemplate.execute("SET REFERENTIAL_INTEGRITY FALSE"); + jdbcTemplate.execute("TRUNCATE TABLE member"); + jdbcTemplate.execute("SET REFERENTIAL_INTEGRITY TRUE"); + followRepository.deleteAll(); memberRepository.deleteAll(); @@ -96,35 +105,35 @@ class FollowMember { @Test @DisplayName("회원 팔로우 성공") + @WithAuthMember(userId = "customUser", memberId = 1L) void follow_member_success() throws Exception { // given Long followerId = member2.getId(); Long followingId = member1.getId(); // when & then - mockMvc.perform(post("/api/profile/follow/{memberId}", followingId) - .param("followerId", String.valueOf(followerId))) + mockMvc.perform(post("/api/profile/follow/{memberId}", followingId)) .andExpect(status().isOk()) .andDo(print()); List savedFollows = followRepository.findAll(); assertThat(savedFollows).size().isEqualTo(2); // 기존 1개 + 새로 1개 추가 assertThat(savedFollows.get(savedFollows.size() - 1) - .getFollower().getId()).isEqualTo(followerId); + .getFollower().getId()).isEqualTo(followingId); assertThat(savedFollows.get(savedFollows.size() - 1) - .getFollowing().getId()).isEqualTo(followingId); + .getFollowing().getId()).isEqualTo(followerId); } @Test @DisplayName("회원 팔로우 실패 -이미 팔로우한 회원을 다시 팔로우하려는 경우 예외 발생") + @WithAuthMember(userId = "customUser", memberId = 1L) void follow_member_already_followed() throws Exception { // given - 이미 팔로우 되어 있는 상태인 member1가 member2 팔로우 상황 가정 Long followerId = member1.getId(); Long followingId = member2.getId(); // when & then - mockMvc.perform(post("/api/profile/follow/{memberId}", followingId) - .param("followerId", String.valueOf(followerId))) + mockMvc.perform(post("/api/profile/follow/{memberId}", followingId)) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.status").value("ERROR")) .andExpect(jsonPath("$.message").value("이미 팔로우한 회원입니다.")) @@ -140,17 +149,16 @@ void follow_member_already_followed() throws Exception { @Test @DisplayName("회원 팔로우 실패 - 존재하지 않는 팔로워 또는 팔로잉을 팔로우하려는 경우 예외 발생") + @WithAuthMember(userId = "customUser", memberId = 1L) void follow_member_not_found() throws Exception { // given - Long followerId = member1.getId() + 999L; - Long followingId = member1.getId(); + Long followingId = member1.getId() + 999L; // when & then - mockMvc.perform(post("/api/profile/follow/{memberId}", followingId) - .param("followerId", String.valueOf(followerId))) + mockMvc.perform(post("/api/profile/follow/{memberId}", followingId)) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.status").value("ERROR")) - .andExpect(jsonPath("$.message").value("해당 ID의 팔로워 회원을 찾을 수 없습니다.")) + .andExpect(jsonPath("$.message").value("해당 ID의 팔로잉 회원을 찾을 수 없습니다.")) .andDo(print()); List savedFollows = followRepository.findAll(); @@ -168,14 +176,14 @@ class UnfollowMember { @Test @DisplayName("회원 언팔로우 성공") + @WithAuthMember(userId = "customUser", memberId = 1L) void unfollow_member_success() throws Exception { // given Long unfollowerId = member1.getId(); Long unfollowingId = member2.getId(); // when & then - mockMvc.perform(delete("/api/profile/follow/{memberId}", unfollowingId) - .param("unfollowerId", String.valueOf(unfollowerId))) + mockMvc.perform(delete("/api/profile/follow/{memberId}", unfollowingId)) .andExpect(status().isNoContent()) // 204 No Content .andDo(print()); @@ -186,14 +194,14 @@ void unfollow_member_success() throws Exception { @Test @DisplayName("회원 언팔로우 실패 - 팔로우 관계가 없는 회원을 언팔로우하려는 경우 예외 발생") + @WithAuthMember(userId = "customUser", memberId = 1L) void unfollow_member_not_followed() throws Exception { // given Long unfollowerId = member2.getId(); // member2가 member1을 팔로우하지 않은 상태 Long unfollowingId = member1.getId(); // when & then - mockMvc.perform(delete("/api/profile/follow/{memberId}", unfollowingId) - .param("unfollowerId", String.valueOf(unfollowerId))) + mockMvc.perform(delete("/api/profile/follow/{memberId}", unfollowingId)) .andExpect(status().isBadRequest()) // 400 Bad Request .andExpect(jsonPath("$.status").value("ERROR")) .andExpect(jsonPath("$.message").value("이미 언팔로우한 회원입니다.")) @@ -206,17 +214,17 @@ void unfollow_member_not_followed() throws Exception { @Test @DisplayName("회원 언팔로우 실패 - 존재하지 않는 팔로워 또는 팔로잉을 언팔로우하려는 경우 예외 발생") + @WithAuthMember(userId = "customUser", memberId = 1L) void unfollow_member_not_found() throws Exception { // given Long unfollowerId = 999L; // 존재하지 않는 팔로워 ID - Long unfollowingId = member2.getId(); + Long unfollowingId = member2.getId() + 999L; // when & then - mockMvc.perform(delete("/api/profile/follow/{memberId}", unfollowingId) - .param("unfollowerId", String.valueOf(unfollowerId))) + mockMvc.perform(delete("/api/profile/follow/{memberId}", unfollowingId)) .andExpect(status().isNotFound()) // 404 Not Found .andExpect(jsonPath("$.status").value("ERROR")) - .andExpect(jsonPath("$.message").value("해당 ID의 언팔로워 회원을 찾을 수 없습니다.")) + .andExpect(jsonPath("$.message").value("해당 ID의 언팔로잉 회원을 찾을 수 없습니다.")) .andDo(print()); // 팔로우 관계가 여전히 존재하는지 확인 diff --git a/src/test/java/com/example/mate/domain/member/integration/MemberIntegrationTest.java b/src/test/java/com/example/mate/domain/member/integration/MemberIntegrationTest.java index e8bfd6ec..8bd9098b 100644 --- a/src/test/java/com/example/mate/domain/member/integration/MemberIntegrationTest.java +++ b/src/test/java/com/example/mate/domain/member/integration/MemberIntegrationTest.java @@ -10,6 +10,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.example.mate.config.WithAuthMember; import com.example.mate.common.security.util.JwtUtil; import com.example.mate.domain.constant.Gender; import com.example.mate.domain.constant.Rating; @@ -40,7 +41,6 @@ import com.example.mate.domain.member.repository.MemberRepository; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.persistence.EntityManager; -import jakarta.transaction.Transactional; import java.time.LocalDateTime; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -53,8 +53,10 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; @SpringBootTest @AutoConfigureMockMvc(addFilters = false) @@ -94,6 +96,9 @@ class MemberIntegrationTest { @Autowired private VisitPartRepository visitPartRepository; + @Autowired + private JdbcTemplate jdbcTemplate; + @Autowired private EntityManager entityManager; @@ -111,6 +116,10 @@ class MemberIntegrationTest { @BeforeEach void setUp() { + jdbcTemplate.execute("SET REFERENTIAL_INTEGRITY FALSE"); + jdbcTemplate.execute("TRUNCATE TABLE member"); + jdbcTemplate.execute("SET REFERENTIAL_INTEGRITY TRUE"); + // 테스트용 회원 및 관련 데이터 생성 member = createMember("홍길동", "tester", "tester@example.com", Gender.MALE, 20); member2 = createMember("김철수", "tester2", "test2@example.com", Gender.MALE, 22); @@ -298,9 +307,10 @@ class GetMyProfile { @Test @DisplayName("내 프로필 조회 성공") + @WithAuthMember(userId = "customUser", memberId = 1L) void find_my_info_success() throws Exception { - mockMvc.perform(get("/api/members/me") - .param("memberId", member.getId().toString())) + System.out.println(member.getId()); + mockMvc.perform(get("/api/members/me")) .andExpect(status().isOk()) .andExpect(jsonPath("$.status").value("SUCCESS")) .andExpect(jsonPath("$.data.nickname").value("tester")) @@ -318,28 +328,16 @@ void find_my_info_success() throws Exception { @Test @DisplayName("내 프로필 조회 실패 - 존재하지 않는 회원 ID") + @WithAuthMember(userId = "customUser", memberId = 999L) void find_member_info_fail_not_found() throws Exception { // 존재하지 않는 memberId를 사용 - mockMvc.perform(get("/api/members/me") - .param("memberId", "99999999")) // 존재하지 않는 ID + mockMvc.perform(get("/api/members/me")) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.status").value("ERROR")) .andExpect(jsonPath("$.message").value("해당 ID의 회원 정보를 찾을 수 없습니다")) .andExpect(jsonPath("$.code").value(404)) .andDo(print()); } - - @Test - @DisplayName("내 프로필 조회 실패 - 회원 ID 누락") - void find_member_info_fail_missing_memberId() throws Exception { - // memberId 파라미터를 누락 - mockMvc.perform(get("/api/members/me")) - .andExpect(status().isBadRequest()) // 400 상태 코드 - .andExpect(jsonPath("$.status").value("ERROR")) - .andExpect(jsonPath("$.message").value("요청에 'memberId' 파라미터가 누락되었습니다.")) - .andExpect(jsonPath("$.code").value(400)) - .andDo(print()); - } } @Test @@ -360,6 +358,7 @@ class UpdateMember { @Test @DisplayName("회원 정보 수정 성공") + @WithAuthMember(userId = "customUser", memberId = 1L) void update_my_profile_success() throws Exception { MemberInfoUpdateRequest request = createMemberInfoUpdateRequest(); MockMultipartFile data = new MockMultipartFile( @@ -428,26 +427,11 @@ class DeleteMember { @Test @DisplayName("회원 탈퇴 성공") + @WithAuthMember(userId = "customUser", memberId = 1L) void delete_member_success() throws Exception { - mockMvc.perform(delete("/api/members/me") - .param("memberId", member.getId().toString())) + mockMvc.perform(delete("/api/members/me")) .andDo(print()) .andExpect(status().isNoContent()); } - - @Test - @DisplayName("회원 탈퇴 실패 - 존재하지 않는 회원") - void delete_member_fail_not_exists_member() throws Exception { - // given - Long memberId = member.getId() + 999L; - - // when & then - mockMvc.perform(delete("/api/members/me") - .param("memberId", memberId.toString())) - .andDo(print()) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.status").value("ERROR")) - .andExpect(jsonPath("$.message").value("해당 ID의 회원 정보를 찾을 수 없습니다")); - } } } \ No newline at end of file diff --git a/src/test/java/com/example/mate/domain/member/integration/ProfileIntegrationTest.java b/src/test/java/com/example/mate/domain/member/integration/ProfileIntegrationTest.java index 1a0bf618..69d538f6 100644 --- a/src/test/java/com/example/mate/domain/member/integration/ProfileIntegrationTest.java +++ b/src/test/java/com/example/mate/domain/member/integration/ProfileIntegrationTest.java @@ -6,6 +6,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.example.mate.config.WithAuthMember; import com.example.mate.common.security.util.JwtUtil; import com.example.mate.domain.constant.Gender; import com.example.mate.domain.constant.Rating; @@ -46,6 +47,7 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.web.servlet.MockMvc; @SpringBootTest @@ -83,6 +85,10 @@ public class ProfileIntegrationTest { @Autowired private GoodsReviewRepository goodsReviewRepository; + @Autowired + private JdbcTemplate jdbcTemplate; + + @Autowired private ObjectMapper objectMapper; @@ -104,6 +110,10 @@ public class ProfileIntegrationTest { @BeforeEach void setUp() { + jdbcTemplate.execute("SET REFERENTIAL_INTEGRITY FALSE"); + jdbcTemplate.execute("TRUNCATE TABLE member"); + jdbcTemplate.execute("SET REFERENTIAL_INTEGRITY TRUE"); + createMember(); goodsPost1 = createGoodsPost(); createGoodsPostImage(goodsPost1); @@ -312,6 +322,7 @@ class ProfileBoughtGoodsPage { @Test @DisplayName("회원 프로필 굿즈 구매기록 페이징 조회 성공") + @WithAuthMember(userId = "customUser", memberId = 4L) void get_bought_goods_page_success() throws Exception { // given Member buyer = memberRepository.save(Member.builder() @@ -360,6 +371,7 @@ void get_bought_goods_page_success() throws Exception { @Test @DisplayName("회원 프로필 굿즈 구매기록 페이징 조회 실패 - 유효하지 않은 회원 아이디로 조회") + @WithAuthMember(userId = "customUser", memberId = 1L) void get_bought_goods_page_invalid_member_id() throws Exception { // given Long invalidMemberId = member1.getId() + 999L; // 존재하지 않는 회원 ID @@ -371,10 +383,10 @@ void get_bought_goods_page_invalid_member_id() throws Exception { .param("page", String.valueOf(page)) .param("size", String.valueOf(size))) .andDo(print()) - .andExpect(status().isNotFound()) + .andExpect(status().isForbidden()) .andExpect(jsonPath("$.status").value("ERROR")) - .andExpect(jsonPath("$.code").value(404)) - .andExpect(jsonPath("$.message").value("해당 ID의 회원 정보를 찾을 수 없습니다")); // 커스텀 에러 메시지 + .andExpect(jsonPath("$.code").value(403)) + .andExpect(jsonPath("$.message").value("해당 회원의 접근 권한이 없습니다.")); // 커스텀 에러 메시지 } } @@ -468,13 +480,14 @@ class ProfileTimelinePage { @Test @DisplayName("회원 타임라인 페이징 조회 성공") + @WithAuthMember(userId = "customUser", memberId = 1L) void get_my_visit_page_success() throws Exception { // given Long memberId = member1.getId(); Pageable pageable = PageRequest.of(0, 10); // when & then - mockMvc.perform(get("/api/profile/timeline/{memberId}", memberId) + mockMvc.perform(get("/api/profile/timeline") .param("page", String.valueOf(pageable.getPageNumber())) .param("size", String.valueOf(pageable.getPageSize()))) .andDo(print()) @@ -485,22 +498,5 @@ void get_my_visit_page_success() throws Exception { .andExpect(jsonPath("$.code").value(200)); } - @Test - @DisplayName("회원 타임라인 페이징 조회 실패 - 유효하지 않은 회원 아이디로 조회") - void get_my_visit_page_fail_invalid_member_id() throws Exception { - // given - Long invalidMemberId = member1.getId() + 999L; // 존재 하지 않는 아이디 - Pageable pageable = PageRequest.of(0, 10); - - // when & then - mockMvc.perform(get("/api/profile/timeline/{memberId}", invalidMemberId) - .param("page", String.valueOf(pageable.getPageNumber())) - .param("size", String.valueOf(pageable.getPageNumber()))) - .andDo(print()) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.status").value("ERROR")) - .andExpect(jsonPath("$.code").value(404)) - .andExpect(jsonPath("$.message").value("해당 ID의 회원 정보를 찾을 수 없습니다")); - } } } \ No newline at end of file