diff --git a/backend/src/main/java/endolphin/backend/domain/auth/AuthController.java b/backend/src/main/java/endolphin/backend/domain/auth/AuthController.java new file mode 100644 index 00000000..78a2003e --- /dev/null +++ b/backend/src/main/java/endolphin/backend/domain/auth/AuthController.java @@ -0,0 +1,33 @@ +package endolphin.backend.domain.auth; + +import endolphin.backend.domain.auth.dto.OAuthResponse; +import endolphin.backend.domain.auth.dto.UrlResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "Auth", description = "인증 관리 API") +@RestController +@RequiredArgsConstructor +public class AuthController { + + private final AuthService authService; + + @Operation(summary = "구글 로그인 URL", description = "구글 로그인 URL을 반환합니다.") + @GetMapping("/api/v1/google") + public ResponseEntity loginUrl() { + UrlResponse response = authService.getGoogleLoginUrl(); + return ResponseEntity.ok(response); + } + + @Operation(summary = "구글 로그인 콜백", description = "사용자가 Google 계정으로 로그인하여 JWT 토큰을 발급받습니다.") + @GetMapping("/oauth2/callback") + public ResponseEntity oauthCallback(@RequestParam("code") String code) { + OAuthResponse response = authService.oauth2Callback(code); + return ResponseEntity.ok(response); + } +} diff --git a/backend/src/main/java/endolphin/backend/domain/auth/AuthService.java b/backend/src/main/java/endolphin/backend/domain/auth/AuthService.java new file mode 100644 index 00000000..a64ffe24 --- /dev/null +++ b/backend/src/main/java/endolphin/backend/domain/auth/AuthService.java @@ -0,0 +1,58 @@ +package endolphin.backend.domain.auth; + +import endolphin.backend.domain.user.UserService; +import endolphin.backend.global.error.exception.ErrorCode; +import endolphin.backend.global.error.exception.OAuthException; +import endolphin.backend.global.google.dto.GoogleTokens; +import endolphin.backend.global.google.dto.GoogleUserInfo; +import endolphin.backend.domain.auth.dto.OAuthResponse; +import endolphin.backend.domain.auth.dto.UrlResponse; +import endolphin.backend.domain.user.entity.User; +import endolphin.backend.global.config.GoogleOAuthProperties; +import endolphin.backend.global.google.GoogleOAuthService; +import endolphin.backend.global.security.JwtProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class AuthService { + + private final GoogleOAuthProperties googleOAuthProperties; + private final GoogleOAuthService googleOAuthService; + private final UserService userService; + private final JwtProvider jwtProvider; + + public UrlResponse getGoogleLoginUrl() { + return new UrlResponse(String.format( + "%s?client_id=%s&redirect_uri=%s&response_type=code&scope=%s&access_type=offline&prompt=consent", + googleOAuthProperties.authUrl(), googleOAuthProperties.clientId(), + googleOAuthProperties.redirectUri(), googleOAuthProperties.scope())); + } + + @Transactional + public OAuthResponse oauth2Callback(String code) { + if (code == null || code.isBlank()) { + throw new OAuthException(ErrorCode.INVALID_OAUTH_CODE); + } + GoogleTokens tokenResponse = googleOAuthService.getGoogleTokens(code); + GoogleUserInfo userInfo = googleOAuthService.getUserInfo(tokenResponse.accessToken()); + validateUserInfo(userInfo); + User user = userService.upsertUser(userInfo, tokenResponse); + String accessToken = jwtProvider.createToken(user.getId(), user.getEmail()); + return new OAuthResponse(accessToken); + } + + private void validateUserInfo(GoogleUserInfo userInfo) { + if (userInfo == null) { + throw new OAuthException(ErrorCode.INVALID_OAUTH_USER_INFO); + } + if (userInfo.email() == null || userInfo.email().isBlank()) { + throw new OAuthException(ErrorCode.INVALID_OAUTH_USER_INFO); + } + if (userInfo.picture() == null || userInfo.picture().isBlank()) { + throw new OAuthException(ErrorCode.INVALID_OAUTH_USER_INFO); + } + } +} diff --git a/backend/src/main/java/endolphin/backend/domain/user/dto/OAuthResponse.java b/backend/src/main/java/endolphin/backend/domain/auth/dto/OAuthResponse.java similarity index 55% rename from backend/src/main/java/endolphin/backend/domain/user/dto/OAuthResponse.java rename to backend/src/main/java/endolphin/backend/domain/auth/dto/OAuthResponse.java index a86724f9..a43d0dcb 100644 --- a/backend/src/main/java/endolphin/backend/domain/user/dto/OAuthResponse.java +++ b/backend/src/main/java/endolphin/backend/domain/auth/dto/OAuthResponse.java @@ -1,4 +1,4 @@ -package endolphin.backend.domain.user.dto; +package endolphin.backend.domain.auth.dto; public record OAuthResponse(String accessToken) { diff --git a/backend/src/main/java/endolphin/backend/domain/user/dto/UrlResponse.java b/backend/src/main/java/endolphin/backend/domain/auth/dto/UrlResponse.java similarity index 50% rename from backend/src/main/java/endolphin/backend/domain/user/dto/UrlResponse.java rename to backend/src/main/java/endolphin/backend/domain/auth/dto/UrlResponse.java index 2a3da11c..d5519f28 100644 --- a/backend/src/main/java/endolphin/backend/domain/user/dto/UrlResponse.java +++ b/backend/src/main/java/endolphin/backend/domain/auth/dto/UrlResponse.java @@ -1,4 +1,4 @@ -package endolphin.backend.domain.user.dto; +package endolphin.backend.domain.auth.dto; public record UrlResponse(String url) { diff --git a/backend/src/main/java/endolphin/backend/domain/user/UserController.java b/backend/src/main/java/endolphin/backend/domain/user/UserController.java index 4cc0fc5d..9da78dce 100644 --- a/backend/src/main/java/endolphin/backend/domain/user/UserController.java +++ b/backend/src/main/java/endolphin/backend/domain/user/UserController.java @@ -1,13 +1,7 @@ package endolphin.backend.domain.user; -import endolphin.backend.domain.user.dto.OAuthResponse; -import endolphin.backend.domain.user.dto.UrlResponse; -import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @Tag(name = "User", description = "사용자 관리 API") @@ -17,17 +11,4 @@ public class UserController { private final UserService userService; - @Operation(summary = "구글 로그인 URL", description = "구글 로그인 URL을 반환합니다.") - @GetMapping("/api/v1/google") - public ResponseEntity loginUrl() { - UrlResponse response = userService.getGoogleLoginUrl(); - return ResponseEntity.ok(response); - } - - @Operation(summary = "구글 로그인 콜백", description = "사용자가 Google 계정으로 로그인하여 JWT 토큰을 발급받습니다.") - @GetMapping("/oauth2/callback") - public ResponseEntity oauthCallback(@RequestParam("code") String code) { - OAuthResponse response = userService.oauth2Callback(code); - return ResponseEntity.ok(response); - } } diff --git a/backend/src/main/java/endolphin/backend/domain/user/UserService.java b/backend/src/main/java/endolphin/backend/domain/user/UserService.java index 1e8df481..6dd0b9d2 100644 --- a/backend/src/main/java/endolphin/backend/domain/user/UserService.java +++ b/backend/src/main/java/endolphin/backend/domain/user/UserService.java @@ -1,112 +1,48 @@ package endolphin.backend.domain.user; -import endolphin.backend.domain.user.dto.GoogleUserInfo; -import endolphin.backend.domain.user.dto.GoogleTokens; -import endolphin.backend.domain.user.dto.OAuthResponse; -import endolphin.backend.domain.user.dto.UrlResponse; +import endolphin.backend.global.google.dto.GoogleUserInfo; +import endolphin.backend.global.google.dto.GoogleTokens; import endolphin.backend.domain.user.entity.User; -import endolphin.backend.global.config.GoogleOAuthProperties; import endolphin.backend.global.error.exception.ApiException; import endolphin.backend.global.error.exception.ErrorCode; -import endolphin.backend.global.security.JwtProvider; import endolphin.backend.global.security.UserContext; import endolphin.backend.global.security.UserInfo; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.web.client.RestTemplate; @Service @RequiredArgsConstructor @Transactional @Slf4j public class UserService { - - private final GoogleOAuthProperties googleOAuthProperties; private final UserRepository userRepository; - private final JwtProvider jwtProvider; - private final RestTemplate restTemplate; - - public UrlResponse getGoogleLoginUrl() { - return new UrlResponse(String.format( - "%s?client_id=%s&redirect_uri=%s&response_type=code&scope=%s&access_type=offline", - googleOAuthProperties.authUrl(), googleOAuthProperties.clientId(), - googleOAuthProperties.redirectUri(), googleOAuthProperties.scope())); - } - - public OAuthResponse oauth2Callback(String code) { - GoogleTokens tokenResponse = getAccessToken(code); - GoogleUserInfo userInfo = getUserInfo(tokenResponse.accessToken()); - User user = createUser(userInfo, tokenResponse); - String accessToken = jwtProvider.createToken(user.getId(), user.getEmail()); - return new OAuthResponse(accessToken); - } + @Transactional(readOnly = true) public User getCurrentUser() { UserInfo userInfo = UserContext.get(); return userRepository.findById(userInfo.userId()) .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); } - private User createUser(GoogleUserInfo userInfo, GoogleTokens tokenResponse) { + public void updateAccessToken(User user, String accessToken) { + user.setAccessToken(accessToken); + userRepository.save(user); + } + + public User upsertUser(GoogleUserInfo userInfo, GoogleTokens tokenResponse) { User user = userRepository.findByEmail(userInfo.email()) .orElseGet(() -> { return User.builder() .email(userInfo.email()) .name(userInfo.name()) .picture(userInfo.picture()) - .accessToken(tokenResponse.accessToken()) - .refreshToken(tokenResponse.refreshToken()) .build(); }); + user.setAccessToken(tokenResponse.accessToken()); + user.setRefreshToken(tokenResponse.refreshToken()); userRepository.save(user); return user; } - - private GoogleTokens getAccessToken(String code) { - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); - - HttpEntity> request = getHttpEntity( - code, headers); - ResponseEntity response = restTemplate.postForEntity( - googleOAuthProperties.tokenUrl(), request, - GoogleTokens.class); - - return response.getBody(); - } - - private HttpEntity> getHttpEntity(String code, - HttpHeaders headers) { - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("client_id", googleOAuthProperties.clientId()); - params.add("client_secret", googleOAuthProperties.clientSecret()); - params.add("redirect_uri", googleOAuthProperties.redirectUri()); - params.add("code", code); - params.add("grant_type", "authorization_code"); - params.add("access_type", "offline"); - - HttpEntity> request = new HttpEntity<>(params, headers); - return request; - } - - private GoogleUserInfo getUserInfo(String accessToken) { - HttpHeaders headers = new HttpHeaders(); - headers.setBearerAuth(accessToken); - - HttpEntity entity = new HttpEntity<>(headers); - - ResponseEntity result = restTemplate.exchange(googleOAuthProperties.userInfoUrl(), - HttpMethod.GET, entity, GoogleUserInfo.class); - - return result.getBody(); - } } diff --git a/backend/src/main/java/endolphin/backend/domain/user/entity/User.java b/backend/src/main/java/endolphin/backend/domain/user/entity/User.java index 53093821..60425d0a 100644 --- a/backend/src/main/java/endolphin/backend/domain/user/entity/User.java +++ b/backend/src/main/java/endolphin/backend/domain/user/entity/User.java @@ -28,8 +28,10 @@ public class User extends BaseTimeEntity { private String email; @Column(nullable = false) private String picture; + @Setter @Column(name = "access_token") private String accessToken; + @Setter @Column(name = "refresh_token") private String refreshToken; @@ -42,4 +44,5 @@ public User(String name, String email, String picture, String accessToken, this.accessToken = accessToken; this.refreshToken = refreshToken; } + } diff --git a/backend/src/main/java/endolphin/backend/global/config/RestTemplateConfig.java b/backend/src/main/java/endolphin/backend/global/config/RestClientConfig.java similarity index 62% rename from backend/src/main/java/endolphin/backend/global/config/RestTemplateConfig.java rename to backend/src/main/java/endolphin/backend/global/config/RestClientConfig.java index 87ed26bd..06f3a5ac 100644 --- a/backend/src/main/java/endolphin/backend/global/config/RestTemplateConfig.java +++ b/backend/src/main/java/endolphin/backend/global/config/RestClientConfig.java @@ -3,20 +3,16 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.SimpleClientHttpRequestFactory; -import org.springframework.web.client.RestTemplate; +import org.springframework.web.client.RestClient; @Configuration -public class RestTemplateConfig { +public class RestClientConfig { @Bean - public RestTemplate restTemplate() { - RestTemplate restTemplate = new RestTemplate(); + public RestClient restClient() { SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); requestFactory.setConnectTimeout(5000); requestFactory.setReadTimeout(5000); - - restTemplate.setRequestFactory(requestFactory); - - return restTemplate; + return RestClient.builder().requestFactory(requestFactory).build(); } } diff --git a/backend/src/main/java/endolphin/backend/global/error/GlobalExceptionHandler.java b/backend/src/main/java/endolphin/backend/global/error/GlobalExceptionHandler.java index 8ee3b147..8362490a 100644 --- a/backend/src/main/java/endolphin/backend/global/error/GlobalExceptionHandler.java +++ b/backend/src/main/java/endolphin/backend/global/error/GlobalExceptionHandler.java @@ -2,6 +2,7 @@ import endolphin.backend.global.error.exception.ApiException; import endolphin.backend.global.error.exception.ErrorCode; +import endolphin.backend.global.error.exception.OAuthException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -20,6 +21,14 @@ public ResponseEntity handleApiException(ApiException e) { return ResponseEntity.status(e.getErrorCode().getHttpStatus()).body(response); } + @ExceptionHandler(OAuthException.class) + public ResponseEntity handleOAuthException(OAuthException e) { + log.error("[OAuth exception] Error code: {}, Message: {}", + e.getErrorCode(), e.getMessage(), e); + ErrorResponse response = ErrorResponse.of(e.getErrorCode()); + return ResponseEntity.status(e.getErrorCode().getHttpStatus()).body(response); + } + @ExceptionHandler(Exception.class) public ResponseEntity handleUnexpectedException(Exception e) { log.error("[Unexpected exception] ", e); diff --git a/backend/src/main/java/endolphin/backend/global/error/exception/ErrorCode.java b/backend/src/main/java/endolphin/backend/global/error/exception/ErrorCode.java index 5cd32364..2582edd3 100644 --- a/backend/src/main/java/endolphin/backend/global/error/exception/ErrorCode.java +++ b/backend/src/main/java/endolphin/backend/global/error/exception/ErrorCode.java @@ -16,8 +16,14 @@ public enum ErrorCode { DISCUSSION_NOT_FOUND(HttpStatus.NOT_FOUND, "D001", "Discussion not found"), //SharedEvent - SHARED_EVENT_NOT_FOUND(HttpStatus.NOT_FOUND, "S001", "Shared Event not found") + SHARED_EVENT_NOT_FOUND(HttpStatus.NOT_FOUND, "S001", "Shared Event not found"), + // OAuth + OAUTH_UNAUTHORIZED_ERROR(HttpStatus.UNAUTHORIZED, "O001", "OAuth Unauthorized Error"), + OAUTH_BAD_REQUEST_ERROR(HttpStatus.BAD_REQUEST, "O002", "OAuth Bad Request Error"), + OAUTH_FORBIDDEN_ERROR(HttpStatus.FORBIDDEN, "O003", "OAuth Forbidden Error"), + INVALID_OAUTH_CODE(HttpStatus.UNAUTHORIZED, "O004", "Invalid OAuth Code"), + INVALID_OAUTH_USER_INFO(HttpStatus.UNAUTHORIZED, "O005", "Invalid OAuth User Info"), ; private final HttpStatus httpStatus; private final String code; diff --git a/backend/src/main/java/endolphin/backend/global/error/exception/OAuthException.java b/backend/src/main/java/endolphin/backend/global/error/exception/OAuthException.java new file mode 100644 index 00000000..5a26a920 --- /dev/null +++ b/backend/src/main/java/endolphin/backend/global/error/exception/OAuthException.java @@ -0,0 +1,25 @@ +package endolphin.backend.global.error.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public class OAuthException extends RuntimeException { + + private final ErrorCode errorCode; + + public OAuthException(HttpStatus status, String message) { + super(message); + switch (status) { + case UNAUTHORIZED -> errorCode = ErrorCode.OAUTH_UNAUTHORIZED_ERROR; + case FORBIDDEN -> errorCode = ErrorCode.OAUTH_FORBIDDEN_ERROR; + case BAD_REQUEST -> errorCode = ErrorCode.OAUTH_BAD_REQUEST_ERROR; + default -> errorCode = ErrorCode.INTERNAL_ERROR; + } + } + + public OAuthException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } +} diff --git a/backend/src/main/java/endolphin/backend/global/google/GoogleOAuthService.java b/backend/src/main/java/endolphin/backend/global/google/GoogleOAuthService.java new file mode 100644 index 00000000..b6ec482b --- /dev/null +++ b/backend/src/main/java/endolphin/backend/global/google/GoogleOAuthService.java @@ -0,0 +1,87 @@ +package endolphin.backend.global.google; + +import endolphin.backend.domain.user.UserService; +import endolphin.backend.domain.user.entity.User; +import endolphin.backend.global.google.dto.GoogleTokens; +import endolphin.backend.global.google.dto.GoogleUserInfo; +import endolphin.backend.global.config.GoogleOAuthProperties; +import endolphin.backend.global.error.exception.OAuthException; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClient.RequestHeadersSpec.ConvertibleClientHttpResponse; + +@Service +@RequiredArgsConstructor +@Slf4j +public class GoogleOAuthService { + + private final UserService userService; + private final GoogleOAuthProperties googleOAuthProperties; + private final RestClient restClient; + + public GoogleTokens getGoogleTokens(String code) { + MultiValueMap params = getStringStringMultiValueMap(); + params.add("code", code); + params.add("grant_type", "authorization_code"); + params.add("access_type", "offline"); + + return restClient.post() + .uri(googleOAuthProperties.tokenUrl()) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(params) + .exchange((request, response) -> { + validateResponse(response); + return response.bodyTo(GoogleTokens.class); + }); + } + + public GoogleUserInfo getUserInfo(String accessToken) { + return restClient.get() + .uri(googleOAuthProperties.userInfoUrl()) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .exchange((request, response) -> { + validateResponse(response); + return response.bodyTo(GoogleUserInfo.class); + }); + } + + public String reissueAccessToken(String refreshToken) { + MultiValueMap params = getStringStringMultiValueMap(); + params.add("grant_type", "refresh_token"); + params.add("refresh_token", refreshToken); + + return restClient.post() + .uri(googleOAuthProperties.tokenUrl()) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(params) + .exchange((request, response) -> { + validateResponse(response); + GoogleTokens tokens = response.bodyTo(GoogleTokens.class); + + return tokens.accessToken(); + }); + } + + private MultiValueMap getStringStringMultiValueMap() { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("client_id", googleOAuthProperties.clientId()); + params.add("client_secret", googleOAuthProperties.clientSecret()); + params.add("redirect_uri", googleOAuthProperties.redirectUri()); + return params; + } + + private void validateResponse(ConvertibleClientHttpResponse response) throws IOException { + if (response.getStatusCode().is4xxClientError()) { + String error = response.bodyTo(String.class); + throw new OAuthException((HttpStatus) response.getStatusCode(), error); + } + } +} diff --git a/backend/src/main/java/endolphin/backend/domain/user/dto/GoogleTokens.java b/backend/src/main/java/endolphin/backend/global/google/dto/GoogleTokens.java similarity index 82% rename from backend/src/main/java/endolphin/backend/domain/user/dto/GoogleTokens.java rename to backend/src/main/java/endolphin/backend/global/google/dto/GoogleTokens.java index db960950..e2d1296b 100644 --- a/backend/src/main/java/endolphin/backend/domain/user/dto/GoogleTokens.java +++ b/backend/src/main/java/endolphin/backend/global/google/dto/GoogleTokens.java @@ -1,4 +1,4 @@ -package endolphin.backend.domain.user.dto; +package endolphin.backend.global.google.dto; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/backend/src/main/java/endolphin/backend/domain/user/dto/GoogleUserInfo.java b/backend/src/main/java/endolphin/backend/global/google/dto/GoogleUserInfo.java similarity index 66% rename from backend/src/main/java/endolphin/backend/domain/user/dto/GoogleUserInfo.java rename to backend/src/main/java/endolphin/backend/global/google/dto/GoogleUserInfo.java index 8a2bf420..ca3ac07c 100644 --- a/backend/src/main/java/endolphin/backend/domain/user/dto/GoogleUserInfo.java +++ b/backend/src/main/java/endolphin/backend/global/google/dto/GoogleUserInfo.java @@ -1,4 +1,4 @@ -package endolphin.backend.domain.user.dto; +package endolphin.backend.global.google.dto; public record GoogleUserInfo(String sub, String name, String email, String picture) { diff --git a/backend/src/test/java/endolphin/backend/domain/user/UserServiceTest.java b/backend/src/test/java/endolphin/backend/domain/auth/AuthServiceTest.java similarity index 60% rename from backend/src/test/java/endolphin/backend/domain/user/UserServiceTest.java rename to backend/src/test/java/endolphin/backend/domain/auth/AuthServiceTest.java index 335d5d68..6cdf4839 100644 --- a/backend/src/test/java/endolphin/backend/domain/user/UserServiceTest.java +++ b/backend/src/test/java/endolphin/backend/domain/auth/AuthServiceTest.java @@ -1,70 +1,62 @@ -package endolphin.backend.domain.user; +package endolphin.backend.domain.auth; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.*; - -import endolphin.backend.domain.user.dto.GoogleTokens; -import endolphin.backend.domain.user.dto.GoogleUserInfo; -import endolphin.backend.domain.user.dto.OAuthResponse; -import endolphin.backend.domain.user.dto.UrlResponse; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; + +import endolphin.backend.domain.auth.dto.OAuthResponse; +import endolphin.backend.domain.auth.dto.UrlResponse; +import endolphin.backend.domain.user.UserRepository; +import endolphin.backend.domain.user.UserService; +import endolphin.backend.global.google.dto.GoogleTokens; +import endolphin.backend.global.google.dto.GoogleUserInfo; import endolphin.backend.domain.user.entity.User; import endolphin.backend.global.config.GoogleOAuthProperties; +import endolphin.backend.global.google.GoogleOAuthService; import endolphin.backend.global.security.JwtProvider; -import java.util.Map; import java.util.Optional; -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.http.HttpEntity; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; -import org.springframework.web.client.RestTemplate; @ExtendWith(MockitoExtension.class) -class UserServiceTest { +class AuthServiceTest { @Mock private GoogleOAuthProperties googleOAuthProperties; @Mock - private UserRepository userRepository; + private UserService userService; @Mock - private JwtProvider jwtProvider; + private GoogleOAuthService googleOAuthService; @Mock - private RestTemplate restTemplate; + private JwtProvider jwtProvider; @InjectMocks - private UserService userService; + private AuthService authService; private final String testAuthUrl = "https://accounts.google.com/o/oauth2/auth"; private final String testClientId = "test-client-id"; private final String testRedirectUri = "http://localhost:8080/test/callback"; private final String testScope = "email profile"; - - @BeforeEach - void setUp() { - given(googleOAuthProperties.clientId()).willReturn(testClientId); - given(googleOAuthProperties.redirectUri()).willReturn(testRedirectUri); - } - @Test @DisplayName("구글 로그인 url 반환 테스트") void getGoogleLoginUrl_ShouldReturnCorrectUrl() { + given(googleOAuthProperties.clientId()).willReturn(testClientId); + given(googleOAuthProperties.redirectUri()).willReturn(testRedirectUri); given(googleOAuthProperties.authUrl()).willReturn(testAuthUrl); given(googleOAuthProperties.scope()).willReturn(testScope); - UrlResponse urlResponse = userService.getGoogleLoginUrl(); + UrlResponse urlResponse = authService.getGoogleLoginUrl(); assertThat(urlResponse.url()).isEqualTo( testAuthUrl + "?client_id=" + testClientId + "&redirect_uri=" + testRedirectUri - + "&response_type=code&scope=" + testScope + "&access_type=offline" + + "&response_type=code&scope=" + testScope + "&access_type=offline&prompt=consent" ); } @@ -72,13 +64,11 @@ void getGoogleLoginUrl_ShouldReturnCorrectUrl() { @DisplayName("로그인 콜백 테스트") void oauth2Callback_ShouldReturnJwtToken() { // Given - String testTokenUrl = "https://accounts.google.com/o/oauth2/"; - given(googleOAuthProperties.tokenUrl()).willReturn(testTokenUrl); - given(googleOAuthProperties.clientSecret()).willReturn("client-secret"); String code = "test-auth-code"; GoogleTokens googleTokens = new GoogleTokens("test-access-token", "test-refresh-token"); GoogleUserInfo googleUserInfo = new GoogleUserInfo("test-sub", "test-name", "test-email", "test-pic"); + User user = User.builder() .email(googleUserInfo.email()) .name(googleUserInfo.name()) @@ -87,21 +77,20 @@ void oauth2Callback_ShouldReturnJwtToken() { .refreshToken(googleTokens.refreshToken()) .build(); - given(restTemplate.postForEntity(eq(testTokenUrl), any(HttpEntity.class), eq(GoogleTokens.class))) - .willReturn(ResponseEntity.ok(googleTokens)); + given(googleOAuthService.getGoogleTokens(anyString())) + .willReturn(googleTokens); - given(restTemplate.exchange(eq(googleOAuthProperties.userInfoUrl()), eq(HttpMethod.GET), - any(), eq(GoogleUserInfo.class))) - .willReturn(ResponseEntity.ok(googleUserInfo)); + given(googleOAuthService.getUserInfo(anyString())) + .willReturn(googleUserInfo); - given(userRepository.findByEmail(googleUserInfo.email())).willReturn(Optional.empty()); - given(userRepository.save(any(User.class))).willReturn(user); + given(userService.upsertUser(any(GoogleUserInfo.class), any(GoogleTokens.class))) + .willReturn(user); given(jwtProvider.createToken(user.getId(), user.getEmail())).willReturn("test-jwt-token"); // When - OAuthResponse response = userService.oauth2Callback(code); + OAuthResponse response = authService.oauth2Callback(code); // Then assertThat(response.accessToken()).isEqualTo("test-jwt-token"); } -} +} \ No newline at end of file