Skip to content

Commit

Permalink
[#43] Add RefreshToken API
Browse files Browse the repository at this point in the history
  • Loading branch information
blyscuit committed Jan 13, 2023
1 parent e021c3e commit 0b64003
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface NetworkDataSource {
fun resetPassword(target: ResetPasswordTargetType): Flow<ResetPasswordMeta>
fun survey(target: SurveySelectionTargetType): Flow<Pair<List<SurveyApiModel>, PaginationMetaApiModel>>
fun profile(target: UserProfileTargetType): Flow<UserApiModel>
fun refreshToken(target: RefreshTokenType): Flow<TokenApiModel>
}

class NetworkDataSourceImpl(private val networkClient: NetworkClient): NetworkDataSource {
Expand All @@ -36,4 +37,8 @@ class NetworkDataSourceImpl(private val networkClient: NetworkClient): NetworkDa
override fun profile(target: UserProfileTargetType): Flow<UserApiModel> {
return networkClient.fetch(target.requestBuilder())
}

override fun refreshToken(target: RefreshTokenType): Flow<TokenApiModel> {
return networkClient.fetch(target.requestBuilder())
}
}
Original file line number Diff line number Diff line change
@@ -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<RefreshTokenType.RefreshTokenInput> {

@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
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ val networkModule = module {
single { NetworkClient() }
single<NetworkDataSource>(named(NETWORK_CLIENT_KOIN)) { NetworkDataSourceImpl(get()) }
single<NetworkDataSource>(named(TOKENIZED_NETWORK_CLIENT_KOIN)) {
NetworkDataSourceImpl(TokenizedNetworkClient(null, get()))
NetworkDataSourceImpl(TokenizedNetworkClient(null, get(), get(named(NETWORK_CLIENT_KOIN))))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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",
Expand All @@ -38,13 +45,26 @@ 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()

@BeforeTest
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
Expand All @@ -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<NetworkMockModel>(request)
.test {
Expand All @@ -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<NetworkMockModel, NetworkMetaMockModel>(request)
.test {
Expand All @@ -97,7 +117,7 @@ class TokenizedNetworkClientTest {
"no access",
path
)
val networkClient = TokenizedNetworkClient(engine = engine, localDataSource)
val networkClient = TokenizedNetworkClient(engine, localDataSource, networkDataSource)
networkClient
.fetch<NetworkMockModel>(request)
.test {
Expand All @@ -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<NetworkMockModel>(request)
.test {
awaitItem().title shouldBe "Hello"
awaitComplete()
}
}
}

0 comments on commit 0b64003

Please sign in to comment.