From 64eed548b00f5ae400ca030db29f309cd0208629 Mon Sep 17 00:00:00 2001 From: Ruben Quadros Date: Thu, 25 Apr 2024 20:27:11 +0530 Subject: [PATCH] Improve test coverage. (#9) --- gradle/libs.versions.toml | 13 +- kovibes/build.gradle.kts | 8 +- .../rubenquadros/kovibes/api/test/KtorTest.kt | 24 +++ .../kovibes/api/test/ktor/KtorServiceTest.kt | 145 ++++++++++++++++++ kovibes/src/commonTest/resources/auth.json | 5 + .../rubenquadros/kovibes/api/test/KtorTest.kt | 27 ++++ 6 files changed, 211 insertions(+), 11 deletions(-) create mode 100644 kovibes/src/androidUnitTest/kotlin/io/github/rubenquadros/kovibes/api/test/KtorTest.kt create mode 100644 kovibes/src/commonTest/kotlin/io/github/rubenquadros/kovibes/api/test/ktor/KtorServiceTest.kt create mode 100644 kovibes/src/commonTest/resources/auth.json create mode 100644 kovibes/src/jvmTest/kotlin/io/github/rubenquadros/kovibes/api/test/KtorTest.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bfb83df..3c674c3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] agp = "8.2.2" -kotlin = "1.9.22" -ktor = "2.3.8" +kotlin = "1.9.23" +ktor = "2.3.9" coroutines = "1.8.0" kover = "0.7.6" android-minSdk = "24" @@ -9,15 +9,14 @@ android-compileSdk = "34" org-jetbrains-kotlin-jvm = "1.9.22" okio = "3.8.0" kotlin1922 = "1.9.22" -core-ktx = "1.12.0" compose-lifecycle = "2.7.0" -compose-activity = "1.8.2" -ksp = "1.9.21-1.0.15" +compose-activity = "1.9.0" +ksp = "1.9.23-1.0.19" koin = "3.5.3" koin-ksp = "1.3.1" immutable-collections = "0.3.7" coil = "2.6.0" -uiTooling = "1.6.2" +uiTooling = "1.6.6" dokka = "1.9.20" [libraries] @@ -49,7 +48,7 @@ coil = { module = "io.coil-kt:coil-compose", version.ref = "coil" } # compose compose-activity = { module = "androidx.activity:activity-compose", version.ref = "compose-activity" } compose-lifecycle = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "compose-lifecycle" } -compose-bom = "androidx.compose:compose-bom:2024.02.01" +compose-bom = "androidx.compose:compose-bom:2024.04.01" compose-ui = { module = "androidx.compose.ui:ui" } compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" } compose-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } diff --git a/kovibes/build.gradle.kts b/kovibes/build.gradle.kts index b881af6..315de93 100644 --- a/kovibes/build.gradle.kts +++ b/kovibes/build.gradle.kts @@ -93,8 +93,8 @@ koverReport { filters { excludes { classes( - "io.github.rubenquadros.kovibes.response.*", - "io.github.rubenquadros.kovibes.request.*", + "io.github.rubenquadros.kovibes.api.response.*", + "io.github.rubenquadros.kovibes.api.request.*", "io.github.rubenquadros.kovibes.api.config.*" ) annotatedBy("io.github.rubenquadros.kovibes.api.ExcludeFromCoverage") @@ -108,8 +108,8 @@ koverReport { filters { excludes { classes( - "io.github.rubenquadros.kovibes.response.*", - "io.github.rubenquadros.kovibes.request.*", + "io.github.rubenquadros.kovibes.api.response.*", + "io.github.rubenquadros.kovibes.api.request.*", "io.github.rubenquadros.kovibes.api.config.*" ) annotatedBy("io.github.rubenquadros.kovibes.api.ExcludeFromCoverage") diff --git a/kovibes/src/androidUnitTest/kotlin/io/github/rubenquadros/kovibes/api/test/KtorTest.kt b/kovibes/src/androidUnitTest/kotlin/io/github/rubenquadros/kovibes/api/test/KtorTest.kt new file mode 100644 index 0000000..816d39a --- /dev/null +++ b/kovibes/src/androidUnitTest/kotlin/io/github/rubenquadros/kovibes/api/test/KtorTest.kt @@ -0,0 +1,24 @@ +package io.github.rubenquadros.kovibes.api.test + +import io.github.rubenquadros.kovibes.api.getKtorEngine +import io.github.rubenquadros.kovibes.api.getKtorLogger +import io.ktor.client.engine.android.AndroidClientEngine +import io.ktor.client.plugins.logging.MessageLengthLimitingLogger +import kotlin.test.Test +import kotlin.test.assertTrue + +class KtorTest { + @Test + fun `ktor engine is provided`() { + val engine = getKtorEngine() + + assertTrue { engine is AndroidClientEngine } + } + + @Test + fun `logger is provided`() { + val logger = getKtorLogger() + + assertTrue { logger is MessageLengthLimitingLogger } + } +} \ No newline at end of file diff --git a/kovibes/src/commonTest/kotlin/io/github/rubenquadros/kovibes/api/test/ktor/KtorServiceTest.kt b/kovibes/src/commonTest/kotlin/io/github/rubenquadros/kovibes/api/test/ktor/KtorServiceTest.kt new file mode 100644 index 0000000..b869190 --- /dev/null +++ b/kovibes/src/commonTest/kotlin/io/github/rubenquadros/kovibes/api/test/ktor/KtorServiceTest.kt @@ -0,0 +1,145 @@ +package io.github.rubenquadros.kovibes.api.test.ktor + +import io.github.rubenquadros.kovibes.api.AuthStorage +import io.github.rubenquadros.kovibes.api.KtorService +import io.github.rubenquadros.kovibes.api.config.logger.LogLevel +import io.github.rubenquadros.kovibes.api.test.MockKtorService +import io.github.rubenquadros.kovibes.api.test.MockResponse +import io.github.rubenquadros.kovibes.api.test.errorResponsePath +import io.github.rubenquadros.kovibes.api.test.getExpectedResponse +import io.ktor.client.engine.HttpClientEngine +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respond +import io.ktor.client.plugins.logging.Logger +import io.ktor.client.plugins.logging.SIMPLE +import io.ktor.client.request.get +import io.ktor.http.Headers +import io.ktor.http.HttpStatusCode +import io.ktor.http.path +import io.ktor.utils.io.ByteReadChannel +import kotlinx.coroutines.test.runTest +import org.junit.Test +import kotlin.test.assertTrue + +class KtorServiceTest { + + private var shouldThrowException = false + + private var shouldThrowAuthError = false + + private val authStorage = AuthStorage() + + private val ktorService = KtorService( + authStorage = authStorage, + logLevel = { LogLevel.NONE }, + ktorEngine = { getKtorEngine() }, + ktorLogger = { Logger.SIMPLE } + ) + + @Test + fun `when authorization fails the auth token is refreshed`() = runTest { + MockKtorService.isSuccess = false + ktorService.client.get { + url { + path("browse/categories") + } + } + + assertTrue { authStorage.getAccessToken() == "token123" } + } + + @Test + fun `when token refresh fails then the auth token is not refreshed`() = runTest { + MockKtorService.isSuccess = false + shouldThrowAuthError = false + shouldThrowException = true + + ktorService.client.get { + url { + path("browse/categories") + } + } + + assertTrue { authStorage.getAccessToken().isEmpty() } + } + + @Test + fun `when token refresh fails then auth token is not refreshed`() = runTest { + MockKtorService.isSuccess = false + shouldThrowException = false + shouldThrowAuthError = true + + ktorService.client.get { + url { + path("browse/categories") + } + } + + assertTrue { authStorage.getAccessToken().isEmpty() } + } + + @Test + fun `when a valid auth token is present then it is not refreshed`() = runTest { + authStorage.updateAccessToken("token456") + + MockKtorService.isSuccess = true + shouldThrowAuthError = false + shouldThrowException = false + + ktorService.client.get { + url { + path("browse/categories") + } + } + + assertTrue { authStorage.getAccessToken() == "token456" } + } + + private fun getKtorEngine(): HttpClientEngine = MockEngine { + val url = it.url.toString() + val mockConfig = mapOf( + "browse/categories" to MockResponse( + expectedSuccessResponsePath = "browse/categories.json", + expectedErrorResponsePath = errorResponsePath + ), + "api/token" to MockResponse( + expectedSuccessResponsePath = "auth.json", + expectedErrorResponsePath = errorResponsePath + ) + ) + + for (config in mockConfig.keys) { + if (url.contains(config)) { + val response = mockConfig[config] + + assert(response != null) { + "There was no response for the path: $config" + } + + val (status, body) = when { + url.contains("browse") && !MockKtorService.isSuccess -> { + MockKtorService.isSuccess = true + HttpStatusCode.Unauthorized to getExpectedResponse(response!!.expectedErrorResponsePath) + } + url.contains("api/token") && shouldThrowException -> { + throw Exception("Error when fetching token from server.") + } + url.contains("api/token") && shouldThrowAuthError -> { + HttpStatusCode.InternalServerError to getExpectedResponse(response!!.expectedSuccessResponsePath) + } + else -> { + HttpStatusCode.OK to getExpectedResponse(response!!.expectedSuccessResponsePath) + } + } + + return@MockEngine respond( + content = ByteReadChannel(body), + status = status, + headers = Headers.build { this["content-type"] = "application/json" } + ) + } + } + + throw Exception("Request url: $url does not match the config. Please check the provided config.") + } +} \ No newline at end of file diff --git a/kovibes/src/commonTest/resources/auth.json b/kovibes/src/commonTest/resources/auth.json new file mode 100644 index 0000000..8eee36e --- /dev/null +++ b/kovibes/src/commonTest/resources/auth.json @@ -0,0 +1,5 @@ +{ + "access_token": "token123", + "token_type": "Access", + "expires_in": 3600 +} \ No newline at end of file diff --git a/kovibes/src/jvmTest/kotlin/io/github/rubenquadros/kovibes/api/test/KtorTest.kt b/kovibes/src/jvmTest/kotlin/io/github/rubenquadros/kovibes/api/test/KtorTest.kt new file mode 100644 index 0000000..b1486b0 --- /dev/null +++ b/kovibes/src/jvmTest/kotlin/io/github/rubenquadros/kovibes/api/test/KtorTest.kt @@ -0,0 +1,27 @@ +package io.github.rubenquadros.kovibes.api.test + +import io.github.rubenquadros.kovibes.api.getKtorEngine +import io.github.rubenquadros.kovibes.api.getKtorLogger +import io.ktor.client.engine.java.JavaHttpEngine +import io.ktor.client.plugins.logging.Logger +import io.ktor.util.reflect.instanceOf +import kotlin.test.Test +import kotlin.test.assertTrue + +class KtorTest { + + @Test + fun `ktor engine is provided`() { + val engine = getKtorEngine() + + assertTrue { engine is JavaHttpEngine } + } + + @Test + fun `logger is provided`() { + val logger = getKtorLogger() + + // We should have checked for SimpleLogger but it is a private class + assertTrue { logger.instanceOf(Logger::class) } + } +} \ No newline at end of file