From 0b640038283138b37af77d37ad6712d12dccdb50 Mon Sep 17 00:00:00 2001 From: Bliss Pisit Wetcha Date: Tue, 3 Jan 2023 17:57:32 +0700 Subject: [PATCH] [#43] Add RefreshToken API --- .../network/core/TokenizedNetworkClient.kt | 23 ++++++- .../network/datasource/NetworkDataSource.kt | 5 ++ .../data/network/target/RefreshTokenType.kt | 28 +++++++++ .../di/koin/modules/NetworkModule.kt | 2 +- .../data/datasource/NetworkDataSourceTest.kt | 15 +++++ .../core/TokenizedNetworkClientTest.kt | 63 +++++++++++++++++-- 6 files changed, 127 insertions(+), 9 deletions(-) create mode 100644 shared/src/commonMain/kotlin/co/nimblehq/blisskmmic/data/network/target/RefreshTokenType.kt diff --git a/shared/src/commonMain/kotlin/co/nimblehq/blisskmmic/data/network/core/TokenizedNetworkClient.kt b/shared/src/commonMain/kotlin/co/nimblehq/blisskmmic/data/network/core/TokenizedNetworkClient.kt index 4f96372f..91385209 100644 --- a/shared/src/commonMain/kotlin/co/nimblehq/blisskmmic/data/network/core/TokenizedNetworkClient.kt +++ b/shared/src/commonMain/kotlin/co/nimblehq/blisskmmic/data/network/core/TokenizedNetworkClient.kt @@ -2,25 +2,32 @@ package co.nimblehq.blisskmmic.data.network.core import co.nimblehq.blisskmmic.BuildKonfig import co.nimblehq.blisskmmic.data.database.datasource.LocalDataSource +import co.nimblehq.blisskmmic.data.database.model.TokenDatabaseModel +import co.nimblehq.blisskmmic.data.network.datasource.NetworkDataSource +import co.nimblehq.blisskmmic.data.network.target.RefreshTokenType import io.ktor.client.* import io.ktor.client.engine.* import io.ktor.client.plugins.auth.* import io.ktor.client.plugins.auth.providers.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.plugins.logging.* -import io.ktor.client.request.* +import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* +import kotlinx.coroutines.flow.last import kotlinx.coroutines.flow.singleOrNull class TokenizedNetworkClient: NetworkClient { private val localDataSource: LocalDataSource + private val networkDataSource: NetworkDataSource constructor( engine: HttpClientEngine? = null, - localDataSource: LocalDataSource + localDataSource: LocalDataSource, + networkDataSource: NetworkDataSource ) : super(engine) { this.localDataSource = localDataSource + this.networkDataSource = networkDataSource } override fun clientConfig(): HttpClientConfig<*>.() -> Unit { @@ -44,6 +51,18 @@ class TokenizedNetworkClient: NetworkClient { BearerTokens(accessToken, refreshToken) } } + sendWithoutRequest { request -> + request.url.host == Url(BuildKonfig.BASE_URL).host + } + refreshTokens { + networkDataSource + .refreshToken(RefreshTokenType(oldTokens?.refreshToken ?: "")) + .last() + .run { + localDataSource.save(TokenDatabaseModel(toToken())) + BearerTokens(accessToken, refreshToken) + } + } } } } diff --git a/shared/src/commonMain/kotlin/co/nimblehq/blisskmmic/data/network/datasource/NetworkDataSource.kt b/shared/src/commonMain/kotlin/co/nimblehq/blisskmmic/data/network/datasource/NetworkDataSource.kt index 9c995204..b8989770 100644 --- a/shared/src/commonMain/kotlin/co/nimblehq/blisskmmic/data/network/datasource/NetworkDataSource.kt +++ b/shared/src/commonMain/kotlin/co/nimblehq/blisskmmic/data/network/datasource/NetworkDataSource.kt @@ -16,6 +16,7 @@ interface NetworkDataSource { fun resetPassword(target: ResetPasswordTargetType): Flow fun survey(target: SurveySelectionTargetType): Flow, PaginationMetaApiModel>> fun profile(target: UserProfileTargetType): Flow + fun refreshToken(target: RefreshTokenType): Flow } class NetworkDataSourceImpl(private val networkClient: NetworkClient): NetworkDataSource { @@ -36,4 +37,8 @@ class NetworkDataSourceImpl(private val networkClient: NetworkClient): NetworkDa override fun profile(target: UserProfileTargetType): Flow { return networkClient.fetch(target.requestBuilder()) } + + override fun refreshToken(target: RefreshTokenType): Flow { + return networkClient.fetch(target.requestBuilder()) + } } diff --git a/shared/src/commonMain/kotlin/co/nimblehq/blisskmmic/data/network/target/RefreshTokenType.kt b/shared/src/commonMain/kotlin/co/nimblehq/blisskmmic/data/network/target/RefreshTokenType.kt new file mode 100644 index 00000000..fb34a4a6 --- /dev/null +++ b/shared/src/commonMain/kotlin/co/nimblehq/blisskmmic/data/network/target/RefreshTokenType.kt @@ -0,0 +1,28 @@ +package co.nimblehq.blisskmmic.data.network.target + +import co.nimblehq.blisskmmic.BuildKonfig +import co.nimblehq.blisskmmic.data.network.helpers.TargetType +import io.ktor.http.* +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +class RefreshTokenType(refreshToken: String): + TargetType { + + @Serializable + data class RefreshTokenInput( + @SerialName("grant_type") val grantType: String, + @SerialName("refresh_token") val refreshToken: String, + @SerialName("client_id") val clientId: String, + @SerialName("client_secret") val clientSecret: String + ) + + override val path = "oauth/token" + override val method = HttpMethod.Post + override val body = RefreshTokenInput( + "refresh_token", + refreshToken, + BuildKonfig.CLIENT_ID, + BuildKonfig.CLIENT_SECRET + ) +} diff --git a/shared/src/commonMain/kotlin/co/nimblehq/blisskmmic/di/koin/modules/NetworkModule.kt b/shared/src/commonMain/kotlin/co/nimblehq/blisskmmic/di/koin/modules/NetworkModule.kt index 041ae88d..2b0ca842 100644 --- a/shared/src/commonMain/kotlin/co/nimblehq/blisskmmic/di/koin/modules/NetworkModule.kt +++ b/shared/src/commonMain/kotlin/co/nimblehq/blisskmmic/di/koin/modules/NetworkModule.kt @@ -13,6 +13,6 @@ val networkModule = module { single { NetworkClient() } single(named(NETWORK_CLIENT_KOIN)) { NetworkDataSourceImpl(get()) } single(named(TOKENIZED_NETWORK_CLIENT_KOIN)) { - NetworkDataSourceImpl(TokenizedNetworkClient(null, get())) + NetworkDataSourceImpl(TokenizedNetworkClient(null, get(), get(named(NETWORK_CLIENT_KOIN)))) } } diff --git a/shared/src/commonTest/kotlin/co/nimblehq/blisskmmic/data/datasource/NetworkDataSourceTest.kt b/shared/src/commonTest/kotlin/co/nimblehq/blisskmmic/data/datasource/NetworkDataSourceTest.kt index 20b17ad0..0e2ca120 100644 --- a/shared/src/commonTest/kotlin/co/nimblehq/blisskmmic/data/datasource/NetworkDataSourceTest.kt +++ b/shared/src/commonTest/kotlin/co/nimblehq/blisskmmic/data/datasource/NetworkDataSourceTest.kt @@ -114,4 +114,19 @@ class NetworkDataSourceTest { awaitComplete() } } + + // Refresh Token + + @Test + fun `When calling refresh token with success response - it returns correct object`() = runTest { + val engine = jsonMockEngine(LOG_IN_JSON_RESULT, "oauth/token") + val networkClient = NetworkClient(engine = engine) + val dataSource = NetworkDataSourceImpl(networkClient) + dataSource + .refreshToken(RefreshTokenType("")) + .test { + awaitItem().refreshToken shouldBe "refresh_token" + awaitComplete() + } + } } diff --git a/shared/src/commonTest/kotlin/co/nimblehq/blisskmmic/data/network/core/TokenizedNetworkClientTest.kt b/shared/src/commonTest/kotlin/co/nimblehq/blisskmmic/data/network/core/TokenizedNetworkClientTest.kt index 93d9ceb1..d234a71b 100644 --- a/shared/src/commonTest/kotlin/co/nimblehq/blisskmmic/data/network/core/TokenizedNetworkClientTest.kt +++ b/shared/src/commonTest/kotlin/co/nimblehq/blisskmmic/data/network/core/TokenizedNetworkClientTest.kt @@ -5,7 +5,11 @@ import co.nimblehq.blisskmmic.BuildKonfig import co.nimblehq.blisskmmic.data.database.datasource.LocalDataSource import co.nimblehq.blisskmmic.data.database.datasource.MockLocalDataSource import co.nimblehq.blisskmmic.data.database.model.TokenDatabaseModel +import co.nimblehq.blisskmmic.data.network.datasource.MockNetworkDataSource +import co.nimblehq.blisskmmic.data.network.datasource.NetworkDataSource import co.nimblehq.blisskmmic.data.network.helpers.API_VERSION +import co.nimblehq.blisskmmic.domain.model.TokenApiModel +import co.nimblehq.blisskmmic.helpers.flow.delayFlowOf import co.nimblehq.blisskmmic.helpers.mock.NETWORK_META_MOCK_MODEL_RESULT import co.nimblehq.blisskmmic.helpers.mock.NETWORK_MOCK_MODEL_RESULT import co.nimblehq.blisskmmic.helpers.mock.NetworkMetaMockModel @@ -14,7 +18,9 @@ import co.nimblehq.blisskmmic.helpers.mock.ktor.jsonTokenizedMockEngine import co.nimblehq.jsonapi.model.JsonApiException import io.kotest.matchers.collections.shouldContain import io.kotest.matchers.shouldBe +import io.ktor.client.engine.mock.* import io.ktor.client.request.* +import io.ktor.http.* import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest @@ -25,11 +31,12 @@ import kotlin.test.Test import kotlin.test.fail @ExperimentalCoroutinesApi -@UsesMocks(LocalDataSource::class) +@UsesMocks(LocalDataSource::class, NetworkDataSource::class) class TokenizedNetworkClientTest { private val mocker = Mocker() private val localDataSource = MockLocalDataSource(mocker) + private val networkDataSource = MockNetworkDataSource(mocker) private val token = TokenDatabaseModel( accessToken = "Access", @@ -38,6 +45,13 @@ class TokenizedNetworkClientTest { refreshToken = "", createdAt = 1 ) + private val refreshedToken = TokenApiModel( + accessToken = "Refreshed Access", + tokenType = "", + expiresIn = 1, + refreshToken = "", + createdAt = 1 + ) private val path = "user" private val request = HttpRequestBuilder() @@ -45,6 +59,12 @@ class TokenizedNetworkClientTest { fun setUp() { mocker.reset() request.url("$BuildKonfig.BASE_URL$API_VERSION$path") + mocker.every { + networkDataSource.refreshToken(isAny()) + } returns delayFlowOf(refreshedToken) + mocker.every { + localDataSource.save(isAny()) + } returns Unit } @Test @@ -54,10 +74,10 @@ class TokenizedNetworkClientTest { } returns flowOf(token) val engine = jsonTokenizedMockEngine( NETWORK_MOCK_MODEL_RESULT, - token.accessToken, + refreshedToken.accessToken, path ) - val networkClient = TokenizedNetworkClient(engine = engine, localDataSource) + val networkClient = TokenizedNetworkClient(engine, localDataSource, networkDataSource) networkClient .fetch(request) .test { @@ -73,10 +93,10 @@ class TokenizedNetworkClientTest { } returns flowOf(token) val engine = jsonTokenizedMockEngine( NETWORK_META_MOCK_MODEL_RESULT, - token.accessToken, + refreshedToken.accessToken, path ) - val networkClient = TokenizedNetworkClient(engine = engine, localDataSource) + val networkClient = TokenizedNetworkClient(engine, localDataSource, networkDataSource) networkClient .fetchWithMeta(request) .test { @@ -97,7 +117,7 @@ class TokenizedNetworkClientTest { "no access", path ) - val networkClient = TokenizedNetworkClient(engine = engine, localDataSource) + val networkClient = TokenizedNetworkClient(engine, localDataSource, networkDataSource) networkClient .fetch(request) .test { @@ -107,4 +127,35 @@ class TokenizedNetworkClientTest { } } } + + @Test + fun `when receiving expired token - it calls refresh token`() = runTest { + mocker.every { + localDataSource.getToken() + } returns delayFlowOf(token) + var attempt = 0 + val engine = MockEngine { _ -> + if(attempt == 0) { + attempt++ + respond( + NETWORK_MOCK_MODEL_RESULT, + HttpStatusCode.Unauthorized, + headersOf(HttpHeaders.ContentType, "application/json") + ) + } else { + respond( + NETWORK_MOCK_MODEL_RESULT, + HttpStatusCode.OK, + headersOf(HttpHeaders.ContentType, "application/json") + ) + } + } + val networkClient = TokenizedNetworkClient(engine, localDataSource, networkDataSource) + networkClient + .fetch(request) + .test { + awaitItem().title shouldBe "Hello" + awaitComplete() + } + } }