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:
Kirill Kamakin
2024-02-17 10:43:36 +01:00
committed by GitHub
parent 80baf11ec4
commit c03c65a96b
12 changed files with 431 additions and 59 deletions

View File

@@ -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 = "") }

View File

@@ -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
}
}

View File

@@ -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
}