From 38547a6e4dd661e25c07d672381628429d4eb984 Mon Sep 17 00:00:00 2001 From: ekgns33 <76658405+ekgns33@users.noreply.github.com> Date: Mon, 22 Jul 2024 17:43:21 +0900 Subject: [PATCH] Bp 24 implement user crud (#9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Bp 25 implement user registration (#6) * feat : 회원가입 api 권한 수정, 엔드포인트 prefix 메소드 추가 * feat : 에러코드, 커스텀 에러 추가, 전역 핸들러에 로직구현 * feat : 회원가입 DTO추가, 멤버 컨트롤러 추가 * feat : 멤버 서비스 추가, 회원가입 기능 추가 * test : 컨트롤러 테스트, 유닛테스트 작성 * feat : 유저 비밀번호 암호화 추가 * rename : 파일 위치 수정 * test : 암호화 로직 추가 * rename : DTO 컨트롤러 패키지로 이동 * refactor : 매직넘버 삭제, HttpStatus사용 * refactor : 기존 ErrorCode를 인터페이스화, 도메인마다 에러코드 정의 * refactor : 에러코드 구조 변경에 따른 코드 수정 * chore : spring-validation 추가 * Bp 26 implement user withdraw (#8) * feat : 객체 생성 시 필드 체크를 위한 Validator생성 * feat : 멤버 엔티티객체 필드 기본값 수정, 탈퇴로직 구현 * feat : 회원 탈퇴 로직 구현 * feat : 시큐리티 회원가입, 로그인 csrf해제 * feat : 시큐리티 유틸성 클래스 추가 * feat : 멤버 서비스 예외 추가와 에러코드 추가 * style : 코드 포맷 수정 * test: 테스트코드 수정, 삭제 테스트코드 작성 * feat : Spring Validation에서 발생하는 에러 핸들링 * test : mock user 어노테이션추가 * refactor : 코드리뷰 적용 * fix ; csrf 비활성화 * feat : 식별자 전략 명시 IDENTITY --- build.gradle | 1 + .../platformcore/PlatformCoreApplication.java | 7 +- .../CustomAuthenticationFailureHandler.java | 10 +- .../CustomAuthenticationSuccessHandler.java | 7 +- .../application/auth/CustomUserDetails.java | 4 +- .../auth/CustomUserDetailsService.java | 8 +- .../exceptions/InvalidUserInfoException.java | 17 -- .../application/member/MemberService.java | 49 +++++ .../member/exceptions/MemberErrorCode.java | 37 ++++ .../UserAlreadyDeletedException.java | 16 ++ .../exceptions/UserAlreadyExistException.java | 15 ++ .../exceptions/UserNotFoundException.java | 16 ++ .../controller/member/MemberController.java | 48 +++++ .../member/MemberRegisterRequest.java | 32 ++++ .../domain/member/entity/Member.java | 50 ++++-- .../global/configs/SecurityConfig.java | 36 ++-- .../global/consts/PlatformConstants.java | 10 ++ .../controller/GlobalExceptionHandler.java | 15 +- .../global/exceptions/BusinessException.java | 11 +- .../global/exceptions/CustomErrorCode.java | 10 ++ .../global/exceptions/ErrorCode.java | 23 --- .../global/exceptions/GlobalErrorCode.java | 36 ++++ .../global/responses/ErrorResponse.java | 10 +- .../global/responses/SuccessResponse.java | 2 + .../global/utils/FieldValidator.java | 15 ++ .../global/utils/SecurityUtils.java | 25 +++ .../annotation/CustomMockUser.java | 16 ++ ...ithMyCustomUserSecurityContextFactory.java | 36 ++++ .../application/member/MemberServiceTest.java | 146 +++++++++++++++ .../controller/auth/AuthControllerTest.java | 6 +- .../member/MemberControllerTest.java | 170 ++++++++++++++++++ 31 files changed, 772 insertions(+), 112 deletions(-) delete mode 100644 src/main/java/gdsc/konkuk/platformcore/application/auth/exceptions/InvalidUserInfoException.java create mode 100644 src/main/java/gdsc/konkuk/platformcore/application/member/MemberService.java create mode 100644 src/main/java/gdsc/konkuk/platformcore/application/member/exceptions/MemberErrorCode.java create mode 100644 src/main/java/gdsc/konkuk/platformcore/application/member/exceptions/UserAlreadyDeletedException.java create mode 100644 src/main/java/gdsc/konkuk/platformcore/application/member/exceptions/UserAlreadyExistException.java create mode 100644 src/main/java/gdsc/konkuk/platformcore/application/member/exceptions/UserNotFoundException.java create mode 100644 src/main/java/gdsc/konkuk/platformcore/controller/member/MemberController.java create mode 100644 src/main/java/gdsc/konkuk/platformcore/controller/member/MemberRegisterRequest.java create mode 100644 src/main/java/gdsc/konkuk/platformcore/global/consts/PlatformConstants.java create mode 100644 src/main/java/gdsc/konkuk/platformcore/global/exceptions/CustomErrorCode.java delete mode 100644 src/main/java/gdsc/konkuk/platformcore/global/exceptions/ErrorCode.java create mode 100644 src/main/java/gdsc/konkuk/platformcore/global/exceptions/GlobalErrorCode.java create mode 100644 src/main/java/gdsc/konkuk/platformcore/global/utils/FieldValidator.java create mode 100644 src/main/java/gdsc/konkuk/platformcore/global/utils/SecurityUtils.java create mode 100644 src/test/java/gdsc/konkuk/platformcore/annotation/CustomMockUser.java create mode 100644 src/test/java/gdsc/konkuk/platformcore/annotation/WithMyCustomUserSecurityContextFactory.java create mode 100644 src/test/java/gdsc/konkuk/platformcore/application/member/MemberServiceTest.java create mode 100644 src/test/java/gdsc/konkuk/platformcore/controller/member/MemberControllerTest.java diff --git a/build.gradle b/build.gradle index 94fa59f..efb52d4 100644 --- a/build.gradle +++ b/build.gradle @@ -41,6 +41,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-security:2.3.3.RELEASE' + implementation 'org.springframework.boot:spring-boot-starter-validation' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' annotationProcessor 'org.projectlombok:lombok' diff --git a/src/main/java/gdsc/konkuk/platformcore/PlatformCoreApplication.java b/src/main/java/gdsc/konkuk/platformcore/PlatformCoreApplication.java index d319705..16ac5cb 100644 --- a/src/main/java/gdsc/konkuk/platformcore/PlatformCoreApplication.java +++ b/src/main/java/gdsc/konkuk/platformcore/PlatformCoreApplication.java @@ -6,8 +6,7 @@ @SpringBootApplication public class PlatformCoreApplication { - public static void main(String[] args) { - SpringApplication.run(PlatformCoreApplication.class, args); - System.out.println("TESTCITEST"); - } + public static void main(String[] args) { + SpringApplication.run(PlatformCoreApplication.class, args); + } } diff --git a/src/main/java/gdsc/konkuk/platformcore/application/auth/CustomAuthenticationFailureHandler.java b/src/main/java/gdsc/konkuk/platformcore/application/auth/CustomAuthenticationFailureHandler.java index 02e3a39..05fc706 100644 --- a/src/main/java/gdsc/konkuk/platformcore/application/auth/CustomAuthenticationFailureHandler.java +++ b/src/main/java/gdsc/konkuk/platformcore/application/auth/CustomAuthenticationFailureHandler.java @@ -8,9 +8,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; -import gdsc.konkuk.platformcore.global.exceptions.ErrorCode; +import gdsc.konkuk.platformcore.application.member.exceptions.MemberErrorCode; import gdsc.konkuk.platformcore.global.responses.ErrorResponse; -import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -22,10 +21,11 @@ public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationF private final ObjectMapper objectMapper; @Override - public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws - IOException, ServletException { + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, + AuthenticationException exception) throws + IOException { - ErrorResponse errorResponse = ErrorResponse.of(ErrorCode.INVALID_USER_INFO); + ErrorResponse errorResponse = ErrorResponse.of(MemberErrorCode.INVALID_USER_INFO); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); diff --git a/src/main/java/gdsc/konkuk/platformcore/application/auth/CustomAuthenticationSuccessHandler.java b/src/main/java/gdsc/konkuk/platformcore/application/auth/CustomAuthenticationSuccessHandler.java index c8169b0..02aae59 100644 --- a/src/main/java/gdsc/konkuk/platformcore/application/auth/CustomAuthenticationSuccessHandler.java +++ b/src/main/java/gdsc/konkuk/platformcore/application/auth/CustomAuthenticationSuccessHandler.java @@ -23,15 +23,16 @@ public class CustomAuthenticationSuccessHandler extends SimpleUrlAuthenticationS private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); @Override - public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { setDefaultTargetUrl("/"); SavedRequest savedRequest = requestCache.getRequest(request, response); - if(savedRequest != null){ + if (savedRequest != null) { String targetUrl = savedRequest.getRedirectUrl(); redirectStrategy.sendRedirect(request, response, targetUrl); - }else { + } else { redirectStrategy.sendRedirect(request, response, getDefaultTargetUrl()); } } diff --git a/src/main/java/gdsc/konkuk/platformcore/application/auth/CustomUserDetails.java b/src/main/java/gdsc/konkuk/platformcore/application/auth/CustomUserDetails.java index cb7f2d7..51f19c8 100644 --- a/src/main/java/gdsc/konkuk/platformcore/application/auth/CustomUserDetails.java +++ b/src/main/java/gdsc/konkuk/platformcore/application/auth/CustomUserDetails.java @@ -31,11 +31,11 @@ public String getPassword() { @Override public String getUsername() { - return member.getMemberId(); + return Long.toString(member.getId()); } @Override public boolean isAccountNonExpired() { - return member.isActivated(); + return member.isMemberActivated(); } } diff --git a/src/main/java/gdsc/konkuk/platformcore/application/auth/CustomUserDetailsService.java b/src/main/java/gdsc/konkuk/platformcore/application/auth/CustomUserDetailsService.java index 901cbc5..cbcfb52 100644 --- a/src/main/java/gdsc/konkuk/platformcore/application/auth/CustomUserDetailsService.java +++ b/src/main/java/gdsc/konkuk/platformcore/application/auth/CustomUserDetailsService.java @@ -5,10 +5,10 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; -import gdsc.konkuk.platformcore.application.auth.exceptions.InvalidUserInfoException; +import gdsc.konkuk.platformcore.application.member.exceptions.MemberErrorCode; import gdsc.konkuk.platformcore.domain.member.entity.Member; import gdsc.konkuk.platformcore.domain.member.repository.MemberRepository; -import gdsc.konkuk.platformcore.global.exceptions.ErrorCode; +import gdsc.konkuk.platformcore.global.exceptions.BusinessException; import lombok.RequiredArgsConstructor; @Service @@ -19,10 +19,8 @@ public class CustomUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String memberId) throws UsernameNotFoundException { - Member member = memberRepository.findByMemberId(memberId) - .orElseThrow(()-> InvalidUserInfoException.of(ErrorCode.USER_NOT_FOUND)); - + .orElseThrow(() -> BusinessException.of(MemberErrorCode.USER_NOT_FOUND)); return new CustomUserDetails(member); } } diff --git a/src/main/java/gdsc/konkuk/platformcore/application/auth/exceptions/InvalidUserInfoException.java b/src/main/java/gdsc/konkuk/platformcore/application/auth/exceptions/InvalidUserInfoException.java deleted file mode 100644 index ecee3f1..0000000 --- a/src/main/java/gdsc/konkuk/platformcore/application/auth/exceptions/InvalidUserInfoException.java +++ /dev/null @@ -1,17 +0,0 @@ -package gdsc.konkuk.platformcore.application.auth.exceptions; - -import gdsc.konkuk.platformcore.global.exceptions.BusinessException; -import gdsc.konkuk.platformcore.global.exceptions.ErrorCode; - -public class InvalidUserInfoException extends BusinessException { - - private InvalidUserInfoException(String message, String logMessage) { - super(message, logMessage); - } - - public static InvalidUserInfoException of(ErrorCode errorCode) { - return new InvalidUserInfoException(errorCode.getMessage(), errorCode.getLogMessage()); - } - -} - diff --git a/src/main/java/gdsc/konkuk/platformcore/application/member/MemberService.java b/src/main/java/gdsc/konkuk/platformcore/application/member/MemberService.java new file mode 100644 index 0000000..fb3a602 --- /dev/null +++ b/src/main/java/gdsc/konkuk/platformcore/application/member/MemberService.java @@ -0,0 +1,49 @@ +package gdsc.konkuk.platformcore.application.member; + +import java.util.Optional; + +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import gdsc.konkuk.platformcore.application.member.exceptions.MemberErrorCode; +import gdsc.konkuk.platformcore.application.member.exceptions.UserAlreadyExistException; +import gdsc.konkuk.platformcore.application.member.exceptions.UserNotFoundException; +import gdsc.konkuk.platformcore.controller.member.MemberRegisterRequest; +import gdsc.konkuk.platformcore.domain.member.entity.Member; +import gdsc.konkuk.platformcore.domain.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MemberService { + + private final PasswordEncoder passwordEncoder; + private final MemberRepository memberRepository; + + @Transactional + public Member register(MemberRegisterRequest registerRequest) { + + if (checkMemberExistWithMemberId(registerRequest.getMemberId())) { + throw UserAlreadyExistException.of(MemberErrorCode.USER_ALREADY_EXISTS); + } + + String encodedPassword = passwordEncoder.encode(registerRequest.getPassword()); + registerRequest.setPassword(encodedPassword); + + return memberRepository.save(MemberRegisterRequest.toEntity(registerRequest)); + } + + @Transactional + public void withdraw(Long currentId) { + Member member = memberRepository.findById(currentId) + .orElseThrow(() -> UserNotFoundException.of(MemberErrorCode.USER_NOT_FOUND)); + member.withdraw(); + } + + private boolean checkMemberExistWithMemberId(String memberId) { + Optional member = memberRepository.findByMemberId(memberId); + return member.isPresent(); + } +} diff --git a/src/main/java/gdsc/konkuk/platformcore/application/member/exceptions/MemberErrorCode.java b/src/main/java/gdsc/konkuk/platformcore/application/member/exceptions/MemberErrorCode.java new file mode 100644 index 0000000..3c8cf79 --- /dev/null +++ b/src/main/java/gdsc/konkuk/platformcore/application/member/exceptions/MemberErrorCode.java @@ -0,0 +1,37 @@ +package gdsc.konkuk.platformcore.application.member.exceptions; + +import gdsc.konkuk.platformcore.global.exceptions.CustomErrorCode; +import lombok.Getter; + +@Getter +public enum MemberErrorCode implements CustomErrorCode { + + USER_NOT_FOUND("사용자가 존재하지 않습니다. 다시 입력해주세요", "[ERROR] : 사용자 정보를 찾을 수 없음"), + INVALID_USER_INFO("사용자 정보가 올바르지 않습니다. 다시 입력해주세요", "[ERROR] : 사용자 정보가 올바르지 않음"), + DEACTIVATED_USER("비활성화된 사용자입니다. 다시 확인해주세요", "[ERROR] : 비활성화된 사용자"), + USER_ALREADY_EXISTS("이미 존재하는 사용자입니다.", "[ERROR] : 이미 존재하는 사용자"), + USER_ALREADY_DELETED("이미 삭제된 사용자입니다.", "[ERROR] : 이미 삭제된 사용자"); + + private final String message; + private final String logMessage; + + MemberErrorCode(String message, String logMessage) { + this.message = message; + this.logMessage = logMessage; + } + + @Override + public String getLogMessage() { + return this.logMessage; + } + + @Override + public String getName() { + return this.name(); + } + + @Override + public String getMessage() { + return this.message; + } +} diff --git a/src/main/java/gdsc/konkuk/platformcore/application/member/exceptions/UserAlreadyDeletedException.java b/src/main/java/gdsc/konkuk/platformcore/application/member/exceptions/UserAlreadyDeletedException.java new file mode 100644 index 0000000..e616eb4 --- /dev/null +++ b/src/main/java/gdsc/konkuk/platformcore/application/member/exceptions/UserAlreadyDeletedException.java @@ -0,0 +1,16 @@ +package gdsc.konkuk.platformcore.application.member.exceptions; + +import gdsc.konkuk.platformcore.global.exceptions.BusinessException; +import gdsc.konkuk.platformcore.global.exceptions.CustomErrorCode; + +public class UserAlreadyDeletedException extends BusinessException { + + protected UserAlreadyDeletedException(CustomErrorCode errorCode, + String logMessage) { + super(errorCode, logMessage); + } + + public static UserAlreadyDeletedException of(CustomErrorCode errorCode) { + return new UserAlreadyDeletedException(errorCode, errorCode.getMessage()); + } +} diff --git a/src/main/java/gdsc/konkuk/platformcore/application/member/exceptions/UserAlreadyExistException.java b/src/main/java/gdsc/konkuk/platformcore/application/member/exceptions/UserAlreadyExistException.java new file mode 100644 index 0000000..899c3af --- /dev/null +++ b/src/main/java/gdsc/konkuk/platformcore/application/member/exceptions/UserAlreadyExistException.java @@ -0,0 +1,15 @@ +package gdsc.konkuk.platformcore.application.member.exceptions; + +import gdsc.konkuk.platformcore.global.exceptions.BusinessException; +import gdsc.konkuk.platformcore.global.exceptions.CustomErrorCode; + +public class UserAlreadyExistException extends BusinessException { + + protected UserAlreadyExistException(CustomErrorCode errorCode, String logMessage) { + super(errorCode, logMessage); + } + + public static UserAlreadyExistException of(CustomErrorCode errorCode) { + return new UserAlreadyExistException(errorCode, errorCode.getLogMessage()); + } +} diff --git a/src/main/java/gdsc/konkuk/platformcore/application/member/exceptions/UserNotFoundException.java b/src/main/java/gdsc/konkuk/platformcore/application/member/exceptions/UserNotFoundException.java new file mode 100644 index 0000000..7da8a03 --- /dev/null +++ b/src/main/java/gdsc/konkuk/platformcore/application/member/exceptions/UserNotFoundException.java @@ -0,0 +1,16 @@ +package gdsc.konkuk.platformcore.application.member.exceptions; + +import gdsc.konkuk.platformcore.global.exceptions.BusinessException; +import gdsc.konkuk.platformcore.global.exceptions.CustomErrorCode; + +public class UserNotFoundException extends BusinessException { + + protected UserNotFoundException(CustomErrorCode errorCode, + String logMessage) { + super(errorCode, logMessage); + } + + public static UserNotFoundException of(CustomErrorCode errorCode) { + return new UserNotFoundException(errorCode, errorCode.getMessage()); + } +} diff --git a/src/main/java/gdsc/konkuk/platformcore/controller/member/MemberController.java b/src/main/java/gdsc/konkuk/platformcore/controller/member/MemberController.java new file mode 100644 index 0000000..0c331e8 --- /dev/null +++ b/src/main/java/gdsc/konkuk/platformcore/controller/member/MemberController.java @@ -0,0 +1,48 @@ +package gdsc.konkuk.platformcore.controller.member; + +import java.net.URI; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import gdsc.konkuk.platformcore.application.member.MemberService; +import gdsc.konkuk.platformcore.domain.member.entity.Member; +import gdsc.konkuk.platformcore.global.utils.SecurityUtils; +import gdsc.konkuk.platformcore.global.responses.Response; +import gdsc.konkuk.platformcore.global.responses.SuccessResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/api/v1/members") +@RequiredArgsConstructor +public class MemberController { + + private final MemberService memberService; + + @PostMapping() + public ResponseEntity signup(@RequestBody @Valid MemberRegisterRequest registerRequest) { + Member registeredMember = memberService.register(registerRequest); + return ResponseEntity.created(getCreatedURI(registeredMember.getId())).body(SuccessResponse.messageOnly()); + } + + @DeleteMapping() + public ResponseEntity withdraw() { + Long currentId = SecurityUtils.getCurrentUserId(); + memberService.withdraw(currentId); + return ResponseEntity.ok(SuccessResponse.messageOnly()); + } + + private URI getCreatedURI(Long memberId) { + return ServletUriComponentsBuilder + .fromCurrentRequest() + .path("/{id}") + .buildAndExpand(memberId) + .toUri(); + } +} diff --git a/src/main/java/gdsc/konkuk/platformcore/controller/member/MemberRegisterRequest.java b/src/main/java/gdsc/konkuk/platformcore/controller/member/MemberRegisterRequest.java new file mode 100644 index 0000000..cac0426 --- /dev/null +++ b/src/main/java/gdsc/konkuk/platformcore/controller/member/MemberRegisterRequest.java @@ -0,0 +1,32 @@ +package gdsc.konkuk.platformcore.controller.member; + +import gdsc.konkuk.platformcore.domain.member.entity.Member; +import jakarta.validation.constraints.NotEmpty; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Builder +public class MemberRegisterRequest { + @NotEmpty + private String memberId; + @NotEmpty + private String password; + @NotEmpty + private String name; + @NotEmpty + private String email; + private int batch; + + public static Member toEntity(MemberRegisterRequest request) { + return Member.builder() + .memberId(request.getMemberId()) + .password(request.getPassword()) + .name(request.getName()) + .email(request.getEmail()) + .batch(request.getBatch()) + .build(); + } +} diff --git a/src/main/java/gdsc/konkuk/platformcore/domain/member/entity/Member.java b/src/main/java/gdsc/konkuk/platformcore/domain/member/entity/Member.java index 45cd484..102a906 100644 --- a/src/main/java/gdsc/konkuk/platformcore/domain/member/entity/Member.java +++ b/src/main/java/gdsc/konkuk/platformcore/domain/member/entity/Member.java @@ -1,12 +1,20 @@ package gdsc.konkuk.platformcore.domain.member.entity; +import static gdsc.konkuk.platformcore.global.consts.PlatformConstants.SOFT_DELETE_RETENTION_MONTHS; +import static gdsc.konkuk.platformcore.global.utils.FieldValidator.validateNotNull; + import java.time.LocalDateTime; +import org.hibernate.annotations.SQLRestriction; + +import gdsc.konkuk.platformcore.application.member.exceptions.MemberErrorCode; +import gdsc.konkuk.platformcore.application.member.exceptions.UserAlreadyDeletedException; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import lombok.AccessLevel; import lombok.Builder; @@ -14,14 +22,16 @@ import lombok.NoArgsConstructor; @Entity +@SQLRestriction("is_deleted = false") @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Member { - @Id @GeneratedValue + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(name = "member_id",unique = true) + @Column(name = "member_id", unique = true) private String memberId; @Column(name = "password") @@ -37,34 +47,46 @@ public class Member { private String profileImageUrl; @Column(name = "is_activated") - private boolean isActivated; + private boolean isActivated = true; @Column(name = "is_deleted") - private boolean isDeleted; + private boolean isDeleted = false; @Column(name = "soft_deleted_at") private LocalDateTime softDeletedAt; @Enumerated(EnumType.STRING) @Column(name = "member_role") - private MemberRole role; + private MemberRole role = MemberRole.MEMBER; @Column(name = "batch") private int batch; + public void withdraw() { + if (isDeleted) { + throw UserAlreadyDeletedException.of(MemberErrorCode.USER_ALREADY_DELETED); + } + isDeleted = true; + softDeletedAt = LocalDateTime.now().plusMonths(SOFT_DELETE_RETENTION_MONTHS); + } + + public Boolean isMemberDeleted() { + return isDeleted; + } + + public Boolean isMemberActivated() { + return isActivated; + } + @Builder public Member(Long id, String memberId, String password, String name, String email, String profileImageUrl, - boolean isActivated, boolean isDeleted, LocalDateTime deletedAt, MemberRole role, int batch) { + int batch) { this.id = id; - this.memberId = memberId; - this.password = password; - this.name = name; - this.email = email; + this.memberId = validateNotNull(memberId, "memberId"); + this.password = validateNotNull(password, "password"); + this.name = validateNotNull(name, "name"); + this.email = validateNotNull(email, "email"); this.profileImageUrl = profileImageUrl; - this.isActivated = isActivated; - this.isDeleted = isDeleted; - this.softDeletedAt = deletedAt; - this.role = role; this.batch = batch; } } diff --git a/src/main/java/gdsc/konkuk/platformcore/global/configs/SecurityConfig.java b/src/main/java/gdsc/konkuk/platformcore/global/configs/SecurityConfig.java index e8ef71d..4480990 100644 --- a/src/main/java/gdsc/konkuk/platformcore/global/configs/SecurityConfig.java +++ b/src/main/java/gdsc/konkuk/platformcore/global/configs/SecurityConfig.java @@ -1,15 +1,17 @@ package gdsc.konkuk.platformcore.global.configs; +import static gdsc.konkuk.platformcore.global.consts.PlatformConstants.*; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.context.SecurityContextPersistenceFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import gdsc.konkuk.platformcore.application.auth.CustomAuthenticationFailureHandler; import gdsc.konkuk.platformcore.application.auth.CustomAuthenticationSuccessHandler; @@ -26,34 +28,32 @@ public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { httpSecurity - .addFilterBefore(new SecurityContextPersistenceFilter(), BasicAuthenticationFilter.class) + //TODO: dev에서만 disable + .csrf(csrf -> csrf.disable() + ) + .addFilterBefore(new SecurityContextPersistenceFilter(), UsernamePasswordAuthenticationFilter.class) .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/docs/**").permitAll() - .requestMatchers("/admin").hasRole("ADMIN") - .requestMatchers("/member").hasRole("MEMBER") - .anyRequest().authenticated()) + .requestMatchers(apiPath("/docs/**")).permitAll() + .requestMatchers(HttpMethod.POST, "/api/v1/members").permitAll() + .requestMatchers(apiPath("/admin/**")).hasRole("ADMIN") + .anyRequest().permitAll()) .formLogin(login -> login .defaultSuccessUrl("/") + .usernameParameter(LOGIN_NAME) .successHandler(customAuthenticationSuccessHandler) .failureHandler(customAuthenticationFailureHandler) - .permitAll() ); return httpSecurity.build(); } - @Bean - public SecurityFilterChain swaggerFilterchain(HttpSecurity httpSecurity) throws Exception { - httpSecurity - .securityMatcher("/docs") - .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/**").authenticated()); - return httpSecurity.build(); - } - @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + + private String apiPath(String path) { + return API_PREFIX + path; + } } diff --git a/src/main/java/gdsc/konkuk/platformcore/global/consts/PlatformConstants.java b/src/main/java/gdsc/konkuk/platformcore/global/consts/PlatformConstants.java new file mode 100644 index 0000000..f537a57 --- /dev/null +++ b/src/main/java/gdsc/konkuk/platformcore/global/consts/PlatformConstants.java @@ -0,0 +1,10 @@ +package gdsc.konkuk.platformcore.global.consts; + +public class PlatformConstants { + + public static final Integer SOFT_DELETE_RETENTION_MONTHS = 3; + + public static final String LOGIN_NAME = "id"; + public static final String API_PREFIX = "/api/v1"; + +} diff --git a/src/main/java/gdsc/konkuk/platformcore/global/controller/GlobalExceptionHandler.java b/src/main/java/gdsc/konkuk/platformcore/global/controller/GlobalExceptionHandler.java index aaebbca..9434652 100644 --- a/src/main/java/gdsc/konkuk/platformcore/global/controller/GlobalExceptionHandler.java +++ b/src/main/java/gdsc/konkuk/platformcore/global/controller/GlobalExceptionHandler.java @@ -2,11 +2,12 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import gdsc.konkuk.platformcore.global.exceptions.BusinessException; -import gdsc.konkuk.platformcore.global.exceptions.ErrorCode; +import gdsc.konkuk.platformcore.global.exceptions.GlobalErrorCode; import gdsc.konkuk.platformcore.global.responses.ErrorResponse; import lombok.extern.slf4j.Slf4j; @@ -14,7 +15,6 @@ @RestControllerAdvice public class GlobalExceptionHandler { - @ExceptionHandler(BusinessException.class) protected ResponseEntity handleBusinessException(final BusinessException e) { log.error("BusinessException Caught! [{}]", e.getLogMessage()); @@ -22,10 +22,17 @@ protected ResponseEntity handleBusinessException(final BusinessEx return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); } + @ExceptionHandler(MethodArgumentNotValidException.class) + protected ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + log.error("MethodArgumentNotValidException Caught! [{}]", e.getBindingResult()); + final ErrorResponse response = ErrorResponse.of(GlobalErrorCode.ARGUMENT_NOT_VALID); + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } + @ExceptionHandler(Exception.class) protected ResponseEntity handleException(Exception e) { - log.error("Exception Uncaught! [{}]", e.getCause().toString()); - final ErrorResponse response = ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR); + log.error("Exception Uncaught!", e); + final ErrorResponse response = ErrorResponse.of(GlobalErrorCode.INTERNAL_SERVER_ERROR); return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); } } diff --git a/src/main/java/gdsc/konkuk/platformcore/global/exceptions/BusinessException.java b/src/main/java/gdsc/konkuk/platformcore/global/exceptions/BusinessException.java index 4fa4daf..d92428e 100644 --- a/src/main/java/gdsc/konkuk/platformcore/global/exceptions/BusinessException.java +++ b/src/main/java/gdsc/konkuk/platformcore/global/exceptions/BusinessException.java @@ -3,21 +3,16 @@ import lombok.Getter; @Getter -public class BusinessException extends RuntimeException{ +public class BusinessException extends RuntimeException { private final String logMessage; - protected BusinessException(String message, String logMessage) { - super(message); - this.logMessage = logMessage; - } - - protected BusinessException(ErrorCode errorCode, String logMessage) { + protected BusinessException(CustomErrorCode errorCode, String logMessage) { super(errorCode.getMessage()); this.logMessage = logMessage; } - public static BusinessException of(ErrorCode errorCode) { + public static BusinessException of(CustomErrorCode errorCode) { return new BusinessException(errorCode, errorCode.getLogMessage()); } diff --git a/src/main/java/gdsc/konkuk/platformcore/global/exceptions/CustomErrorCode.java b/src/main/java/gdsc/konkuk/platformcore/global/exceptions/CustomErrorCode.java new file mode 100644 index 0000000..ba4bd4d --- /dev/null +++ b/src/main/java/gdsc/konkuk/platformcore/global/exceptions/CustomErrorCode.java @@ -0,0 +1,10 @@ +package gdsc.konkuk.platformcore.global.exceptions; + +public interface CustomErrorCode { + + String getLogMessage(); + + String getName(); + + String getMessage(); +} diff --git a/src/main/java/gdsc/konkuk/platformcore/global/exceptions/ErrorCode.java b/src/main/java/gdsc/konkuk/platformcore/global/exceptions/ErrorCode.java deleted file mode 100644 index 67018f8..0000000 --- a/src/main/java/gdsc/konkuk/platformcore/global/exceptions/ErrorCode.java +++ /dev/null @@ -1,23 +0,0 @@ -package gdsc.konkuk.platformcore.global.exceptions; - -import lombok.Getter; -import lombok.ToString; - -@Getter -@ToString -public enum ErrorCode { - - INTERNAL_SERVER_ERROR("서버 오류입니다. 잠시후 재시도 해주세요", "[ERROR] : 예상치못한 에러 발생", 500), - USER_NOT_FOUND("사용자가 존재하지 않습니다. 다시 입력해주세요", "[ERROR] : 사용자 정보를 찾을 수 없음", 404), - INVALID_USER_INFO("사용자 정보가 올바르지 않습니다. 다시 입력해주세요", "[ERROR] : 사용자 정보가 올바르지 않음", 400), - DEACTIVATED_USER("탈퇴한 사용자입니다. 다시 확인해주세요", "[ERROR] : 탈퇴한 사용자", 400); - private final String message; - private final String logMessage; - private final int statusCode; - - ErrorCode(String message, String logMessage, int statusCode) { - this.message = message; - this.logMessage = logMessage; - this.statusCode = statusCode; - } -} diff --git a/src/main/java/gdsc/konkuk/platformcore/global/exceptions/GlobalErrorCode.java b/src/main/java/gdsc/konkuk/platformcore/global/exceptions/GlobalErrorCode.java new file mode 100644 index 0000000..6c3115b --- /dev/null +++ b/src/main/java/gdsc/konkuk/platformcore/global/exceptions/GlobalErrorCode.java @@ -0,0 +1,36 @@ +package gdsc.konkuk.platformcore.global.exceptions; + +import lombok.Getter; + +@Getter +public enum GlobalErrorCode implements CustomErrorCode { + + // DTO Validation에서 발생한 에러 처리를 위한 코드 + ARGUMENT_NOT_VALID("잘못된 입력입니다. 다시 확인해주세요", "[ERROR] : 400 컨트롤러 벨리데이션 실패 잘못된 인자"), + + NOT_FOUND("찾을 수 없습니다. 다시 확인해주세요", "[ERROR] : 404 에러 발생"), + INTERNAL_SERVER_ERROR("서버 오류입니다. 잠시후 재시도 해주세요", "[ERROR] : 500 예상치못한 에러 발생"); + + private final String message; + private final String logMessage; + + GlobalErrorCode(String message, String logMessage) { + this.message = message; + this.logMessage = logMessage; + } + + @Override + public String getLogMessage() { + return this.logMessage; + } + + @Override + public String getName() { + return this.name(); + } + + @Override + public String getMessage() { + return this.message; + } +} diff --git a/src/main/java/gdsc/konkuk/platformcore/global/responses/ErrorResponse.java b/src/main/java/gdsc/konkuk/platformcore/global/responses/ErrorResponse.java index 4287781..b52a76d 100644 --- a/src/main/java/gdsc/konkuk/platformcore/global/responses/ErrorResponse.java +++ b/src/main/java/gdsc/konkuk/platformcore/global/responses/ErrorResponse.java @@ -1,10 +1,10 @@ package gdsc.konkuk.platformcore.global.responses; -import gdsc.konkuk.platformcore.global.exceptions.ErrorCode; +import gdsc.konkuk.platformcore.global.exceptions.CustomErrorCode; import lombok.Getter; @Getter -public class ErrorResponse extends Response{ +public class ErrorResponse extends Response { private final String errorCode; @@ -13,12 +13,12 @@ private ErrorResponse(final String message, final String errorCode) { this.errorCode = errorCode; } - private ErrorResponse(final ErrorCode errorCode) { + private ErrorResponse(final CustomErrorCode errorCode) { super(false, errorCode.getMessage()); - this.errorCode = errorCode.name(); + this.errorCode = errorCode.getName(); } - public static ErrorResponse of(ErrorCode errorCode) { + public static ErrorResponse of(CustomErrorCode errorCode) { return new ErrorResponse(errorCode); } diff --git a/src/main/java/gdsc/konkuk/platformcore/global/responses/SuccessResponse.java b/src/main/java/gdsc/konkuk/platformcore/global/responses/SuccessResponse.java index a679a8b..0e7c2f6 100644 --- a/src/main/java/gdsc/konkuk/platformcore/global/responses/SuccessResponse.java +++ b/src/main/java/gdsc/konkuk/platformcore/global/responses/SuccessResponse.java @@ -11,9 +11,11 @@ private SuccessResponse(String message, Object data) { super(true, message); this.data = data; } + public static SuccessResponse messageOnly() { return new SuccessResponse("SUCCESS", null); } + public static SuccessResponse of(Object data) { return new SuccessResponse("SUCCESS", data); } diff --git a/src/main/java/gdsc/konkuk/platformcore/global/utils/FieldValidator.java b/src/main/java/gdsc/konkuk/platformcore/global/utils/FieldValidator.java new file mode 100644 index 0000000..c3ba4bf --- /dev/null +++ b/src/main/java/gdsc/konkuk/platformcore/global/utils/FieldValidator.java @@ -0,0 +1,15 @@ +package gdsc.konkuk.platformcore.global.utils; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class FieldValidator { + + public static T validateNotNull(T value, String fieldName) { + if (value == null) { + throw new IllegalArgumentException(fieldName + " must not be null"); + } + return value; + } +} diff --git a/src/main/java/gdsc/konkuk/platformcore/global/utils/SecurityUtils.java b/src/main/java/gdsc/konkuk/platformcore/global/utils/SecurityUtils.java new file mode 100644 index 0000000..4c88cc5 --- /dev/null +++ b/src/main/java/gdsc/konkuk/platformcore/global/utils/SecurityUtils.java @@ -0,0 +1,25 @@ +package gdsc.konkuk.platformcore.global.utils; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class SecurityUtils { + + public static Long getCurrentUserId() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null) { + throw new IllegalStateException("인증 정보가 없습니다."); + } + + if (authentication.isAuthenticated()) { + return Long.valueOf(authentication.getName()); + } + + throw new IllegalStateException("비정상적인 인증정보입니다."); + } +} diff --git a/src/test/java/gdsc/konkuk/platformcore/annotation/CustomMockUser.java b/src/test/java/gdsc/konkuk/platformcore/annotation/CustomMockUser.java new file mode 100644 index 0000000..2834bf0 --- /dev/null +++ b/src/test/java/gdsc/konkuk/platformcore/annotation/CustomMockUser.java @@ -0,0 +1,16 @@ +package gdsc.konkuk.platformcore.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.springframework.security.test.context.support.WithSecurityContext; + +import gdsc.konkuk.platformcore.domain.member.entity.MemberRole; + +@Retention(RetentionPolicy.RUNTIME) +@WithSecurityContext(factory = WithMyCustomUserSecurityContextFactory.class) +public @interface CustomMockUser { + String memberId() default "202011288"; + + MemberRole role() default MemberRole.MEMBER; +} diff --git a/src/test/java/gdsc/konkuk/platformcore/annotation/WithMyCustomUserSecurityContextFactory.java b/src/test/java/gdsc/konkuk/platformcore/annotation/WithMyCustomUserSecurityContextFactory.java new file mode 100644 index 0000000..e24882c --- /dev/null +++ b/src/test/java/gdsc/konkuk/platformcore/annotation/WithMyCustomUserSecurityContextFactory.java @@ -0,0 +1,36 @@ +package gdsc.konkuk.platformcore.annotation; + +import java.util.List; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +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; + +import gdsc.konkuk.platformcore.domain.member.entity.MemberRole; + +public class WithMyCustomUserSecurityContextFactory implements WithSecurityContextFactory { + @Override + public SecurityContext createSecurityContext(CustomMockUser annotation) { + String memberId = annotation.memberId(); + MemberRole role = annotation.role(); + + Authentication auth = new UsernamePasswordAuthenticationToken( + memberId, + "", + buildGrantedAuthoritiesFromRole(role) + ); + + SecurityContext context = SecurityContextHolder.getContext(); + context.setAuthentication(auth); + + return context; + } + + private List buildGrantedAuthoritiesFromRole(final MemberRole role) { + return List.of(new SimpleGrantedAuthority(role.toString())); + } +} \ No newline at end of file diff --git a/src/test/java/gdsc/konkuk/platformcore/application/member/MemberServiceTest.java b/src/test/java/gdsc/konkuk/platformcore/application/member/MemberServiceTest.java new file mode 100644 index 0000000..c2efba2 --- /dev/null +++ b/src/test/java/gdsc/konkuk/platformcore/application/member/MemberServiceTest.java @@ -0,0 +1,146 @@ +package gdsc.konkuk.platformcore.application.member; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.security.crypto.password.PasswordEncoder; + +import gdsc.konkuk.platformcore.application.member.exceptions.UserAlreadyDeletedException; +import gdsc.konkuk.platformcore.application.member.exceptions.UserAlreadyExistException; +import gdsc.konkuk.platformcore.application.member.exceptions.UserNotFoundException; +import gdsc.konkuk.platformcore.controller.member.MemberRegisterRequest; +import gdsc.konkuk.platformcore.domain.member.entity.Member; +import gdsc.konkuk.platformcore.domain.member.repository.MemberRepository; + +class MemberServiceTest { + + private MemberService subject; + + @Mock + private Member member; + + @Mock + private MemberRepository memberRepository; + + @Mock + private PasswordEncoder passwordEncoder; + + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + subject = new MemberService(passwordEncoder, memberRepository); + } + + @Test + @DisplayName("Register : 새로운 멤버 회원가입 성공") + void should_success_when_newMember_register() { + //given + MemberRegisterRequest memberRegisterRequest = + MemberRegisterRequest.builder() + .memberId("202011288") + .password("password") + .email("example@konkuk.ac.kr") + .name("홍길동") + .batch(2024) + .build(); + given(memberRepository.findByMemberId(any())).willReturn(Optional.empty()); + given(memberRepository.save(any(Member.class))).willReturn(member); + given(passwordEncoder.encode(any())).willReturn("password"); + //when + Member actual = subject.register(memberRegisterRequest); + //then + assertNotNull(actual); + } + + @Test + @DisplayName("Register : 이미 존재하는 멤버 회원가입 실패") + void should_fail_when_already_exist_member_register() { + //given + MemberRegisterRequest memberRegisterRequest = + MemberRegisterRequest.builder() + .memberId("202011288") + .password("password") + .email("example@konkuk.ac.kr") + .name("홍길동") + .batch(2024) + .build(); + given(memberRepository.findByMemberId(any())) + .willReturn(Optional.of(MemberRegisterRequest + .toEntity(memberRegisterRequest))); + + //then + assertThrows(UserAlreadyExistException.class, () -> { + //when + subject.register(memberRegisterRequest); + }); + } + + @Test + @DisplayName("withdraw : 존재하는 멤버 탈퇴 성공") + void should_success_when_user_exists() { + // given + Long targetId = 1L; + Member member = Member.builder() + .id(1L) + .memberId("202011288") + .password("password") + .name("문다훈") + .email("example@gmail.com") + .batch(2024) + .build(); + // when + when(memberRepository.findById(any(Long.class))).thenReturn(Optional.of(member)); + subject.withdraw(targetId); + //then + assertTrue(member.isMemberDeleted()); + assertNotNull(member.getSoftDeletedAt()); + } + + @Test + @DisplayName("withdraw : 존재하지 않는 멤버 탈퇴 실패") + void should_fail_when_user_not_exists() { + // given + Long targetId = 1L; + when(memberRepository.findById(any(Long.class))).thenReturn(Optional.empty()); + + // then + assertThrows(UserNotFoundException.class, () -> { + // when + subject.withdraw(targetId); + }); + } + + @Test + @DisplayName("withdraw : 이미 삭제된 멤버 탈퇴 실패") + void should_fail_when_user_already_deleted() { + // given + Long targetId = 1L; + Member member = Member.builder() + .id(1L) + .memberId("202011288") + .password("password") + .name("문다훈") + .email("example@gmail.com") + .batch(2024) + .build(); + // when + when(memberRepository.findById(any(Long.class))).thenReturn(Optional.of(member)); + // Member soft deleted. + subject.withdraw(targetId); + + // then + assertThrows(UserAlreadyDeletedException.class, () -> { + // when + subject.withdraw(targetId); + }); + } +} \ No newline at end of file diff --git a/src/test/java/gdsc/konkuk/platformcore/controller/auth/AuthControllerTest.java b/src/test/java/gdsc/konkuk/platformcore/controller/auth/AuthControllerTest.java index dd549aa..0791a46 100644 --- a/src/test/java/gdsc/konkuk/platformcore/controller/auth/AuthControllerTest.java +++ b/src/test/java/gdsc/konkuk/platformcore/controller/auth/AuthControllerTest.java @@ -35,7 +35,6 @@ import com.epages.restdocs.apispec.ResourceSnippetParameters; import gdsc.konkuk.platformcore.domain.member.entity.Member; -import gdsc.konkuk.platformcore.domain.member.entity.MemberRole; import gdsc.konkuk.platformcore.domain.member.repository.MemberRepository; @SpringBootTest @@ -70,9 +69,8 @@ void loginSuccess() throws Exception { when(memberRepository.findByMemberId(any())).thenReturn(Optional.of(Member.builder() .memberId("202011288") .password(passwordEncoder.encode("gdscgdsc")) - .isActivated(true) - .role(MemberRole.MEMBER) - .profileImageUrl("") + .name("우이산") + .email("helloworld@gmail.com") .batch(2024) .build())); diff --git a/src/test/java/gdsc/konkuk/platformcore/controller/member/MemberControllerTest.java b/src/test/java/gdsc/konkuk/platformcore/controller/member/MemberControllerTest.java new file mode 100644 index 0000000..fed07a5 --- /dev/null +++ b/src/test/java/gdsc/konkuk/platformcore/controller/member/MemberControllerTest.java @@ -0,0 +1,170 @@ +package gdsc.konkuk.platformcore.controller.member; + +import static com.epages.restdocs.apispec.ResourceDocumentation.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.*; +import static org.springframework.http.MediaType.*; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.fasterxml.jackson.databind.ObjectMapper; + +import gdsc.konkuk.platformcore.annotation.CustomMockUser; +import gdsc.konkuk.platformcore.application.member.MemberService; +import gdsc.konkuk.platformcore.application.member.exceptions.UserAlreadyExistException; +import gdsc.konkuk.platformcore.domain.member.entity.Member; + +@SpringBootTest +@ExtendWith({RestDocumentationExtension.class}) +class MemberControllerTest { + + MockMvc mockMvc; + + @Mock + Member member; + + @MockBean + private MemberService memberService; + + @Autowired + private ObjectMapper objectMapper; + + @BeforeEach + void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) { + MockitoAnnotations.openMocks(this); + mockMvc = MockMvcBuilders + .webAppContextSetup(webApplicationContext) + .apply(springSecurity()) + .apply(documentationConfiguration(restDocumentation)) + .build(); + } + + @Test + @DisplayName("새로운 멤버 회원 가입 성공") + void should_success_when_newMember() throws Exception { + //given + MemberRegisterRequest memberRegisterRequest = + MemberRegisterRequest.builder() + .memberId("202011288") + .password("password") + .email("example@konkuk.ac.kr") + .name("홍길동") + .batch(2024) + .build(); + + given(memberService.register(any(MemberRegisterRequest.class))).willReturn(member); + //when + mockMvc.perform( + RestDocumentationRequestBuilders.post("/api/v1/members") + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(memberRegisterRequest)) + .with(csrf()) + + ) + + .andExpect(status().isCreated()) + .andDo(print()) + .andDo( + document("member/register", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .description("새로운 멤버 회원 가입 성공") + .tag("member") + .requestFields( + fieldWithPath("memberId").description("회원 아이디"), + fieldWithPath("password").description("비밀번호"), + fieldWithPath("email").description("이메일"), + fieldWithPath("name").description("이름"), + fieldWithPath("batch").description("배치") + ) + .responseFields( + fieldWithPath("success").description(true), + fieldWithPath("message").description("회원 가입 성공"), + fieldWithPath("data").description("null") + ) + .build() + ) + ) + ); + } + + @Test + @DisplayName("이미 존재하는 유저 회원 가입 실패") + void should_fail_when_existingMember() throws Exception { + //given + MemberRegisterRequest memberRegisterRequest = + MemberRegisterRequest.builder() + .memberId("202011288") + .password("password") + .email("example@konkuk.ac.kr") + .name("홍길동") + .batch(2024) + .build(); + + given(memberService.register(any(MemberRegisterRequest.class))).willThrow(UserAlreadyExistException.class); + //when + mockMvc.perform( + RestDocumentationRequestBuilders.post("/api/v1/members") + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(memberRegisterRequest)) + .with(csrf()) + ) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + + @Test + @DisplayName("회원 탈퇴 성공") + @CustomMockUser + void should_success_when_delete_member() throws Exception { + //given + + //when + mockMvc.perform( + RestDocumentationRequestBuilders.delete("/api/v1/members") + .contentType(APPLICATION_JSON) + .with(csrf()) + ) + .andExpect(status().isOk()) + .andDo(print()) + .andDo( + document("member/delete", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource(ResourceSnippetParameters.builder() + .description("존재하는 회원 탈퇴") + .tag("member") + .responseFields( + fieldWithPath("success").description(true), + fieldWithPath("message").description("회원 탈퇴 성공"), + fieldWithPath("data").description("null") + ) + .build() + ) + ) + ); + } +} \ No newline at end of file