diff --git a/account-service/src/main/java/com/synapse/account_service/convert/DelegatingProviderUserConverter.java b/account-service/src/main/java/com/synapse/account_service/convert/DelegatingProviderUserConverter.java new file mode 100644 index 0000000..b8e63fe --- /dev/null +++ b/account-service/src/main/java/com/synapse/account_service/convert/DelegatingProviderUserConverter.java @@ -0,0 +1,43 @@ +package com.synapse.account_service.convert; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +import com.synapse.account_service.domain.ProviderUser; + +@Component +public final class DelegatingProviderUserConverter implements ProviderUserConverter { + + private final List> converters; + + public DelegatingProviderUserConverter() { + + List> providerUserConverters = Arrays.asList( + new UserDetailsProviderUserConverter(), + new OAuth2GoogleProviderUserConverter(), + new OAuth2KakaoProviderUserConverter(), + new OAuth2KakaoOidcProviderUserConverter()); + + this.converters = Collections.unmodifiableList(new LinkedList<>(providerUserConverters)); + } + + @Nullable + @Override + public ProviderUser convert(ProviderUserRequest providerUserRequest) { + Assert.notNull(providerUserRequest, "providerUserRequest cannot be null"); + + for (ProviderUserConverter converter : this.converters) { + ProviderUser providerUser = converter.convert(providerUserRequest); + if (providerUser != null) { + return providerUser; + } + } + return null; + } +} diff --git a/account-service/src/main/java/com/synapse/account_service/convert/OAuth2GoogleProviderUserConverter.java b/account-service/src/main/java/com/synapse/account_service/convert/OAuth2GoogleProviderUserConverter.java new file mode 100644 index 0000000..91a2f96 --- /dev/null +++ b/account-service/src/main/java/com/synapse/account_service/convert/OAuth2GoogleProviderUserConverter.java @@ -0,0 +1,22 @@ +package com.synapse.account_service.convert; + +import com.synapse.account_service.domain.ProviderUser; +import com.synapse.account_service.domain.enums.OAuth2Config; +import com.synapse.account_service.domain.socials.GoogleUser; +import com.synapse.account_service.util.OAuth2Utils; + +public final class OAuth2GoogleProviderUserConverter implements ProviderUserConverter { + + @Override + public ProviderUser convert(ProviderUserRequest providerUserRequest) { + + if (!providerUserRequest.clientRegistration().getRegistrationId().equals(OAuth2Config.SocialType.GOOGLE.getSocialName())) { + return null; + } + + return new GoogleUser(OAuth2Utils.getMainAttributes( + providerUserRequest.oAuth2User()), + providerUserRequest.oAuth2User(), + providerUserRequest.clientRegistration()); + } +} diff --git a/account-service/src/main/java/com/synapse/account_service/convert/OAuth2KakaoOidcProviderUserConverter.java b/account-service/src/main/java/com/synapse/account_service/convert/OAuth2KakaoOidcProviderUserConverter.java new file mode 100644 index 0000000..8d65b5d --- /dev/null +++ b/account-service/src/main/java/com/synapse/account_service/convert/OAuth2KakaoOidcProviderUserConverter.java @@ -0,0 +1,28 @@ +package com.synapse.account_service.convert; + +import org.springframework.security.oauth2.core.oidc.user.OidcUser; + +import com.synapse.account_service.domain.ProviderUser; +import com.synapse.account_service.domain.enums.OAuth2Config; +import com.synapse.account_service.domain.socials.KakaoOidcUser; +import com.synapse.account_service.util.OAuth2Utils; + +public final class OAuth2KakaoOidcProviderUserConverter implements ProviderUserConverter { + + @Override + public ProviderUser convert(ProviderUserRequest providerUserRequest) { + + if (!providerUserRequest.clientRegistration().getRegistrationId().equals(OAuth2Config.SocialType.KAKAO.getSocialName())) { + return null; + } + + if (!(providerUserRequest.oAuth2User() instanceof OidcUser)) { + return null; + } + + return new KakaoOidcUser(OAuth2Utils.getMainAttributes( + providerUserRequest.oAuth2User()), + providerUserRequest.oAuth2User(), + providerUserRequest.clientRegistration()); + } +} diff --git a/account-service/src/main/java/com/synapse/account_service/convert/OAuth2KakaoProviderUserConverter.java b/account-service/src/main/java/com/synapse/account_service/convert/OAuth2KakaoProviderUserConverter.java new file mode 100644 index 0000000..48f7e81 --- /dev/null +++ b/account-service/src/main/java/com/synapse/account_service/convert/OAuth2KakaoProviderUserConverter.java @@ -0,0 +1,28 @@ +package com.synapse.account_service.convert; + +import org.springframework.security.oauth2.core.oidc.user.OidcUser; + +import com.synapse.account_service.domain.ProviderUser; +import com.synapse.account_service.domain.enums.OAuth2Config; +import com.synapse.account_service.domain.socials.KakaoUser; +import com.synapse.account_service.util.OAuth2Utils; + +public final class OAuth2KakaoProviderUserConverter implements ProviderUserConverter { + + @Override + public ProviderUser convert(ProviderUserRequest providerUserRequest) { + + if (!providerUserRequest.clientRegistration().getRegistrationId().equals(OAuth2Config.SocialType.KAKAO.getSocialName())) { + return null; + } + + if (providerUserRequest.oAuth2User() instanceof OidcUser) { + return null; + } + + return new KakaoUser(OAuth2Utils.getOtherAttributes( + providerUserRequest.oAuth2User(), "kakao_account", "profile"), + providerUserRequest.oAuth2User(), + providerUserRequest.clientRegistration()); + } +} diff --git a/account-service/src/main/java/com/synapse/account_service/convert/ProviderUserConverter.java b/account-service/src/main/java/com/synapse/account_service/convert/ProviderUserConverter.java new file mode 100644 index 0000000..638384b --- /dev/null +++ b/account-service/src/main/java/com/synapse/account_service/convert/ProviderUserConverter.java @@ -0,0 +1,5 @@ +package com.synapse.account_service.convert; + +public interface ProviderUserConverter { + R convert(T t); +} diff --git a/account-service/src/main/java/com/synapse/account_service/convert/UserDetailsProviderUserConverter.java b/account-service/src/main/java/com/synapse/account_service/convert/UserDetailsProviderUserConverter.java new file mode 100644 index 0000000..2c2be19 --- /dev/null +++ b/account-service/src/main/java/com/synapse/account_service/convert/UserDetailsProviderUserConverter.java @@ -0,0 +1,23 @@ +package com.synapse.account_service.convert; + +import com.synapse.account_service.domain.Member; +import com.synapse.account_service.domain.ProviderUser; +import com.synapse.account_service.domain.forms.FormUser; + +public final class UserDetailsProviderUserConverter implements ProviderUserConverter { + + @Override + public ProviderUser convert(ProviderUserRequest providerUserRequest) { + + Member member = providerUserRequest.member(); + + return FormUser.builder() + .id(member.getId()) + .username(member.getUsername()) + .password(member.getPassword()) + .email(member.getEmail()) + .provider(member.getProvider()) + .authorities(member.getRole().getAuthorities()) + .build(); + } +} diff --git a/account-service/src/main/java/com/synapse/account_service/domain/Attributes.java b/account-service/src/main/java/com/synapse/account_service/domain/Attributes.java new file mode 100644 index 0000000..b043ba9 --- /dev/null +++ b/account-service/src/main/java/com/synapse/account_service/domain/Attributes.java @@ -0,0 +1,25 @@ +package com.synapse.account_service.domain; + +import java.util.Map; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class Attributes { + + private Map mainAttributes; + private Map subAttributes; + private Map otherAttributes; + + public Attributes(Map mainAttributes) { + this.mainAttributes = mainAttributes; + } + + @Builder + public Attributes(Map mainAttributes, Map subAttributes, Map otherAttributes) { + this.mainAttributes = mainAttributes; + this.subAttributes = subAttributes; + this.otherAttributes = otherAttributes; + } +} diff --git a/account-service/src/main/java/com/synapse/account_service/domain/PrincipalUser.java b/account-service/src/main/java/com/synapse/account_service/domain/PrincipalUser.java index b9e1d82..a978373 100644 --- a/account-service/src/main/java/com/synapse/account_service/domain/PrincipalUser.java +++ b/account-service/src/main/java/com/synapse/account_service/domain/PrincipalUser.java @@ -67,11 +67,11 @@ public Map getClaims() { @Override public OidcUserInfo getUserInfo() { - return providerUser != null ? providerUser.getUserInfo() : null; + return null; } @Override public OidcIdToken getIdToken() { - return providerUser != null ? providerUser.getIdToken() : null; + return null; } } diff --git a/account-service/src/main/java/com/synapse/account_service/domain/ProviderUser.java b/account-service/src/main/java/com/synapse/account_service/domain/ProviderUser.java index fcebcf6..971de02 100644 --- a/account-service/src/main/java/com/synapse/account_service/domain/ProviderUser.java +++ b/account-service/src/main/java/com/synapse/account_service/domain/ProviderUser.java @@ -4,8 +4,6 @@ import java.util.Map; import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.oauth2.core.oidc.OidcIdToken; -import org.springframework.security.oauth2.core.oidc.OidcUserInfo; import org.springframework.security.oauth2.core.user.OAuth2User; public interface ProviderUser { @@ -26,8 +24,4 @@ public interface ProviderUser { Map getAttributes(); OAuth2User getOAuth2User(); - - OidcIdToken getIdToken(); - - OidcUserInfo getUserInfo(); } diff --git a/account-service/src/main/java/com/synapse/account_service/domain/enums/OAuth2Config.java b/account-service/src/main/java/com/synapse/account_service/domain/enums/OAuth2Config.java new file mode 100644 index 0000000..737297b --- /dev/null +++ b/account-service/src/main/java/com/synapse/account_service/domain/enums/OAuth2Config.java @@ -0,0 +1,18 @@ +package com.synapse.account_service.domain.enums; + +public class OAuth2Config { + public enum SocialType { + GOOGLE("google"), + KAKAO("kakao"); + + private final String socialName; + + private SocialType(String socialName) { + this.socialName = socialName; + } + + public String getSocialName() { + return socialName; + } + } +} diff --git a/account-service/src/main/java/com/synapse/account_service/domain/forms/FormUser.java b/account-service/src/main/java/com/synapse/account_service/domain/forms/FormUser.java index 00fbf86..a91e6b2 100644 --- a/account-service/src/main/java/com/synapse/account_service/domain/forms/FormUser.java +++ b/account-service/src/main/java/com/synapse/account_service/domain/forms/FormUser.java @@ -5,8 +5,6 @@ import java.util.UUID; import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.oauth2.core.oidc.OidcIdToken; -import org.springframework.security.oauth2.core.oidc.OidcUserInfo; import org.springframework.security.oauth2.core.user.OAuth2User; import com.synapse.account_service.domain.ProviderUser; @@ -70,14 +68,4 @@ public Map getAttributes() { public OAuth2User getOAuth2User() { return null; } - - @Override - public OidcIdToken getIdToken() { - return null; - } - - @Override - public OidcUserInfo getUserInfo() { - return null; - } } diff --git a/account-service/src/main/java/com/synapse/account_service/domain/socials/GoogleUser.java b/account-service/src/main/java/com/synapse/account_service/domain/socials/GoogleUser.java index 2f70fe4..1c5f389 100644 --- a/account-service/src/main/java/com/synapse/account_service/domain/socials/GoogleUser.java +++ b/account-service/src/main/java/com/synapse/account_service/domain/socials/GoogleUser.java @@ -1,30 +1,14 @@ package com.synapse.account_service.domain.socials; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.stream.Collectors; - -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.core.oidc.OidcIdToken; -import org.springframework.security.oauth2.core.oidc.OidcUserInfo; -import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.core.user.OAuth2User; -import com.synapse.account_service.domain.ProviderUser; - -public class GoogleUser implements ProviderUser { +import com.synapse.account_service.domain.Attributes; - private Map attributes; - private OAuth2User oAuth2User; - private ClientRegistration clientRegistration; +public class GoogleUser extends OAuth2ProviderUser { - public GoogleUser(Map attributes, OAuth2User oAuth2User, ClientRegistration clientRegistration) { - this.attributes = attributes; - this.oAuth2User = oAuth2User; - this.clientRegistration = clientRegistration; + public GoogleUser(Attributes attributes, OAuth2User oAuth2User, ClientRegistration clientRegistration) { + super(attributes.getMainAttributes(), oAuth2User, clientRegistration); } @Override @@ -41,54 +25,4 @@ public String getUsername() { public String getPicture() { return null; } - - @Override - public String getPassword() { - return UUID.randomUUID().toString(); - } - - @Override - public String getEmail() { - return (String) attributes.get("email"); - } - - @Override - public String getProvider() { - return clientRegistration.getRegistrationId(); - } - - @Override - public List getAuthorities() { - return oAuth2User.getAuthorities().stream() - .map(authority -> new SimpleGrantedAuthority(authority.getAuthority())).collect(Collectors.toList()); - } - - @Override - public Map getAttributes() { - return this.attributes; - } - - @Override - public OAuth2User getOAuth2User() { - return this.oAuth2User; - } - - @Override - public OidcIdToken getIdToken() { - if(oAuth2User instanceof OidcUser) { - OidcUser oidcUser = (OidcUser) oAuth2User; - return oidcUser.getIdToken(); - } - return null; - } - - @Override - public OidcUserInfo getUserInfo() { - if(oAuth2User instanceof OidcUser) { - OidcUser oidcUser = (OidcUser) oAuth2User; - return oidcUser.getUserInfo(); - } - return null; - } - -} \ No newline at end of file +} diff --git a/account-service/src/main/java/com/synapse/account_service/domain/socials/KakaoOidcUser.java b/account-service/src/main/java/com/synapse/account_service/domain/socials/KakaoOidcUser.java new file mode 100644 index 0000000..1304d5b --- /dev/null +++ b/account-service/src/main/java/com/synapse/account_service/domain/socials/KakaoOidcUser.java @@ -0,0 +1,28 @@ +package com.synapse.account_service.domain.socials; + +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import com.synapse.account_service.domain.Attributes; + +public class KakaoOidcUser extends OAuth2ProviderUser { + + public KakaoOidcUser(Attributes attributes, OAuth2User oAuth2User, ClientRegistration clientRegistration) { + super(attributes.getMainAttributes(), oAuth2User, clientRegistration); + } + + @Override + public String getId() { + return (String) getAttributes().get("id"); + } + + @Override + public String getUsername() { + return (String) getAttributes().get("nickname"); + } + + @Override + public String getPicture() { + return (String) getAttributes().get("profile_image_url"); + } +} \ No newline at end of file diff --git a/account-service/src/main/java/com/synapse/account_service/domain/socials/KakaoUser.java b/account-service/src/main/java/com/synapse/account_service/domain/socials/KakaoUser.java new file mode 100644 index 0000000..b5276c8 --- /dev/null +++ b/account-service/src/main/java/com/synapse/account_service/domain/socials/KakaoUser.java @@ -0,0 +1,34 @@ +package com.synapse.account_service.domain.socials; + +import java.util.Map; + +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import com.synapse.account_service.domain.Attributes; + +public class KakaoUser extends OAuth2ProviderUser { + + private final Map subAttributes; + + public KakaoUser(Attributes attributes, OAuth2User oAuth2User, ClientRegistration clientRegistration) { + super(attributes.getSubAttributes(), oAuth2User, clientRegistration); + this.subAttributes = attributes.getOtherAttributes(); + + } + + @Override + public String getId() { + return (String) getAttributes().get("id"); + } + + @Override + public String getUsername() { + return (String) subAttributes.get("nickname"); + } + + @Override + public String getPicture() { + return (String) subAttributes.get("profile_image_url"); + } +} diff --git a/account-service/src/main/java/com/synapse/account_service/domain/socials/OAuth2ProviderUser.java b/account-service/src/main/java/com/synapse/account_service/domain/socials/OAuth2ProviderUser.java new file mode 100644 index 0000000..aabb8ba --- /dev/null +++ b/account-service/src/main/java/com/synapse/account_service/domain/socials/OAuth2ProviderUser.java @@ -0,0 +1,50 @@ +package com.synapse.account_service.domain.socials; + +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import com.synapse.account_service.domain.ProviderUser; + +import lombok.Data; + +@Data +public abstract class OAuth2ProviderUser implements ProviderUser { + + private Map attributes; + private OAuth2User oAuth2User; + private ClientRegistration clientRegistration; + + public OAuth2ProviderUser(Map attributes, OAuth2User oAuth2User, ClientRegistration clientRegistration) { + this.attributes = attributes; + this.oAuth2User = oAuth2User; + this.clientRegistration = clientRegistration; + } + + @Override + public String getPassword() { + return UUID.randomUUID().toString(); + } + + @Override + public String getEmail() { + return (String) attributes.get("email"); + } + + @Override + public String getProvider() { + return clientRegistration.getRegistrationId(); + } + + @Override + public List getAuthorities() { + return oAuth2User.getAuthorities().stream() + .map(authority -> new SimpleGrantedAuthority(authority.getAuthority())).collect(Collectors.toList()); + } +} diff --git a/account-service/src/main/java/com/synapse/account_service/repository/MemberRepository.java b/account-service/src/main/java/com/synapse/account_service/repository/MemberRepository.java index 56d6b48..ced555f 100644 --- a/account-service/src/main/java/com/synapse/account_service/repository/MemberRepository.java +++ b/account-service/src/main/java/com/synapse/account_service/repository/MemberRepository.java @@ -4,6 +4,8 @@ import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import com.synapse.account_service.domain.Member; @@ -11,4 +13,7 @@ public interface MemberRepository extends JpaRepository { Optional findByEmail(String email); Optional findByUsername(String username); Optional findByProviderAndRegistrationId(String provider, String registrationId); + + @Query("SELECT m FROM Member m WHERE (m.provider = :provider AND m.registrationId = :registrationId) OR m.email = :email OR m.username = :username") + Optional findBySocialIdOrEmailOrUsername(@Param("provider") String provider, @Param("registrationId") String registrationId, @Param("email") String email, @Param("username") String username); } diff --git a/account-service/src/main/java/com/synapse/account_service/service/AbstractOAuth2UserService.java b/account-service/src/main/java/com/synapse/account_service/service/AbstractOAuth2UserService.java new file mode 100644 index 0000000..d07fc94 --- /dev/null +++ b/account-service/src/main/java/com/synapse/account_service/service/AbstractOAuth2UserService.java @@ -0,0 +1,34 @@ +package com.synapse.account_service.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.stereotype.Service; + +import com.synapse.account_service.convert.ProviderUserConverter; +import com.synapse.account_service.convert.ProviderUserRequest; +import com.synapse.account_service.domain.Member; +import com.synapse.account_service.domain.ProviderUser; +import com.synapse.account_service.repository.MemberRepository; + +@Service +public abstract class AbstractOAuth2UserService { + + @Autowired + protected MemberRepository memberRepository; + + @Autowired + private MemberRegistrationService memberRegistrationService; + + @Autowired + private ProviderUserConverter providerUserConverter; + + public Member register(ProviderUser providerUser, OAuth2UserRequest userRequest) { + ClientRegistration clientRegistration = userRequest.getClientRegistration(); + return memberRegistrationService.registerOauthUser(clientRegistration.getRegistrationId(), providerUser); + } + + public ProviderUser providerUser(ProviderUserRequest providerUserRequest) { + return providerUserConverter.convert(providerUserRequest); + } +} diff --git a/account-service/src/main/java/com/synapse/account_service/service/CustomOAuth2UserService.java b/account-service/src/main/java/com/synapse/account_service/service/CustomOAuth2UserService.java index 0fa7ff6..98bf8ff 100644 --- a/account-service/src/main/java/com/synapse/account_service/service/CustomOAuth2UserService.java +++ b/account-service/src/main/java/com/synapse/account_service/service/CustomOAuth2UserService.java @@ -1,7 +1,5 @@ package com.synapse.account_service.service; -import java.util.Optional; - import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; @@ -9,30 +7,23 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; -import org.springframework.beans.factory.annotation.Autowired; import com.synapse.account_service.convert.ProviderUserRequest; import com.synapse.account_service.domain.Member; import com.synapse.account_service.domain.PrincipalUser; import com.synapse.account_service.domain.ProviderUser; -import com.synapse.account_service.domain.socials.GoogleUser; -import com.synapse.account_service.repository.MemberRepository; @Service -public class CustomOAuth2UserService implements OAuth2UserService{ +public class CustomOAuth2UserService extends AbstractOAuth2UserService implements OAuth2UserService{ private final OAuth2UserService oAuth2UserService; - private final MemberRegistrationService registrationService; - @Autowired - public CustomOAuth2UserService(MemberRepository memberRepository, MemberRegistrationService registrationService) { + public CustomOAuth2UserService() { this.oAuth2UserService = new DefaultOAuth2UserService(); - this.registrationService = registrationService; } // 테스트용 생성자 - public CustomOAuth2UserService(OAuth2UserService oAuth2UserService, MemberRegistrationService registrationService) { + public CustomOAuth2UserService(OAuth2UserService oAuth2UserService) { this.oAuth2UserService = oAuth2UserService; - this.registrationService = registrationService; } @Override @@ -43,43 +34,8 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic ProviderUserRequest providerUserRequest = new ProviderUserRequest(clientRegistration, oAuth2User); ProviderUser providerUser = providerUser(providerUserRequest); - Member member = findOrRegisterMember(providerUser, userRequest); + Member member = super.register(providerUser, userRequest); return new PrincipalUser(providerUser, member); } - - private Member findOrRegisterMember(ProviderUser providerUser, OAuth2UserRequest userRequest) { - Optional memberOptional = registrationService.findByProviderAndRegistrationId( - providerUser.getProvider(), - providerUser.getId() - ); - - if (memberOptional.isPresent()) { - return memberOptional.get(); - } - - if(providerUser.getEmail() != null) { - Optional memberByEmailOpt = registrationService.findByEmail(providerUser.getEmail()); - if(memberByEmailOpt.isPresent()) { - Member existingMember = memberByEmailOpt.get(); - existingMember.linkSocialAccount(providerUser.getProvider(), providerUser.getId()); - return existingMember; - } - } - - return registrationService.registerOauthUser(providerUser); - } - - private ProviderUser providerUser(ProviderUserRequest providerUserRequest) { - if (!providerUserRequest.clientRegistration().getRegistrationId().equals("google")) { - return null; - } - - return new GoogleUser( - providerUserRequest.oAuth2User().getAttributes(), - providerUserRequest.oAuth2User(), - providerUserRequest.clientRegistration() - ); - } - } diff --git a/account-service/src/main/java/com/synapse/account_service/service/CustomOidcUserService.java b/account-service/src/main/java/com/synapse/account_service/service/CustomOidcUserService.java new file mode 100644 index 0000000..087b3cb --- /dev/null +++ b/account-service/src/main/java/com/synapse/account_service/service/CustomOidcUserService.java @@ -0,0 +1,52 @@ +package com.synapse.account_service.service; + +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.stereotype.Service; + +import com.synapse.account_service.convert.ProviderUserRequest; +import com.synapse.account_service.domain.Member; +import com.synapse.account_service.domain.PrincipalUser; +import com.synapse.account_service.domain.ProviderUser; + +@Service +public class CustomOidcUserService extends AbstractOAuth2UserService implements OAuth2UserService { + + private final OidcUserService oidcUserService; + + public CustomOidcUserService() { + this.oidcUserService = new OidcUserService(); + } + + // 테스트용 생성자 + public CustomOidcUserService(OidcUserService oidcUserService) { + this.oidcUserService = oidcUserService; + } + + @Override + public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { + // Open ID Connect 인 경우 User name Attribute Key 가 sub 이기 때문에 재정의함 + ClientRegistration clientRegistration = ClientRegistration + .withClientRegistration(userRequest.getClientRegistration()) + .userNameAttributeName("sub") + .build(); + + OidcUserRequest oidcUserRequest = new OidcUserRequest( + clientRegistration, userRequest.getAccessToken(), userRequest.getIdToken(), userRequest.getAdditionalParameters() + ); + + OidcUser oidcUser = oidcUserService.loadUser(oidcUserRequest); + + ProviderUserRequest providerUserRequest = new ProviderUserRequest(clientRegistration, oidcUser); + ProviderUser providerUser = providerUser(providerUserRequest); + + Member member = super.register(providerUser, oidcUserRequest); + + return new PrincipalUser(providerUser, member); + } + +} diff --git a/account-service/src/main/java/com/synapse/account_service/service/CustomUserDetailsService.java b/account-service/src/main/java/com/synapse/account_service/service/CustomUserDetailsService.java index 20efb07..f28daa1 100644 --- a/account-service/src/main/java/com/synapse/account_service/service/CustomUserDetailsService.java +++ b/account-service/src/main/java/com/synapse/account_service/service/CustomUserDetailsService.java @@ -9,17 +9,11 @@ import com.synapse.account_service.domain.Member; import com.synapse.account_service.domain.PrincipalUser; import com.synapse.account_service.domain.ProviderUser; -import com.synapse.account_service.domain.forms.FormUser; import com.synapse.account_service.exception.ExceptionType; import com.synapse.account_service.exception.NotFoundException; -import com.synapse.account_service.repository.MemberRepository; - -import lombok.RequiredArgsConstructor; @Service -@RequiredArgsConstructor -public class CustomUserDetailsService implements UserDetailsService { - private final MemberRepository memberRepository; +public class CustomUserDetailsService extends AbstractOAuth2UserService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { @@ -31,18 +25,4 @@ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundEx return new PrincipalUser(providerUser); } - - private ProviderUser providerUser(ProviderUserRequest providerUserRequest) { - Member member = providerUserRequest.member(); - - return FormUser.builder() - .id(member.getId()) - .username(member.getUsername()) - .password(member.getPassword()) - .email(member.getEmail()) - .provider(member.getProvider()) - .authorities(member.getRole().getAuthorities()) - .build(); - } - } diff --git a/account-service/src/main/java/com/synapse/account_service/service/MemberRegistrationService.java b/account-service/src/main/java/com/synapse/account_service/service/MemberRegistrationService.java index cdced08..5664c5b 100644 --- a/account-service/src/main/java/com/synapse/account_service/service/MemberRegistrationService.java +++ b/account-service/src/main/java/com/synapse/account_service/service/MemberRegistrationService.java @@ -13,8 +13,6 @@ import com.synapse.account_service.domain.Subscription; import com.synapse.account_service.domain.enums.MemberRole; import com.synapse.account_service.domain.enums.SubscriptionTier; -import com.synapse.account_service.exception.DuplicatedException; -import com.synapse.account_service.exception.ExceptionType; import com.synapse.account_service.repository.MemberRepository; import lombok.RequiredArgsConstructor; @@ -26,35 +24,31 @@ public class MemberRegistrationService { private final MemberRepository memberRepository; - public Optional findByProviderAndRegistrationId(String provider, String registrationId) { - return memberRepository.findByProviderAndRegistrationId(provider, registrationId); - } + @Transactional + public Member registerOauthUser(String provider, ProviderUser providerUser) { + Optional memberOptional = memberRepository.findBySocialIdOrEmailOrUsername( + provider, providerUser.getId(), providerUser.getEmail(), providerUser.getUsername() + ); - public Optional findByEmail(String email) { - return memberRepository.findByEmail(email); - } + if (memberOptional.isPresent()) { + Member existingMember = memberOptional.get(); + if (existingMember.getProvider() == null || existingMember.getRegistrationId() == null) { + existingMember.linkSocialAccount(provider, providerUser.getId()); + } + return existingMember; + } - @Transactional - public Member registerOauthUser(ProviderUser providerUser) { + // 신규 회원 생성 return createAndSaveNewMember( providerUser.getEmail(), providerUser.getUsername(), providerUser.getPassword(), - providerUser.getProvider(), + provider, providerUser.getId() ); } private Member createAndSaveNewMember(String email, String username, String password, String provider, String registrationId) { - // 중복 검사 - memberRepository.findByEmail(email).ifPresent(m -> { - throw new DuplicatedException(ExceptionType.DUPLICATED_EMAIL); - }); - - memberRepository.findByUsername(username).ifPresent(m -> { - throw new DuplicatedException(ExceptionType.DUPLICATED_USERNAME); - }); - Member member = Member.builder() .email(email) .password(password) diff --git a/account-service/src/main/java/com/synapse/account_service/util/OAuth2Utils.java b/account-service/src/main/java/com/synapse/account_service/util/OAuth2Utils.java new file mode 100644 index 0000000..f1de400 --- /dev/null +++ b/account-service/src/main/java/com/synapse/account_service/util/OAuth2Utils.java @@ -0,0 +1,29 @@ +package com.synapse.account_service.util; + +import java.util.Map; + +import org.springframework.security.oauth2.core.user.OAuth2User; + +import com.synapse.account_service.domain.Attributes; + +public class OAuth2Utils { + + public static Attributes getMainAttributes(OAuth2User oAuth2User) { + + return Attributes.builder() + .mainAttributes(oAuth2User.getAttributes()) + .build(); + } + + @SuppressWarnings("unchecked") + public static Attributes getOtherAttributes(OAuth2User oAuth2User, String subAttributesKey, String otherAttributesKey) { + + Map subAttributes = (Map) oAuth2User.getAttributes().get(subAttributesKey); + Map otherAttributes = (Map) subAttributes.get(otherAttributesKey); + + return Attributes.builder() + .subAttributes(subAttributes) + .otherAttributes(otherAttributes) + .build(); + } +} diff --git a/account-service/src/test/java/com/synapse/account_service/integrationtest/CustomOAuth2UserServiceTest.java b/account-service/src/test/java/com/synapse/account_service/integrationtest/CustomOAuth2UserServiceTest.java new file mode 100644 index 0000000..96e7cd4 --- /dev/null +++ b/account-service/src/test/java/com/synapse/account_service/integrationtest/CustomOAuth2UserServiceTest.java @@ -0,0 +1,108 @@ +package com.synapse.account_service.integrationtest; + +import com.synapse.account_service.convert.ProviderUserConverter; +import com.synapse.account_service.convert.ProviderUserRequest; +import com.synapse.account_service.domain.Attributes; +import com.synapse.account_service.domain.Member; +import com.synapse.account_service.domain.PrincipalUser; +import com.synapse.account_service.domain.ProviderUser; +import com.synapse.account_service.domain.socials.GoogleUser; +import com.synapse.account_service.service.CustomOAuth2UserService; +import com.synapse.account_service.service.MemberRegistrationService; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.Instant; +import java.util.Collections; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@Import(CustomOAuth2UserService.class) +@ExtendWith(MockitoExtension.class) +public class CustomOAuth2UserServiceTest { + + @Autowired + private CustomOAuth2UserService customOAuth2UserService; + + @Mock + private MemberRegistrationService registrationService; + + @Mock + private ProviderUserConverter providerUserConverter; + + @Mock + private OAuth2UserService oAuth2UserService; + + + private String userEmail = "google_user@example.com"; + private String username = "구글유저"; + private String providerId = "1234567890"; + + @BeforeEach + void setUp() { + customOAuth2UserService = new CustomOAuth2UserService(oAuth2UserService); + ReflectionTestUtils.setField(customOAuth2UserService, "memberRegistrationService", registrationService); + ReflectionTestUtils.setField(customOAuth2UserService, "providerUserConverter", providerUserConverter); + } + + @Test + @DisplayName("소셜 로그인 시 회원가입/로그인 처리 통합 테스트") + void loadUser_registersOrLogsIn() { + // given + ClientRegistration clientRegistration = ClientRegistration.withRegistrationId("google") + .clientId("test-id") + .userNameAttributeName("sub") + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("uri") + .tokenUri("uri") + .authorizationUri("uri") + .userInfoUri("uri") + .build(); + + OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "test-token", Instant.now(), Instant.now().plusSeconds(60)); + OAuth2UserRequest userRequest = new OAuth2UserRequest(clientRegistration, accessToken); + + Map attributes = Map.of("sub", providerId, "name", username, "email", userEmail); + OAuth2User mockOAuth2User = new DefaultOAuth2User(Collections.emptyList(), attributes, "sub"); + + ProviderUser mockProviderUser = new GoogleUser(new Attributes(attributes), mockOAuth2User, clientRegistration); + + Member mockMember = Member.builder().email(userEmail).username(username).build(); + + given(oAuth2UserService.loadUser(any(OAuth2UserRequest.class))).willReturn(mockOAuth2User); + given(providerUserConverter.convert(any(ProviderUserRequest.class))).willReturn(mockProviderUser); + given(registrationService.registerOauthUser("google", mockProviderUser)).willReturn(mockMember); + + // when + OAuth2User result = customOAuth2UserService.loadUser(userRequest); + + // then + verify(oAuth2UserService, times(1)).loadUser(any(OAuth2UserRequest.class)); + verify(providerUserConverter, times(1)).convert(any(ProviderUserRequest.class)); + verify(registrationService, times(1)).registerOauthUser("google", mockProviderUser); + + assertThat(result).isInstanceOf(PrincipalUser.class); + assertThat(((PrincipalUser) result).getUsername()).isEqualTo(username); + assertThat(((PrincipalUser) result).providerUser().getUsername()).isEqualTo(username); + } +} diff --git a/account-service/src/test/java/com/synapse/account_service/integrationtest/CustomOidcUserServiceTest.java b/account-service/src/test/java/com/synapse/account_service/integrationtest/CustomOidcUserServiceTest.java new file mode 100644 index 0000000..5b17bcf --- /dev/null +++ b/account-service/src/test/java/com/synapse/account_service/integrationtest/CustomOidcUserServiceTest.java @@ -0,0 +1,103 @@ +package com.synapse.account_service.integrationtest; + +import com.synapse.account_service.convert.ProviderUserConverter; +import com.synapse.account_service.convert.ProviderUserRequest; +import com.synapse.account_service.domain.Attributes; +import com.synapse.account_service.domain.Member; +import com.synapse.account_service.domain.PrincipalUser; +import com.synapse.account_service.domain.ProviderUser; +import com.synapse.account_service.domain.socials.KakaoOidcUser; +import com.synapse.account_service.service.CustomOidcUserService; +import com.synapse.account_service.service.MemberRegistrationService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.Instant; +import java.util.Collections; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +public class CustomOidcUserServiceTest { + + private CustomOidcUserService customOidcUserService; + + @Mock + private MemberRegistrationService registrationService; + + @Mock + private ProviderUserConverter providerUserConverter; + + @Mock + private OidcUserService oidcUserService; + + private String userEmail = "kakao_user@example.com"; + private String username = "카카오유저"; + private String providerId = "1234567890"; + + @BeforeEach + void setUp() { + customOidcUserService = new CustomOidcUserService(oidcUserService); + ReflectionTestUtils.setField(customOidcUserService, "memberRegistrationService", registrationService); + ReflectionTestUtils.setField(customOidcUserService, "providerUserConverter", providerUserConverter); + } + + @Test + @DisplayName("OIDC 소셜 로그인 시 회원가입/로그인 처리 통합 테스트") + void loadUser_registersOrLogsIn() { + // given + ClientRegistration clientRegistration = ClientRegistration.withRegistrationId("kakao") + .clientId("test-id") + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("uri") + .tokenUri("uri") + .authorizationUri("uri") + .userInfoUri("uri") + .build(); + + Map claims = Map.of("sub", providerId, "nickname", username, "email", userEmail); + OidcIdToken idToken = new OidcIdToken("test-token", Instant.now(), Instant.now().plusSeconds(60), claims); + OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "test-token", Instant.now(), Instant.now().plusSeconds(60)); + OidcUserRequest userRequest = new OidcUserRequest(clientRegistration, accessToken, idToken); + + OidcUser mockOidcUser = new DefaultOidcUser(Collections.emptyList(), idToken, "sub"); + + ProviderUser mockProviderUser = new KakaoOidcUser(new Attributes(claims), mockOidcUser, clientRegistration); + + Member mockMember = Member.builder().email(userEmail).username(username).build(); + + given(oidcUserService.loadUser(any(OidcUserRequest.class))).willReturn(mockOidcUser); + given(providerUserConverter.convert(any(ProviderUserRequest.class))).willReturn(mockProviderUser); + given(registrationService.registerOauthUser("kakao", mockProviderUser)).willReturn(mockMember); + + // when + OidcUser result = customOidcUserService.loadUser(userRequest); + + // then + verify(oidcUserService, times(1)).loadUser(any(OidcUserRequest.class)); + verify(providerUserConverter, times(1)).convert(any(ProviderUserRequest.class)); + verify(registrationService, times(1)).registerOauthUser("kakao", mockProviderUser); + + assertThat(result).isInstanceOf(PrincipalUser.class); + assertThat(((PrincipalUser) result).getUsername()).isEqualTo(username); + assertThat(((PrincipalUser) result).providerUser().getUsername()).isEqualTo(username); + } +} \ No newline at end of file diff --git a/account-service/src/test/java/com/synapse/account_service/service/CustomOAuth2UserServiceTest.java b/account-service/src/test/java/com/synapse/account_service/service/CustomOAuth2UserServiceTest.java deleted file mode 100644 index cd0f193..0000000 --- a/account-service/src/test/java/com/synapse/account_service/service/CustomOAuth2UserServiceTest.java +++ /dev/null @@ -1,140 +0,0 @@ -package com.synapse.account_service.service; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - - -import static org.mockito.BDDMockito.given; -import static org.assertj.core.api.Assertions.assertThat; - -import java.time.Instant; -import java.util.Optional; -import java.util.Collections; -import java.util.Map; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; -import org.springframework.security.oauth2.core.AuthorizationGrantType; -import org.springframework.security.oauth2.core.OAuth2AccessToken; -import org.springframework.security.oauth2.core.user.DefaultOAuth2User; -import org.springframework.security.oauth2.core.user.OAuth2User; - -import com.synapse.account_service.domain.Member; -import com.synapse.account_service.domain.PrincipalUser; -import com.synapse.account_service.domain.ProviderUser; - -@ExtendWith(MockitoExtension.class) -public class CustomOAuth2UserServiceTest { - - @InjectMocks - private CustomOAuth2UserService customOAuth2UserService; - - @Mock - private OAuth2UserService oAuth2UserService; - - @Mock - private MemberRegistrationService registrationService; - - private OAuth2UserRequest googleUserRequest; - private OAuth2User mockGoogleUser; - private String userEmail = "google_user@example.com"; - private String providerId = "1234567890"; - - @BeforeEach - void setUp() { - // 테스트 대상 서비스 수동 생성 및 의존성 주입 - customOAuth2UserService = new CustomOAuth2UserService(oAuth2UserService, registrationService); - - ClientRegistration clientRegistration = ClientRegistration.withRegistrationId("google") - .clientId("test-id") - .clientSecret("test-secret") - .userNameAttributeName("sub") - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") - .tokenUri("https://www.googleapis.com/oauth2/v4/token") - .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") - .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") - .build(); - - Map attributes = Map.of( - "sub", providerId, - "name", "구글유저", - "email", userEmail - ); - - mockGoogleUser = new DefaultOAuth2User(Collections.emptyList(), attributes, "sub"); - OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "test-token", Instant.now(), Instant.now().plusSeconds(60)); - googleUserRequest = new OAuth2UserRequest(clientRegistration, accessToken); - } - - @Test - @DisplayName("시나리오 1: 신규 소셜 사용자일 경우, 회원가입 로직이 호출된다") - void loadUser_whenNewUser_shouldRegisterMember() { - // given - given(oAuth2UserService.loadUser(any(OAuth2UserRequest.class))).willReturn(mockGoogleUser); - given(registrationService.findByProviderAndRegistrationId(anyString(), anyString())).willReturn(Optional.empty()); - given(registrationService.findByEmail(anyString())).willReturn(Optional.empty()); - - // 1차, 2차 조회 모두 실패하여 사용자가 없다고 가정 - Member newMember = Member.builder().email(userEmail).username("구글유저").build(); - given(registrationService.registerOauthUser(any(ProviderUser.class))).willReturn(newMember); - - // when - OAuth2User result = customOAuth2UserService.loadUser(googleUserRequest); - - verify(registrationService, times(1)).registerOauthUser(any(ProviderUser.class)); - - assertThat(result).isInstanceOf(PrincipalUser.class); - assertThat(((PrincipalUser) result).member()).isNotNull(); - assertThat(((PrincipalUser) result).member().getEmail()).isEqualTo(userEmail); - } - - @Test - @DisplayName("시나리오 2: 이미 동일한 소셜 계정으로 가입한 경우, 회원가입 없이 로그인 처리된다") - void loadUser_whenExistingSocialUser_shouldNotRegister() { - // given - given(oAuth2UserService.loadUser(any(OAuth2UserRequest.class))).willReturn(mockGoogleUser); - - Member existingMember = Member.builder().email(userEmail).build(); - // 1차 조회에서 사용자를 찾았다고 가정 - given(registrationService.findByProviderAndRegistrationId("google", providerId)).willReturn(Optional.of(existingMember)); - - // when - OAuth2User result = customOAuth2UserService.loadUser(googleUserRequest); - - // then - assertThat(result).isInstanceOf(PrincipalUser.class); - assertThat(((PrincipalUser) result).member().getEmail()).isEqualTo(userEmail); - } - - @Test - @DisplayName("시나리오 3: 이미 동일한 이메일의 일반 계정이 있을 경우, 계정이 연동된다") - void loadUser_whenExistingLocalUser_shouldLinkAccount() { - // given - given(oAuth2UserService.loadUser(any(OAuth2UserRequest.class))).willReturn(mockGoogleUser); - - Member existingMember = mock(Member.class); - - given(registrationService.findByProviderAndRegistrationId(anyString(), anyString())).willReturn(Optional.empty()); - given(registrationService.findByEmail(userEmail)).willReturn(Optional.of(existingMember)); - - // when - OAuth2User result = customOAuth2UserService.loadUser(googleUserRequest); - - // then - verify(existingMember, times(1)).linkSocialAccount("google", providerId); - - assertThat(result).isInstanceOf(PrincipalUser.class); - } -} diff --git a/account-service/src/test/java/com/synapse/account_service/service/CustomUserDetailsServiceTest.java b/account-service/src/test/java/com/synapse/account_service/service/CustomUserDetailsServiceTest.java index 91636f6..bd4beff 100644 --- a/account-service/src/test/java/com/synapse/account_service/service/CustomUserDetailsServiceTest.java +++ b/account-service/src/test/java/com/synapse/account_service/service/CustomUserDetailsServiceTest.java @@ -1,69 +1,91 @@ package com.synapse.account_service.service; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.BDDMockito.given; - -import java.util.Optional; -import java.util.UUID; - +import com.synapse.account_service.convert.ProviderUserConverter; +import com.synapse.account_service.convert.ProviderUserRequest; +import com.synapse.account_service.domain.Member; +import com.synapse.account_service.domain.PrincipalUser; +import com.synapse.account_service.domain.ProviderUser; +import com.synapse.account_service.domain.enums.MemberRole; +import com.synapse.account_service.domain.forms.FormUser; +import com.synapse.account_service.exception.NotFoundException; +import com.synapse.account_service.repository.MemberRepository; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.test.util.ReflectionTestUtils; -import com.synapse.account_service.domain.Member; -import com.synapse.account_service.domain.PrincipalUser; -import com.synapse.account_service.domain.enums.MemberRole; -import com.synapse.account_service.exception.NotFoundException; -import com.synapse.account_service.repository.MemberRepository; +import java.util.Optional; +import java.util.UUID; + +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.BDDMockito.given; @ExtendWith(MockitoExtension.class) -public class CustomUserDetailsServiceTest { - @InjectMocks +class CustomUserDetailsServiceTest { + private CustomUserDetailsService customUserDetailsService; @Mock private MemberRepository memberRepository; + @Mock + private ProviderUserConverter providerUserConverter; + + @BeforeEach + void setUp() { + customUserDetailsService = new CustomUserDetailsService(); + ReflectionTestUtils.setField(customUserDetailsService, "memberRepository", memberRepository); + ReflectionTestUtils.setField(customUserDetailsService, "providerUserConverter", providerUserConverter); + } + @Test @DisplayName("사용자 조회 성공: DB에 존재하는 사용자를 PrincipalUser 객체로 변환하여 반환한다") - void loadUserByUsername_success() { + void loadUserByUsername_whenUserExists_returnsPrincipalUser() { // given - String username = "test@example.com"; + String username = "testuser"; + String email = "test@test.com"; Member mockMember = Member.builder() .id(UUID.randomUUID()) .username(username) - .password("encodedPassword") + .email(email) + .password("password") .role(MemberRole.USER) .build(); + + ProviderUser mockProviderUser = FormUser.builder() + .id(mockMember.getId()) + .username(mockMember.getUsername()) + .password(mockMember.getPassword()) + .email(mockMember.getEmail()) + .authorities(mockMember.getRole().getAuthorities()) + .build(); given(memberRepository.findByUsername(username)).willReturn(Optional.of(mockMember)); + given(providerUserConverter.convert(any(ProviderUserRequest.class))).willReturn(mockProviderUser); // when - UserDetails userDetails = customUserDetailsService.loadUserByUsername(username); + PrincipalUser principalUser = (PrincipalUser) customUserDetailsService.loadUserByUsername(username); // then - assertThat(userDetails).isInstanceOf(PrincipalUser.class); - assertThat(userDetails.getUsername()).isEqualTo(username); - assertThat(userDetails.getPassword()).isEqualTo("encodedPassword"); - assertThat(userDetails.getAuthorities()).hasSize(1); - assertThat(userDetails.getAuthorities().iterator().next().getAuthority()).isEqualTo("USER"); + assertThat(principalUser).isNotNull(); + assertThat(principalUser.getUsername()).isEqualTo(username); + assertThat(principalUser.providerUser().getUsername()).isEqualTo(username); } @Test - @DisplayName("사용자 조회 실패: DB에 사용자가 없으면 UsernameNotFoundException을 던진다") - void loadUserByUsername_fail_userNotFound() { + @DisplayName("사용자 조회 실패: DB에 사용자가 없으면 NotFoundException을 던진다") + void loadUserByUsername_whenUserNotExists_throwsException() { // given - String username = "notfound@example.com"; + String username = "nonexistent"; given(memberRepository.findByUsername(username)).willReturn(Optional.empty()); // when & then - assertThrows(NotFoundException.class, () -> { - customUserDetailsService.loadUserByUsername(username); - }); + assertThatThrownBy(() -> customUserDetailsService.loadUserByUsername(username)) + .isInstanceOf(NotFoundException.class); } } diff --git a/account-service/src/test/java/com/synapse/account_service/service/MemberRegistrationServiceTest.java b/account-service/src/test/java/com/synapse/account_service/service/MemberRegistrationServiceTest.java new file mode 100644 index 0000000..2efcc72 --- /dev/null +++ b/account-service/src/test/java/com/synapse/account_service/service/MemberRegistrationServiceTest.java @@ -0,0 +1,114 @@ +package com.synapse.account_service.service; + +import com.synapse.account_service.domain.Attributes; +import com.synapse.account_service.domain.Member; +import com.synapse.account_service.domain.ProviderUser; +import com.synapse.account_service.domain.socials.GoogleUser; +import com.synapse.account_service.repository.MemberRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; + +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class MemberRegistrationServiceTest { + + @InjectMocks + private MemberRegistrationService memberRegistrationService; + + @Mock + private MemberRepository memberRepository; + + private final String provider = "google"; + private final String providerId = "123456789"; + private final String email = "test@example.com"; + private final String username = "testuser"; + + private ProviderUser createMockProviderUser() { + Map attributes = Map.of("sub", providerId, "email", email, "name", username); + var oAuth2User = new DefaultOAuth2User(Collections.emptyList(), attributes, "sub"); + return new GoogleUser(new Attributes(attributes), oAuth2User, null); // ClientRegistration은 이 테스트에서 사용되지 않음 + } + + @Test + @DisplayName("시나리오 1: 신규 사용자일 경우, 새 Member를 생성하고 저장한다") + void registerOauthUser_whenNewUser_createsAndSavesMember() { + // given + ProviderUser providerUser = createMockProviderUser(); + given(memberRepository.findBySocialIdOrEmailOrUsername(provider, providerId, email, username)) + .willReturn(Optional.empty()); + + // registerOauthUser 내부에서 save가 호출될 때 반환할 Member 객체를 준비 + Member savedMember = Member.builder().id(UUID.randomUUID()).email(email).username(username).provider(provider).registrationId(providerId).build(); + given(memberRepository.save(any(Member.class))).willReturn(savedMember); + + // when + Member result = memberRegistrationService.registerOauthUser(provider, providerUser); + + // then + verify(memberRepository, times(1)).save(any(Member.class)); + assertThat(result.getEmail()).isEqualTo(email); + assertThat(result.getProvider()).isEqualTo(provider); + assertThat(result.getRegistrationId()).isEqualTo(providerId); + } + + @Test + @DisplayName("시나리오 2: 이메일/이름이 일치하는 기존 사용자일 경우, 소셜 정보를 연동한다") + void registerOauthUser_whenExistingUser_linksSocialAccount() { + // given + ProviderUser providerUser = createMockProviderUser(); + + // 소셜 정보는 없고, 이메일만 있는 기존 Member Mock + Member existingMember = mock(Member.class); + given(memberRepository.findBySocialIdOrEmailOrUsername(provider, providerId, email, username)) + .willReturn(Optional.of(existingMember)); + + // getProvider()가 null을 반환하도록 설정하여 연동 조건 만족 + given(existingMember.getProvider()).willReturn(null); + + // when + Member result = memberRegistrationService.registerOauthUser(provider, providerUser); + + // then + verify(memberRepository, never()).save(any(Member.class)); // save는 호출되면 안 됨 + verify(existingMember, times(1)).linkSocialAccount(provider, providerId); // linkSocialAccount는 호출되어야 함 + assertThat(result).isEqualTo(existingMember); + } + + @Test + @DisplayName("시나리오 3: 소셜 정보가 이미 등록된 사용자일 경우, 아무 작업 없이 반환한다") + void registerOauthUser_whenSocialAccountExists_returnsMember() { + // given + ProviderUser providerUser = createMockProviderUser(); + + // 소셜 정보가 이미 있는 기존 Member Mock + Member existingMember = mock(Member.class); + given(memberRepository.findBySocialIdOrEmailOrUsername(provider, providerId, email, username)) + .willReturn(Optional.of(existingMember)); + + // getProvider()와 getRegistrationId()가 모두 값을 반환하도록 설정하여 연동 조건 불만족 + given(existingMember.getProvider()).willReturn(provider); + given(existingMember.getRegistrationId()).willReturn(providerId); + + // when + Member result = memberRegistrationService.registerOauthUser(provider, providerUser); + + // then + verify(memberRepository, never()).save(any(Member.class)); + verify(existingMember, never()).linkSocialAccount(anyString(), anyString()); + assertThat(result).isEqualTo(existingMember); + } +} \ No newline at end of file