Skip to content

Commit

Permalink
Improve test coverage. (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
rubenquadros authored Apr 25, 2024
1 parent 01f4bf9 commit 64eed54
Show file tree
Hide file tree
Showing 6 changed files with 211 additions and 11 deletions.
13 changes: 6 additions & 7 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
[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"
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]
Expand Down Expand Up @@ -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" }
Expand Down
8 changes: 4 additions & 4 deletions kovibes/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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 }
}
}
Original file line number Diff line number Diff line change
@@ -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.")
}
}
5 changes: 5 additions & 0 deletions kovibes/src/commonTest/resources/auth.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"access_token": "token123",
"token_type": "Access",
"expires_in": 3600
}
Original file line number Diff line number Diff line change
@@ -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) }
}
}

0 comments on commit 64eed54

Please sign in to comment.