diff --git a/src/main/java/com/evenly/took/feature/auth/client/google/GoogleAuthCodeRequestUrlProvider.java b/src/main/java/com/evenly/took/feature/auth/client/google/GoogleAuthCodeRequestUrlProvider.java new file mode 100644 index 0000000..f14ab5b --- /dev/null +++ b/src/main/java/com/evenly/took/feature/auth/client/google/GoogleAuthCodeRequestUrlProvider.java @@ -0,0 +1,36 @@ +package com.evenly.took.feature.auth.client.google; + +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; + +import com.evenly.took.feature.auth.client.AuthCodeRequestUrlProvider; +import com.evenly.took.feature.auth.domain.OAuthType; +import com.evenly.took.global.config.properties.auth.GoogleProperties; +import com.evenly.took.global.config.properties.auth.GoogleUrlProperties; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class GoogleAuthCodeRequestUrlProvider implements AuthCodeRequestUrlProvider { + + private final GoogleProperties googleProperties; + private final GoogleUrlProperties googleUrlProperties; + + @Override + public OAuthType supportType() { + return OAuthType.GOOGLE; + } + + @Override + public String provide() { + return UriComponentsBuilder.fromUriString(googleUrlProperties.authorizationUri()) + .queryParam("client_id", googleProperties.clientId()) + .queryParam("redirect_uri", googleProperties.redirectUri()) + .queryParam("response_type", "code") + .queryParam("scope", googleProperties.scope()) + .queryParam("access_type", "offline") + .build(true) + .toUriString(); + } +} diff --git a/src/main/java/com/evenly/took/feature/auth/client/google/GoogleTokenProvider.java b/src/main/java/com/evenly/took/feature/auth/client/google/GoogleTokenProvider.java new file mode 100644 index 0000000..13eb891 --- /dev/null +++ b/src/main/java/com/evenly/took/feature/auth/client/google/GoogleTokenProvider.java @@ -0,0 +1,46 @@ +package com.evenly.took.feature.auth.client.google; + +import java.nio.charset.StandardCharsets; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +import com.evenly.took.feature.auth.client.google.dto.request.GoogleTokenRequest; +import com.evenly.took.feature.auth.client.google.dto.response.GoogleTokenResponse; +import com.evenly.took.feature.auth.client.google.error.GoogleTokenProviderErrorHandler; +import com.evenly.took.global.config.properties.auth.GoogleProperties; +import com.evenly.took.global.config.properties.auth.GoogleUrlProperties; + +@Component +public class GoogleTokenProvider { + + private final RestClient restClient; + private final GoogleProperties googleProperties; + private final GoogleUrlProperties googleUrlProperties; + + public GoogleTokenProvider(GoogleProperties googleProperties, + GoogleUrlProperties googleUrlProperties, + RestClient.Builder restClientBuilder, + GoogleTokenProviderErrorHandler errorHandler) { + + this.googleProperties = googleProperties; + this.googleUrlProperties = googleUrlProperties; + this.restClient = restClientBuilder + .defaultStatusHandler(errorHandler) + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE, + StandardCharsets.UTF_8.name()) + .build(); + } + + public GoogleTokenResponse fetchAccessToken(String authCode) { + GoogleTokenRequest request = GoogleTokenRequest.of(googleProperties, authCode); + return restClient.post() + .uri(googleUrlProperties.tokenUri()) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .body(request.toMultiValueMap()) + .retrieve() + .body(GoogleTokenResponse.class); + } +} diff --git a/src/main/java/com/evenly/took/feature/auth/client/google/GoogleUserClient.java b/src/main/java/com/evenly/took/feature/auth/client/google/GoogleUserClient.java new file mode 100644 index 0000000..77c0cc3 --- /dev/null +++ b/src/main/java/com/evenly/took/feature/auth/client/google/GoogleUserClient.java @@ -0,0 +1,52 @@ +package com.evenly.took.feature.auth.client.google; + +import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; + +import com.evenly.took.feature.auth.client.UserClient; +import com.evenly.took.feature.auth.client.google.dto.response.GoogleTokenResponse; +import com.evenly.took.feature.auth.client.google.dto.response.GoogleUserInfoResponse; +import com.evenly.took.feature.auth.domain.OAuthIdentifier; +import com.evenly.took.feature.auth.domain.OAuthType; +import com.evenly.took.feature.auth.exception.AuthErrorCode; +import com.evenly.took.feature.common.exception.TookException; +import com.evenly.took.feature.user.domain.User; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class GoogleUserClient implements UserClient { + + private final GoogleTokenProvider googleTokenProvider; + private final GoogleUserInfoProvider googleUserInfoProvider; + + @Override + public OAuthType supportType() { + return OAuthType.GOOGLE; + } + + @Override + public User fetch(String authCode) { + try { + GoogleTokenResponse tokenResponse = googleTokenProvider.fetchAccessToken(authCode); + GoogleUserInfoResponse userInfoResponse = googleUserInfoProvider.fetchUserInfo(tokenResponse.accessToken()); + + return generateUser(userInfoResponse.sub(), userInfoResponse.name()); + } catch (HttpClientErrorException ex) { + throw new TookException(AuthErrorCode.INVALID_GOOGLE_CONNECTION); + } + } + + private User generateUser(String oauthId, String name) { + OAuthIdentifier oauthIdentifier = OAuthIdentifier.builder() + .oauthId(oauthId) + .oauthType(OAuthType.GOOGLE) + .build(); + + return User.builder() + .oauthIdentifier(oauthIdentifier) + .name(name) + .build(); + } +} diff --git a/src/main/java/com/evenly/took/feature/auth/client/google/GoogleUserInfoProvider.java b/src/main/java/com/evenly/took/feature/auth/client/google/GoogleUserInfoProvider.java new file mode 100644 index 0000000..6ee40e1 --- /dev/null +++ b/src/main/java/com/evenly/took/feature/auth/client/google/GoogleUserInfoProvider.java @@ -0,0 +1,39 @@ +package com.evenly.took.feature.auth.client.google; + +import java.nio.charset.StandardCharsets; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +import com.evenly.took.feature.auth.client.google.dto.response.GoogleUserInfoResponse; +import com.evenly.took.feature.auth.client.google.error.GoogleUserInfoProviderErrorHandler; +import com.evenly.took.global.config.properties.auth.GoogleUrlProperties; + +@Component +public class GoogleUserInfoProvider { + + private final RestClient restClient; + private final GoogleUrlProperties googleUrlProperties; + + public GoogleUserInfoProvider(GoogleUrlProperties googleUrlProperties, + RestClient.Builder restClientBuilder, + GoogleUserInfoProviderErrorHandler errorHandler) { + + this.googleUrlProperties = googleUrlProperties; + this.restClient = restClientBuilder + .defaultStatusHandler(errorHandler) + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE, + StandardCharsets.UTF_8.name()) + .build(); + } + + public GoogleUserInfoResponse fetchUserInfo(String accessToken) { + return restClient.get() + .uri(googleUrlProperties.userInfoUrl()) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .retrieve() + .body(GoogleUserInfoResponse.class); + } +} diff --git a/src/main/java/com/evenly/took/feature/auth/client/google/dto/request/GoogleTokenRequest.java b/src/main/java/com/evenly/took/feature/auth/client/google/dto/request/GoogleTokenRequest.java new file mode 100644 index 0000000..cf2f772 --- /dev/null +++ b/src/main/java/com/evenly/took/feature/auth/client/google/dto/request/GoogleTokenRequest.java @@ -0,0 +1,33 @@ +package com.evenly.took.feature.auth.client.google.dto.request; + +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import com.evenly.took.global.config.properties.auth.GoogleProperties; + +public record GoogleTokenRequest( + String grantType, + String clientId, + String redirectUri, + String code, + String clientSecret +) { + + public static GoogleTokenRequest of(GoogleProperties googleProperties, String code) { + return new GoogleTokenRequest("authorization_code", + googleProperties.clientId(), + googleProperties.redirectUri(), + code, + googleProperties.clientSecret()); + } + + public MultiValueMap toMultiValueMap() { + MultiValueMap map = new LinkedMultiValueMap<>(); + map.add("grant_type", grantType); + map.add("client_id", clientId); + map.add("redirect_uri", redirectUri); + map.add("code", code); + map.add("client_secret", clientSecret); + return map; + } +} diff --git a/src/main/java/com/evenly/took/feature/auth/client/google/dto/response/GoogleTokenResponse.java b/src/main/java/com/evenly/took/feature/auth/client/google/dto/response/GoogleTokenResponse.java new file mode 100644 index 0000000..031b534 --- /dev/null +++ b/src/main/java/com/evenly/took/feature/auth/client/google/dto/response/GoogleTokenResponse.java @@ -0,0 +1,22 @@ +package com.evenly.took.feature.auth.client.google.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record GoogleTokenResponse( + + @JsonProperty("access_token") + String accessToken, + + @JsonProperty("expires_in") + Integer expiresIn, + + @JsonProperty("scope") + String scope, + + @JsonProperty("token_type") + String tokenType, + + @JsonProperty("id_token") + String idToken +) { +} diff --git a/src/main/java/com/evenly/took/feature/auth/client/google/dto/response/GoogleUserInfoResponse.java b/src/main/java/com/evenly/took/feature/auth/client/google/dto/response/GoogleUserInfoResponse.java new file mode 100644 index 0000000..08b54fb --- /dev/null +++ b/src/main/java/com/evenly/took/feature/auth/client/google/dto/response/GoogleUserInfoResponse.java @@ -0,0 +1,13 @@ +package com.evenly.took.feature.auth.client.google.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record GoogleUserInfoResponse( + + @JsonProperty("sub") + String sub, + + @JsonProperty("name") + String name +) { +} diff --git a/src/main/java/com/evenly/took/feature/auth/client/google/error/GoogleTokenProviderErrorHandler.java b/src/main/java/com/evenly/took/feature/auth/client/google/error/GoogleTokenProviderErrorHandler.java new file mode 100644 index 0000000..c0a65d6 --- /dev/null +++ b/src/main/java/com/evenly/took/feature/auth/client/google/error/GoogleTokenProviderErrorHandler.java @@ -0,0 +1,38 @@ +package com.evenly.took.feature.auth.client.google.error; + +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; + +import org.springframework.http.HttpMethod; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.stereotype.Component; +import org.springframework.util.StreamUtils; +import org.springframework.web.client.ResponseErrorHandler; + +import com.evenly.took.feature.auth.exception.AuthErrorCode; +import com.evenly.took.feature.common.exception.TookException; + +import lombok.extern.slf4j.Slf4j; + +@Component +@Slf4j +public class GoogleTokenProviderErrorHandler implements ResponseErrorHandler { + + @Override + public boolean hasError(ClientHttpResponse response) throws IOException { + return response.getStatusCode().isError(); + } + + @Override + public void handleError(URI uri, HttpMethod method, ClientHttpResponse response) throws IOException { + String responseBody = StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8); + + if (response.getStatusCode().is4xxClientError()) { + throw new TookException(AuthErrorCode.INVALID_GOOGLE_TOKEN_REQUEST); + } + + log.error("Google 승인토큰 오류: {}", responseBody); + throw new TookException(AuthErrorCode.INVALID_GOOGLE_CONNECTION); + } +} diff --git a/src/main/java/com/evenly/took/feature/auth/client/google/error/GoogleUserInfoProviderErrorHandler.java b/src/main/java/com/evenly/took/feature/auth/client/google/error/GoogleUserInfoProviderErrorHandler.java new file mode 100644 index 0000000..7b871eb --- /dev/null +++ b/src/main/java/com/evenly/took/feature/auth/client/google/error/GoogleUserInfoProviderErrorHandler.java @@ -0,0 +1,42 @@ +package com.evenly.took.feature.auth.client.google.error; + +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; + +import org.springframework.http.HttpMethod; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.stereotype.Component; +import org.springframework.util.StreamUtils; +import org.springframework.web.client.ResponseErrorHandler; + +import com.evenly.took.feature.auth.exception.AuthErrorCode; +import com.evenly.took.feature.common.exception.TookException; + +import lombok.extern.slf4j.Slf4j; + +@Component +@Slf4j +public class GoogleUserInfoProviderErrorHandler implements ResponseErrorHandler { + + @Override + public boolean hasError(ClientHttpResponse response) throws IOException { + return response.getStatusCode().isError(); + } + + @Override + public void handleError(URI uri, HttpMethod method, ClientHttpResponse response) throws IOException { + String responseBody = StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8); + + if (response.getStatusCode().is4xxClientError()) { + throw new TookException(AuthErrorCode.INVALID_GOOGLE_USER_REQUEST); + } + + if (response.getStatusCode().is5xxServerError()) { + throw new TookException(AuthErrorCode.INVALID_GOOGLE_SERVER_ERROR); + } + + log.error("Google 사용자정보 알 수 없는 오류: {}", responseBody); + throw new TookException(AuthErrorCode.INVALID_GOOGLE_CONNECTION); + } +} diff --git a/src/main/java/com/evenly/took/feature/auth/exception/AuthErrorCode.java b/src/main/java/com/evenly/took/feature/auth/exception/AuthErrorCode.java index 1f806d3..297a4cd 100644 --- a/src/main/java/com/evenly/took/feature/auth/exception/AuthErrorCode.java +++ b/src/main/java/com/evenly/took/feature/auth/exception/AuthErrorCode.java @@ -15,6 +15,13 @@ public enum AuthErrorCode implements ErrorCode { JWT_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "JWT를 찾을 수 없습니다."), EXPIRED_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "refresh token이 만료되었습니다."), INVALID_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "access token이 유효하지 않습니다."), + INVALID_GOOGLE_TOKEN_REQUEST(HttpStatus.BAD_REQUEST, "Google 승인코드 요청 중 잘못된 요청으로 오류가 발생했습니다."), + INVALID_GOOGLE_USER_REQUEST(HttpStatus.BAD_REQUEST, + "Google OAuth V2 정보 요청 중 잘못된 요청으로 오류가 발생했습니다."), + INVALID_GOOGLE_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, + "Google OAuth V2 정보 요청 중 만료된 토큰 또는 승인코드로 오류가 발생했습니다."), + INVALID_GOOGLE_CONNECTION(HttpStatus.INTERNAL_SERVER_ERROR, + "Google OAuth 통신 오류: 구글 로그인 서버와의 연결 과정 중 문제가 발생했습니다."), KAKAO_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "카카오 로그인 서버와의 연결 과정에서 문제가 발생하였습니다."), KAKAO_INVALID_APP_INFO(HttpStatus.INTERNAL_SERVER_ERROR, "카카오 애플리케이션 정보가 유효하지 않습니다."), KAKAO_INVALID_AUTH_CODE(HttpStatus.BAD_REQUEST, "카카오 인증 코드가 유효하지 않습니다."), diff --git a/src/main/java/com/evenly/took/global/config/properties/auth/GoogleProperties.java b/src/main/java/com/evenly/took/global/config/properties/auth/GoogleProperties.java new file mode 100644 index 0000000..3fb2abe --- /dev/null +++ b/src/main/java/com/evenly/took/global/config/properties/auth/GoogleProperties.java @@ -0,0 +1,12 @@ +package com.evenly.took.global.config.properties.auth; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "oauth.google") +public record GoogleProperties( + String redirectUri, + String clientId, + String clientSecret, + String scope +) { +} diff --git a/src/main/java/com/evenly/took/global/config/properties/auth/GoogleUrlProperties.java b/src/main/java/com/evenly/took/global/config/properties/auth/GoogleUrlProperties.java new file mode 100644 index 0000000..e1d2020 --- /dev/null +++ b/src/main/java/com/evenly/took/global/config/properties/auth/GoogleUrlProperties.java @@ -0,0 +1,11 @@ +package com.evenly.took.global.config.properties.auth; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "oauth.google.url") +public record GoogleUrlProperties( + String authorizationUri, + String tokenUri, + String userInfoUrl +) { +} diff --git a/src/main/java/com/evenly/took/global/config/security/WebSecurityConfig.java b/src/main/java/com/evenly/took/global/config/security/WebSecurityConfig.java index 1537c0d..20d2322 100644 --- a/src/main/java/com/evenly/took/global/config/security/WebSecurityConfig.java +++ b/src/main/java/com/evenly/took/global/config/security/WebSecurityConfig.java @@ -63,7 +63,7 @@ private void defaultFilterChain(HttpSecurity http) throws Exception { @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); - configuration.addAllowedOrigin("*"); + configuration.addAllowedOriginPattern("*"); configuration.addAllowedHeader("*"); configuration.addAllowedMethod("*"); configuration.setAllowCredentials(true); diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 9caec7b..7c0e61c 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -50,6 +50,15 @@ auth: refresh-token-expiration-time: ${REFRESH_TOKEN_EXPIRATION_TIME} oauth: + google: + redirect-uri: ${GOOGLE_OAUTH_REDIRECT_URI} + client-id: ${GOOGLE_OAUTH_CLIENT_ID} + client-secret: ${GOOGLE_OAUTH_CLIENT_SECRET} + scope: ${GOOGLE_OAUTH_SCOPE} + url: + authorization-uri: ${GOOGLE_AUTHORIZATION_URL} + token-uri: ${GOOGLE_OAUTH_TOKEN_URI} + user-info-url: ${GOOGLE_OAUTH_USER_INFO_URL} kakao: client-id: ${KAKAO_CLIENT_ID} redirect-uri: ${KAKAO_REDIRECT_URI} diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index fa51929..0b202a3 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -60,6 +60,15 @@ auth: refresh-token-expiration-time: ${REFRESH_TOKEN_EXPIRATION_TIME} oauth: + google: + redirect-uri: ${GOOGLE_OAUTH_REDIRECT_URI} + client-id: ${GOOGLE_OAUTH_CLIENT_ID} + client-secret: ${GOOGLE_OAUTH_CLIENT_SECRET} + scope: ${GOOGLE_OAUTH_SCOPE} + url: + authorization-uri: ${GOOGLE_AUTHORIZATION_URL} + token-uri: ${GOOGLE_OAUTH_TOKEN_URI} + user-info-url: ${GOOGLE_OAUTH_USER_INFO_URL} kakao: client-id: ${KAKAO_CLIENT_ID} redirect-uri: ${KAKAO_REDIRECT_URI} diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 1dff92e..aa849cd 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -50,6 +50,15 @@ auth: refresh-token-expiration-time: ${REFRESH_TOKEN_EXPIRATION_TIME} oauth: + google: + redirect-uri: ${GOOGLE_OAUTH_REDIRECT_URI} + client-id: ${GOOGLE_OAUTH_CLIENT_ID} + client-secret: ${GOOGLE_OAUTH_CLIENT_SECRET} + scope: ${GOOGLE_OAUTH_SCOPE} + url: + authorization-uri: ${GOOGLE_AUTHORIZATION_URL} + token-uri: ${GOOGLE_OAUTH_TOKEN_URI} + user-info-url: ${GOOGLE_OAUTH_USER_INFO_URL} kakao: client-id: ${KAKAO_CLIENT_ID} redirect-uri: ${KAKAO_REDIRECT_URI} diff --git a/src/test/java/com/evenly/took/feature/auth/client/google/GoogleTokenProviderTest.java b/src/test/java/com/evenly/took/feature/auth/client/google/GoogleTokenProviderTest.java new file mode 100644 index 0000000..5c5bb80 --- /dev/null +++ b/src/test/java/com/evenly/took/feature/auth/client/google/GoogleTokenProviderTest.java @@ -0,0 +1,85 @@ +package com.evenly.took.feature.auth.client.google; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClient; + +import com.evenly.took.feature.auth.client.google.dto.response.GoogleTokenResponse; +import com.evenly.took.feature.auth.client.google.error.GoogleTokenProviderErrorHandler; +import com.evenly.took.feature.auth.exception.AuthErrorCode; +import com.evenly.took.feature.common.exception.TookException; + +public class GoogleTokenProviderTest extends MockGoogleProviderTest { + + @Mock + private GoogleTokenProviderErrorHandler errorHandler; + + private GoogleTokenProvider googleTokenProvider; + + @BeforeEach + void setUpTokenProvider() { + googleTokenProvider = + new GoogleTokenProvider(googleProperties, googleUrlProperties, restClientBuilder, errorHandler); + } + + @Nested + class 성공_케이스 { + + @Test + void 유효한_인증코드_주어졌을때_토큰_반환() { + // given + String authCode = "validAuthCode"; + String tokenUri = "http://dummy-token-uri"; + when(googleUrlProperties.tokenUri()).thenReturn(tokenUri); + + GoogleTokenResponse expectedResponse = new GoogleTokenResponse( + "dummyAccessToken", 3600, "scope", "Bearer", "dummyIdToken" + ); + + RestClient.RequestBodySpec requestBodySpec = restClient.post().uri(tokenUri); + when(requestBodySpec.header(anyString(), (String[])any())).thenReturn(requestBodySpec); + lenient().when(requestBodySpec.body(any(MultiValueMap.class))).thenReturn(requestBodySpec); + + RestClient.ResponseSpec responseSpec = requestBodySpec.retrieve(); + when(responseSpec.body(eq(GoogleTokenResponse.class))).thenReturn(expectedResponse); + + // when + GoogleTokenResponse actualResponse = googleTokenProvider.fetchAccessToken(authCode); + + // then + assertThat(actualResponse).isNotNull(); + assertThat(actualResponse.accessToken()).isEqualTo("dummyAccessToken"); + } + } + + @Nested + class 실패_케이스 { + + @Test + void 토큰_조회시_예외발생() { + // given + String authCode = "invalidAuthCode"; + String tokenUri = "http://dummy-token-uri"; + when(googleUrlProperties.tokenUri()).thenReturn(tokenUri); + + RestClient.RequestBodySpec requestBodySpec = restClient.post().uri(tokenUri); + when(requestBodySpec.header(anyString(), (String[])any())).thenReturn(requestBodySpec); + lenient().when(requestBodySpec.body(any(MultiValueMap.class))).thenReturn(requestBodySpec); + + when(requestBodySpec.retrieve()) + .thenThrow(new TookException(AuthErrorCode.INVALID_GOOGLE_TOKEN_REQUEST)); + + // when, then + assertThatThrownBy(() -> googleTokenProvider.fetchAccessToken(authCode)) + .isInstanceOf(TookException.class) + .hasFieldOrPropertyWithValue("errorCode", AuthErrorCode.INVALID_GOOGLE_TOKEN_REQUEST); + } + } +} diff --git a/src/test/java/com/evenly/took/feature/auth/client/google/GoogleUserClientTest.java b/src/test/java/com/evenly/took/feature/auth/client/google/GoogleUserClientTest.java new file mode 100644 index 0000000..c07f008 --- /dev/null +++ b/src/test/java/com/evenly/took/feature/auth/client/google/GoogleUserClientTest.java @@ -0,0 +1,71 @@ +package com.evenly.took.feature.auth.client.google; + +import static org.assertj.core.api.AssertionsForClassTypes.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.http.HttpStatus; +import org.springframework.web.client.HttpClientErrorException; + +import com.evenly.took.feature.auth.client.google.dto.response.GoogleTokenResponse; +import com.evenly.took.feature.auth.client.google.dto.response.GoogleUserInfoResponse; +import com.evenly.took.feature.auth.domain.OAuthType; +import com.evenly.took.feature.auth.exception.AuthErrorCode; +import com.evenly.took.feature.common.exception.TookException; +import com.evenly.took.feature.user.domain.User; +import com.evenly.took.global.service.MockTest; + +public class GoogleUserClientTest extends MockTest { + + @Mock + private GoogleTokenProvider googleTokenProvider; + + @Mock + private GoogleUserInfoProvider googleUserInfoProvider; + + @InjectMocks + private GoogleUserClient googleUserClient; + + @Nested + class 성공_케이스 { + @Test + void 유효한_인증코드_주어졌을때_유저_반환() { + // given + String authCode = "validAuthCode"; + GoogleTokenResponse tokenResponse = new GoogleTokenResponse("dummyAccessToken", 3600, "scope", "Bearer", + "dummyIdToken"); + GoogleUserInfoResponse userInfoResponse = new GoogleUserInfoResponse("dummySub", "홍길동"); + + when(googleTokenProvider.fetchAccessToken(authCode)).thenReturn(tokenResponse); + when(googleUserInfoProvider.fetchUserInfo(tokenResponse.accessToken())).thenReturn(userInfoResponse); + + // when + User user = googleUserClient.fetch(authCode); + + // then + assertThat(user).isNotNull(); + assertThat(user.getName()).isEqualTo("홍길동"); + assertThat(user.getOauthIdentifier().getOauthId()).isEqualTo("dummySub"); + assertThat(user.getOauthIdentifier().getOauthType()).isEqualTo(OAuthType.GOOGLE); + } + } + + @Nested + class 실패_케이스 { + @Test + void 잘못된_인증코드_주어졌을때_예외_발생() { + // given + String authCode = "invalidAuthCode"; + when(googleTokenProvider.fetchAccessToken(authCode)) + .thenThrow(new HttpClientErrorException(HttpStatus.BAD_REQUEST)); + + // when, then + assertThatThrownBy(() -> googleUserClient.fetch(authCode)) + .isInstanceOf(TookException.class) + .hasFieldOrPropertyWithValue("errorCode", AuthErrorCode.INVALID_GOOGLE_CONNECTION); + } + } +} diff --git a/src/test/java/com/evenly/took/feature/auth/client/google/GoogleUserInfoProviderTest.java b/src/test/java/com/evenly/took/feature/auth/client/google/GoogleUserInfoProviderTest.java new file mode 100644 index 0000000..714f5e3 --- /dev/null +++ b/src/test/java/com/evenly/took/feature/auth/client/google/GoogleUserInfoProviderTest.java @@ -0,0 +1,80 @@ +package com.evenly.took.feature.auth.client.google; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.springframework.web.client.RestClient; + +import com.evenly.took.feature.auth.client.google.dto.response.GoogleUserInfoResponse; +import com.evenly.took.feature.auth.client.google.error.GoogleUserInfoProviderErrorHandler; +import com.evenly.took.feature.auth.exception.AuthErrorCode; +import com.evenly.took.feature.common.exception.TookException; + +public class GoogleUserInfoProviderTest extends MockGoogleProviderTest { + + @Mock + private GoogleUserInfoProviderErrorHandler errorHandler; + + private GoogleUserInfoProvider googleUserInfoProvider; + + @BeforeEach + void setUpUserInfoProvider() { + googleUserInfoProvider = new GoogleUserInfoProvider(googleUrlProperties, restClientBuilder, errorHandler); + } + + @Nested + class 성공_케이스 { + + @Test + void 유효한_액세스토큰_주어졌을때_사용자정보_반환() { + // given + String accessToken = "validAccessToken"; + String userInfoUrl = "http://dummy-userinfo-url"; + when(googleUrlProperties.userInfoUrl()).thenReturn(userInfoUrl); + + GoogleUserInfoResponse expectedResponse = new GoogleUserInfoResponse("dummySub", "홍길동"); + + RestClient.RequestHeadersSpec requestSpec = restClient.get().uri(userInfoUrl); + when(requestSpec.header("Authorization", "Bearer " + accessToken)).thenReturn(requestSpec); + RestClient.ResponseSpec responseSpec = requestSpec.retrieve(); + when(responseSpec.body(eq(GoogleUserInfoResponse.class))).thenReturn(expectedResponse); + + // when + GoogleUserInfoResponse actualResponse = googleUserInfoProvider.fetchUserInfo(accessToken); + + // then + assertThat(actualResponse).isNotNull(); + assertThat(actualResponse.sub()).isEqualTo("dummySub"); + assertThat(actualResponse.name()).isEqualTo("홍길동"); + } + } + + @Nested + class 실패_케이스 { + + @Test + void 사용자정보_조회시_예외발생() { + // given + String accessToken = "invalidAccessToken"; + String userInfoUrl = "http://dummy-userinfo-url"; + when(googleUrlProperties.userInfoUrl()).thenReturn(userInfoUrl); + + RestClient.RequestHeadersSpec requestSpec = restClient.get().uri(userInfoUrl); + when(requestSpec.header("Authorization", "Bearer " + accessToken)).thenReturn(requestSpec); + + RestClient.ResponseSpec responseSpec = requestSpec.retrieve(); + when(responseSpec.body(any(Class.class))) + .thenThrow(new TookException(AuthErrorCode.INVALID_GOOGLE_USER_REQUEST)); + + // when, then + assertThatThrownBy(() -> googleUserInfoProvider.fetchUserInfo(accessToken)) + .isInstanceOf(TookException.class) + .hasFieldOrPropertyWithValue("errorCode", AuthErrorCode.INVALID_GOOGLE_USER_REQUEST); + } + } +} diff --git a/src/test/java/com/evenly/took/feature/auth/client/google/MockGoogleProviderTest.java b/src/test/java/com/evenly/took/feature/auth/client/google/MockGoogleProviderTest.java new file mode 100644 index 0000000..9ab4c2f --- /dev/null +++ b/src/test/java/com/evenly/took/feature/auth/client/google/MockGoogleProviderTest.java @@ -0,0 +1,36 @@ +package com.evenly.took.feature.auth.client.google; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.mockito.Answers; +import org.mockito.Mock; +import org.springframework.web.client.RestClient; + +import com.evenly.took.global.config.properties.auth.GoogleProperties; +import com.evenly.took.global.config.properties.auth.GoogleUrlProperties; +import com.evenly.took.global.service.MockTest; + +public abstract class MockGoogleProviderTest extends MockTest { + + @Mock + protected GoogleProperties googleProperties; + + @Mock + protected GoogleUrlProperties googleUrlProperties; + + @Mock + protected RestClient.Builder restClientBuilder; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + protected RestClient restClient; + + @BeforeEach + public void setUpCommon() { + when(restClientBuilder.defaultStatusHandler(any())).thenReturn(restClientBuilder); + when(restClientBuilder.defaultHeader("Content-Type", "application/x-www-form-urlencoded", "UTF-8")) + .thenReturn(restClientBuilder); + when(restClientBuilder.build()).thenReturn(restClient); + } +}