Skip to content

Commit 11ad0d0

Browse files
ArnyminerZCopilot
andauthored
Replace okhttp with ktor (okhttp engine) (#620)
* Use ktor dependency Signed-off-by: Arnau Mora <arnyminerz@proton.me> * Migrate to ktor Signed-off-by: Arnau Mora <arnyminerz@proton.me> * Update app/src/main/java/at/bitfire/icsdroid/HttpClient.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Cleanup Signed-off-by: Arnau Mora <arnyminerz@proton.me> * Updated entry point Signed-off-by: Arnau Mora <arnyminerz@proton.me> * Do not use `removeFirst` Signed-off-by: Arnau Mora <arnyminerz@proton.me> * Add `Uri.toURL` Signed-off-by: Arnau Mora <arnyminerz@proton.me> * Move `HttpClient` to lateinit * Simplify initialization * Make `Response` private * Rename `MockEngineWrapper` to `MockServer` * Rename `HttpClient` to `AppHttpClient` * Move companion object to the bottom --------- Signed-off-by: Arnau Mora <arnyminerz@proton.me> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent eb24c06 commit 11ad0d0

12 files changed

Lines changed: 295 additions & 205 deletions

File tree

app/build.gradle.kts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,9 @@ dependencies {
153153
implementation(libs.aboutLibs.compose)
154154
implementation(libs.jodaTime)
155155

156-
implementation(libs.okhttp.base)
156+
implementation(libs.ktor.core)
157+
implementation(libs.ktor.okhttp)
157158
implementation(libs.okhttp.brotli)
158-
implementation(libs.okhttp.coroutines)
159159

160160
// Room Database
161161
implementation(libs.room.base)
@@ -167,7 +167,7 @@ dependencies {
167167
androidTestImplementation(libs.androidx.test.runner)
168168
androidTestImplementation(libs.androidx.arch.core.testing)
169169
androidTestImplementation(libs.junit)
170-
androidTestImplementation(libs.okhttp.mockwebserver)
170+
androidTestImplementation(libs.ktor.mock)
171171
androidTestImplementation(libs.androidx.work.testing)
172172
androidTestImplementation(libs.hilt.android.testing)
173173

app/src/androidTest/java/at/bitfire/icsdroid/CalendarFetcherTest.kt

Lines changed: 52 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,20 @@ import android.content.ContentResolver
88
import android.content.Context
99
import android.net.Uri
1010
import androidx.test.platform.app.InstrumentationRegistry
11-
import at.bitfire.icsdroid.HttpUtils.toAndroidUri
1211
import at.bitfire.icsdroid.test.BuildConfig
1312
import at.bitfire.icsdroid.test.R
14-
import kotlinx.coroutines.Dispatchers
13+
import io.ktor.client.utils.buildHeaders
14+
import io.ktor.http.ContentType
15+
import io.ktor.http.HttpHeaders
16+
import io.ktor.http.HttpStatusCode
17+
import io.ktor.http.headers
1518
import kotlinx.coroutines.runBlocking
16-
import kotlinx.coroutines.withContext
17-
import okhttp3.MediaType
18-
import okhttp3.mockwebserver.MockResponse
19-
import okhttp3.mockwebserver.MockWebServer
20-
import org.junit.AfterClass
2119
import org.junit.Assert.assertArrayEquals
2220
import org.junit.Assert.assertEquals
23-
import org.junit.BeforeClass
21+
import org.junit.Before
2422
import org.junit.Test
2523
import java.io.IOException
2624
import java.io.InputStream
27-
import java.net.HttpURLConnection
2825
import java.util.LinkedList
2926

3027
class CalendarFetcherTest {
@@ -34,29 +31,24 @@ class CalendarFetcherTest {
3431
val appContext: Context by lazy { InstrumentationRegistry.getInstrumentation().targetContext }
3532
val testContext: Context by lazy { InstrumentationRegistry.getInstrumentation().context }
3633

37-
val server = MockWebServer()
34+
}
3835

39-
@BeforeClass
40-
@JvmStatic
41-
fun setUp() {
42-
server.start()
43-
}
36+
private lateinit var client: AppHttpClient
4437

45-
@AfterClass
46-
@JvmStatic
47-
fun tearDown() {
48-
server.shutdown()
49-
}
38+
@Before
39+
fun setUp() {
40+
MockServer.clear()
5041

42+
client = MockServer.httpClient(appContext)
5143
}
5244

5345
@Test
5446
fun testFetchLocal_readsCorrectly() {
5547
val uri = Uri.parse("${ContentResolver.SCHEME_ANDROID_RESOURCE}://${BuildConfig.APPLICATION_ID}/${R.raw.vienna_evolution}")
5648

5749
var ical: String? = null
58-
val fetcher = object: CalendarFetcher(appContext, uri) {
59-
override suspend fun onSuccess(data: InputStream, contentType: MediaType?, eTag: String?, lastModified: Long?, displayName: String?) {
50+
val fetcher = object: CalendarFetcher(appContext, uri, client) {
51+
override suspend fun onSuccess(data: InputStream, contentType: ContentType?, eTag: String?, lastModified: Long?, displayName: String?) {
6052
ical = data.bufferedReader().use { it.readText() }
6153
}
6254
}
@@ -79,18 +71,21 @@ class CalendarFetcherTest {
7971
}
8072

8173
// create mock response
82-
server.enqueue(MockResponse()
83-
.setResponseCode(HttpURLConnection.HTTP_OK)
84-
.addHeader("ETag", etagCorrect)
85-
.addHeader("Last-Modified", lastModifiedCorrect)
86-
.setBody(icalCorrect))
74+
MockServer.enqueue(
75+
content = icalCorrect,
76+
status = HttpStatusCode.OK,
77+
headers = buildHeaders {
78+
append(HttpHeaders.ETag, etagCorrect)
79+
append(HttpHeaders.LastModified, lastModifiedCorrect)
80+
}
81+
)
8782

8883
// make request to local mock server
8984
var ical: String? = null
9085
var etag: String? = null
9186
var lastmod: Long? = null
92-
val fetcher = object: CalendarFetcher(appContext, server.url("/").toAndroidUri()) {
93-
override suspend fun onSuccess(data: InputStream, contentType: MediaType?, eTag: String?, lastModified: Long?, displayName: String?) {
87+
val fetcher = object: CalendarFetcher(appContext, MockServer.uri(), client) {
88+
override suspend fun onSuccess(data: InputStream, contentType: ContentType?, eTag: String?, lastModified: Long?, displayName: String?) {
9489
ical = data.bufferedReader().use { it.readText() }
9590
etag = eTag
9691
lastmod = lastModified
@@ -110,28 +105,37 @@ class CalendarFetcherTest {
110105
fun testFetchNetwork_onRedirectWithLocation() {
111106
// create mock responses:
112107
// 1. redirect with absolute target URL
113-
server.enqueue(MockResponse()
114-
.setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
115-
.addHeader("Location", server.url("new-location/vienna-evolution.ics")))
108+
MockServer.enqueue(
109+
status = HttpStatusCode.TemporaryRedirect,
110+
headers = headers {
111+
append(
112+
HttpHeaders.Location, MockServer.uri("new-location", "vienna-evolution.ics").toString()
113+
)
114+
}
115+
)
116116
// 2. redirect with relative target URL
117-
server.enqueue(MockResponse()
118-
.setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
119-
.addHeader("Location", "the-file-is-here"))
117+
MockServer.enqueue(
118+
status = HttpStatusCode.TemporaryRedirect,
119+
headers = headers {
120+
append(HttpHeaders.Location, "the-file-is-here")
121+
}
122+
)
120123
// 3. finally the real resource
121-
server.enqueue(MockResponse()
122-
.setResponseCode(HttpURLConnection.HTTP_OK)
123-
.setBody("icalCorrect"))
124+
MockServer.enqueue(
125+
content = "icalCorrect",
126+
status = HttpStatusCode.OK
127+
)
124128

125129
// make initial request to local mock server
126-
val baseUrl = server.url("/").toAndroidUri()
130+
val baseUrl = MockServer.uri()
127131
var ical: String? = null
128132
val redirects = LinkedList<Uri>()
129-
val fetcher = object: CalendarFetcher(appContext, baseUrl) {
130-
override suspend fun onRedirect(httpCode: Int, target: Uri) {
133+
val fetcher = object: CalendarFetcher(appContext, baseUrl, client) {
134+
override suspend fun onRedirect(httpCode: HttpStatusCode, target: Uri) {
131135
redirects += target
132136
super.onRedirect(httpCode, target)
133137
}
134-
override suspend fun onSuccess(data: InputStream, contentType: MediaType?, eTag: String?, lastModified: Long?, displayName: String?) {
138+
override suspend fun onSuccess(data: InputStream, contentType: ContentType?, eTag: String?, lastModified: Long?, displayName: String?) {
135139
ical = data.bufferedReader().use { it.readText() }
136140
}
137141
}
@@ -157,12 +161,11 @@ class CalendarFetcherTest {
157161

158162
@Test
159163
fun testFetchNetwork_onRedirectWithoutLocation() {
160-
server.enqueue(MockResponse()
161-
.setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP))
164+
MockServer.enqueue(status = HttpStatusCode.TemporaryRedirect)
162165

163166
var e: Exception? = null
164167
runBlocking {
165-
object : CalendarFetcher(appContext, server.url("/").toAndroidUri()) {
168+
object : CalendarFetcher(appContext, MockServer.uri(), client) {
166169
override suspend fun onError(error: Exception) {
167170
e = error
168171
}
@@ -174,12 +177,11 @@ class CalendarFetcherTest {
174177

175178
@Test
176179
fun testFetchNetwork_onNotModified() {
177-
server.enqueue(MockResponse()
178-
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED))
180+
MockServer.enqueue(status = HttpStatusCode.NotModified)
179181

180182
var notModified = false
181183
runBlocking {
182-
object : CalendarFetcher(appContext, server.url("/").toAndroidUri()) {
184+
object : CalendarFetcher(appContext, MockServer.uri(), client) {
183185
override suspend fun onNotModified() {
184186
notModified = true
185187
}
@@ -191,12 +193,11 @@ class CalendarFetcherTest {
191193

192194
@Test
193195
fun testFetchNetwork_onError() {
194-
server.enqueue(MockResponse()
195-
.setResponseCode(HttpURLConnection.HTTP_NOT_FOUND))
196+
MockServer.enqueue(status = HttpStatusCode.NotFound)
196197

197198
var e: Exception? = null
198199
runBlocking {
199-
object : CalendarFetcher(appContext, server.url("/").toAndroidUri()) {
200+
object : CalendarFetcher(appContext, MockServer.uri(), client) {
200201
override suspend fun onError(error: Exception) {
201202
e = error
202203
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package at.bitfire.icsdroid
2+
3+
import android.content.Context
4+
import at.bitfire.icsdroid.HttpUtils.toUri
5+
import io.ktor.client.engine.mock.MockEngine
6+
import io.ktor.client.engine.mock.respond
7+
import io.ktor.http.Headers
8+
import io.ktor.http.HttpStatusCode
9+
import io.ktor.http.URLBuilder
10+
import io.ktor.http.appendPathSegments
11+
import io.ktor.http.headersOf
12+
import io.ktor.http.toURI
13+
import java.util.concurrent.locks.ReentrantLock
14+
import kotlin.concurrent.withLock
15+
16+
object MockServer {
17+
private val lock = ReentrantLock()
18+
19+
private val queue = mutableListOf<Response>()
20+
21+
val engine = MockEngine {
22+
if (queue.isNotEmpty()) {
23+
val response = lock.withLock { queue.removeAt(0) }
24+
respond(response.content, response.status, response.headers)
25+
} else {
26+
respond("No more responses", HttpStatusCode.NotImplemented)
27+
}
28+
}
29+
30+
fun clear() {
31+
queue.clear()
32+
}
33+
34+
private fun enqueue(response: Response) {
35+
lock.withLock {
36+
queue.add(response)
37+
}
38+
}
39+
40+
fun enqueue(
41+
content: String = "",
42+
status: HttpStatusCode = HttpStatusCode.OK,
43+
headers: Headers = headersOf()
44+
) = enqueue(Response(content, status, headers))
45+
46+
fun uri(vararg segments: String) = URLBuilder("http://localhost")
47+
.appendPathSegments(*segments)
48+
.build()
49+
.toURI()
50+
.toUri()
51+
52+
fun httpClient(context: Context) = AppHttpClient(context, engine)
53+
54+
private class Response(val content: String, val status: HttpStatusCode, val headers: Headers)
55+
}

app/src/androidTest/java/at/bitfire/icsdroid/model/ValidationUseCaseTest.kt

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,13 @@ import android.app.Application
88
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
99
import androidx.test.platform.app.InstrumentationRegistry
1010
import at.bitfire.ical4android.Css3Color
11-
import at.bitfire.icsdroid.HttpUtils.toAndroidUri
11+
import at.bitfire.icsdroid.AppHttpClient
12+
import at.bitfire.icsdroid.MockServer
1213
import at.bitfire.icsdroid.ui.ResourceInfo
1314
import kotlinx.coroutines.runBlocking
14-
import okhttp3.mockwebserver.MockResponse
15-
import okhttp3.mockwebserver.MockWebServer
16-
import org.junit.AfterClass
1715
import org.junit.Assert.assertEquals
1816
import org.junit.Assert.assertNull
19-
import org.junit.BeforeClass
17+
import org.junit.Before
2018
import org.junit.Rule
2119
import org.junit.Test
2220

@@ -26,26 +24,15 @@ class ValidationUseCaseTest {
2624

2725
val app = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application
2826

29-
lateinit var server: MockWebServer
30-
31-
@BeforeClass
32-
@JvmStatic
33-
fun setUp() {
34-
server = MockWebServer()
35-
server.start()
36-
}
37-
38-
@AfterClass
39-
@JvmStatic
40-
fun tearDown() {
41-
server.shutdown()
42-
}
43-
4427
}
4528

4629
@get:Rule
4730
val instantTaskExecutor = InstantTaskExecutorRule()
4831

32+
@Before
33+
fun setUp() {
34+
MockServer.clear()
35+
}
4936

5037
@Test
5138
fun testModelInitialize_CalendarProperties_None() {
@@ -96,12 +83,13 @@ class ValidationUseCaseTest {
9683
}
9784

9885
private fun validate(iCal: String): ResourceInfo {
99-
server.enqueue(MockResponse().setBody(iCal))
86+
MockServer.enqueue(content = iCal)
10087

101-
val model = ValidationUseCase(app)
88+
val client = AppHttpClient(app, MockServer.engine)
89+
val model = ValidationUseCase(app, client)
10290
runBlocking {
10391
// Wait until the validation completed
104-
model.validate(server.url("/").toAndroidUri(), null, null).join()
92+
model.validate(MockServer.uri(), null, null).join()
10593
}
10694

10795
return model.uiState.result!!

0 commit comments

Comments
 (0)