Add Kotlinx Kover test coverage calculator (#199)
* Add Kotlin Kover * Add AuthKtorConfiguration tests * Ensure at least 25% code coverage * Exclude Previews from code coverage * Specify Kover report path for SonarQube * Add Kover xml report task * Extract sonar to a separate step * Add some exclusions and minimum coverage * Exclude Hilt-generated classes * Add shopping list view model tests * Reduce the coverage requirement
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
package gq.kirmanak.mealient.datasource.ktor
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import gq.kirmanak.mealient.datasource.AuthenticationProvider
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import io.ktor.client.HttpClientConfig
|
||||
import io.ktor.client.engine.HttpClientEngineConfig
|
||||
import io.ktor.client.plugins.auth.Auth
|
||||
import io.ktor.client.plugins.auth.providers.BearerTokens
|
||||
import io.ktor.client.plugins.auth.providers.RefreshTokensParams
|
||||
import io.ktor.client.plugins.auth.providers.bearer
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import javax.inject.Inject
|
||||
@@ -27,14 +29,7 @@ internal class AuthKtorConfiguration @Inject constructor(
|
||||
}
|
||||
|
||||
refreshTokens {
|
||||
val newTokens = getTokens()
|
||||
val sameAccessToken = newTokens?.accessToken == oldTokens?.accessToken
|
||||
if (sameAccessToken && response.status == HttpStatusCode.Unauthorized) {
|
||||
authenticationProvider.logout()
|
||||
null
|
||||
} else {
|
||||
newTokens
|
||||
}
|
||||
refreshTokens()
|
||||
}
|
||||
|
||||
sendWithoutRequest { true }
|
||||
@@ -42,7 +37,20 @@ internal class AuthKtorConfiguration @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getTokens(): BearerTokens? {
|
||||
@VisibleForTesting
|
||||
suspend fun RefreshTokensParams.refreshTokens(): BearerTokens? {
|
||||
val newTokens = getTokens()
|
||||
val sameAccessToken = newTokens?.accessToken == oldTokens?.accessToken
|
||||
return if (sameAccessToken && response.status == HttpStatusCode.Unauthorized) {
|
||||
authenticationProvider.logout()
|
||||
null
|
||||
} else {
|
||||
newTokens
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
suspend fun getTokens(): BearerTokens? {
|
||||
val token = authenticationProvider.getAuthToken()
|
||||
logger.v { "getTokens(): token = $token" }
|
||||
return token?.let { BearerTokens(accessToken = it, refreshToken = "") }
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package gq.kirmanak.mealient.datasource
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import gq.kirmanak.mealient.datasource.ktor.AuthKtorConfiguration
|
||||
import gq.kirmanak.mealient.test.BaseUnitTest
|
||||
import io.ktor.client.plugins.auth.providers.BearerTokens
|
||||
import io.ktor.client.plugins.auth.providers.RefreshTokensParams
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
private const val AUTH_TOKEN = "token"
|
||||
|
||||
internal class AuthKtorConfigurationTest : BaseUnitTest() {
|
||||
|
||||
@MockK(relaxUnitFun = true)
|
||||
lateinit var authenticationProvider: AuthenticationProvider
|
||||
|
||||
private lateinit var subject: AuthKtorConfiguration
|
||||
|
||||
@Before
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
coEvery { authenticationProvider.getAuthToken() } returns AUTH_TOKEN
|
||||
subject = AuthKtorConfiguration(FakeProvider(authenticationProvider), logger)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getTokens returns BearerTokens with auth token`() = runTest {
|
||||
val bearerTokens = subject.getTokens()
|
||||
assertThat(bearerTokens?.accessToken).isEqualTo(AUTH_TOKEN)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getTokens returns BearerTokens without refresh token`() = runTest {
|
||||
val bearerTokens = subject.getTokens()
|
||||
assertThat(bearerTokens?.refreshToken).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refreshTokens returns new auth token if it doesn't match old`() = runTest {
|
||||
val refreshTokensParams = mockRefreshTokenParams(HttpStatusCode.Unauthorized, "old token")
|
||||
val actual = with(subject) { refreshTokensParams.refreshTokens() }
|
||||
assertThat(actual?.accessToken).isEqualTo(AUTH_TOKEN)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refreshTokens returns empty refresh token if auth token doesn't match old`() = runTest {
|
||||
val refreshTokensParams = mockRefreshTokenParams(HttpStatusCode.Unauthorized, "old token")
|
||||
val actual = with(subject) { refreshTokensParams.refreshTokens() }
|
||||
assertThat(actual?.refreshToken).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refreshTokens returns null if auth token matches old`() = runTest {
|
||||
val refreshTokensParams = mockRefreshTokenParams(HttpStatusCode.Unauthorized, AUTH_TOKEN)
|
||||
val actual = with(subject) { refreshTokensParams.refreshTokens() }
|
||||
assertThat(actual).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refreshTokens calls logout if auth token matches old`() = runTest {
|
||||
val refreshTokensParams = mockRefreshTokenParams(HttpStatusCode.Unauthorized, AUTH_TOKEN)
|
||||
with(subject) { refreshTokensParams.refreshTokens() }
|
||||
coVerify { authenticationProvider.logout() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refreshTokens does not logout if status code is not found`() = runTest {
|
||||
val refreshTokensParams = mockRefreshTokenParams(HttpStatusCode.NotFound, AUTH_TOKEN)
|
||||
with(subject) { refreshTokensParams.refreshTokens() }
|
||||
coVerify(inverse = true) { authenticationProvider.logout() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refreshTokens returns same access token if status code is not found`() = runTest {
|
||||
val refreshTokensParams = mockRefreshTokenParams(HttpStatusCode.NotFound, AUTH_TOKEN)
|
||||
val actual = with(subject) { refreshTokensParams.refreshTokens() }
|
||||
assertThat(actual?.accessToken).isEqualTo(AUTH_TOKEN)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refreshTokens returns empty refresh token if status code is not found`() = runTest {
|
||||
val refreshTokensParams = mockRefreshTokenParams(HttpStatusCode.NotFound, AUTH_TOKEN)
|
||||
val actual = with(subject) { refreshTokensParams.refreshTokens() }
|
||||
assertThat(actual?.refreshToken).isEmpty()
|
||||
}
|
||||
|
||||
private fun mockRefreshTokenParams(
|
||||
responseStatusCode: HttpStatusCode,
|
||||
oldAccessToken: String,
|
||||
): RefreshTokensParams {
|
||||
val notFoundResponse = mockk<HttpResponse> {
|
||||
every { status } returns responseStatusCode
|
||||
}
|
||||
val refreshTokensParams = mockk<RefreshTokensParams> {
|
||||
every { response } returns notFoundResponse
|
||||
every { oldTokens } returns BearerTokens(oldAccessToken, "")
|
||||
}
|
||||
return refreshTokensParams
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package gq.kirmanak.mealient.datasource
|
||||
|
||||
import javax.inject.Provider
|
||||
|
||||
data class FakeProvider<T>(
|
||||
val value: T,
|
||||
) : Provider<T> {
|
||||
|
||||
override fun get(): T = value
|
||||
}
|
||||
Reference in New Issue
Block a user