Skip to content

Commit a9f47c0

Browse files
authored
test(KtorRepoItemApiTest) (#28)
1 parent 75618d9 commit a9f47c0

File tree

6 files changed

+3440
-12
lines changed

6 files changed

+3440
-12
lines changed

shared/src/androidMain/kotlin/com/hoc081098/github_search_kmm/data/DataModule.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.hoc081098.github_search_kmm.data.remote.DaggerKtorRepoItemApi
44
import com.hoc081098.github_search_kmm.data.remote.GithubLanguageColorApi
55
import com.hoc081098.github_search_kmm.data.remote.RepoItemApi
66
import com.hoc081098.github_search_kmm.data.remote.createHttpClient
7+
import com.hoc081098.github_search_kmm.data.remote.createJson
78
import com.hoc081098.github_search_kmm.domain.repository.RepoItemRepository
89
import dagger.Binds
910
import dagger.Module
@@ -15,6 +16,7 @@ import io.ktor.client.engine.okhttp.OkHttp
1516
import io.ktor.http.Url
1617
import javax.inject.Qualifier
1718
import javax.inject.Singleton
19+
import kotlinx.serialization.json.Json
1820

1921
@MustBeDocumented
2022
@Retention(AnnotationRetention.RUNTIME)
@@ -57,8 +59,13 @@ internal interface DataModule {
5759

5860
@Provides
5961
@Singleton
60-
internal fun httpClient(): HttpClient = createHttpClient(
61-
engineFactory = OkHttp
62+
internal fun json(): Json = createJson()
63+
64+
@Provides
65+
@Singleton
66+
internal fun httpClient(json: Json): HttpClient = createHttpClient(
67+
engineFactory = OkHttp,
68+
json = json
6269
) {}
6370
}
6471
}

shared/src/commonMain/kotlin/com/hoc081098/github_search_kmm/data/remote/HttpClient.kt

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import io.github.aakira.napier.Napier
44
import io.ktor.client.HttpClient
55
import io.ktor.client.engine.HttpClientEngineConfig
66
import io.ktor.client.engine.HttpClientEngineFactory
7+
import io.ktor.client.plugins.HttpTimeout
78
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
89
import io.ktor.client.plugins.logging.LogLevel
910
import io.ktor.client.plugins.logging.Logger
@@ -21,22 +22,34 @@ import kotlinx.serialization.encoding.Encoder
2122
import kotlinx.serialization.json.Json
2223
import kotlinx.serialization.modules.SerializersModule
2324

25+
fun createJson(): Json = Json {
26+
serializersModule = SerializersModule {
27+
contextual(Instant::class, InstantSerializer)
28+
}
29+
ignoreUnknownKeys = true
30+
coerceInputValues = true
31+
prettyPrint = true
32+
isLenient = true
33+
encodeDefaults = true
34+
allowSpecialFloatingPointValues = true
35+
allowStructuredMapKeys = true
36+
useArrayPolymorphism = false
37+
}
38+
2439
fun <T : HttpClientEngineConfig> createHttpClient(
2540
engineFactory: HttpClientEngineFactory<T>,
41+
json: Json,
2642
block: T.() -> Unit,
2743
): HttpClient = HttpClient(engineFactory) {
2844
engine(block)
2945

46+
install(HttpTimeout) {
47+
requestTimeoutMillis = 15_000
48+
connectTimeoutMillis = 10_000
49+
socketTimeoutMillis = 10_000
50+
}
51+
3052
install(ContentNegotiation) {
31-
val json = Json {
32-
serializersModule = SerializersModule {
33-
contextual(Instant::class, InstantSerializer)
34-
}
35-
ignoreUnknownKeys = true
36-
coerceInputValues = true
37-
prettyPrint = true
38-
isLenient = true
39-
}
4053
json(json)
4154
register(
4255
ContentType.Text.Plain,

shared/src/commonTest/kotlin/com/hoc081098/github_search_kmm/data/RepoItemRepositoryImplTest.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ package com.hoc081098.github_search_kmm.data
33
import arrow.core.getOrHandle
44
import arrow.core.left
55
import arrow.core.right
6+
import com.hoc081098.github_search_kmm.TestAntilog
67
import com.hoc081098.github_search_kmm.TestAppCoroutineDispatchers
78
import com.hoc081098.github_search_kmm.data.remote.GithubLanguageColorApi
89
import com.hoc081098.github_search_kmm.data.remote.RepoItemApi
910
import com.hoc081098.github_search_kmm.data.remote.response.RepoItemsSearchResponse
1011
import com.hoc081098.github_search_kmm.domain.model.AppError
1112
import com.hoc081098.github_search_kmm.domain.model.ArgbColor
13+
import io.github.aakira.napier.Napier
1214
import io.mockative.Mock
1315
import io.mockative.given
1416
import io.mockative.mock
@@ -40,10 +42,12 @@ class RepoItemRepositoryImplTest {
4042
private lateinit var errorMapper: AppErrorMapper
4143

4244
private val appCoroutineDispatchers = TestAppCoroutineDispatchers()
45+
private val antilog = TestAntilog()
4346

4447
@BeforeTest
4548
fun setup() {
4649
Dispatchers.setMain(appCoroutineDispatchers.testCoroutineDispatcher)
50+
Napier.base(antilog)
4751

4852
repoItemApi = mock(RepoItemApi::class)
4953
githubLanguageColorApi = mock(GithubLanguageColorApi::class)
@@ -63,6 +67,8 @@ class RepoItemRepositoryImplTest {
6367
verify(it).hasNoUnverifiedExpectations()
6468
verify(it).hasNoUnmetExpectations()
6569
}
70+
71+
Napier.takeLogarithm(antilog)
6672
Dispatchers.resetMain()
6773
}
6874

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package com.hoc081098.github_search_kmm.data.remote
2+
3+
import arrow.core.getOrHandle
4+
import arrow.core.identity
5+
import com.hoc081098.github_search_kmm.TestAntilog
6+
import com.hoc081098.github_search_kmm.TestAppCoroutineDispatchers
7+
import com.hoc081098.github_search_kmm.readTextResource
8+
import io.github.aakira.napier.Napier
9+
import io.ktor.client.HttpClient
10+
import io.ktor.client.engine.mock.MockEngine
11+
import io.ktor.client.engine.mock.MockRequestHandler
12+
import io.ktor.client.engine.mock.respond
13+
import io.ktor.http.ContentType
14+
import io.ktor.http.HttpHeaders
15+
import io.ktor.http.HttpMethod
16+
import io.ktor.http.HttpStatusCode
17+
import io.ktor.http.Url
18+
import io.ktor.http.headersOf
19+
import kotlin.test.AfterTest
20+
import kotlin.test.BeforeTest
21+
import kotlin.test.Test
22+
import kotlin.test.assertEquals
23+
import kotlin.test.fail
24+
import kotlinx.coroutines.channels.Channel
25+
import kotlinx.coroutines.test.runTest
26+
import kotlinx.serialization.decodeFromString
27+
28+
class KtorRepoItemApiTest {
29+
private lateinit var ktorRepoItemApi: KtorRepoItemApi
30+
private lateinit var httpClient: HttpClient
31+
private lateinit var handlerChannel: Channel<MockRequestHandler>
32+
33+
private val baseUrl = Url("https://127.0.0.1:8080")
34+
private val testAppCoroutineDispatchers = TestAppCoroutineDispatchers()
35+
private val json = createJson()
36+
private val antilog = TestAntilog()
37+
38+
@BeforeTest
39+
fun setup() {
40+
Napier.base(antilog)
41+
42+
handlerChannel = Channel(Channel.UNLIMITED)
43+
httpClient = createHttpClient(MockEngine, json) {
44+
addHandler { request ->
45+
handlerChannel.receive()(request)
46+
}
47+
}
48+
ktorRepoItemApi = KtorRepoItemApi(
49+
httpClient = httpClient,
50+
baseUrl = baseUrl,
51+
appCoroutineDispatchers = testAppCoroutineDispatchers
52+
)
53+
}
54+
55+
@AfterTest
56+
fun teardown() {
57+
httpClient.close()
58+
handlerChannel.close()
59+
Napier.takeLogarithm(antilog)
60+
}
61+
62+
@Test
63+
fun `searchRepoItems returns a Right WHEN httpClient returns a successful response`() =
64+
runTest(testAppCoroutineDispatchers.testCoroutineDispatcher) {
65+
handlerChannel.trySend { request ->
66+
when (request.url.encodedPath) {
67+
"/search/repositories" -> {
68+
check(request.method == HttpMethod.Get)
69+
checkNotNull(request.url.parameters["q"])
70+
checkNotNull(request.url.parameters["page"])
71+
72+
respond(
73+
content = readTextResource("search_repositories_response.json"),
74+
status = HttpStatusCode.OK,
75+
headers = headersOf(HttpHeaders.ContentType, ContentType.Text.Plain.toString())
76+
)
77+
}
78+
else -> error("Unhandled request ${request.url}")
79+
}
80+
}
81+
82+
val either = ktorRepoItemApi.searchRepoItems(
83+
term = "kmm",
84+
page = 1
85+
)
86+
87+
assertEquals(
88+
json.decodeFromString(readTextResource("search_repositories_response.json")),
89+
either.getOrHandle { throw it }
90+
)
91+
}
92+
93+
@Test
94+
fun `searchRepoItems returns a Left WHEN httpClient returns a failure response`() =
95+
runTest(testAppCoroutineDispatchers.testCoroutineDispatcher) {
96+
handlerChannel.trySend { request ->
97+
when (request.url.encodedPath) {
98+
"/search/repositories" -> {
99+
check(request.method == HttpMethod.Get)
100+
checkNotNull(request.url.parameters["q"])
101+
checkNotNull(request.url.parameters["page"])
102+
103+
respond(
104+
"{}",
105+
status = HttpStatusCode.InternalServerError,
106+
headers = headersOf(
107+
HttpHeaders.ContentType,
108+
ContentType.Application.Json.toString()
109+
)
110+
)
111+
}
112+
else -> error("Unhandled request ${request.url}")
113+
}
114+
}
115+
116+
val either = ktorRepoItemApi.searchRepoItems(
117+
term = "kmm",
118+
page = 1
119+
)
120+
121+
either.fold(ifLeft = ::identity, ifRight = { fail("Expected Left, but got Right") })
122+
}
123+
}

0 commit comments

Comments
 (0)