diff --git a/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClient.java b/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClient.java index 41c4eec4e..48009cc82 100644 --- a/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClient.java +++ b/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClient.java @@ -6,7 +6,10 @@ import com.example.solidconnection.auth.client.config.AppleOAuthClientProperties; import com.example.solidconnection.auth.dto.oauth.AppleTokenDto; import com.example.solidconnection.auth.dto.oauth.AppleUserInfoDto; +import com.example.solidconnection.auth.dto.oauth.OAuthUserInfoDto; +import com.example.solidconnection.auth.service.oauth.OAuthClient; import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.siteuser.domain.AuthType; import io.jsonwebtoken.Jwts; import java.security.PublicKey; import java.util.Objects; @@ -27,20 +30,26 @@ * */ @Component @RequiredArgsConstructor -public class AppleOAuthClient { +public class AppleOAuthClient implements OAuthClient { private final RestTemplate restTemplate; private final AppleOAuthClientProperties properties; private final AppleOAuthClientSecretProvider clientSecretProvider; private final ApplePublicKeyProvider publicKeyProvider; - public AppleUserInfoDto processOAuth(String code) { + @Override + public AuthType getAuthType() { + return AuthType.APPLE; + } + + @Override + public OAuthUserInfoDto getUserInfo(String code) { String idToken = requestIdToken(code); PublicKey applePublicKey = publicKeyProvider.getApplePublicKey(idToken); return new AppleUserInfoDto(parseEmailFromToken(applePublicKey, idToken)); } - public String requestIdToken(String code) { + private String requestIdToken(String code) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); MultiValueMap formData = buildFormData(code); diff --git a/src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java b/src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java index eb30746b4..a25743f7d 100644 --- a/src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java +++ b/src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java @@ -7,7 +7,10 @@ import com.example.solidconnection.auth.client.config.KakaoOAuthClientProperties; import com.example.solidconnection.auth.dto.oauth.KakaoTokenDto; import com.example.solidconnection.auth.dto.oauth.KakaoUserInfoDto; +import com.example.solidconnection.auth.dto.oauth.OAuthUserInfoDto; +import com.example.solidconnection.auth.service.oauth.OAuthClient; import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.siteuser.domain.AuthType; import java.util.Objects; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpEntity; @@ -27,12 +30,18 @@ * */ @Component @RequiredArgsConstructor -public class KakaoOAuthClient { +public class KakaoOAuthClient implements OAuthClient { private final RestTemplate restTemplate; private final KakaoOAuthClientProperties kakaoOAuthClientProperties; - public KakaoUserInfoDto getUserInfo(String code) { + @Override + public AuthType getAuthType() { + return AuthType.KAKAO; + } + + @Override + public OAuthUserInfoDto getUserInfo(String code) { String kakaoAccessToken = getKakaoAccessToken(code); return getKakaoUserInfo(kakaoAccessToken); } diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index 2e581ab71..085343dfd 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -15,8 +15,7 @@ import com.example.solidconnection.auth.service.EmailSignInService; import com.example.solidconnection.auth.service.EmailSignUpService; import com.example.solidconnection.auth.service.EmailSignUpTokenProvider; -import com.example.solidconnection.auth.service.oauth.AppleOAuthService; -import com.example.solidconnection.auth.service.oauth.KakaoOAuthService; +import com.example.solidconnection.auth.service.oauth.OAuthService; import com.example.solidconnection.auth.service.oauth.OAuthSignUpService; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.common.exception.ErrorCode; @@ -40,8 +39,7 @@ public class AuthController { private final AuthService authService; private final OAuthSignUpService oAuthSignUpService; - private final AppleOAuthService appleOAuthService; - private final KakaoOAuthService kakaoOAuthService; + private final OAuthService oAuthService; private final EmailSignInService emailSignInService; private final EmailSignUpService emailSignUpService; private final EmailSignUpTokenProvider emailSignUpTokenProvider; @@ -53,7 +51,7 @@ public ResponseEntity processAppleOAuth( @Valid @RequestBody OAuthCodeRequest oAuthCodeRequest, HttpServletResponse httpServletResponse ) { - OAuthResponse oAuthResponse = appleOAuthService.processOAuth(oAuthCodeRequest); + OAuthResponse oAuthResponse = oAuthService.processOAuth(AuthType.APPLE, oAuthCodeRequest); if (oAuthResponse instanceof OAuthSignInResponse signInResponse) { refreshTokenCookieManager.setCookie(httpServletResponse, signInResponse.refreshToken()); } @@ -65,7 +63,7 @@ public ResponseEntity processKakaoOAuth( @Valid @RequestBody OAuthCodeRequest oAuthCodeRequest, HttpServletResponse httpServletResponse ) { - OAuthResponse oAuthResponse = kakaoOAuthService.processOAuth(oAuthCodeRequest); + OAuthResponse oAuthResponse = oAuthService.processOAuth(AuthType.KAKAO, oAuthCodeRequest); if (oAuthResponse instanceof OAuthSignInResponse signInResponse) { refreshTokenCookieManager.setCookie(httpServletResponse, signInResponse.refreshToken()); } diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/AppleOAuthService.java b/src/main/java/com/example/solidconnection/auth/service/oauth/AppleOAuthService.java deleted file mode 100644 index 2605ad89f..000000000 --- a/src/main/java/com/example/solidconnection/auth/service/oauth/AppleOAuthService.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.solidconnection.auth.service.oauth; - -import com.example.solidconnection.auth.client.AppleOAuthClient; -import com.example.solidconnection.auth.dto.oauth.OAuthUserInfoDto; -import com.example.solidconnection.auth.service.SignInService; -import com.example.solidconnection.siteuser.domain.AuthType; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import org.springframework.stereotype.Service; - -@Service -public class AppleOAuthService extends OAuthService { - - private final AppleOAuthClient appleOAuthClient; - - public AppleOAuthService(OAuthSignUpTokenProvider OAuthSignUpTokenProvider, SiteUserRepository siteUserRepository, - AppleOAuthClient appleOAuthClient, SignInService signInService) { - super(OAuthSignUpTokenProvider, siteUserRepository, signInService); - this.appleOAuthClient = appleOAuthClient; - } - - @Override - protected OAuthUserInfoDto getOAuthUserInfo(String code) { - return appleOAuthClient.processOAuth(code); - } - - @Override - protected AuthType getAuthType() { - return AuthType.APPLE; - } -} diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/KakaoOAuthService.java b/src/main/java/com/example/solidconnection/auth/service/oauth/KakaoOAuthService.java deleted file mode 100644 index c2202ab2a..000000000 --- a/src/main/java/com/example/solidconnection/auth/service/oauth/KakaoOAuthService.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.solidconnection.auth.service.oauth; - -import com.example.solidconnection.auth.client.KakaoOAuthClient; -import com.example.solidconnection.auth.dto.oauth.OAuthUserInfoDto; -import com.example.solidconnection.auth.service.SignInService; -import com.example.solidconnection.siteuser.domain.AuthType; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import org.springframework.stereotype.Service; - -@Service -public class KakaoOAuthService extends OAuthService { - - private final KakaoOAuthClient kakaoOAuthClient; - - public KakaoOAuthService(OAuthSignUpTokenProvider OAuthSignUpTokenProvider, SiteUserRepository siteUserRepository, - KakaoOAuthClient kakaoOAuthClient, SignInService signInService) { - super(OAuthSignUpTokenProvider, siteUserRepository, signInService); - this.kakaoOAuthClient = kakaoOAuthClient; - } - - @Override - protected OAuthUserInfoDto getOAuthUserInfo(String code) { - return kakaoOAuthClient.getUserInfo(code); - } - - @Override - protected AuthType getAuthType() { - return AuthType.KAKAO; - } -} diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthClient.java b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthClient.java new file mode 100644 index 000000000..a62872f0c --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthClient.java @@ -0,0 +1,11 @@ +package com.example.solidconnection.auth.service.oauth; + +import com.example.solidconnection.auth.dto.oauth.OAuthUserInfoDto; +import com.example.solidconnection.siteuser.domain.AuthType; + +public interface OAuthClient { + + OAuthUserInfoDto getUserInfo(String code); + + AuthType getAuthType(); +} diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthClientMap.java b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthClientMap.java new file mode 100644 index 000000000..45e510136 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthClientMap.java @@ -0,0 +1,32 @@ +package com.example.solidconnection.auth.service.oauth; + +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.common.exception.ErrorCode; +import com.example.solidconnection.siteuser.domain.AuthType; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.springframework.stereotype.Component; + +@Component +public class OAuthClientMap { + + private final Map oauthClientMap; + + public OAuthClientMap(List oAuthClientList) { + this.oauthClientMap = oAuthClientList.stream() + .collect(Collectors.toMap(OAuthClient::getAuthType, Function.identity())); + } + + public OAuthClient getOAuthClient(AuthType authType) { + OAuthClient oauthClient = oauthClientMap.get(authType); + if (oauthClient == null) { + throw new CustomException( + ErrorCode.NOT_DEFINED_ERROR, + "처리할 수 있는 OAuthClient가 없습니다. authType: " + authType.name() + ); + } + return oauthClient; + } +} diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java index d02377341..bb34a3739 100644 --- a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java @@ -1,6 +1,5 @@ package com.example.solidconnection.auth.service.oauth; - import com.example.solidconnection.auth.dto.SignInResponse; import com.example.solidconnection.auth.dto.oauth.OAuthCodeRequest; import com.example.solidconnection.auth.dto.oauth.OAuthResponse; @@ -12,6 +11,8 @@ import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; /* @@ -19,43 +20,36 @@ * 기존 회원 : 로그인한다. * 신규 회원 : 회원가입할 때 필요한 정보를 제공한다. * */ -public abstract class OAuthService { +@Service +@RequiredArgsConstructor +public class OAuthService { private final OAuthSignUpTokenProvider OAuthSignUpTokenProvider; private final SignInService signInService; private final SiteUserRepository siteUserRepository; - - protected OAuthService(OAuthSignUpTokenProvider OAuthSignUpTokenProvider, SiteUserRepository siteUserRepository, SignInService signInService) { - this.OAuthSignUpTokenProvider = OAuthSignUpTokenProvider; - this.siteUserRepository = siteUserRepository; - this.signInService = signInService; - } + private final OAuthClientMap oauthClientMap; @Transactional - public OAuthResponse processOAuth(OAuthCodeRequest oauthCodeRequest) { - OAuthUserInfoDto userInfoDto = getOAuthUserInfo(oauthCodeRequest.code()); - String email = userInfoDto.getEmail(); - Optional optionalSiteUser = siteUserRepository.findByEmailAndAuthType(email, getAuthType()); + public OAuthResponse processOAuth(AuthType authType, OAuthCodeRequest codeRequest) { + OAuthClient oauthClient = oauthClientMap.getOAuthClient(authType); + OAuthUserInfoDto userInfo = oauthClient.getUserInfo(codeRequest.code()); + Optional optionalSiteUser = siteUserRepository.findByEmailAndAuthType(userInfo.getEmail(), authType); if (optionalSiteUser.isPresent()) { SiteUser siteUser = optionalSiteUser.get(); return getSignInResponse(siteUser); } - return getSignUpPrepareResponse(userInfoDto); + return getSignUpPrepareResponse(userInfo, authType); } - protected final OAuthSignInResponse getSignInResponse(SiteUser siteUser) { + private OAuthSignInResponse getSignInResponse(SiteUser siteUser) { SignInResponse signInResponse = signInService.signIn(siteUser); return new OAuthSignInResponse(true, signInResponse.accessToken(), signInResponse.refreshToken()); } - protected final SignUpPrepareResponse getSignUpPrepareResponse(OAuthUserInfoDto userInfoDto) { - String signUpToken = OAuthSignUpTokenProvider.generateAndSaveSignUpToken(userInfoDto.getEmail(), getAuthType()); + private SignUpPrepareResponse getSignUpPrepareResponse(OAuthUserInfoDto userInfoDto, AuthType authType) { + String signUpToken = OAuthSignUpTokenProvider.generateAndSaveSignUpToken(userInfoDto.getEmail(), authType); return SignUpPrepareResponse.of(userInfoDto, signUpToken); } - - protected abstract OAuthUserInfoDto getOAuthUserInfo(String code); - - protected abstract AuthType getAuthType(); } diff --git a/src/test/java/com/example/solidconnection/auth/service/oauth/OAuthClientMapTest.java b/src/test/java/com/example/solidconnection/auth/service/oauth/OAuthClientMapTest.java new file mode 100644 index 000000000..da4ce868b --- /dev/null +++ b/src/test/java/com/example/solidconnection/auth/service/oauth/OAuthClientMapTest.java @@ -0,0 +1,53 @@ +package com.example.solidconnection.auth.service.oauth; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.common.exception.ErrorCode; +import com.example.solidconnection.siteuser.domain.AuthType; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("OAuthClientMap 테스트") +class OAuthClientMapTest { + + @Test + void AuthType에_해당하는_Client를_반환한다() { + // given + OAuthClient appleClient = mock(OAuthClient.class); + OAuthClient kakaoClient = mock(OAuthClient.class); + given(appleClient.getAuthType()).willReturn(AuthType.APPLE); + given(kakaoClient.getAuthType()).willReturn(AuthType.KAKAO); + + OAuthClientMap oAuthClientMap = new OAuthClientMap( + List.of(appleClient, kakaoClient) + ); + + // when & then + assertAll( + () -> assertThat(oAuthClientMap.getOAuthClient(AuthType.APPLE)).isEqualTo(appleClient), + () -> assertThat(oAuthClientMap.getOAuthClient(AuthType.KAKAO)).isEqualTo(kakaoClient) + ); + } + + @Test + void AuthType에_매칭되는_Client가_없으면_예외가_발생한다() { + // given + OAuthClient appleClient = mock(OAuthClient.class); + given(appleClient.getAuthType()).willReturn(AuthType.APPLE); + + OAuthClientMap oAuthClientMap = new OAuthClientMap( + List.of(appleClient) + ); + + // when & then + assertThatCode(() -> oAuthClientMap.getOAuthClient(AuthType.KAKAO)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(ErrorCode.NOT_DEFINED_ERROR.getMessage()); + } +} diff --git a/src/test/java/com/example/solidconnection/auth/service/oauth/OAuthServiceTest.java b/src/test/java/com/example/solidconnection/auth/service/oauth/OAuthServiceTest.java new file mode 100644 index 000000000..427701399 --- /dev/null +++ b/src/test/java/com/example/solidconnection/auth/service/oauth/OAuthServiceTest.java @@ -0,0 +1,88 @@ +package com.example.solidconnection.auth.service.oauth; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +import com.example.solidconnection.auth.dto.oauth.OAuthCodeRequest; +import com.example.solidconnection.auth.dto.oauth.OAuthResponse; +import com.example.solidconnection.auth.dto.oauth.OAuthSignInResponse; +import com.example.solidconnection.auth.dto.oauth.OAuthUserInfoDto; +import com.example.solidconnection.auth.dto.oauth.SignUpPrepareResponse; +import com.example.solidconnection.siteuser.domain.AuthType; +import com.example.solidconnection.siteuser.fixture.SiteUserFixture; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; + +@DisplayName("OAuth 서비스 테스트") +@TestContainerSpringBootTest +class OAuthServiceTest { + + @Autowired + private OAuthService oAuthService; + + @Autowired + private SiteUserFixture siteUserFixture; + + @MockBean + private OAuthClientMap oauthClientMap; + + private final AuthType authType = AuthType.KAKAO; + private final String oauthCode = "code"; + private final String email = "test@test.com"; + private final String profileImageUrl = "profile.jpg"; + private final String nickname = "testUser"; + + @BeforeEach + void setUp() { // 실제 client 호출하지 않도록 mocking + OAuthUserInfoDto oauthUserInfoDto = mock(OAuthUserInfoDto.class); + given(oauthUserInfoDto.getEmail()).willReturn(email); + given(oauthUserInfoDto.getProfileImageUrl()).willReturn(profileImageUrl); + given(oauthUserInfoDto.getNickname()).willReturn(nickname); + + OAuthClient oAuthClient = mock(OAuthClient.class); + given(oauthClientMap.getOAuthClient(authType)).willReturn(oAuthClient); + given(oAuthClient.getAuthType()).willReturn(authType); + given(oAuthClient.getUserInfo(oauthCode)).willReturn(oauthUserInfoDto); + } + + @Test + void 기존_회원이라면_로그인한다() { + // given + siteUserFixture.사용자(email, authType); + + // when + OAuthResponse response = oAuthService.processOAuth(authType, new OAuthCodeRequest(oauthCode)); + + // then + assertThat(response).isInstanceOf(OAuthSignInResponse.class); + OAuthSignInResponse signInResponse = (OAuthSignInResponse) response; + assertAll( + () -> assertThat(signInResponse.isRegistered()).isTrue(), + () -> assertThat(signInResponse.accessToken()).isNotBlank(), + () -> assertThat(signInResponse.refreshToken()).isNotBlank() + ); + } + + @Test + void 신규_회원이라면_회원가입에_필요한_정보를_응답한다() { + // when + OAuthResponse response = oAuthService.processOAuth(authType, new OAuthCodeRequest(oauthCode)); + + // then + assertThat(response).isInstanceOf(SignUpPrepareResponse.class); + SignUpPrepareResponse signUpPrepareResponse = (SignUpPrepareResponse) response; + assertAll( + () -> assertThat(signUpPrepareResponse.isRegistered()).isFalse(), + () -> assertThat(signUpPrepareResponse.signUpToken()).isNotBlank(), + () -> assertThat(signUpPrepareResponse.email()).isEqualTo(email), + () -> assertThat(signUpPrepareResponse.profileImageUrl()).isEqualTo(profileImageUrl), + () -> assertThat(signUpPrepareResponse.nickname()).isEqualTo(nickname) + ); + } +}