diff --git a/src/main/java/com/atwoz/member/application/auth/AuthService.java b/src/main/java/com/atwoz/member/application/auth/AuthService.java new file mode 100644 index 00000000..638e7d66 --- /dev/null +++ b/src/main/java/com/atwoz/member/application/auth/AuthService.java @@ -0,0 +1,54 @@ +package com.atwoz.member.application.auth; + +import com.atwoz.global.event.Events; +import com.atwoz.member.application.auth.dto.LoginRequest; +import com.atwoz.member.application.auth.dto.SignupRequest; +import com.atwoz.member.domain.auth.RegisteredEvent; +import com.atwoz.member.domain.auth.TokenProvider; +import com.atwoz.member.domain.member.Member; +import com.atwoz.member.domain.member.MemberRepository; +import com.atwoz.member.domain.member.NicknameGenerator; +import com.atwoz.member.exception.exceptions.member.MemberAlreadyExistedException; +import com.atwoz.member.exception.exceptions.member.MemberNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class AuthService { + + private final MemberRepository memberRepository; + private final TokenProvider tokenProvider; + private final NicknameGenerator nicknameGenerator; + + @Transactional + public String signup(final SignupRequest request) { + validateExistedMember(request.email()); + + Member member = Member.createDefaultRole(request.email(), request.password(), nicknameGenerator); + Member signupMember = memberRepository.save(member); + Events.raise(new RegisteredEvent(member.getId(), member.getEmail(), member.getNickname())); + + return tokenProvider.create(signupMember.getId()); + } + + private void validateExistedMember(final String email) { + if (memberRepository.existsByEmail(email)) { + throw new MemberAlreadyExistedException(); + } + } + + @Transactional(readOnly = true) + public String login(final LoginRequest request) { + Member member = findMemberByEmail(request.email()); + member.validatePassword(request.password()); + + return tokenProvider.create(member.getId()); + } + + private Member findMemberByEmail(final String email) { + return memberRepository.findByEmail(email) + .orElseThrow(MemberNotFoundException::new); + } +} diff --git a/src/main/java/com/atwoz/member/application/auth/dto/LoginRequest.java b/src/main/java/com/atwoz/member/application/auth/dto/LoginRequest.java new file mode 100644 index 00000000..b17c7fc4 --- /dev/null +++ b/src/main/java/com/atwoz/member/application/auth/dto/LoginRequest.java @@ -0,0 +1,12 @@ +package com.atwoz.member.application.auth.dto; + +import jakarta.validation.constraints.NotBlank; + +public record LoginRequest( + @NotBlank(message = "이메일을 입력해주세요.") + String email, + + @NotBlank(message = "패스워드를 입력해주세요.") + String password +) { +} diff --git a/src/main/java/com/atwoz/member/application/auth/dto/SignupRequest.java b/src/main/java/com/atwoz/member/application/auth/dto/SignupRequest.java new file mode 100644 index 00000000..58ef3216 --- /dev/null +++ b/src/main/java/com/atwoz/member/application/auth/dto/SignupRequest.java @@ -0,0 +1,12 @@ +package com.atwoz.member.application.auth.dto; + +import jakarta.validation.constraints.NotBlank; + +public record SignupRequest( + @NotBlank(message = "이메일을 입력해주세요.") + String email, + + @NotBlank(message = "패스워드를 입력해주세요.") + String password +) { +} diff --git a/src/main/java/com/atwoz/member/config/AuthConfig.java b/src/main/java/com/atwoz/member/config/AuthConfig.java new file mode 100644 index 00000000..3ceec047 --- /dev/null +++ b/src/main/java/com/atwoz/member/config/AuthConfig.java @@ -0,0 +1,52 @@ +package com.atwoz.member.config; + +import com.atwoz.member.ui.auth.interceptor.LoginValidCheckerInterceptor; +import com.atwoz.member.ui.auth.interceptor.ParseMemberIdFromTokenInterceptor; +import com.atwoz.member.ui.auth.interceptor.PathMatcherInterceptor; +import com.atwoz.member.ui.auth.support.resolver.AuthArgumentResolver; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +import static com.atwoz.member.ui.auth.interceptor.HttpMethod.ANY; +import static com.atwoz.member.ui.auth.interceptor.HttpMethod.OPTIONS; + +@RequiredArgsConstructor +@Configuration +public class AuthConfig implements WebMvcConfigurer { + + private final AuthArgumentResolver authArgumentResolver; + private final ParseMemberIdFromTokenInterceptor parseMemberIdFromTokenInterceptor; + private final LoginValidCheckerInterceptor loginValidCheckerInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(parseMemberIdFromTokenInterceptor()); + registry.addInterceptor(loginValidCheckerInterceptor()); + } + + private HandlerInterceptor parseMemberIdFromTokenInterceptor() { + return new PathMatcherInterceptor(parseMemberIdFromTokenInterceptor) + .excludePathPattern("/**", OPTIONS) + .addPathPatterns("/admin/**", ANY); + } + + /** + * @AuthMember를 통해서 인증이 필요한 경우에 해당 메서드에 URI를 추가해주면 된다. + * 추가를 해야지 인증,인가 가능 + */ + private HandlerInterceptor loginValidCheckerInterceptor() { + return new PathMatcherInterceptor(loginValidCheckerInterceptor) + .excludePathPattern("/**", OPTIONS); + } + + @Override + public void addArgumentResolvers(final List resolvers) { + resolvers.add(authArgumentResolver); + } +} diff --git a/src/main/java/com/atwoz/member/domain/auth/RegisteredEvent.java b/src/main/java/com/atwoz/member/domain/auth/RegisteredEvent.java new file mode 100644 index 00000000..40f620ac --- /dev/null +++ b/src/main/java/com/atwoz/member/domain/auth/RegisteredEvent.java @@ -0,0 +1,14 @@ +package com.atwoz.member.domain.auth; + +import com.atwoz.global.event.Event; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class RegisteredEvent extends Event { + + private final Long memberId; + private final String email; + private final String nickname; +} diff --git a/src/main/java/com/atwoz/member/domain/auth/TokenProvider.java b/src/main/java/com/atwoz/member/domain/auth/TokenProvider.java new file mode 100644 index 00000000..82ad3cfa --- /dev/null +++ b/src/main/java/com/atwoz/member/domain/auth/TokenProvider.java @@ -0,0 +1,8 @@ +package com.atwoz.member.domain.auth; + +public interface TokenProvider { + + String create(final Long id); + + Long extract(final String token); +} diff --git a/src/main/java/com/atwoz/member/domain/member/Member.java b/src/main/java/com/atwoz/member/domain/member/Member.java new file mode 100644 index 00000000..887ffdf6 --- /dev/null +++ b/src/main/java/com/atwoz/member/domain/member/Member.java @@ -0,0 +1,64 @@ +package com.atwoz.member.domain.member; + +import com.atwoz.global.domain.BaseEntity; +import com.atwoz.member.exception.exceptions.member.PasswordNotMatchedException; +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.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@EqualsAndHashCode(of = "id", callSuper = false) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class Member extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String email; + + @Column(nullable = false) + private String password; + + @Column(nullable = false) + private String nickname; + + @Enumerated(value = EnumType.STRING) + @Column(nullable = false) + private MemberRole memberRole; + + public boolean isAdmin() { + return this.memberRole.isAdministrator(); + } + + public static Member createDefaultRole(final String email, + final String password, + final NicknameGenerator nicknameGenerator) { + return Member.builder() + .email(email) + .password(password) + .nickname(nicknameGenerator.createRandomNickname()) + .memberRole(MemberRole.MEMBER) + .build(); + } + + public void validatePassword(final String password) { + if (!this.password.equals(password)) { + throw new PasswordNotMatchedException(); + } + } +} diff --git a/src/main/java/com/atwoz/member/domain/member/MemberRepository.java b/src/main/java/com/atwoz/member/domain/member/MemberRepository.java new file mode 100644 index 00000000..a889022e --- /dev/null +++ b/src/main/java/com/atwoz/member/domain/member/MemberRepository.java @@ -0,0 +1,16 @@ +package com.atwoz.member.domain.member; + +import java.util.Optional; + +public interface MemberRepository { + + Optional findById(final Long id); + + Optional findByNickname(final String nickname); + + Optional findByEmail(final String email); + + Member save(final Member member); + + boolean existsByEmail(final String email); +} diff --git a/src/main/java/com/atwoz/member/domain/member/MemberRole.java b/src/main/java/com/atwoz/member/domain/member/MemberRole.java new file mode 100644 index 00000000..25f958f8 --- /dev/null +++ b/src/main/java/com/atwoz/member/domain/member/MemberRole.java @@ -0,0 +1,30 @@ +package com.atwoz.member.domain.member; + +import com.atwoz.member.exception.exceptions.member.RoleNotFoundException; +import lombok.Getter; + +import java.util.Arrays; + +@Getter +public enum MemberRole { + + MEMBER("member"), + ADMIN("admin"); + + private final String role; + + MemberRole(final String role) { + this.role = role; + } + + public static MemberRole from(final String role) { + return Arrays.stream(values()) + .filter(value -> value.role.equalsIgnoreCase(role)) + .findFirst() + .orElseThrow(RoleNotFoundException::new); + } + + public boolean isAdministrator() { + return this.equals(ADMIN); + } +} diff --git a/src/main/java/com/atwoz/member/domain/member/NicknameGenerator.java b/src/main/java/com/atwoz/member/domain/member/NicknameGenerator.java new file mode 100644 index 00000000..ab8a2b2d --- /dev/null +++ b/src/main/java/com/atwoz/member/domain/member/NicknameGenerator.java @@ -0,0 +1,6 @@ +package com.atwoz.member.domain.member; + +public interface NicknameGenerator { + + String createRandomNickname(); +} diff --git a/src/main/java/com/atwoz/member/exception/MemberExceptionHandler.java b/src/main/java/com/atwoz/member/exception/MemberExceptionHandler.java new file mode 100644 index 00000000..7ff906fd --- /dev/null +++ b/src/main/java/com/atwoz/member/exception/MemberExceptionHandler.java @@ -0,0 +1,92 @@ +package com.atwoz.member.exception; + +import com.atwoz.member.exception.exceptions.auth.ExpiredTokenException; +import com.atwoz.member.exception.exceptions.auth.LoginInvalidException; +import com.atwoz.member.exception.exceptions.auth.SignatureInvalidException; +import com.atwoz.member.exception.exceptions.auth.TokenFormInvalidException; +import com.atwoz.member.exception.exceptions.auth.TokenInvalidException; +import com.atwoz.member.exception.exceptions.auth.UnsupportedTokenException; +import com.atwoz.member.exception.exceptions.member.MemberAlreadyExistedException; +import com.atwoz.member.exception.exceptions.member.MemberNotFoundException; +import com.atwoz.member.exception.exceptions.member.PasswordNotMatchedException; +import com.atwoz.member.exception.exceptions.member.RoleNotFoundException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class MemberExceptionHandler { + + // member + @ExceptionHandler(RoleNotFoundException.class) + public ResponseEntity handleRoleNotFoundException(final RoleNotFoundException e) { + return getNotFoundResponse(e); + } + + @ExceptionHandler(MemberAlreadyExistedException.class) + public ResponseEntity handleMemberAlreadyExistedException(final MemberAlreadyExistedException e) { + return getConflicted(e); + } + + @ExceptionHandler(MemberNotFoundException.class) + public ResponseEntity handleMemberNotFoundException(final MemberNotFoundException e) { + return getNotFoundResponse(e); + } + + @ExceptionHandler(PasswordNotMatchedException.class) + public ResponseEntity handlePasswordNotMatchedException(final PasswordNotMatchedException e) { + return getConflicted(e); + } + + // auth + @ExceptionHandler(SignatureInvalidException.class) + public ResponseEntity handleSignatureInvalidException(final SignatureInvalidException e) { + return getUnauthorized(e); + } + + @ExceptionHandler(TokenFormInvalidException.class) + public ResponseEntity handleTokenFormInvalidException(final TokenFormInvalidException e) { + return getUnauthorized(e); + } + + @ExceptionHandler(ExpiredTokenException.class) + public ResponseEntity handleExpiredTokenException(final ExpiredTokenException e) { + return getUnauthorized(e); + } + + @ExceptionHandler(UnsupportedTokenException.class) + public ResponseEntity handleUnsupportedTokenException(final UnsupportedTokenException e) { + return getUnauthorized(e); + } + + @ExceptionHandler(TokenInvalidException.class) + public ResponseEntity handleTokenInvalidException(final TokenInvalidException e) { + return getUnauthorized(e); + } + + @ExceptionHandler(LoginInvalidException.class) + public ResponseEntity handleLoginInvalidException(final LoginInvalidException e) { + return getUnauthorized(e); + } + + private ResponseEntity getNotFoundResponse(final Exception e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(e.getMessage()); + } + + private ResponseEntity getUnauthorized(final Exception e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(e.getMessage()); + } + + private ResponseEntity getConflicted(final Exception e) { + return ResponseEntity.status(HttpStatus.CONFLICT) + .body(e.getMessage()); + } + + private ResponseEntity getBadRequest(final Exception e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(e.getMessage()); + } +} diff --git a/src/main/java/com/atwoz/member/exception/exceptions/auth/ExpiredTokenException.java b/src/main/java/com/atwoz/member/exception/exceptions/auth/ExpiredTokenException.java new file mode 100644 index 00000000..4212e7db --- /dev/null +++ b/src/main/java/com/atwoz/member/exception/exceptions/auth/ExpiredTokenException.java @@ -0,0 +1,8 @@ +package com.atwoz.member.exception.exceptions.auth; + +public class ExpiredTokenException extends RuntimeException { + + public ExpiredTokenException() { + super("이미 만료된 토큰입니다"); + } +} diff --git a/src/main/java/com/atwoz/member/exception/exceptions/auth/LoginInvalidException.java b/src/main/java/com/atwoz/member/exception/exceptions/auth/LoginInvalidException.java new file mode 100644 index 00000000..1716d862 --- /dev/null +++ b/src/main/java/com/atwoz/member/exception/exceptions/auth/LoginInvalidException.java @@ -0,0 +1,8 @@ +package com.atwoz.member.exception.exceptions.auth; + +public class LoginInvalidException extends RuntimeException { + + public LoginInvalidException() { + super("로그인 정보를 찾을 수 없습니다."); + } +} diff --git a/src/main/java/com/atwoz/member/exception/exceptions/auth/SignatureInvalidException.java b/src/main/java/com/atwoz/member/exception/exceptions/auth/SignatureInvalidException.java new file mode 100644 index 00000000..cb15a379 --- /dev/null +++ b/src/main/java/com/atwoz/member/exception/exceptions/auth/SignatureInvalidException.java @@ -0,0 +1,8 @@ +package com.atwoz.member.exception.exceptions.auth; + +public class SignatureInvalidException extends RuntimeException { + + public SignatureInvalidException() { + super("서명을 확인하지 못했습니다."); + } +} diff --git a/src/main/java/com/atwoz/member/exception/exceptions/auth/TokenFormInvalidException.java b/src/main/java/com/atwoz/member/exception/exceptions/auth/TokenFormInvalidException.java new file mode 100644 index 00000000..aa894755 --- /dev/null +++ b/src/main/java/com/atwoz/member/exception/exceptions/auth/TokenFormInvalidException.java @@ -0,0 +1,8 @@ +package com.atwoz.member.exception.exceptions.auth; + +public class TokenFormInvalidException extends RuntimeException { + + public TokenFormInvalidException() { + super("토큰의 길이 및 형식이 올바르지 않습니다."); + } +} diff --git a/src/main/java/com/atwoz/member/exception/exceptions/auth/TokenInvalidException.java b/src/main/java/com/atwoz/member/exception/exceptions/auth/TokenInvalidException.java new file mode 100644 index 00000000..674a157a --- /dev/null +++ b/src/main/java/com/atwoz/member/exception/exceptions/auth/TokenInvalidException.java @@ -0,0 +1,8 @@ +package com.atwoz.member.exception.exceptions.auth; + +public class TokenInvalidException extends RuntimeException { + + public TokenInvalidException() { + super("토큰이 유효하지 않습니다."); + } +} diff --git a/src/main/java/com/atwoz/member/exception/exceptions/auth/UnsupportedTokenException.java b/src/main/java/com/atwoz/member/exception/exceptions/auth/UnsupportedTokenException.java new file mode 100644 index 00000000..52a2a021 --- /dev/null +++ b/src/main/java/com/atwoz/member/exception/exceptions/auth/UnsupportedTokenException.java @@ -0,0 +1,8 @@ +package com.atwoz.member.exception.exceptions.auth; + +public class UnsupportedTokenException extends RuntimeException { + + public UnsupportedTokenException() { + super("지원하지 않는 토큰입니다."); + } +} diff --git a/src/main/java/com/atwoz/member/exception/exceptions/member/MemberAlreadyExistedException.java b/src/main/java/com/atwoz/member/exception/exceptions/member/MemberAlreadyExistedException.java new file mode 100644 index 00000000..8097180b --- /dev/null +++ b/src/main/java/com/atwoz/member/exception/exceptions/member/MemberAlreadyExistedException.java @@ -0,0 +1,8 @@ +package com.atwoz.member.exception.exceptions.member; + +public class MemberAlreadyExistedException extends RuntimeException { + + public MemberAlreadyExistedException() { + super("이미 존재하는 Email입니다."); + } +} diff --git a/src/main/java/com/atwoz/member/exception/exceptions/member/MemberNotFoundException.java b/src/main/java/com/atwoz/member/exception/exceptions/member/MemberNotFoundException.java new file mode 100644 index 00000000..9ebb3a21 --- /dev/null +++ b/src/main/java/com/atwoz/member/exception/exceptions/member/MemberNotFoundException.java @@ -0,0 +1,8 @@ +package com.atwoz.member.exception.exceptions.member; + +public class MemberNotFoundException extends RuntimeException { + + public MemberNotFoundException() { + super("Member를 찾을 수 없습니다."); + } +} diff --git a/src/main/java/com/atwoz/member/exception/exceptions/member/PasswordNotMatchedException.java b/src/main/java/com/atwoz/member/exception/exceptions/member/PasswordNotMatchedException.java new file mode 100644 index 00000000..236bb5eb --- /dev/null +++ b/src/main/java/com/atwoz/member/exception/exceptions/member/PasswordNotMatchedException.java @@ -0,0 +1,8 @@ +package com.atwoz.member.exception.exceptions.member; + +public class PasswordNotMatchedException extends RuntimeException { + + public PasswordNotMatchedException() { + super("패스워드가 일치하지 않습니다."); + } +} diff --git a/src/main/java/com/atwoz/member/exception/exceptions/member/RoleNotFoundException.java b/src/main/java/com/atwoz/member/exception/exceptions/member/RoleNotFoundException.java new file mode 100644 index 00000000..3cb7489b --- /dev/null +++ b/src/main/java/com/atwoz/member/exception/exceptions/member/RoleNotFoundException.java @@ -0,0 +1,8 @@ +package com.atwoz.member.exception.exceptions.member; + +public class RoleNotFoundException extends RuntimeException { + + public RoleNotFoundException() { + super("권한을 찾을 수 없습니다."); + } +} diff --git a/src/main/java/com/atwoz/member/infrastructure/auth/JwtTokenProvider.java b/src/main/java/com/atwoz/member/infrastructure/auth/JwtTokenProvider.java new file mode 100644 index 00000000..7170172c --- /dev/null +++ b/src/main/java/com/atwoz/member/infrastructure/auth/JwtTokenProvider.java @@ -0,0 +1,96 @@ +package com.atwoz.member.infrastructure.auth; + +import com.atwoz.member.domain.auth.TokenProvider; +import com.atwoz.member.exception.exceptions.auth.ExpiredTokenException; +import com.atwoz.member.exception.exceptions.auth.SignatureInvalidException; +import com.atwoz.member.exception.exceptions.auth.TokenFormInvalidException; +import com.atwoz.member.exception.exceptions.auth.TokenInvalidException; +import com.atwoz.member.exception.exceptions.auth.UnsupportedTokenException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; + +@Getter +@NoArgsConstructor +@Component +public class JwtTokenProvider implements TokenProvider { + + @Value("${jwt.secret}") + private String secret; + + @Value("${jwt.expiration-period}") + private int expirationPeriod; + + private Key key; + + @PostConstruct + private void init() { + key = Keys.hmacShaKeyFor(secret.getBytes()); + } + + @Override + public String create(final Long id) { + Claims claims = Jwts.claims(); + claims.put("id", id); + return createToken(claims); + } + + private String createToken(final Claims claims) { + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(issuedAt()) + .setExpiration(expiredAt()) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + } + + private Date issuedAt() { + LocalDateTime now = LocalDateTime.now(); + + return Date.from(now.atZone(ZoneId.systemDefault()) + .toInstant()); + } + + private Date expiredAt() { + LocalDateTime now = LocalDateTime.now(); + + return Date.from(now.plusHours(expirationPeriod) + .atZone(ZoneId.systemDefault()) + .toInstant()); + } + + @Override + public Long extract(final String token) { + try { + return Jwts.parser() + .setSigningKey(secret.getBytes()) + .parseClaimsJws(token) + .getBody() + .get("id", Long.class); + } catch (SecurityException e) { + throw new SignatureInvalidException(); + } catch (MalformedJwtException e) { + throw new TokenFormInvalidException(); + } catch (ExpiredJwtException e) { + throw new ExpiredTokenException(); + } catch (UnsupportedJwtException e) { + throw new UnsupportedTokenException(); + } catch (IllegalArgumentException e) { + throw new TokenInvalidException(); + } + } +} diff --git a/src/main/java/com/atwoz/member/infrastructure/member/MemberJpaRepository.java b/src/main/java/com/atwoz/member/infrastructure/member/MemberJpaRepository.java new file mode 100644 index 00000000..5c19d35d --- /dev/null +++ b/src/main/java/com/atwoz/member/infrastructure/member/MemberJpaRepository.java @@ -0,0 +1,15 @@ +package com.atwoz.member.infrastructure.member; + +import com.atwoz.member.domain.member.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface MemberJpaRepository extends JpaRepository { + + Optional findByEmail(final String email); + + Optional findByNickname(final String nickname); + + boolean existsByEmail(final String email); +} diff --git a/src/main/java/com/atwoz/member/infrastructure/member/MemberRepositoryImpl.java b/src/main/java/com/atwoz/member/infrastructure/member/MemberRepositoryImpl.java new file mode 100644 index 00000000..d5e7870f --- /dev/null +++ b/src/main/java/com/atwoz/member/infrastructure/member/MemberRepositoryImpl.java @@ -0,0 +1,40 @@ +package com.atwoz.member.infrastructure.member; + +import com.atwoz.member.domain.member.Member; +import com.atwoz.member.domain.member.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@RequiredArgsConstructor +@Repository +public class MemberRepositoryImpl implements MemberRepository { + + private final MemberJpaRepository memberJpaRepository; + + @Override + public Optional findById(final Long id) { + return memberJpaRepository.findById(id); + } + + @Override + public Optional findByNickname(final String nickname) { + return memberJpaRepository.findByNickname(nickname); + } + + @Override + public Optional findByEmail(final String email) { + return memberJpaRepository.findByEmail(email); + } + + @Override + public Member save(final Member member) { + return memberJpaRepository.save(member); + } + + @Override + public boolean existsByEmail(final String email) { + return memberJpaRepository.existsByEmail(email); + } +} diff --git a/src/main/java/com/atwoz/member/infrastructure/member/NicknameCandidate.java b/src/main/java/com/atwoz/member/infrastructure/member/NicknameCandidate.java new file mode 100644 index 00000000..9f7ffa8e --- /dev/null +++ b/src/main/java/com/atwoz/member/infrastructure/member/NicknameCandidate.java @@ -0,0 +1,22 @@ +package com.atwoz.member.infrastructure.member; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum NicknameCandidate { + + CANDIDATE_01("강아지"), + CANDIDATE_02("고양이"), + CANDIDATE_03("돼지"), + CANDIDATE_04("기린"), + CANDIDATE_05("원숭이"); + + private final String candidate; + + public static String getCandidate() { + int pick = (int) (Math.random() * values().length); + return values()[pick].candidate; + } +} diff --git a/src/main/java/com/atwoz/member/infrastructure/member/NicknameGeneratorImpl.java b/src/main/java/com/atwoz/member/infrastructure/member/NicknameGeneratorImpl.java new file mode 100644 index 00000000..2af42e12 --- /dev/null +++ b/src/main/java/com/atwoz/member/infrastructure/member/NicknameGeneratorImpl.java @@ -0,0 +1,29 @@ +package com.atwoz.member.infrastructure.member; + +import com.atwoz.member.domain.member.NicknameGenerator; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +@Component +public class NicknameGeneratorImpl implements NicknameGenerator { + + private static final String SEPARATOR = "-"; + private static final String BLANK = ""; + private static final int LENGTH_OF_UUID = 8; + + @Override + public String createRandomNickname() { + return NicknamePrefix.getPrefix() + + NicknameCandidate.getCandidate() + + "_" + + generateUUID(); + + } + + private String generateUUID() { + return UUID.randomUUID().toString() + .replaceAll(SEPARATOR, BLANK) + .substring(0, LENGTH_OF_UUID); + } +} diff --git a/src/main/java/com/atwoz/member/infrastructure/member/NicknamePrefix.java b/src/main/java/com/atwoz/member/infrastructure/member/NicknamePrefix.java new file mode 100644 index 00000000..4009bb1c --- /dev/null +++ b/src/main/java/com/atwoz/member/infrastructure/member/NicknamePrefix.java @@ -0,0 +1,22 @@ +package com.atwoz.member.infrastructure.member; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum NicknamePrefix { + + PREFIX_01("현명한"), + PREFIX_02("귀여운"), + PREFIX_03("검정색의"), + PREFIX_04("핑크색의"), + PREFIX_05("매력적인"); + + private final String prefix; + + public static String getPrefix() { + int pick = (int) (Math.random() * values().length); + return values()[pick].prefix; + } +} diff --git a/src/main/java/com/atwoz/member/ui/auth/AuthController.java b/src/main/java/com/atwoz/member/ui/auth/AuthController.java new file mode 100644 index 00000000..a3fce1ca --- /dev/null +++ b/src/main/java/com/atwoz/member/ui/auth/AuthController.java @@ -0,0 +1,33 @@ +package com.atwoz.member.ui.auth; + +import com.atwoz.member.application.auth.AuthService; +import com.atwoz.member.application.auth.dto.LoginRequest; +import com.atwoz.member.application.auth.dto.SignupRequest; +import com.atwoz.member.ui.auth.dto.TokenResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +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; + +@RequiredArgsConstructor +@RequestMapping("/api") +@RestController +public class AuthController { + + private final AuthService authService; + + @PostMapping("/signup") + public ResponseEntity signup(@RequestBody @Valid final SignupRequest request) { + String token = authService.signup(request); + return ResponseEntity.ok(new TokenResponse(token)); + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody @Valid final LoginRequest request) { + String token = authService.login(request); + return ResponseEntity.ok(new TokenResponse(token)); + } +} diff --git a/src/main/java/com/atwoz/member/ui/auth/dto/TokenResponse.java b/src/main/java/com/atwoz/member/ui/auth/dto/TokenResponse.java new file mode 100644 index 00000000..2d4b07f8 --- /dev/null +++ b/src/main/java/com/atwoz/member/ui/auth/dto/TokenResponse.java @@ -0,0 +1,6 @@ +package com.atwoz.member.ui.auth.dto; + +public record TokenResponse( + String token +) { +} diff --git a/src/main/java/com/atwoz/member/ui/auth/interceptor/HttpMethod.java b/src/main/java/com/atwoz/member/ui/auth/interceptor/HttpMethod.java new file mode 100644 index 00000000..d61ba25e --- /dev/null +++ b/src/main/java/com/atwoz/member/ui/auth/interceptor/HttpMethod.java @@ -0,0 +1,20 @@ +package com.atwoz.member.ui.auth.interceptor; + +public enum HttpMethod { + + GET, + POST, + PUT, + PATCH, + DELETE, + OPTIONS, + HEAD, + TRACE, + CONNECT, + ANY; + + public boolean matches(final String pathMethod) { + return this == ANY || + this.name().equalsIgnoreCase(pathMethod); + } +} diff --git a/src/main/java/com/atwoz/member/ui/auth/interceptor/LoginValidCheckerInterceptor.java b/src/main/java/com/atwoz/member/ui/auth/interceptor/LoginValidCheckerInterceptor.java new file mode 100644 index 00000000..8a0d747b --- /dev/null +++ b/src/main/java/com/atwoz/member/ui/auth/interceptor/LoginValidCheckerInterceptor.java @@ -0,0 +1,32 @@ +package com.atwoz.member.ui.auth.interceptor; + +import com.atwoz.member.domain.auth.TokenProvider; +import com.atwoz.member.exception.exceptions.auth.LoginInvalidException; +import com.atwoz.member.ui.auth.support.AuthenticationContext; +import com.atwoz.member.ui.auth.support.AuthenticationExtractor; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +@RequiredArgsConstructor +@Component +public class LoginValidCheckerInterceptor implements HandlerInterceptor { + + private final TokenProvider tokenProvider; + private final AuthenticationContext authenticationContext; + + @Override + public boolean preHandle(final HttpServletRequest request, + final HttpServletResponse response, + final Object handler) throws Exception { + String token = AuthenticationExtractor.extract(request) + .orElseThrow(LoginInvalidException::new); + + Long memberId = tokenProvider.extract(token); + authenticationContext.setAuthentication(memberId); + + return true; + } +} diff --git a/src/main/java/com/atwoz/member/ui/auth/interceptor/ParseMemberIdFromTokenInterceptor.java b/src/main/java/com/atwoz/member/ui/auth/interceptor/ParseMemberIdFromTokenInterceptor.java new file mode 100644 index 00000000..dcd0d90e --- /dev/null +++ b/src/main/java/com/atwoz/member/ui/auth/interceptor/ParseMemberIdFromTokenInterceptor.java @@ -0,0 +1,29 @@ +package com.atwoz.member.ui.auth.interceptor; + +import com.atwoz.member.ui.auth.support.AuthenticationContext; +import com.atwoz.member.ui.auth.support.AuthenticationExtractor; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +@RequiredArgsConstructor +@Component +public class ParseMemberIdFromTokenInterceptor implements HandlerInterceptor { + + private final LoginValidCheckerInterceptor loginValidCheckerInterceptor; + private final AuthenticationContext authenticationContext; + + @Override + public boolean preHandle(final HttpServletRequest request, + final HttpServletResponse response, + final Object handler) throws Exception { + if (AuthenticationExtractor.extract(request).isEmpty()) { + authenticationContext.setAnonymous(); + return true; + } + + return loginValidCheckerInterceptor.preHandle(request, response, handler); + } +} diff --git a/src/main/java/com/atwoz/member/ui/auth/interceptor/PathContainer.java b/src/main/java/com/atwoz/member/ui/auth/interceptor/PathContainer.java new file mode 100644 index 00000000..53bffbf0 --- /dev/null +++ b/src/main/java/com/atwoz/member/ui/auth/interceptor/PathContainer.java @@ -0,0 +1,42 @@ +package com.atwoz.member.ui.auth.interceptor; + +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; + +import java.util.ArrayList; +import java.util.List; + +public class PathContainer { + + private final PathMatcher pathMatcher; + private final List includePatterns; + private final List excludePatterns; + + public PathContainer() { + this.pathMatcher = new AntPathMatcher(); + this.includePatterns = new ArrayList<>(); + this.excludePatterns = new ArrayList<>(); + } + + public boolean isNotIncludedPath(final String targetPath, final String pathMethod) { + boolean isExcludePattern = excludePatterns.stream() + .anyMatch(pathPattern -> pathPattern.matches(pathMatcher, targetPath, pathMethod)); + + boolean isNotIncludePattern = includePatterns.stream() + .noneMatch(pathPattern -> pathPattern.matches(pathMatcher, targetPath, pathMethod)); + + return isExcludePattern || isNotIncludePattern; + } + + public void addIncludePatterns(final String path, final HttpMethod... method) { + for (HttpMethod httpMethod : method) { + includePatterns.add(new PathRequest(path, httpMethod)); + } + } + + public void addExcludePatterns(final String path, final HttpMethod... method) { + for (HttpMethod httpMethod : method) { + excludePatterns.add(new PathRequest(path, httpMethod)); + } + } +} diff --git a/src/main/java/com/atwoz/member/ui/auth/interceptor/PathMatcherInterceptor.java b/src/main/java/com/atwoz/member/ui/auth/interceptor/PathMatcherInterceptor.java new file mode 100644 index 00000000..ffbacf8d --- /dev/null +++ b/src/main/java/com/atwoz/member/ui/auth/interceptor/PathMatcherInterceptor.java @@ -0,0 +1,36 @@ +package com.atwoz.member.ui.auth.interceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.servlet.HandlerInterceptor; + +public class PathMatcherInterceptor implements HandlerInterceptor { + + private final HandlerInterceptor handlerInterceptor; + private final PathContainer pathContainer; + + public PathMatcherInterceptor(final HandlerInterceptor handlerInterceptor) { + this.handlerInterceptor = handlerInterceptor; + this.pathContainer = new PathContainer(); + } + + @Override + public boolean preHandle(final HttpServletRequest request, + final HttpServletResponse response, + final Object handler) throws Exception { + if (pathContainer.isNotIncludedPath(request.getServletPath(), request.getMethod())) { + return true; + } + return handlerInterceptor.preHandle(request, response, handler); + } + + public PathMatcherInterceptor addPathPatterns(final String pathPattern, final HttpMethod... httpMethod) { + pathContainer.addIncludePatterns(pathPattern, httpMethod); + return this; + } + + public PathMatcherInterceptor excludePathPattern(final String pathPattern, final HttpMethod... pathMethod) { + pathContainer.addExcludePatterns(pathPattern, pathMethod); + return this; + } +} diff --git a/src/main/java/com/atwoz/member/ui/auth/interceptor/PathRequest.java b/src/main/java/com/atwoz/member/ui/auth/interceptor/PathRequest.java new file mode 100644 index 00000000..44301134 --- /dev/null +++ b/src/main/java/com/atwoz/member/ui/auth/interceptor/PathRequest.java @@ -0,0 +1,21 @@ +package com.atwoz.member.ui.auth.interceptor; + +import org.springframework.util.PathMatcher; + +public class PathRequest { + + private final String path; + private final HttpMethod httpMethod; + + public PathRequest(final String path, final HttpMethod httpMethod) { + this.path = path; + this.httpMethod = httpMethod; + } + + public boolean matches(final PathMatcher pathMatcher, + final String targetPath, + final String pathMethod) { + return pathMatcher.match(path, targetPath) && + httpMethod.matches(pathMethod); + } +} diff --git a/src/main/java/com/atwoz/member/ui/auth/support/AuthMember.java b/src/main/java/com/atwoz/member/ui/auth/support/AuthMember.java new file mode 100644 index 00000000..4a1f9058 --- /dev/null +++ b/src/main/java/com/atwoz/member/ui/auth/support/AuthMember.java @@ -0,0 +1,11 @@ +package com.atwoz.member.ui.auth.support; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface AuthMember { +} diff --git a/src/main/java/com/atwoz/member/ui/auth/support/AuthenticationContext.java b/src/main/java/com/atwoz/member/ui/auth/support/AuthenticationContext.java new file mode 100644 index 00000000..88c8a9e6 --- /dev/null +++ b/src/main/java/com/atwoz/member/ui/auth/support/AuthenticationContext.java @@ -0,0 +1,32 @@ +package com.atwoz.member.ui.auth.support; + +import com.atwoz.member.exception.exceptions.auth.LoginInvalidException; +import org.springframework.stereotype.Component; +import org.springframework.web.context.annotation.RequestScope; + +import java.util.Objects; + +@RequestScope +@Component +public class AuthenticationContext { + + private static final Long ANONYMOUS_MEMBER = -1L; + + private Long memberId; + + public void setAuthentication(Long memberId) { + this.memberId = memberId; + } + + public Long getPrincipal() { + if (Objects.isNull(this.memberId)) { + throw new LoginInvalidException(); + } + + return memberId; + } + + public void setAnonymous() { + this.memberId = ANONYMOUS_MEMBER; + } +} diff --git a/src/main/java/com/atwoz/member/ui/auth/support/AuthenticationExtractor.java b/src/main/java/com/atwoz/member/ui/auth/support/AuthenticationExtractor.java new file mode 100644 index 00000000..592e169f --- /dev/null +++ b/src/main/java/com/atwoz/member/ui/auth/support/AuthenticationExtractor.java @@ -0,0 +1,35 @@ +package com.atwoz.member.ui.auth.support; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.util.StringUtils; + +import java.util.Optional; + +public class AuthenticationExtractor { + + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String BEARER = "Bearer"; + private static final String HEADER_SPLIT_DELIMITER = " "; + private static final int TOKEN_TYPE_INDEX = 0; + private static final int TOKEN_VALUE_INDEX = 1; + private static final int VALID_HEADER_SPLIT_LENGTH = 2; + + public static Optional extract(final HttpServletRequest request) { + String header = request.getHeader(AUTHORIZATION_HEADER); + + if (!StringUtils.hasText(header)) { + return Optional.empty(); + } + + return extractFromHeader(header.split(HEADER_SPLIT_DELIMITER)); + } + + public static Optional extractFromHeader(final String[] headerParts) { + if (headerParts.length == VALID_HEADER_SPLIT_LENGTH && + headerParts[TOKEN_TYPE_INDEX].equals(BEARER)) { + return Optional.ofNullable(headerParts[TOKEN_VALUE_INDEX]); + } + + return Optional.empty(); + } +} diff --git a/src/main/java/com/atwoz/member/ui/auth/support/resolver/AuthArgumentResolver.java b/src/main/java/com/atwoz/member/ui/auth/support/resolver/AuthArgumentResolver.java new file mode 100644 index 00000000..b0fe5370 --- /dev/null +++ b/src/main/java/com/atwoz/member/ui/auth/support/resolver/AuthArgumentResolver.java @@ -0,0 +1,41 @@ +package com.atwoz.member.ui.auth.support.resolver; + +import com.atwoz.member.exception.exceptions.auth.LoginInvalidException; +import com.atwoz.member.ui.auth.support.AuthMember; +import com.atwoz.member.ui.auth.support.AuthenticationContext; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@RequiredArgsConstructor +@Component +public class AuthArgumentResolver implements HandlerMethodArgumentResolver { + + private static final int ANONYMOUS = -1; + + private final AuthenticationContext authenticationContext; + + @Override + public boolean supportsParameter(final MethodParameter parameter) { + return parameter.hasParameterAnnotation(AuthMember.class) && + parameter.getParameterType().equals(Long.class); + } + + @Override + public Object resolveArgument(final MethodParameter parameter, + final ModelAndViewContainer mavContainer, + final NativeWebRequest webRequest, + final WebDataBinderFactory binderFactory) throws Exception { + Long memberId = authenticationContext.getPrincipal(); + + if (memberId == ANONYMOUS) { + throw new LoginInvalidException(); + } + + return memberId; + } +} diff --git a/src/test/java/com/atwoz/helper/MockBeanInjection.java b/src/test/java/com/atwoz/helper/MockBeanInjection.java index 55e21349..33255538 100644 --- a/src/test/java/com/atwoz/helper/MockBeanInjection.java +++ b/src/test/java/com/atwoz/helper/MockBeanInjection.java @@ -1,11 +1,20 @@ package com.atwoz.helper; +import com.atwoz.member.application.auth.AuthService; +import com.atwoz.member.domain.auth.TokenProvider; +import com.atwoz.member.ui.auth.support.AuthenticationContext; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; @MockBean(JpaMetamodelMappingContext.class) public class MockBeanInjection { -// @MockBean - // ex. protected TokenProvider tokenProvider; + @MockBean + protected TokenProvider tokenProvider; + + @MockBean + protected AuthenticationContext authenticationContext; + + @MockBean + protected AuthService authService; } diff --git a/src/test/java/com/atwoz/member/application/auth/AuthServiceTest.java b/src/test/java/com/atwoz/member/application/auth/AuthServiceTest.java new file mode 100644 index 00000000..7f128a9d --- /dev/null +++ b/src/test/java/com/atwoz/member/application/auth/AuthServiceTest.java @@ -0,0 +1,123 @@ +package com.atwoz.member.application.auth; + +import com.atwoz.member.application.auth.dto.LoginRequest; +import com.atwoz.member.application.auth.dto.SignupRequest; +import com.atwoz.member.domain.auth.TokenProvider; +import com.atwoz.member.domain.member.Member; +import com.atwoz.member.domain.member.MemberRepository; +import com.atwoz.member.exception.exceptions.member.MemberAlreadyExistedException; +import com.atwoz.member.exception.exceptions.member.MemberNotFoundException; +import com.atwoz.member.exception.exceptions.member.PasswordNotMatchedException; +import com.atwoz.member.infrastructure.member.MemberFakeRepository; +import com.atwoz.member.infrastructure.member.NicknameFakeGenerator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static com.atwoz.member.fixture.member.MemberFixture.일반_유저_생성; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +@ExtendWith(MockitoExtension.class) +class AuthServiceTest { + + @Mock + private TokenProvider tokenProvider; + private AuthService authService; + private MemberRepository memberRepository; + + @BeforeEach + void setup() { + memberRepository = new MemberFakeRepository(); + authService = new AuthService(memberRepository, tokenProvider, new NicknameFakeGenerator()); + } + + @DisplayName("회원가입을 진행한다") + @Nested + class Signup { + + @Test + void 회원가입을_성공한다() { + // given + SignupRequest req = new SignupRequest("email", "password"); + + String expectedToken = "token"; + when(tokenProvider.create(anyLong())).thenReturn(expectedToken); + + // when + String result = authService.signup(req); + + // then + assertThat(result).isEqualTo(expectedToken); + } + + @Test + void 이미_존재하는_이메일이라면_예외를_발생한다() { + // given + Member existedMember = 일반_유저_생성(); + memberRepository.save(existedMember); + + SignupRequest req = new SignupRequest(existedMember.getEmail(), "password"); + + // when & then + assertThatThrownBy(() -> authService.signup(req)) + .isInstanceOf(MemberAlreadyExistedException.class); + } + } + + @DisplayName("로그인을 진행한다") + @Nested + class Login { + + @Test + void 로그인을_성공적으로_진행한다() { + // given + Member member = memberRepository.save(일반_유저_생성()); + LoginRequest request = new LoginRequest(member.getEmail(), member.getPassword()); + + String expectedToken = "token"; + when(tokenProvider.create(any())).thenReturn(expectedToken); + + // when + String result = authService.login(request); + + // then + assertThat(result).isEqualTo(expectedToken); + } + + @Test + void 존재하지_않는_이메일로_로그인시_예외를_발생한다() { + // given + Member member = memberRepository.save(일반_유저_생성()); + String wrongEmail = "wrong"; + LoginRequest request = new LoginRequest(wrongEmail, member.getPassword()); + + // when & then + assertThatThrownBy(() -> authService.login(request)) + .isInstanceOf(MemberNotFoundException.class); + } + + @Test + void 패스워드가_틀리면_예외를_발생한다() { + // given + Member member = memberRepository.save(일반_유저_생성()); + String wrongPassword = "wrong"; + LoginRequest request = new LoginRequest(member.getEmail(), wrongPassword); + + // when & then + assertThatThrownBy(() -> authService.login(request)) + .isInstanceOf(PasswordNotMatchedException.class); + } + } +} diff --git a/src/test/java/com/atwoz/member/domain/member/MemberTest.java b/src/test/java/com/atwoz/member/domain/member/MemberTest.java new file mode 100644 index 00000000..35b08c69 --- /dev/null +++ b/src/test/java/com/atwoz/member/domain/member/MemberTest.java @@ -0,0 +1,61 @@ +package com.atwoz.member.domain.member; + +import com.atwoz.member.exception.exceptions.member.PasswordNotMatchedException; +import com.atwoz.member.infrastructure.member.NicknameFakeGenerator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static com.atwoz.member.fixture.member.MemberFixture.어드민_유저_생성; +import static com.atwoz.member.fixture.member.MemberFixture.일반_유저_생성; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class MemberTest { + + private NicknameGenerator nicknameGenerator; + + @BeforeEach + void setup() { + nicknameGenerator = new NicknameFakeGenerator(); + } + + @Test + void 어드민인_경우에_true를_반환한다() { + // given + Member admin = 어드민_유저_생성(); + + // when + boolean result = admin.isAdmin(); + + // then + assertThat(result).isTrue(); + } + + @Test + void 패스워드가_다른_경우에_예외를_발생한다() { + // given + Member member = 일반_유저_생성(); + String givenPassword = "wrongPassword"; + + // when & then + assertThatThrownBy(() -> member.validatePassword(givenPassword)) + .isInstanceOf(PasswordNotMatchedException.class); + } + + @Test + void 회원가입시_기본적으로_MEMBER_ROLE과_랜덤한_닉네임으로_생성된다() { + // when + Member member = Member.createDefaultRole("email@email.com", "password", nicknameGenerator); + + // then + assertSoftly(softly -> { + softly.assertThat(member.getMemberRole()).isEqualTo(MemberRole.MEMBER); + softly.assertThat(member.getNickname()).isEqualTo("nickname"); + }); + } +} diff --git a/src/test/java/com/atwoz/member/fixture/member/MemberFixture.java b/src/test/java/com/atwoz/member/fixture/member/MemberFixture.java new file mode 100644 index 00000000..6604aa52 --- /dev/null +++ b/src/test/java/com/atwoz/member/fixture/member/MemberFixture.java @@ -0,0 +1,25 @@ +package com.atwoz.member.fixture.member; + +import com.atwoz.member.domain.member.Member; +import com.atwoz.member.domain.member.MemberRole; + +public class MemberFixture { + + public static Member 일반_유저_생성() { + return Member.builder() + .email("email@email.com") + .password("password") + .nickname("nickname") + .memberRole(MemberRole.MEMBER) + .build(); + } + + public static Member 어드민_유저_생성() { + return Member.builder() + .email("email@email.com") + .password("password") + .nickname("nickname") + .memberRole(MemberRole.ADMIN) + .build(); + } +} diff --git a/src/test/java/com/atwoz/member/infrastructure/member/MemberFakeRepository.java b/src/test/java/com/atwoz/member/infrastructure/member/MemberFakeRepository.java new file mode 100644 index 00000000..86398c72 --- /dev/null +++ b/src/test/java/com/atwoz/member/infrastructure/member/MemberFakeRepository.java @@ -0,0 +1,55 @@ +package com.atwoz.member.infrastructure.member; + +import com.atwoz.member.domain.member.Member; +import com.atwoz.member.domain.member.MemberRepository; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class MemberFakeRepository implements MemberRepository { + + private final Map map = new HashMap<>(); + private Long id = 0L; + + @Override + public Optional findById(final Long id) { + return Optional.of(map.get(id)); + } + + @Override + public Optional findByNickname(final String nickname) { + return map.values().stream() + .filter(member -> member.getNickname().equals(nickname)) + .findAny(); + } + + @Override + public Optional findByEmail(final String email) { + return map.values().stream() + .filter(member -> member.getEmail().equals(email)) + .findAny(); + } + + @Override + public Member save(final Member member) { + Member saved = Member.builder() + .id(id) + .email(member.getEmail()) + .password(member.getPassword()) + .nickname(member.getNickname()) + .memberRole(member.getMemberRole()) + .build(); + + map.put(id, member); + + id++; + return saved; + } + + @Override + public boolean existsByEmail(final String email) { + return map.values().stream() + .anyMatch(member -> member.getEmail().equals(email)); + } +} diff --git a/src/test/java/com/atwoz/member/infrastructure/member/MemberJpaRepositoryTest.java b/src/test/java/com/atwoz/member/infrastructure/member/MemberJpaRepositoryTest.java new file mode 100644 index 00000000..21f64693 --- /dev/null +++ b/src/test/java/com/atwoz/member/infrastructure/member/MemberJpaRepositoryTest.java @@ -0,0 +1,74 @@ +package com.atwoz.member.infrastructure.member; + +import com.atwoz.member.domain.member.Member; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import java.util.Optional; + +import static com.atwoz.member.fixture.member.MemberFixture.일반_유저_생성; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +@DataJpaTest +class MemberJpaRepositoryTest { + + @Autowired + private MemberJpaRepository memberRepository; + + private Member member; + + @BeforeEach + void setup() { + member = 일반_유저_생성(); + memberRepository.save(member); + } + + @DisplayName("멤버를 찾는다") + @Nested + class FindMember { + + @Test + void 아이디_값으로_멤버를_찾는다() { + // when + Optional result = memberRepository.findById(member.getId()); + + // then + assertSoftly(softly -> { + softly.assertThat(result).isPresent(); + softly.assertThat(result.get()).usingRecursiveComparison().isEqualTo(member); + }); + } + + @Test + void 닉네임_값으로_멤버를_찾는다() { + // when + Optional result = memberRepository.findByNickname(member.getNickname()); + + // then + assertSoftly(softly -> { + softly.assertThat(result).isPresent(); + softly.assertThat(result.get()).usingRecursiveComparison().isEqualTo(member); + }); + } + + @Test + void 이메일_값으로_멤버를_찾는다() { + // when + Optional result = memberRepository.findByEmail(member.getEmail()); + + // then + assertSoftly(softly -> { + softly.assertThat(result).isPresent(); + softly.assertThat(result.get()).usingRecursiveComparison().isEqualTo(member); + }); + } + } +} diff --git a/src/test/java/com/atwoz/member/infrastructure/member/NicknameFakeGenerator.java b/src/test/java/com/atwoz/member/infrastructure/member/NicknameFakeGenerator.java new file mode 100644 index 00000000..235c2270 --- /dev/null +++ b/src/test/java/com/atwoz/member/infrastructure/member/NicknameFakeGenerator.java @@ -0,0 +1,13 @@ +package com.atwoz.member.infrastructure.member; + +import com.atwoz.member.domain.member.NicknameGenerator; + +public class NicknameFakeGenerator implements NicknameGenerator { + + private static final String FAKE_NICKNAME = "nickname"; + + @Override + public String createRandomNickname() { + return FAKE_NICKNAME; + } +} diff --git a/src/test/java/com/atwoz/member/infrastructure/member/NicknameGeneratorImplTest.java b/src/test/java/com/atwoz/member/infrastructure/member/NicknameGeneratorImplTest.java new file mode 100644 index 00000000..d6e2ad0f --- /dev/null +++ b/src/test/java/com/atwoz/member/infrastructure/member/NicknameGeneratorImplTest.java @@ -0,0 +1,24 @@ +package com.atwoz.member.infrastructure.member; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class NicknameGeneratorImplTest { + + private final NicknameGeneratorImpl nicknameGenerator = new NicknameGeneratorImpl(); + + @Test + void 닉네임이_성공적으로_생성된다() { + // when + String createdNickname = nicknameGenerator.createRandomNickname(); + + // then + assertThat(createdNickname).isNotBlank(); + } + +} diff --git a/src/test/java/com/atwoz/member/ui/auth/AuthControllerAcceptanceFixture.java b/src/test/java/com/atwoz/member/ui/auth/AuthControllerAcceptanceFixture.java new file mode 100644 index 00000000..57b05771 --- /dev/null +++ b/src/test/java/com/atwoz/member/ui/auth/AuthControllerAcceptanceFixture.java @@ -0,0 +1,39 @@ +package com.atwoz.member.ui.auth; + +import com.atwoz.helper.IntegrationHelper; +import com.atwoz.member.application.auth.dto.LoginRequest; +import com.atwoz.member.application.auth.dto.SignupRequest; +import com.atwoz.member.domain.member.Member; +import com.atwoz.member.ui.auth.dto.TokenResponse; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import io.restassured.response.ExtractableResponse; + +import static org.assertj.core.api.Assertions.assertThat; + +class AuthControllerAcceptanceFixture extends IntegrationHelper { + + protected SignupRequest 회원_가입_데이터를_요청한다() { + return new SignupRequest("email", "password"); + } + + protected ExtractableResponse 요청한다(final T request, final String url) { + return RestAssured.given().log().all() + .body(request) + .contentType(ContentType.JSON) + .when() + .post(url) + .then().log().all() + .extract(); + } + + protected void 토큰_생성_검증(final ExtractableResponse actual) { + var result = actual.as(TokenResponse.class); + + assertThat(result.token()).isNotBlank(); + } + + protected LoginRequest 로그인_데이터를_요청한다(final Member member) { + return new LoginRequest(member.getEmail(), member.getPassword()); + } +} diff --git a/src/test/java/com/atwoz/member/ui/auth/AuthControllerAcceptanceTest.java b/src/test/java/com/atwoz/member/ui/auth/AuthControllerAcceptanceTest.java new file mode 100644 index 00000000..69aca65a --- /dev/null +++ b/src/test/java/com/atwoz/member/ui/auth/AuthControllerAcceptanceTest.java @@ -0,0 +1,50 @@ +package com.atwoz.member.ui.auth; + +import com.atwoz.member.domain.member.Member; +import com.atwoz.member.domain.member.MemberRepository; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static com.atwoz.member.fixture.member.MemberFixture.일반_유저_생성; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AuthControllerAcceptanceTest extends AuthControllerAcceptanceFixture { + + private static final String 회원가입_url = "/api/signup"; + private static final String 로그인_url = "/api/login"; + + @Autowired + private MemberRepository memberRepository; + + @Test + void 회원가입을_진행한다() { + // given + var 회원가입_요청_데이터 = 회원_가입_데이터를_요청한다(); + + // when + var 회원가입_결과 = 요청한다(회원가입_요청_데이터, 회원가입_url); + + // then + 토큰_생성_검증(회원가입_결과); + } + + @Test + void 로그인을_진행한다() { + // given + var 회원 = 회원_생성(); + var 로그인_요청_데이터 = 로그인_데이터를_요청한다(회원); + + // when + var 로그인_결과 = 요청한다(로그인_요청_데이터, 로그인_url); + + // then + 토큰_생성_검증(로그인_결과); + } + + private Member 회원_생성() { + return memberRepository.save(일반_유저_생성()); + } +} diff --git a/src/test/java/com/atwoz/member/ui/auth/AuthControllerWebMvcTest.java b/src/test/java/com/atwoz/member/ui/auth/AuthControllerWebMvcTest.java new file mode 100644 index 00000000..b5f22ec9 --- /dev/null +++ b/src/test/java/com/atwoz/member/ui/auth/AuthControllerWebMvcTest.java @@ -0,0 +1,79 @@ +package com.atwoz.member.ui.auth; + +import com.atwoz.helper.MockBeanInjection; +import com.atwoz.member.application.auth.dto.LoginRequest; +import com.atwoz.member.application.auth.dto.SignupRequest; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import static com.atwoz.helper.RestDocsHelper.customDocument; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +@AutoConfigureRestDocs +@WebMvcTest(AuthController.class) +class AuthControllerWebMvcTest extends MockBeanInjection { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Test + void 회원가입을_진행한다() throws Exception { + // given + SignupRequest req = new SignupRequest("email@email.com", "passsword"); + when(authService.signup(req)).thenReturn("response_token_info"); + + // when & then + mockMvc.perform(post("/api/signup") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req)) + ).andExpect(status().isOk()) + .andDo(customDocument("do_signup", + requestFields( + fieldWithPath("email").description("이메일"), + fieldWithPath("password").description("패스워드") + ), + responseFields( + fieldWithPath("token").description("발급되는 토큰") + ) + )); + } + + @Test + void 로그인을_진행한다() throws Exception { + // given + LoginRequest req = new LoginRequest("email@email.com", "password"); + when(authService.login(req)).thenReturn("response_token_info"); + + // when & then + mockMvc.perform(post("/api/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req)) + ).andExpect(status().isOk()) + .andDo(customDocument("do_login", + requestFields( + fieldWithPath("email").description("이메일"), + fieldWithPath("password").description("패스워드") + ), + responseFields( + fieldWithPath("token").description("발급되는 토큰") + ) + )); + } +} diff --git a/src/test/java/com/atwoz/member/ui/auth/interceptor/HttpMethodTest.java b/src/test/java/com/atwoz/member/ui/auth/interceptor/HttpMethodTest.java new file mode 100644 index 00000000..524f43f6 --- /dev/null +++ b/src/test/java/com/atwoz/member/ui/auth/interceptor/HttpMethodTest.java @@ -0,0 +1,24 @@ +package com.atwoz.member.ui.auth.interceptor; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class HttpMethodTest { + + @Test + void http_메서드가_같은지_확인한다() { + // given + HttpMethod httpMethod = HttpMethod.GET; + + // when + boolean result = httpMethod.matches(HttpMethod.GET.name()); + + // then + assertThat(result).isTrue(); + } +} diff --git a/src/test/java/com/atwoz/member/ui/auth/interceptor/LoginValidCheckerInterceptorTest.java b/src/test/java/com/atwoz/member/ui/auth/interceptor/LoginValidCheckerInterceptorTest.java new file mode 100644 index 00000000..a7c720b6 --- /dev/null +++ b/src/test/java/com/atwoz/member/ui/auth/interceptor/LoginValidCheckerInterceptorTest.java @@ -0,0 +1,37 @@ +package com.atwoz.member.ui.auth.interceptor; + +import com.atwoz.member.exception.exceptions.auth.LoginInvalidException; +import com.atwoz.member.infrastructure.auth.JwtTokenProvider; +import com.atwoz.member.ui.auth.support.AuthenticationContext; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class LoginValidCheckerInterceptorTest { + + private final HttpServletRequest req = mock(HttpServletRequest.class); + private final HttpServletResponse res = mock(HttpServletResponse.class); + + @Test + void token이_없다면_예외를_발생한다() { + // given + LoginValidCheckerInterceptor loginValidCheckerInterceptor = new LoginValidCheckerInterceptor( + new JwtTokenProvider(), + new AuthenticationContext() + ); + + when(req.getHeader("any")).thenReturn(null); + + // when + assertThatThrownBy(() -> loginValidCheckerInterceptor.preHandle(req, res, new Object())) + .isInstanceOf(LoginInvalidException.class); + } +} diff --git a/src/test/java/com/atwoz/member/ui/auth/interceptor/PathContainerTest.java b/src/test/java/com/atwoz/member/ui/auth/interceptor/PathContainerTest.java new file mode 100644 index 00000000..3776ec64 --- /dev/null +++ b/src/test/java/com/atwoz/member/ui/auth/interceptor/PathContainerTest.java @@ -0,0 +1,65 @@ +package com.atwoz.member.ui.auth.interceptor; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class PathContainerTest { + + private PathContainer container; + + @BeforeEach + void setup() { + container = new PathContainer(); + } + + @Test + void include로_등록한_메서드와_uri가_같으면_false를_반환한다() { + // given + String uri = "/url/test"; + HttpMethod method = HttpMethod.GET; + + container.addIncludePatterns(uri, method); + + // when + boolean result = container.isNotIncludedPath(uri, method.name()); + + // then + assertThat(result).isFalse(); + } + + @Test + void include로_등록한_메서드와_uri가_다르면_true를_반환한다() { + // given + String uri = "/url/test"; + HttpMethod method = HttpMethod.GET; + + container.addIncludePatterns(uri, method); + + // when + boolean result = container.isNotIncludedPath(uri + "wrong", HttpMethod.GET.name()); + + // then + assertThat(result).isTrue(); + } + + @Test + void exclude로_등록한_메서드와_uri가_같으면_true를_반환한다() { + // given + String uri = "/url/test"; + HttpMethod method = HttpMethod.GET; + + container.addExcludePatterns(uri, method); + + // when + boolean result = container.isNotIncludedPath(uri, method.name()); + + // then + assertThat(result).isTrue(); + } +} diff --git a/src/test/java/com/atwoz/member/ui/auth/interceptor/PathRequestTest.java b/src/test/java/com/atwoz/member/ui/auth/interceptor/PathRequestTest.java new file mode 100644 index 00000000..7c914b53 --- /dev/null +++ b/src/test/java/com/atwoz/member/ui/auth/interceptor/PathRequestTest.java @@ -0,0 +1,28 @@ +package com.atwoz.member.ui.auth.interceptor; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.util.AntPathMatcher; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class PathRequestTest { + + @Test + void uri와_method가_같은지_확인한다() { + // given + String path = "/path"; + HttpMethod method = HttpMethod.GET; + + PathRequest pathRequest = new PathRequest(path, method); + + // when + boolean result = pathRequest.matches(new AntPathMatcher(), path, method.name()); + + // then + assertThat(result).isTrue(); + } +} diff --git a/src/test/java/com/atwoz/member/ui/auth/support/AuthenticationContextTest.java b/src/test/java/com/atwoz/member/ui/auth/support/AuthenticationContextTest.java new file mode 100644 index 00000000..c4b519e6 --- /dev/null +++ b/src/test/java/com/atwoz/member/ui/auth/support/AuthenticationContextTest.java @@ -0,0 +1,56 @@ +package com.atwoz.member.ui.auth.support; + +import com.atwoz.member.exception.exceptions.auth.LoginInvalidException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AuthenticationContextTest { + + private AuthenticationContext authenticationContext; + + @BeforeEach + void setup() { + authenticationContext = new AuthenticationContext(); + } + + @Test + void member_id를_반환한다() { + // given + authenticationContext.setAuthentication(1L); + + // when + Long result = authenticationContext.getPrincipal(); + + // then + assertThat(result).isEqualTo(1L); + } + + @Test + void member_id가_없다면_예외를_발생한다() { + // given + authenticationContext.setAuthentication(null); + + // when & then + assertThatThrownBy(() -> authenticationContext.getPrincipal()) + .isInstanceOf(LoginInvalidException.class); + } + + @Test + void 미확인_유저로_바꾼다() { + // given + authenticationContext.setAnonymous(); + + // when + Long result = authenticationContext.getPrincipal(); + + // then + assertThat(result).isEqualTo(-1L); + } +} diff --git a/src/test/java/com/atwoz/member/ui/auth/support/AuthenticationExtractorTest.java b/src/test/java/com/atwoz/member/ui/auth/support/AuthenticationExtractorTest.java new file mode 100644 index 00000000..9ede034d --- /dev/null +++ b/src/test/java/com/atwoz/member/ui/auth/support/AuthenticationExtractorTest.java @@ -0,0 +1,51 @@ +package com.atwoz.member.ui.auth.support; + +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.mockito.Mockito.when; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AuthenticationExtractorTest { + + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String BEARER = "Bearer"; + + private HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + + @Test + void 토큰이_정상적으로_조회된다() { + // given + String expectedResponseToken = "Bearer tokenSignature"; + when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn(expectedResponseToken); + + // when + Optional result = AuthenticationExtractor.extract(request); + + // then + assertSoftly(softly -> { + softly.assertThat(result).isPresent(); + softly.assertThat(result).isEqualTo(Optional.of("tokenSignature")); + }); + } + + @Test + void 토큰_헤더가_없다면_빈_값이_반환된다() { + // given + when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("InvalidType token"); + + // when + Optional result = AuthenticationExtractor.extract(request); + + // then + assertThat(result).isEmpty(); + } +}