Skip to content
This repository was archived by the owner on Jul 7, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,13 @@ class OAuthWebClientConfig {
.baseUrl("https://oauth2.googleapis.com")
.defaultHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.build()

@Bean
@Qualifier("naverWebClient")
fun naverWebClient(): WebClient =
WebClient
.builder()
.baseUrl("https://openapi.naver.com")
.defaultHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.build()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.asap.client.oauth.platform

import com.asap.client.oauth.OAuthRetrieveHandler
import com.asap.client.oauth.exception.OAuthException
import org.springframework.web.reactive.function.client.WebClient

abstract class AbstractOAuthRetrieveHandler<T>(
private val webClient: WebClient,
) : OAuthRetrieveHandler {

override fun getOAuthInfo(request: OAuthRetrieveHandler.OAuthRequest): OAuthRetrieveHandler.OAuthResponse {
val response = webClient
.get()
.uri(getApiEndpoint())
.header("Authorization", "Bearer ${request.accessToken}")
.retrieve()
.onStatus({ it.isError }, {
throw OAuthException.OAuthRetrieveFailedException(getErrorMessage())
})
.bodyToMono(getResponseType())
.block()

if (response == null) {
throw OAuthException.OAuthRetrieveFailedException(getErrorMessage())
}

return mapToOAuthResponse(response)
}

/**
* Returns the API endpoint URI for the specific OAuth provider
*/
protected abstract fun getApiEndpoint(): String

/**
* Returns the error message for the specific OAuth provider
*/
protected abstract fun getErrorMessage(): String

/**
* Returns the response type class for the specific OAuth provider
*/
protected abstract fun getResponseType(): Class<T>

/**
* Maps the provider-specific response to a common OAuthResponse
*/
protected abstract fun mapToOAuthResponse(response: T): OAuthRetrieveHandler.OAuthResponse
}
Original file line number Diff line number Diff line change
@@ -1,37 +1,27 @@
package com.asap.client.oauth.platform

import com.asap.client.oauth.OAuthRetrieveHandler
import com.asap.client.oauth.exception.OAuthException
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.client.WebClient

@Component
class GoogleOAuthRetrieveHandler(
@Qualifier("googleWebClient") private val googleWebClient: WebClient,
) : OAuthRetrieveHandler {
override fun getOAuthInfo(request: OAuthRetrieveHandler.OAuthRequest): OAuthRetrieveHandler.OAuthResponse {
val googleUserInfo =
googleWebClient
.get()
.uri("/userinfo/v2/me")
.header("Authorization", "Bearer ${request.accessToken}")
.retrieve()
.onStatus({ it.isError }, {
throw OAuthException.OAuthRetrieveFailedException("Google 사용자 정보를 가져오는데 실패했습니다.")
})
.bodyToMono(GoogleUserInfo::class.java)
.block()
@Qualifier("googleWebClient") googleWebClient: WebClient,
) : AbstractOAuthRetrieveHandler<GoogleOAuthRetrieveHandler.GoogleUserInfo>(googleWebClient) {

if (googleUserInfo == null) {
throw OAuthException.OAuthRetrieveFailedException("Google 사용자 정보를 가져오는데 실패했습니다.")
}
override fun getApiEndpoint(): String = "/userinfo/v2/me"

override fun getErrorMessage(): String = "Google 사용자 정보를 가져오는데 실패했습니다."

override fun getResponseType(): Class<GoogleUserInfo> = GoogleUserInfo::class.java

override fun mapToOAuthResponse(response: GoogleUserInfo): OAuthRetrieveHandler.OAuthResponse {
return OAuthRetrieveHandler.OAuthResponse(
username = googleUserInfo.name,
socialId = googleUserInfo.id,
email = googleUserInfo.email,
profileImage = googleUserInfo.picture,
username = response.name,
socialId = response.id,
email = response.email,
profileImage = response.picture,
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,28 @@
package com.asap.client.oauth.platform

import com.asap.client.oauth.OAuthRetrieveHandler
import com.asap.client.oauth.exception.OAuthException
import com.fasterxml.jackson.annotation.JsonProperty
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.client.WebClient

@Component
class KakaoOAuthRetrieveHandler(
@Qualifier("kakaoWebClient") private val kakaoWebClient: WebClient,
) : OAuthRetrieveHandler {
override fun getOAuthInfo(request: OAuthRetrieveHandler.OAuthRequest): OAuthRetrieveHandler.OAuthResponse {
val kakaoUserInfo =
kakaoWebClient
.get()
.uri("/v2/user/me")
.header("Authorization", "Bearer ${request.accessToken}")
.retrieve()
.onStatus({ it.isError }, {
throw OAuthException.OAuthRetrieveFailedException("Kakao 사용자 정보를 가져오는데 실패했습니다.")
})
.bodyToMono(KakaoUserInfo::class.java)
.block()

if (kakaoUserInfo == null) {
throw OAuthException.OAuthRetrieveFailedException("Kakao 사용자 정보를 가져오는데 실패했습니다.")
}
@Qualifier("kakaoWebClient") kakaoWebClient: WebClient,
) : AbstractOAuthRetrieveHandler<KakaoOAuthRetrieveHandler.KakaoUserInfo>(kakaoWebClient) {

override fun getApiEndpoint(): String = "/v2/user/me"

override fun getErrorMessage(): String = "Kakao 사용자 정보를 가져오는데 실패했습니다."

override fun getResponseType(): Class<KakaoUserInfo> = KakaoUserInfo::class.java

override fun mapToOAuthResponse(response: KakaoUserInfo): OAuthRetrieveHandler.OAuthResponse {
return OAuthRetrieveHandler.OAuthResponse(
username = kakaoUserInfo.properties.nickname,
socialId = kakaoUserInfo.id,
profileImage = kakaoUserInfo.properties.profileImage,
email = kakaoUserInfo.kakaoAccount.email,
username = response.properties.nickname,
socialId = response.id,
profileImage = response.properties.profileImage,
email = response.kakaoAccount.email,
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.asap.client.oauth.platform

import com.asap.client.oauth.OAuthRetrieveHandler
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.client.WebClient

@Component
class NaverOAuthRetrieveHandler(
@Qualifier("naverWebClient") naverWebClient: WebClient,
) : AbstractOAuthRetrieveHandler<NaverOAuthRetrieveHandler.NaverApiResponse>(naverWebClient) {

override fun getApiEndpoint(): String = "/v1/nid/me"

override fun getErrorMessage(): String = "네이버 사용자 정보를 가져오는데 실패했습니다."

override fun getResponseType(): Class<NaverApiResponse> = NaverApiResponse::class.java

override fun mapToOAuthResponse(response: NaverApiResponse): OAuthRetrieveHandler.OAuthResponse {
return OAuthRetrieveHandler.OAuthResponse(
username = response.response.nickname,
socialId = response.response.id,
email = response.response.email,
profileImage = response.response.profile_image,
)
}

data class NaverApiResponse(
val resultcode: String,
val message: String,
val response: NaverUserResponse,
)

data class NaverUserResponse(
val id: String,
val nickname: String,
val name: String,
val email: String,
val gender: String,
val age: String,
val birthday: String,
val profile_image: String,
val birthyear: String,
val mobile: String,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.asap.client.oauth.platform

import com.asap.client.oauth.OAuthRetrieveHandler
import com.asap.client.oauth.exception.OAuthException
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.shouldBe
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.springframework.http.MediaType
import org.springframework.web.reactive.function.client.WebClient

class NaverOAuthRetrieveHandlerTest :
BehaviorSpec({
var mockWebServer = MockWebServer().also {
it.start()
}
var naverWebClient: WebClient =
WebClient
.builder()
.baseUrl(mockWebServer.url("/").toString())
.defaultHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.build()
var naverOAuthRetrieveHandler = NaverOAuthRetrieveHandler(naverWebClient)

given("OAuth 요청이 성공적으로 처리되었을 때") {
val accessToken = "test-access-token"
val request = OAuthRetrieveHandler.OAuthRequest(accessToken)

val responseBody =
"""
{
"resultcode": "00",
"message": "success",
"response": {
"id": "12345",
"nickname": "Test User",
"name": "Test Name",
"email": "test@example.com",
"gender": "M",
"age": "20-29",
"birthday": "01-01",
"profile_image": "https://example.com/profile.jpg",
"birthyear": "1990",
"mobile": "010-1234-5678"
}
}
""".trimIndent()

mockWebServer.enqueue(
MockResponse()
.setResponseCode(200)
.setHeader("Content-Type", "application/json")
.setBody(responseBody),
)

`when`("getOAuthInfo 메소드를 호출하면") {
val response = naverOAuthRetrieveHandler.getOAuthInfo(request)

then("올바른 OAuthResponse를 반환해야 한다") {
response.username shouldBe "Test User"
response.socialId shouldBe "12345"
response.email shouldBe "test@example.com"
response.profileImage shouldBe "https://example.com/profile.jpg"

// 요청 검증
val recordedRequest = mockWebServer.takeRequest()
recordedRequest.path shouldBe "/v1/nid/me"
recordedRequest.getHeader("Authorization") shouldBe "Bearer test-access-token"
}
}
}

given("API가 오류를 반환할 때") {
val accessToken = "test-access-token"
val request = OAuthRetrieveHandler.OAuthRequest(accessToken)

mockWebServer.enqueue(
MockResponse()
.setResponseCode(401)
.setHeader("Content-Type", "application/json")
.setBody("{\"error\": \"invalid_token\"}"),
)

`when`("getOAuthInfo 메소드를 호출하면") {
then("OAuthRetrieveFailedException이 발생해야 한다") {
shouldThrow<OAuthException.OAuthRetrieveFailedException> {
naverOAuthRetrieveHandler.getOAuthInfo(request)
}
}
}
}

given("응답이 null일 때") {
val accessToken = "test-access-token"
val request = OAuthRetrieveHandler.OAuthRequest(accessToken)

mockWebServer.enqueue(
MockResponse()
.setResponseCode(200)
.setHeader("Content-Type", "application/json")
.setBody("null"),
)

`when`("getOAuthInfo 메소드를 호출하면") {
then("OAuthRetrieveFailedException이 발생해야 한다") {
shouldThrow<OAuthException.OAuthRetrieveFailedException> {
naverOAuthRetrieveHandler.getOAuthInfo(request)
}
}
}
}
})