Skip to content

Commit ea3bc4d

Browse files
committed
Merge branch 'dev' into nav3-migration-add-subscription-activity
# Conflicts: # app/src/main/AndroidManifest.xml # app/src/main/java/at/bitfire/icsdroid/MainActivity.kt # app/src/main/java/at/bitfire/icsdroid/ui/nav/Destination.kt # app/src/main/java/at/bitfire/icsdroid/ui/nav/MainApp.kt # app/src/main/java/at/bitfire/icsdroid/ui/screen/SubscriptionsScreen.kt # app/src/main/java/at/bitfire/icsdroid/ui/views/AddSubscriptionActivity.kt
2 parents 8855545 + bf05202 commit ea3bc4d

17 files changed

Lines changed: 315 additions & 226 deletions

File tree

.github/workflows/codeql.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525

2626
steps:
2727
- name: Checkout repository
28-
uses: actions/checkout@v4
28+
uses: actions/checkout@v5
2929
with:
3030
submodules: recursive
3131
- uses: actions/setup-java@v4

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
contents: write
1111
runs-on: ubuntu-latest
1212
steps:
13-
- uses: actions/checkout@v4
13+
- uses: actions/checkout@v5
1414
with:
1515
submodules: true
1616
- uses: actions/setup-java@v4

.github/workflows/test-dev.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ jobs:
55
name: Tests without emulator
66
runs-on: ubuntu-latest
77
steps:
8-
- uses: actions/checkout@v4
8+
- uses: actions/checkout@v5
99
with:
1010
submodules: true
1111
- uses: actions/setup-java@v4
@@ -33,7 +33,7 @@ jobs:
3333
matrix:
3434
api-level: [31]
3535
steps:
36-
- uses: actions/checkout@v4
36+
- uses: actions/checkout@v5
3737
with:
3838
submodules: true
3939
- uses: actions/setup-java@v4

app/build.gradle.kts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,9 @@ dependencies {
157157
implementation(libs.aboutLibs.compose)
158158
implementation(libs.jodaTime)
159159

160-
implementation(libs.okhttp.base)
160+
implementation(libs.ktor.core)
161+
implementation(libs.ktor.okhttp)
161162
implementation(libs.okhttp.brotli)
162-
implementation(libs.okhttp.coroutines)
163163

164164
// Room Database
165165
implementation(libs.room.base)
@@ -173,7 +173,7 @@ dependencies {
173173
androidTestImplementation(libs.androidx.test.runner)
174174
androidTestImplementation(libs.androidx.arch.core.testing)
175175
androidTestImplementation(libs.junit)
176-
androidTestImplementation(libs.okhttp.mockwebserver)
176+
androidTestImplementation(libs.ktor.mock)
177177
androidTestImplementation(libs.androidx.work.testing)
178178
androidTestImplementation(libs.hilt.android.testing)
179179

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)