diff --git a/src/main/java/com/zimdugo/auth/application/AccountWithdrawalService.java b/src/main/java/com/zimdugo/auth/application/AccountWithdrawalService.java index 05b8f9d..cf7b90d 100644 --- a/src/main/java/com/zimdugo/auth/application/AccountWithdrawalService.java +++ b/src/main/java/com/zimdugo/auth/application/AccountWithdrawalService.java @@ -4,10 +4,10 @@ import com.zimdugo.core.exception.BusinessException; import com.zimdugo.core.exception.ErrorCode; import com.zimdugo.user.application.UserQueryService; +import com.zimdugo.user.domain.SocialAccountStore; import com.zimdugo.user.domain.User; import com.zimdugo.user.domain.UserStatus; -import com.zimdugo.user.infrastructure.SocialAccountJpaRepository; -import com.zimdugo.user.infrastructure.UserJpaRepository; +import com.zimdugo.user.domain.UserStore; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -20,8 +20,8 @@ public class AccountWithdrawalService { private final AccessTokenValidationService accessTokenValidationService; private final JwtTokenProvider jwtTokenProvider; private final UserQueryService userQueryService; - private final UserJpaRepository userJpaRepository; - private final SocialAccountJpaRepository socialAccountJpaRepository; + private final UserStore userStore; + private final SocialAccountStore socialAccountStore; private final RefreshTokenRepository refreshTokenRepository; public void withdraw(String accessToken) { @@ -39,9 +39,9 @@ public void withdraw(String accessToken) { } user.changeStatus(UserStatus.DELETED); - userJpaRepository.save(user); + userStore.store(user); - socialAccountJpaRepository.deleteAllByUserId(userId); + socialAccountStore.deleteAllByUserId(userId); refreshTokenRepository.deleteAllByUserId(userId); } } diff --git a/src/main/java/com/zimdugo/auth/application/CustomOAuth2UserService.java b/src/main/java/com/zimdugo/auth/application/CustomOAuth2UserService.java index 6d1f894..56d597f 100644 --- a/src/main/java/com/zimdugo/auth/application/CustomOAuth2UserService.java +++ b/src/main/java/com/zimdugo/auth/application/CustomOAuth2UserService.java @@ -3,14 +3,12 @@ import com.zimdugo.auth.domain.OAuth2UserInfo; import com.zimdugo.auth.domain.OAuth2UserInfoFactory; import com.zimdugo.user.domain.SocialAccount; +import com.zimdugo.user.domain.SocialAccountReader; +import com.zimdugo.user.domain.SocialAccountStore; import com.zimdugo.user.domain.User; import com.zimdugo.user.domain.UserRole; import com.zimdugo.user.domain.UserStatus; -import com.zimdugo.user.infrastructure.SocialAccountJpaRepository; -import com.zimdugo.user.infrastructure.UserJpaRepository; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import com.zimdugo.user.domain.UserStore; import lombok.RequiredArgsConstructor; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; @@ -23,13 +21,18 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + @Service @RequiredArgsConstructor @Transactional public class CustomOAuth2UserService implements OAuth2UserService { - private final UserJpaRepository userJpaRepository; - private final SocialAccountJpaRepository socialAccountJpaRepository; + private final UserStore userStore; + private final SocialAccountReader socialAccountReader; + private final SocialAccountStore socialAccountStore; @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { @@ -45,51 +48,67 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic Map attributes = new HashMap<>(oAuth2User.getAttributes()); attributes.put("userId", user.getId()); - attributes.put("email", user.getEmail()); + attributes.put("email", user.getEmail()); // null 가능 attributes.put("nickname", user.getNickname()); attributes.put("role", user.getRoleOrDefault().name()); - String nameAttributeKey = resolveNameAttributeKey(user); + String nameAttributeKey = resolveNameAttributeKey(user, userInfo); return new DefaultOAuth2User( - List.of(new SimpleGrantedAuthority(toAuthority(user.getRoleOrDefault()))), - attributes, - nameAttributeKey + List.of(new SimpleGrantedAuthority(toAuthority(user.getRoleOrDefault()))), + attributes, + nameAttributeKey ); } private void validateRequiredFields(OAuth2UserInfo userInfo, String registrationId) { if (userInfo.getProviderUserId() == null || userInfo.getProviderUserId().isBlank()) { throw new OAuth2AuthenticationException( - new OAuth2Error("invalid_user_info"), - registrationId + " 사용자 식별자(providerUserId)를 가져오지 못했습니다." + new OAuth2Error("invalid_user_info"), + registrationId + " 사용자 식별값(providerUserId)을 가져오지 못했습니다." ); } } private User findOrCreateUser(OAuth2UserInfo userInfo) { - return socialAccountJpaRepository + return socialAccountReader .findByProviderAndProviderUserId(userInfo.getProvider(), userInfo.getProviderUserId()) - .map(SocialAccount::getUser) + .map(socialAccount -> syncAndGetUser(socialAccount, userInfo)) .orElseGet(() -> createNewUser(userInfo)); } + private User syncAndGetUser(SocialAccount socialAccount, OAuth2UserInfo userInfo) { + socialAccount.updateProviderProfile( + normalize(userInfo.getEmail()), + normalize(userInfo.getProfileImageUrl()) + ); + SocialAccount saved = socialAccountStore.store(socialAccount); + return saved.getUser(); + } + private User createNewUser(OAuth2UserInfo userInfo) { String email = normalize(userInfo.getEmail()); String nickname = resolveNickname(userInfo); String profileImageUrl = normalize(userInfo.getProfileImageUrl()); - User user = new User(email, nickname, profileImageUrl, UserStatus.ACTIVE); - User savedUser = userJpaRepository.save(user); + User user = new User( + email, // 카카오는 null일 수 있음 + nickname, + profileImageUrl, + UserStatus.ACTIVE + ); + + User savedUser = userStore.store(user); SocialAccount socialAccount = new SocialAccount( - savedUser, - userInfo.getProvider(), - userInfo.getProviderUserId(), - email, - profileImageUrl + savedUser, + userInfo.getProvider(), + userInfo.getProviderUserId(), + email, // null 가능 + profileImageUrl ); - socialAccountJpaRepository.save(socialAccount); + + socialAccountStore.store(socialAccount); return savedUser; } @@ -116,7 +135,7 @@ private String normalize(String value) { return trimmed.isBlank() ? null : trimmed; } - private String resolveNameAttributeKey(User user) { + private String resolveNameAttributeKey(User user, OAuth2UserInfo userInfo) { if (user.getEmail() != null && !user.getEmail().isBlank()) { return "email"; } diff --git a/src/main/java/com/zimdugo/auth/domain/FacebookOAuth2UserInfo.java b/src/main/java/com/zimdugo/auth/domain/FacebookOAuth2UserInfo.java index 02f5fba..5d8bf8e 100644 --- a/src/main/java/com/zimdugo/auth/domain/FacebookOAuth2UserInfo.java +++ b/src/main/java/com/zimdugo/auth/domain/FacebookOAuth2UserInfo.java @@ -1,6 +1,6 @@ package com.zimdugo.auth.domain; -import com.zimdugo.user.domain.AuthProvider; +import com.zimdugo.identity.domain.AuthProvider; import java.util.Map; diff --git a/src/main/java/com/zimdugo/auth/domain/GoogleOAuth2UserInfo.java b/src/main/java/com/zimdugo/auth/domain/GoogleOAuth2UserInfo.java index 45264bc..85d22f5 100644 --- a/src/main/java/com/zimdugo/auth/domain/GoogleOAuth2UserInfo.java +++ b/src/main/java/com/zimdugo/auth/domain/GoogleOAuth2UserInfo.java @@ -1,6 +1,6 @@ package com.zimdugo.auth.domain; -import com.zimdugo.user.domain.AuthProvider; +import com.zimdugo.identity.domain.AuthProvider; import java.util.Map; diff --git a/src/main/java/com/zimdugo/auth/domain/KakaoOAuth2UserInfo.java b/src/main/java/com/zimdugo/auth/domain/KakaoOAuth2UserInfo.java index a2accd8..5aec566 100644 --- a/src/main/java/com/zimdugo/auth/domain/KakaoOAuth2UserInfo.java +++ b/src/main/java/com/zimdugo/auth/domain/KakaoOAuth2UserInfo.java @@ -1,6 +1,6 @@ package com.zimdugo.auth.domain; -import com.zimdugo.user.domain.AuthProvider; +import com.zimdugo.identity.domain.AuthProvider; import java.util.Map; diff --git a/src/main/java/com/zimdugo/auth/domain/NaverOAuth2UserInfo.java b/src/main/java/com/zimdugo/auth/domain/NaverOAuth2UserInfo.java index 64ef338..4cf628e 100644 --- a/src/main/java/com/zimdugo/auth/domain/NaverOAuth2UserInfo.java +++ b/src/main/java/com/zimdugo/auth/domain/NaverOAuth2UserInfo.java @@ -1,6 +1,6 @@ package com.zimdugo.auth.domain; -import com.zimdugo.user.domain.AuthProvider; +import com.zimdugo.identity.domain.AuthProvider; import java.util.Map; diff --git a/src/main/java/com/zimdugo/auth/domain/OAuth2UserInfo.java b/src/main/java/com/zimdugo/auth/domain/OAuth2UserInfo.java index 294adec..205417f 100644 --- a/src/main/java/com/zimdugo/auth/domain/OAuth2UserInfo.java +++ b/src/main/java/com/zimdugo/auth/domain/OAuth2UserInfo.java @@ -1,6 +1,6 @@ package com.zimdugo.auth.domain; -import com.zimdugo.user.domain.AuthProvider; +import com.zimdugo.identity.domain.AuthProvider; public interface OAuth2UserInfo { diff --git a/src/main/java/com/zimdugo/user/domain/AuthProvider.java b/src/main/java/com/zimdugo/identity/domain/AuthProvider.java similarity index 65% rename from src/main/java/com/zimdugo/user/domain/AuthProvider.java rename to src/main/java/com/zimdugo/identity/domain/AuthProvider.java index fa5f417..1fe0c90 100644 --- a/src/main/java/com/zimdugo/user/domain/AuthProvider.java +++ b/src/main/java/com/zimdugo/identity/domain/AuthProvider.java @@ -1,8 +1,8 @@ -package com.zimdugo.user.domain; +package com.zimdugo.identity.domain; public enum AuthProvider { GOOGLE, KAKAO, NAVER, FACEBOOK -} \ No newline at end of file +} diff --git a/src/main/java/com/zimdugo/user/application/UserQueryService.java b/src/main/java/com/zimdugo/user/application/UserQueryService.java index cf02dc1..7c8d356 100644 --- a/src/main/java/com/zimdugo/user/application/UserQueryService.java +++ b/src/main/java/com/zimdugo/user/application/UserQueryService.java @@ -1,46 +1,45 @@ package com.zimdugo.user.application; import com.zimdugo.user.domain.SocialAccount; +import com.zimdugo.user.domain.SocialAccountReader; import com.zimdugo.user.domain.User; -import com.zimdugo.user.infrastructure.SocialAccountJpaRepository; -import com.zimdugo.user.infrastructure.UserJpaRepository; +import com.zimdugo.user.domain.UserReader; +import com.zimdugo.core.exception.BusinessException; +import com.zimdugo.core.exception.ErrorCode; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; - @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class UserQueryService { - private final UserJpaRepository userJpaRepository; - private final SocialAccountJpaRepository socialAccountJpaRepository; + private final UserReader userReader; + private final SocialAccountReader socialAccountReader; public UserProfileResponse getProfile(Long userId) { User user = findById(userId); - List socialAccounts = - socialAccountJpaRepository.findAllByUserId(userId); + List socialAccounts = socialAccountReader.findAllByUserId(userId); List providers = socialAccounts.stream() - .map(sa -> sa.getProvider().name().toLowerCase()) - .toList(); + .map(sa -> sa.getProvider().name().toLowerCase()) + .toList(); return new UserProfileResponse( - user.getId(), - user.getEmail(), - user.getNickname(), - user.getProfileImageUrl(), - user.getStatus().name(), - providers + user.getId(), + user.getEmail(), + user.getNickname(), + user.getProfileImageUrl(), + user.getStatus().name(), + providers ); } - // AuthController에서 재발급 시 User 조회용 public User findById(Long userId) { - return userJpaRepository.findById(userId) - .orElseThrow(() -> new IllegalArgumentException("user not found. id=" + userId)); + return userReader.findById(userId) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); } } diff --git a/src/main/java/com/zimdugo/user/domain/SocialAccount.java b/src/main/java/com/zimdugo/user/domain/SocialAccount.java index 4776fc1..7be3e4a 100644 --- a/src/main/java/com/zimdugo/user/domain/SocialAccount.java +++ b/src/main/java/com/zimdugo/user/domain/SocialAccount.java @@ -1,78 +1,51 @@ package com.zimdugo.user.domain; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.PrePersist; -import jakarta.persistence.Table; -import jakarta.persistence.UniqueConstraint; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; - +import com.zimdugo.identity.domain.AuthProvider; import java.time.LocalDateTime; +import lombok.Getter; -@Entity -@Table( - name = "social_account", - uniqueConstraints = { - @UniqueConstraint( - name = "uk_provider_provider_user_id", - columnNames = {"provider", "provider_user_id"} - ) - } -) @Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) public class SocialAccount { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - - @JoinColumn(name = "user_id", nullable = false) - @ManyToOne(fetch = FetchType.LAZY, optional = false) private User user; - - @Enumerated(EnumType.STRING) - @Column(nullable = false, length = 20) private AuthProvider provider; - - @Column(name = "provider_user_id", nullable = false, length = 100) private String providerUserId; - - @Column(length = 100) private String providerEmail; - - @Column(length = 255) private String providerProfileImageUrl; - - @Column(nullable = false) private LocalDateTime linkedAt; public SocialAccount( - User user, - AuthProvider provider, - String providerUserId, - String providerEmail, - String providerProfileImageUrl + User user, + AuthProvider provider, + String providerUserId, + String providerEmail, + String providerProfileImageUrl ) { + this(null, user, provider, providerUserId, providerEmail, providerProfileImageUrl, null); + } + + @SuppressWarnings("checkstyle:ParameterNumber") + public SocialAccount( + Long id, + User user, + AuthProvider provider, + String providerUserId, + String providerEmail, + String providerProfileImageUrl, + LocalDateTime linkedAt + ) { + this.id = id; this.user = user; this.provider = provider; this.providerUserId = providerUserId; this.providerEmail = providerEmail; this.providerProfileImageUrl = providerProfileImageUrl; + this.linkedAt = linkedAt; } - @PrePersist - protected void onCreate() { - this.linkedAt = LocalDateTime.now(); + public void updateProviderProfile(String providerEmail, String providerProfileImageUrl) { + this.providerEmail = providerEmail; + this.providerProfileImageUrl = providerProfileImageUrl; } } diff --git a/src/main/java/com/zimdugo/user/domain/SocialAccountReader.java b/src/main/java/com/zimdugo/user/domain/SocialAccountReader.java new file mode 100644 index 0000000..a76cdad --- /dev/null +++ b/src/main/java/com/zimdugo/user/domain/SocialAccountReader.java @@ -0,0 +1,15 @@ +package com.zimdugo.user.domain; + +import com.zimdugo.identity.domain.AuthProvider; +import java.util.List; +import java.util.Optional; + +public interface SocialAccountReader { + + Optional findByProviderAndProviderUserId( + AuthProvider provider, + String providerUserId + ); + + List findAllByUserId(Long userId); +} diff --git a/src/main/java/com/zimdugo/user/domain/SocialAccountStore.java b/src/main/java/com/zimdugo/user/domain/SocialAccountStore.java new file mode 100644 index 0000000..4107dac --- /dev/null +++ b/src/main/java/com/zimdugo/user/domain/SocialAccountStore.java @@ -0,0 +1,8 @@ +package com.zimdugo.user.domain; + +public interface SocialAccountStore { + + SocialAccount store(SocialAccount socialAccount); + + void deleteAllByUserId(Long userId); +} diff --git a/src/main/java/com/zimdugo/user/domain/User.java b/src/main/java/com/zimdugo/user/domain/User.java index 9feeb56..c5bf882 100644 --- a/src/main/java/com/zimdugo/user/domain/User.java +++ b/src/main/java/com/zimdugo/user/domain/User.java @@ -1,78 +1,47 @@ package com.zimdugo.user.domain; -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 jakarta.persistence.PrePersist; -import jakarta.persistence.PreUpdate; -import jakarta.persistence.Table; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; - import java.time.LocalDateTime; +import lombok.Getter; -@Entity -@Table(name = "users") @Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) public class User { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - - @Column(length = 100) private String email; - - @Column(nullable = false, length = 50) private String nickname; - - @Column(length = 255) private String profileImageUrl; - - @Enumerated(EnumType.STRING) - @Column(nullable = false, length = 20) private UserStatus status; - - @Enumerated(EnumType.STRING) - @Column(length = 20) private UserRole role; - - @Column(nullable = false) private LocalDateTime createdAt; - - @Column(nullable = false) private LocalDateTime updatedAt; public User(String email, String nickname, String profileImageUrl, UserStatus status) { - this.email = email; - this.nickname = nickname; - this.profileImageUrl = profileImageUrl; - this.status = status; - this.role = UserRole.USER; + this(null, email, nickname, profileImageUrl, status, UserRole.USER, null, null); } - @PrePersist - protected void onCreate() { - LocalDateTime now = LocalDateTime.now(); - this.createdAt = now; - this.updatedAt = now; - if (this.role == null) { - this.role = UserRole.USER; - } + public User(String email, String nickname, String profileImageUrl, UserStatus status, UserRole role) { + this(null, email, nickname, profileImageUrl, status, role, null, null); } - @PreUpdate - protected void onUpdate() { - this.updatedAt = LocalDateTime.now(); - if (this.role == null) { - this.role = UserRole.USER; - } + @SuppressWarnings("checkstyle:ParameterNumber") + public User( + Long id, + String email, + String nickname, + String profileImageUrl, + UserStatus status, + UserRole role, + LocalDateTime createdAt, + LocalDateTime updatedAt + ) { + this.id = id; + this.email = email; + this.nickname = nickname; + this.profileImageUrl = profileImageUrl; + this.status = status; + this.role = role != null ? role : UserRole.USER; + this.createdAt = createdAt; + this.updatedAt = updatedAt; } public void updateProfile(String nickname, String profileImageUrl) { diff --git a/src/main/java/com/zimdugo/user/domain/UserReader.java b/src/main/java/com/zimdugo/user/domain/UserReader.java new file mode 100644 index 0000000..3d8282e --- /dev/null +++ b/src/main/java/com/zimdugo/user/domain/UserReader.java @@ -0,0 +1,8 @@ +package com.zimdugo.user.domain; + +import java.util.Optional; + +public interface UserReader { + + Optional findById(Long id); +} diff --git a/src/main/java/com/zimdugo/user/domain/UserStore.java b/src/main/java/com/zimdugo/user/domain/UserStore.java new file mode 100644 index 0000000..97d23bd --- /dev/null +++ b/src/main/java/com/zimdugo/user/domain/UserStore.java @@ -0,0 +1,6 @@ +package com.zimdugo.user.domain; + +public interface UserStore { + + User store(User user); +} diff --git a/src/main/java/com/zimdugo/user/entrypoint/UserController.java b/src/main/java/com/zimdugo/user/entrypoint/UserController.java index a519e39..f2d001a 100644 --- a/src/main/java/com/zimdugo/user/entrypoint/UserController.java +++ b/src/main/java/com/zimdugo/user/entrypoint/UserController.java @@ -2,8 +2,8 @@ import com.zimdugo.core.exception.BusinessException; import com.zimdugo.core.exception.ErrorCode; -import com.zimdugo.user.application.UserProfileResponse; import com.zimdugo.user.application.UserQueryService; +import com.zimdugo.user.application.UserProfileResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; diff --git a/src/main/java/com/zimdugo/user/infrastructure/SocialAccountEntityMapper.java b/src/main/java/com/zimdugo/user/infrastructure/SocialAccountEntityMapper.java new file mode 100644 index 0000000..6b3c28b --- /dev/null +++ b/src/main/java/com/zimdugo/user/infrastructure/SocialAccountEntityMapper.java @@ -0,0 +1,35 @@ +package com.zimdugo.user.infrastructure; + +import com.zimdugo.user.domain.SocialAccount; +import com.zimdugo.user.infrastructure.persistence.SocialAccountJpaEntity; +import com.zimdugo.user.infrastructure.persistence.UserJpaEntity; + +final class SocialAccountEntityMapper { + + private SocialAccountEntityMapper() { + } + + static SocialAccount toDomain(SocialAccountJpaEntity entity) { + return new SocialAccount( + entity.getId(), + UserEntityMapper.toDomain(entity.getUser()), + entity.getProvider(), + entity.getProviderUserId(), + entity.getProviderEmail(), + entity.getProviderProfileImageUrl(), + entity.getLinkedAt() + ); + } + + static SocialAccountJpaEntity toEntity(SocialAccount socialAccount, UserJpaEntity userEntity) { + return new SocialAccountJpaEntity( + socialAccount.getId(), + userEntity, + socialAccount.getProvider(), + socialAccount.getProviderUserId(), + socialAccount.getProviderEmail(), + socialAccount.getProviderProfileImageUrl(), + socialAccount.getLinkedAt() + ); + } +} diff --git a/src/main/java/com/zimdugo/user/infrastructure/SocialAccountJpaRepository.java b/src/main/java/com/zimdugo/user/infrastructure/SocialAccountJpaRepository.java index 4bbc822..864438e 100644 --- a/src/main/java/com/zimdugo/user/infrastructure/SocialAccountJpaRepository.java +++ b/src/main/java/com/zimdugo/user/infrastructure/SocialAccountJpaRepository.java @@ -1,18 +1,19 @@ package com.zimdugo.user.infrastructure; -import com.zimdugo.user.domain.AuthProvider; -import com.zimdugo.user.domain.SocialAccount; -import org.springframework.data.jpa.repository.JpaRepository; - +import com.zimdugo.identity.domain.AuthProvider; +import com.zimdugo.user.infrastructure.persistence.SocialAccountJpaEntity; import java.util.List; import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; -public interface SocialAccountJpaRepository extends JpaRepository { +public interface SocialAccountJpaRepository extends JpaRepository { - Optional findByProviderAndProviderUserId( - AuthProvider provider, String providerUserId); + Optional findByProviderAndProviderUserId( + AuthProvider provider, + String providerUserId + ); - List findAllByUserId(Long userId); + List findAllByUserId(Long userId); void deleteAllByUserId(Long userId); } diff --git a/src/main/java/com/zimdugo/user/infrastructure/SocialAccountReaderAdapter.java b/src/main/java/com/zimdugo/user/infrastructure/SocialAccountReaderAdapter.java new file mode 100644 index 0000000..979b5e9 --- /dev/null +++ b/src/main/java/com/zimdugo/user/infrastructure/SocialAccountReaderAdapter.java @@ -0,0 +1,29 @@ +package com.zimdugo.user.infrastructure; + +import com.zimdugo.identity.domain.AuthProvider; +import com.zimdugo.user.domain.SocialAccount; +import com.zimdugo.user.domain.SocialAccountReader; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class SocialAccountReaderAdapter implements SocialAccountReader { + + private final SocialAccountJpaRepository socialAccountJpaRepository; + + @Override + public Optional findByProviderAndProviderUserId(AuthProvider provider, String providerUserId) { + return socialAccountJpaRepository.findByProviderAndProviderUserId(provider, providerUserId) + .map(SocialAccountEntityMapper::toDomain); + } + + @Override + public List findAllByUserId(Long userId) { + return socialAccountJpaRepository.findAllByUserId(userId).stream() + .map(SocialAccountEntityMapper::toDomain) + .toList(); + } +} diff --git a/src/main/java/com/zimdugo/user/infrastructure/SocialAccountStoreAdapter.java b/src/main/java/com/zimdugo/user/infrastructure/SocialAccountStoreAdapter.java new file mode 100644 index 0000000..a282233 --- /dev/null +++ b/src/main/java/com/zimdugo/user/infrastructure/SocialAccountStoreAdapter.java @@ -0,0 +1,31 @@ +package com.zimdugo.user.infrastructure; + +import com.zimdugo.user.domain.SocialAccount; +import com.zimdugo.user.domain.SocialAccountStore; +import com.zimdugo.user.infrastructure.persistence.UserJpaEntity; +import java.util.NoSuchElementException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class SocialAccountStoreAdapter implements SocialAccountStore { + + private final SocialAccountJpaRepository socialAccountJpaRepository; + private final UserJpaRepository userJpaRepository; + + @Override + public SocialAccount store(SocialAccount socialAccount) { + Long userId = socialAccount.getUser().getId(); + UserJpaEntity userEntity = userJpaRepository.findById(userId) + .orElseThrow(() -> new NoSuchElementException("user not found. id=" + userId)); + return SocialAccountEntityMapper.toDomain( + socialAccountJpaRepository.save(SocialAccountEntityMapper.toEntity(socialAccount, userEntity)) + ); + } + + @Override + public void deleteAllByUserId(Long userId) { + socialAccountJpaRepository.deleteAllByUserId(userId); + } +} diff --git a/src/main/java/com/zimdugo/user/infrastructure/UserEntityMapper.java b/src/main/java/com/zimdugo/user/infrastructure/UserEntityMapper.java new file mode 100644 index 0000000..674ebd7 --- /dev/null +++ b/src/main/java/com/zimdugo/user/infrastructure/UserEntityMapper.java @@ -0,0 +1,36 @@ +package com.zimdugo.user.infrastructure; + +import com.zimdugo.user.domain.User; +import com.zimdugo.user.infrastructure.persistence.UserJpaEntity; + +final class UserEntityMapper { + + private UserEntityMapper() { + } + + static User toDomain(UserJpaEntity entity) { + return new User( + entity.getId(), + entity.getEmail(), + entity.getNickname(), + entity.getProfileImageUrl(), + entity.getStatus(), + entity.getRole(), + entity.getCreatedAt(), + entity.getUpdatedAt() + ); + } + + static UserJpaEntity toEntity(User user) { + return new UserJpaEntity( + user.getId(), + user.getEmail(), + user.getNickname(), + user.getProfileImageUrl(), + user.getStatus(), + user.getRoleOrDefault(), + user.getCreatedAt(), + user.getUpdatedAt() + ); + } +} diff --git a/src/main/java/com/zimdugo/user/infrastructure/UserJpaRepository.java b/src/main/java/com/zimdugo/user/infrastructure/UserJpaRepository.java index 113769f..c434b98 100644 --- a/src/main/java/com/zimdugo/user/infrastructure/UserJpaRepository.java +++ b/src/main/java/com/zimdugo/user/infrastructure/UserJpaRepository.java @@ -1,10 +1,10 @@ package com.zimdugo.user.infrastructure; -import com.zimdugo.user.domain.User; +import com.zimdugo.user.infrastructure.persistence.UserJpaEntity; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; -public interface UserJpaRepository extends JpaRepository { - Optional findByEmail(String email); -} \ No newline at end of file +public interface UserJpaRepository extends JpaRepository { + Optional findByEmail(String email); +} diff --git a/src/main/java/com/zimdugo/user/infrastructure/UserReaderAdapter.java b/src/main/java/com/zimdugo/user/infrastructure/UserReaderAdapter.java new file mode 100644 index 0000000..f2678ce --- /dev/null +++ b/src/main/java/com/zimdugo/user/infrastructure/UserReaderAdapter.java @@ -0,0 +1,19 @@ +package com.zimdugo.user.infrastructure; + +import com.zimdugo.user.domain.User; +import com.zimdugo.user.domain.UserReader; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class UserReaderAdapter implements UserReader { + + private final UserJpaRepository userJpaRepository; + + @Override + public Optional findById(Long id) { + return userJpaRepository.findById(id).map(UserEntityMapper::toDomain); + } +} diff --git a/src/main/java/com/zimdugo/user/infrastructure/UserStoreAdapter.java b/src/main/java/com/zimdugo/user/infrastructure/UserStoreAdapter.java new file mode 100644 index 0000000..ba0655e --- /dev/null +++ b/src/main/java/com/zimdugo/user/infrastructure/UserStoreAdapter.java @@ -0,0 +1,20 @@ +package com.zimdugo.user.infrastructure; + +import com.zimdugo.user.domain.User; +import com.zimdugo.user.domain.UserStore; +import com.zimdugo.user.infrastructure.persistence.UserJpaEntity; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class UserStoreAdapter implements UserStore { + + private final UserJpaRepository userJpaRepository; + + @Override + public User store(User user) { + UserJpaEntity saved = userJpaRepository.save(UserEntityMapper.toEntity(user)); + return UserEntityMapper.toDomain(saved); + } +} diff --git a/src/main/java/com/zimdugo/user/infrastructure/persistence/SocialAccountJpaEntity.java b/src/main/java/com/zimdugo/user/infrastructure/persistence/SocialAccountJpaEntity.java new file mode 100644 index 0000000..0979ffb --- /dev/null +++ b/src/main/java/com/zimdugo/user/infrastructure/persistence/SocialAccountJpaEntity.java @@ -0,0 +1,85 @@ +package com.zimdugo.user.infrastructure.persistence; + +import com.zimdugo.identity.domain.AuthProvider; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table( + name = "social_account", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_provider_provider_user_id", + columnNames = {"provider", "provider_user_id"} + ) + } +) +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class SocialAccountJpaEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(name = "user_id", nullable = false) + @ManyToOne(fetch = FetchType.LAZY, optional = false) + private UserJpaEntity user; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 20) + private AuthProvider provider; + + @Column(name = "provider_user_id", nullable = false, length = 100) + private String providerUserId; + + @Column(length = 100) + private String providerEmail; + + @Column(length = 255) + private String providerProfileImageUrl; + + @Column(nullable = false) + private LocalDateTime linkedAt; + + @SuppressWarnings("checkstyle:ParameterNumber") + public SocialAccountJpaEntity( + Long id, + UserJpaEntity user, + AuthProvider provider, + String providerUserId, + String providerEmail, + String providerProfileImageUrl, + LocalDateTime linkedAt + ) { + this.id = id; + this.user = user; + this.provider = provider; + this.providerUserId = providerUserId; + this.providerEmail = providerEmail; + this.providerProfileImageUrl = providerProfileImageUrl; + this.linkedAt = linkedAt; + } + + @PrePersist + protected void onCreate() { + if (this.linkedAt == null) { + this.linkedAt = LocalDateTime.now(); + } + } +} diff --git a/src/main/java/com/zimdugo/user/infrastructure/persistence/UserJpaEntity.java b/src/main/java/com/zimdugo/user/infrastructure/persistence/UserJpaEntity.java new file mode 100644 index 0000000..bc38905 --- /dev/null +++ b/src/main/java/com/zimdugo/user/infrastructure/persistence/UserJpaEntity.java @@ -0,0 +1,91 @@ +package com.zimdugo.user.infrastructure.persistence; + +import com.zimdugo.user.domain.UserRole; +import com.zimdugo.user.domain.UserStatus; +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 jakarta.persistence.PrePersist; +import jakarta.persistence.PreUpdate; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "users") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class UserJpaEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(length = 100) + private String email; + + @Column(nullable = false, length = 50) + private String nickname; + + @Column(length = 255) + private String profileImageUrl; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 20) + private UserStatus status; + + @Enumerated(EnumType.STRING) + @Column(length = 20) + private UserRole role; + + @Column(nullable = false) + private LocalDateTime createdAt; + + @Column(nullable = false) + private LocalDateTime updatedAt; + + @SuppressWarnings("checkstyle:ParameterNumber") + public UserJpaEntity( + Long id, + String email, + String nickname, + String profileImageUrl, + UserStatus status, + UserRole role, + LocalDateTime createdAt, + LocalDateTime updatedAt + ) { + this.id = id; + this.email = email; + this.nickname = nickname; + this.profileImageUrl = profileImageUrl; + this.status = status; + this.role = role; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + @PrePersist + protected void onCreate() { + LocalDateTime now = LocalDateTime.now(); + this.createdAt = now; + this.updatedAt = now; + if (this.role == null) { + this.role = UserRole.USER; + } + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + if (this.role == null) { + this.role = UserRole.USER; + } + } +} diff --git a/src/test/java/com/zimdugo/architecture/LayerDependencyTest.java b/src/test/java/com/zimdugo/architecture/LayerDependencyTest.java index 371f6f8..98261f6 100644 --- a/src/test/java/com/zimdugo/architecture/LayerDependencyTest.java +++ b/src/test/java/com/zimdugo/architecture/LayerDependencyTest.java @@ -1,9 +1,5 @@ package com.zimdugo.architecture; -import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; -import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; -import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices; - import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.core.importer.ImportOption; @@ -12,6 +8,10 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; +import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices; + class LayerDependencyTest { private static JavaClasses importedClasses; @@ -24,11 +24,11 @@ static void setUp() { } @Nested - @DisplayName("Layer dependency rules") + @DisplayName("Layer Dependency Rules") class LayerDependencyRules { @Test - @DisplayName("domain does not depend on entrypoint/application/infrastructure") + @DisplayName("domain should not depend on other layers") void domain_should_not_depend_on_other_layers() { noClasses() .that().resideInAPackage("..domain..") @@ -39,30 +39,30 @@ void domain_should_not_depend_on_other_layers() { } @Test - @DisplayName("application does not depend on entrypoint") - void application_should_not_depend_on_entrypoint() { + @DisplayName("application should not depend on entrypoint or infrastructure") + void application_should_only_depend_on_domain() { noClasses() .that().resideInAPackage("..application..") .should().dependOnClassesThat() - .resideInAnyPackage("..entrypoint..") + .resideInAnyPackage("..entrypoint..", "..infrastructure..") .allowEmptyShould(true) .check(importedClasses); } @Test - @DisplayName("entrypoint does not depend on infrastructure") - void entrypoint_should_not_depend_on_infrastructure() { + @DisplayName("entrypoint should not depend on domain or infrastructure") + void entrypoint_should_only_depend_on_application() { noClasses() .that().resideInAPackage("..entrypoint..") .should().dependOnClassesThat() - .resideInAnyPackage("..infrastructure..") + .resideInAnyPackage("..domain..", "..infrastructure..") .allowEmptyShould(true) .check(importedClasses); } @Test - @DisplayName("infrastructure does not depend on entrypoint/application") - void infrastructure_should_not_depend_on_upper_layers() { + @DisplayName("infrastructure should not depend on entrypoint or application") + void infrastructure_should_only_depend_on_domain() { noClasses() .that().resideInAPackage("..infrastructure..") .should().dependOnClassesThat() @@ -73,11 +73,11 @@ void infrastructure_should_not_depend_on_upper_layers() { } @Nested - @DisplayName("Class location rules") + @DisplayName("Class Location Rules") class ClassLocationRules { @Test - @DisplayName("@RestController classes should be in entrypoint") + @DisplayName("@RestController should reside in entrypoint package") void rest_controllers_should_reside_in_entrypoint() { classes() .that().areAnnotatedWith("org.springframework.web.bind.annotation.RestController") @@ -87,22 +87,22 @@ void rest_controllers_should_reside_in_entrypoint() { } @Test - @DisplayName("@Entity classes should be in domain") - void entities_should_reside_in_domain() { + @DisplayName("@Entity should reside in infrastructure package") + void entities_should_reside_in_infrastructure() { classes() .that().areAnnotatedWith("jakarta.persistence.Entity") - .should().resideInAPackage("..domain..") + .should().resideInAPackage("..infrastructure..") .allowEmptyShould(true) .check(importedClasses); } } @Nested - @DisplayName("Domain cycle rules") + @DisplayName("Slice Rules") class DomainSliceRules { @Test - @DisplayName("no cyclic dependencies between domains") + @DisplayName("slices should be free of cycles") void no_circular_dependencies_between_domains() { slices() .matching("com.zimdugo.(*)..") diff --git a/src/test/java/com/zimdugo/auth/integration/AuthFlowIntegrationTest.java b/src/test/java/com/zimdugo/auth/integration/AuthFlowIntegrationTest.java index 5f05ea8..53e8fde 100644 --- a/src/test/java/com/zimdugo/auth/integration/AuthFlowIntegrationTest.java +++ b/src/test/java/com/zimdugo/auth/integration/AuthFlowIntegrationTest.java @@ -12,10 +12,13 @@ import com.zimdugo.auth.application.JwtTokenProvider; import com.zimdugo.auth.domain.AuthTokens; import com.zimdugo.auth.domain.RefreshTokenRepository; -import com.zimdugo.user.domain.AuthProvider; +import com.zimdugo.identity.domain.AuthProvider; import com.zimdugo.user.domain.SocialAccount; +import com.zimdugo.user.domain.SocialAccountStore; import com.zimdugo.user.domain.User; +import com.zimdugo.user.domain.UserReader; import com.zimdugo.user.domain.UserStatus; +import com.zimdugo.user.domain.UserStore; import com.zimdugo.user.infrastructure.SocialAccountJpaRepository; import com.zimdugo.user.infrastructure.UserJpaRepository; import jakarta.servlet.http.Cookie; @@ -78,16 +81,21 @@ static void properties(DynamicPropertyRegistry registry) { @Autowired private SocialAccountJpaRepository socialAccountJpaRepository; + @Autowired + private UserStore userStore; + + @Autowired + private UserReader userReader; + + @Autowired + private SocialAccountStore socialAccountStore; + @Autowired private StringRedisTemplate stringRedisTemplate; @BeforeEach void setUp() { - stringRedisTemplate.getConnectionFactory() - .getConnection() - .serverCommands() - .flushAll(); - + stringRedisTemplate.getConnectionFactory().getConnection().serverCommands().flushAll(); socialAccountJpaRepository.deleteAll(); userJpaRepository.deleteAll(); } @@ -95,24 +103,28 @@ void setUp() { @Test @DisplayName("refresh with valid RT returns new access token") void refresh_withValidRefreshToken_returnsNewAccessToken() throws Exception { - User savedUser = userJpaRepository.save( + User savedUser = userStore.store( new User("test@zimdugo.com", "test", null, UserStatus.ACTIVE) ); + Long userId = savedUser.getId(); + assertThat(userId).isNotNull(); AuthTokens tokens = jwtTokenProvider.generateTokens( - savedUser.getId(), + userId, "test@zimdugo.com", "USER", "test-sid" ); refreshTokenRepository.save( - savedUser.getId(), + userId, tokens.sid(), tokens.refreshToken(), Duration.ofDays(30) ); + assertThat(refreshTokenRepository.matches(userId, tokens.sid(), tokens.refreshToken())).isTrue(); + mockMvc.perform(post("/api/auth/refresh") .with(csrf()) .cookie(new Cookie("refreshToken", tokens.refreshToken()))) @@ -122,33 +134,70 @@ void refresh_withValidRefreshToken_returnsNewAccessToken() throws Exception { } @Test - @DisplayName("logout invalidates only current session") - void logout_invalidatesOnlyCurrentSession() throws Exception { - User savedUser = userJpaRepository.save( - new User("multi@zimdugo.com", "multi", null, UserStatus.ACTIVE) + @DisplayName("logout returns 200 and expires cookie") + void logout_returns200AndExpiresCookie() throws Exception { + User savedUser = userStore.store( + new User("logout@zimdugo.com", "logout-user", null, UserStatus.ACTIVE) + ); + Long userId = savedUser.getId(); + assertThat(userId).isNotNull(); + + AuthTokens tokens = jwtTokenProvider.generateTokens( + userId, + "logout@zimdugo.com", + "USER", + "logout-sid" + ); + + refreshTokenRepository.save( + userId, + tokens.sid(), + tokens.refreshToken(), + Duration.ofDays(30) + ); + + assertThat(refreshTokenRepository.matches(userId, tokens.sid(), tokens.refreshToken())).isTrue(); + + mockMvc.perform(post("/api/auth/logout") + .with(csrf()) + .header("Authorization", "Bearer " + tokens.accessToken())) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(header().exists("Set-Cookie")); + + assertThat(refreshTokenRepository.matches(userId, tokens.sid(), tokens.refreshToken())).isFalse(); + } + + @Test + @DisplayName("logout invalidates only current session token") + void logout_invalidatesOnlyCurrentSessionToken() throws Exception { + User savedUser = userStore.store( + new User("multi@zimdugo.com", "multi-session", null, UserStatus.ACTIVE) ); + Long userId = savedUser.getId(); + assertThat(userId).isNotNull(); AuthTokens session1 = jwtTokenProvider.generateTokens( - savedUser.getId(), + userId, "multi@zimdugo.com", "USER", "sid-1" ); AuthTokens session2 = jwtTokenProvider.generateTokens( - savedUser.getId(), + userId, "multi@zimdugo.com", "USER", "sid-2" ); refreshTokenRepository.save( - savedUser.getId(), + userId, session1.sid(), session1.refreshToken(), Duration.ofDays(30) ); refreshTokenRepository.save( - savedUser.getId(), + userId, session2.sid(), session2.refreshToken(), Duration.ofDays(30) @@ -157,34 +206,45 @@ void logout_invalidatesOnlyCurrentSession() throws Exception { mockMvc.perform(post("/api/auth/logout") .with(csrf()) .header("Authorization", "Bearer " + session1.accessToken())) - .andExpect(status().isOk()) - .andExpect(header().exists("Set-Cookie")); + .andExpect(status().isOk()); + + assertThat(refreshTokenRepository.matches(userId, session1.sid(), session1.refreshToken())).isFalse(); + assertThat(refreshTokenRepository.matches(userId, session2.sid(), session2.refreshToken())).isTrue(); - assertThat(refreshTokenRepository.matches(savedUser.getId(), session1.sid(), session1.refreshToken())) - .isFalse(); - assertThat(refreshTokenRepository.matches(savedUser.getId(), session2.sid(), session2.refreshToken())) - .isTrue(); + mockMvc.perform(post("/api/auth/refresh") + .with(csrf()) + .cookie(new Cookie("refreshToken", session2.refreshToken()))) + .andExpect(status().isOk()); } @Test - @DisplayName("withdraw sets user status to deleted and revokes refresh token") - void withdraw_deletesUserAndRefreshTokens() throws Exception { - User savedUser = userJpaRepository.save( - new User("withdraw@zimdugo.com", "withdraw", null, UserStatus.ACTIVE) + @DisplayName("withdraw sets user as deleted, removes social links, and revokes refresh token") + void withdraw_deletesSessionAndDeactivatesUser() throws Exception { + User savedUser = userStore.store( + new User("withdraw@zimdugo.com", "withdraw-user", null, UserStatus.ACTIVE) ); - socialAccountJpaRepository.save( - new SocialAccount(savedUser, AuthProvider.KAKAO, "provider-user-id", null, null) + Long userId = savedUser.getId(); + assertThat(userId).isNotNull(); + + socialAccountStore.store( + new SocialAccount( + savedUser, + AuthProvider.KAKAO, + "withdraw-provider-id", + null, + null + ) ); AuthTokens tokens = jwtTokenProvider.generateTokens( - savedUser.getId(), + userId, "withdraw@zimdugo.com", "USER", "withdraw-sid" ); refreshTokenRepository.save( - savedUser.getId(), + userId, tokens.sid(), tokens.refreshToken(), Duration.ofDays(30) @@ -193,12 +253,18 @@ void withdraw_deletesUserAndRefreshTokens() throws Exception { mockMvc.perform(post("/api/auth/withdraw") .with(csrf()) .header("Authorization", "Bearer " + tokens.accessToken())) - .andExpect(status().isOk()); + .andExpect(status().isOk()) + .andExpect(header().exists("Set-Cookie")); - User withdrawnUser = userJpaRepository.findById(savedUser.getId()).orElseThrow(); + User withdrawnUser = userReader.findById(userId).orElseThrow(); assertThat(withdrawnUser.getStatus()).isEqualTo(UserStatus.DELETED); - assertThat(socialAccountJpaRepository.findAllByUserId(savedUser.getId())).isEmpty(); - assertThat(refreshTokenRepository.matches(savedUser.getId(), tokens.sid(), tokens.refreshToken())).isFalse(); + assertThat(socialAccountJpaRepository.findAllByUserId(userId)).isEmpty(); + assertThat(refreshTokenRepository.matches(userId, tokens.sid(), tokens.refreshToken())).isFalse(); + + mockMvc.perform(post("/api/auth/refresh") + .with(csrf()) + .cookie(new Cookie("refreshToken", tokens.refreshToken()))) + .andExpect(status().is4xxClientError()); mockMvc.perform(get("/api/v1/me") .header("Authorization", "Bearer " + tokens.accessToken()))