From d7d9079b933b52a64d5fd248d93649c1140d9693 Mon Sep 17 00:00:00 2001
From: ammann <161159040+agammann@users.noreply.github.com>
Date: Tue, 15 Sep 2026 23:17:47 -0700
Subject: [PATCH 1/3] fix: read current state after firstSettled refresh
---
.../domain/base/DataStateObservableUseCase.kt | 3 +-
.../domain/base/FirstSettledTest.kt | 74 +++++++++++++++++++
2 files changed, 76 insertions(+), 1 deletion(-)
create mode 100644 domain/src/commonTest/kotlin/com/blockstream/domain/base/FirstSettledTest.kt
diff --git a/domain/src/commonMain/kotlin/com/blockstream/domain/base/DataStateObservableUseCase.kt b/domain/src/commonMain/kotlin/com/blockstream/domain/base/DataStateObservableUseCase.kt
index 54b570f61..54218c63e 100644
--- a/domain/src/commonMain/kotlin/com/blockstream/domain/base/DataStateObservableUseCase.kt
+++ b/domain/src/commonMain/kotlin/com/blockstream/domain/base/DataStateObservableUseCase.kt
@@ -122,7 +122,8 @@ abstract class DataStateObservableUseCase
: ObservableUseCase
DataStateObservableUseCase
.firstSettled(params: P): DataState {
invoke(params)
- return observe().first { !it.isLoading() }
+ // The shared observer may still replay the value from before this invocation.
+ return get().first { !it.isLoading() }
}
/**
diff --git a/domain/src/commonTest/kotlin/com/blockstream/domain/base/FirstSettledTest.kt b/domain/src/commonTest/kotlin/com/blockstream/domain/base/FirstSettledTest.kt
new file mode 100644
index 000000000..71e65d92b
--- /dev/null
+++ b/domain/src/commonTest/kotlin/com/blockstream/domain/base/FirstSettledTest.kt
@@ -0,0 +1,74 @@
+package com.blockstream.domain.base
+
+import com.blockstream.data.data.DataState
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+
+class FirstSettledTest {
+ private class RefreshUseCase : DataStateObservableUseCase() {
+ var next: DataState = DataState.Success(1)
+ var canExecute = true
+ val releaseObserver = CompletableDeferred()
+
+ override fun shouldExecute(params: Unit) = canExecute
+
+ override suspend fun doAsyncWork(params: Unit) = set(next)
+
+ override fun createObservable(params: Unit): Flow> = get().onEach {
+ // Keep the shared observer behind a completed refresh, without relying on timing.
+ if (it != DataState.Success(1)) releaseObserver.await()
+ }
+
+ fun publish(state: DataState) = set(state)
+ }
+
+ @Test
+ fun completedRefreshDoesNotReturnThePreviousSharedReplay() = runTest {
+ val results = listOf(DataState.Success(2), DataState.Empty, DataState.Error(Exception("refresh failed")))
+ for (result in results) {
+ val useCase = RefreshUseCase()
+ try {
+ useCase(Unit)
+ assertEquals(DataState.Success(1), useCase.observe().first())
+ useCase.next = result
+
+ assertEquals(result, useCase.firstSettled())
+ assertEquals(result, useCase.getCurrent())
+ } finally {
+ useCase.releaseObserver.complete(Unit)
+ }
+ }
+ }
+
+ @Test
+ fun skippedRefreshReturnsTheCurrentSettledState() = runTest {
+ val useCase = RefreshUseCase()
+ useCase(Unit)
+ useCase.canExecute = false
+ useCase.next = DataState.Success(2)
+
+ assertEquals(DataState.Success(1), useCase.firstSettled(Unit))
+ }
+
+ @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+ @Test
+ fun skippedRefreshWaitsWhileTheCurrentStateIsLoading() = runTest {
+ val useCase = RefreshUseCase()
+ useCase.canExecute = false
+ val result = async { useCase.firstSettled() }
+ runCurrent()
+ assertFalse(result.isCompleted)
+
+ useCase.publish(DataState.Empty)
+
+ assertEquals(DataState.Empty, result.await())
+ }
+}
From 6fea6ab2fdf098ceb541f3c660583bb273f5593b Mon Sep 17 00:00:00 2001
From: ammann <161159040+agammann@users.noreply.github.com>
Date: Tue, 15 Sep 2026 23:52:03 -0700
Subject: [PATCH 2/3] fix: handle empty and incomplete chart price history
---
.../mapper/BitcoinChartPriceMapper.kt | 6 +-
.../model/NetworkBitcoinPriceData.kt | 6 +-
.../mapper/BitcoinChartPriceMapperTest.kt | 57 +++++++++++++++++++
3 files changed, 63 insertions(+), 6 deletions(-)
create mode 100644 data/src/commonTest/kotlin/com/blockstream/data/btcpricehistory/mapper/BitcoinChartPriceMapperTest.kt
diff --git a/data/src/commonMain/kotlin/com/blockstream/data/btcpricehistory/mapper/BitcoinChartPriceMapper.kt b/data/src/commonMain/kotlin/com/blockstream/data/btcpricehistory/mapper/BitcoinChartPriceMapper.kt
index a4368eeef..dfc2158dc 100644
--- a/data/src/commonMain/kotlin/com/blockstream/data/btcpricehistory/mapper/BitcoinChartPriceMapper.kt
+++ b/data/src/commonMain/kotlin/com/blockstream/data/btcpricehistory/mapper/BitcoinChartPriceMapper.kt
@@ -6,11 +6,12 @@ import com.blockstream.data.btcpricehistory.model.NetworkBitcoinPriceData
import com.blockstream.data.btcpricehistory.model.timeAgoInMillis
import kotlin.time.Clock
-fun NetworkBitcoinPriceData.asChartData(): BitcoinChartData {
+fun NetworkBitcoinPriceData.asChartData(): BitcoinChartData? {
val data = this
val prices = mutableMapOf>>() //timestamp, price
val dailyPrices = data.dailyPrices.mapAndSortNotNullPrices()
+ val currentPrice = dailyPrices.lastOrNull()?.second ?: return null
val monthlyPrices = data.monthlyPrices.mapAndSortNotNullPrices()
val fullPrices = data.fullPrices.mapAndSortNotNullPrices()
@@ -21,7 +22,6 @@ fun NetworkBitcoinPriceData.asChartData(): BitcoinChartData {
prices[BitcoinChartPeriod.ONE_YEAR] = fullPrices.filter { it.first >= BitcoinChartPeriod.ONE_YEAR.timeAgoInMillis() }
prices[BitcoinChartPeriod.FIVE_YEAR] = fullPrices.filter { it.first >= BitcoinChartPeriod.FIVE_YEAR.timeAgoInMillis() }
- val currentPrice = dailyPrices.last().second
val lastRefreshedAt = Clock.System.now().toEpochMilliseconds()
return BitcoinChartData(
@@ -43,4 +43,4 @@ private fun List>.mapAndSortNotNullPrices(): List> = emptyList()
- get() = field.sortedBy { it[0] }
+ get() = field.sortedBy { it.getOrNull(0) }
@SerialName("prices_full")
val fullPrices: List> = emptyList()
- get() = field.sortedBy { it[0] }
+ get() = field.sortedBy { it.getOrNull(0) }
@SerialName("prices_month")
val monthlyPrices: List> = emptyList()
- get() = field.sortedBy { it[0] }
+ get() = field.sortedBy { it.getOrNull(0) }
}
diff --git a/data/src/commonTest/kotlin/com/blockstream/data/btcpricehistory/mapper/BitcoinChartPriceMapperTest.kt b/data/src/commonTest/kotlin/com/blockstream/data/btcpricehistory/mapper/BitcoinChartPriceMapperTest.kt
new file mode 100644
index 000000000..8ac09ae71
--- /dev/null
+++ b/data/src/commonTest/kotlin/com/blockstream/data/btcpricehistory/mapper/BitcoinChartPriceMapperTest.kt
@@ -0,0 +1,57 @@
+package com.blockstream.data.btcpricehistory.mapper
+
+import com.blockstream.data.btcpricehistory.model.BitcoinChartPeriod
+import com.blockstream.data.btcpricehistory.model.NetworkBitcoinPriceData
+import com.blockstream.data.data.DataState
+import kotlinx.serialization.json.Json
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.time.Clock
+
+class BitcoinChartPriceMapperTest {
+ @Test
+ fun missingOrUnusableDailyPricesProduceAnEmptyState() {
+ val responses = listOf(
+ """{"currency":"USD"}""",
+ """{"currency":"USD","prices_day":[]}""",
+ """{"currency":"USD","prices_day":[[],[1000],[null,10],[1000,null]]}""",
+ """{"currency":"USD","prices_day":[],"prices_month":[[1000,10]],"prices_full":[[1000,10]]}""",
+ )
+ for (response in responses) {
+ val chart = Json.decodeFromString(response).asChartData()
+ assertNull(chart, response)
+ assertEquals(DataState.Empty, DataState.successOrEmpty(chart))
+ }
+ }
+
+ @Test
+ fun malformedRowsAreSkippedInEveryPriceSeries() {
+ val now = Clock.System.now().toEpochMilliseconds()
+ val earlier = now - 1000
+ val rows = "[[],[$now,20],[$earlier],[$earlier,10],[null,30],[$now,null]]"
+ val response = """{"currency":"USD","prices_day":$rows,"prices_month":$rows,"prices_full":$rows}"""
+
+ val chart = assertNotNull(Json.decodeFromString(response).asChartData())
+
+ assertEquals("USD", chart.currency)
+ assertEquals(20f, chart.currentPrice)
+ for (period in BitcoinChartPeriod.entries) {
+ assertEquals(listOf(earlier to 10f, now to 20f), chart.prices[period], period.name)
+ }
+ }
+
+ @Test
+ fun validDailyPricesAreSortedAndTheLatestPriceIsUsed() {
+ val response = """{"currency":"EUR","prices_day":[[2000,20],[1000,10]]}"""
+
+ val chart = assertNotNull(Json.decodeFromString(response).asChartData())
+
+ assertEquals("EUR", chart.currency)
+ assertEquals(20f, chart.currentPrice)
+ assertEquals(listOf(1000L to 10f, 2000L to 20f), chart.prices[BitcoinChartPeriod.ONE_DAY])
+ assertEquals(emptyList(), chart.prices[BitcoinChartPeriod.ONE_MONTH])
+ assertEquals(emptyList(), chart.prices[BitcoinChartPeriod.FIVE_YEAR])
+ }
+}
From 85d057705a8dcedcf9823507e1c6bccee6ca5a9c Mon Sep 17 00:00:00 2001
From: ammann <161159040+agammann@users.noreply.github.com>
Date: Tue, 15 Sep 2026 23:52:57 -0700
Subject: [PATCH 3/3] test: exercise Meld HTTP contracts without production
service calls
---
data/build.gradle.kts | 3 +-
.../blockstream/data/meld/MeldHttpClient.kt | 10 +-
.../commonTest/kotlin/MeldRepositoryTest.kt | 148 ++++++++++++------
gradle/libs.versions.toml | 1 +
.../com/blockstream/network/AppHttpClient.kt | 16 +-
5 files changed, 123 insertions(+), 55 deletions(-)
diff --git a/data/build.gradle.kts b/data/build.gradle.kts
index ea205b695..5ac96dac6 100644
--- a/data/build.gradle.kts
+++ b/data/build.gradle.kts
@@ -158,6 +158,7 @@ kotlin {
}
commonTest.dependencies {
+ implementation(libs.ktor.client.mock)
implementation(libs.kotlin.test)
implementation(libs.kotlinx.coroutines.test)
implementation(libs.koin.test)
@@ -231,4 +232,4 @@ tasks.configureEach {
tasks.getByName("clean").doFirst {
delete(project.file("src/include"))
delete(project.file("src/libs"))
-}
\ No newline at end of file
+}
diff --git a/data/src/commonMain/kotlin/com/blockstream/data/meld/MeldHttpClient.kt b/data/src/commonMain/kotlin/com/blockstream/data/meld/MeldHttpClient.kt
index b7fe3ab8b..a152db453 100644
--- a/data/src/commonMain/kotlin/com/blockstream/data/meld/MeldHttpClient.kt
+++ b/data/src/commonMain/kotlin/com/blockstream/data/meld/MeldHttpClient.kt
@@ -2,12 +2,16 @@ package com.blockstream.data.meld
import com.blockstream.utils.Loggable
import com.blockstream.network.AppHttpClient
+import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.defaultRequest
import io.ktor.http.ContentType
import io.ktor.http.contentType
-class MeldHttpClient(appInfo: com.blockstream.data.config.AppInfo) : AppHttpClient(appInfo.isDevelopmentOrDebug, {
+class MeldHttpClient(
+ appInfo: com.blockstream.data.config.AppInfo,
+ engine: HttpClientEngine? = null,
+) : AppHttpClient(appInfo.isDevelopmentOrDebug, {
install(HttpTimeout) {
this.requestTimeoutMillis = 60_000
this.connectTimeoutMillis = 30_000
@@ -18,9 +22,9 @@ class MeldHttpClient(appInfo: com.blockstream.data.config.AppInfo) : AppHttpClie
?: MELD_SANDBOX)
contentType(ContentType.Application.Json)
}
-}) {
+}, engine) {
companion object : Loggable() {
private const val MELD_PRODUCTION = "https://ramps.blockstream.com"
private const val MELD_SANDBOX = "https://ramps.sandbox.blockstream.com"
}
-}
\ No newline at end of file
+}
diff --git a/data/src/commonTest/kotlin/MeldRepositoryTest.kt b/data/src/commonTest/kotlin/MeldRepositoryTest.kt
index 6e81b2569..0d927ae7e 100644
--- a/data/src/commonTest/kotlin/MeldRepositoryTest.kt
+++ b/data/src/commonTest/kotlin/MeldRepositoryTest.kt
@@ -1,73 +1,129 @@
package com.blockstream.green.data.meld
import com.blockstream.data.config.AppInfo
-import com.blockstream.data.dataModule
+import com.blockstream.data.meld.MeldHttpClient
+import com.blockstream.data.meld.MeldRepository
import com.blockstream.data.meld.data.CryptoQuoteRequest
+import com.blockstream.data.meld.datasource.MeldLocalDataSource
+import com.blockstream.data.meld.datasource.MeldRemoteDataSource
+import com.blockstream.network.NetworkResponse
import com.blockstream.network.dataOrThrow
-import com.blockstream.utils.Loggable
+import io.ktor.client.engine.mock.MockEngine
+import io.ktor.client.engine.mock.respond
+import io.ktor.client.engine.mock.toByteArray
+import io.ktor.http.HttpHeaders
+import io.ktor.http.HttpMethod
+import io.ktor.http.HttpStatusCode
+import io.ktor.http.headersOf
import kotlinx.coroutines.test.runTest
-import org.koin.core.context.startKoin
-import org.koin.core.context.stopKoin
-import org.koin.dsl.module
-import org.koin.test.KoinTest
-import org.koin.test.inject
-import kotlin.test.AfterTest
-import kotlin.test.BeforeTest
+import kotlinx.serialization.json.Json
import kotlin.test.Test
-import kotlin.test.assertNotEquals
-import kotlin.test.assertNotNull
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
-class MeldRepositoryTest : KoinTest {
-
- companion object : Loggable()
-
- private val meldRepository: com.blockstream.data.meld.MeldRepository by inject()
-
- @BeforeTest
- fun beforeTest() {
- startKoin {
- modules(
- module {
- single {
- AppInfo(userAgent = "test", "0.0.0", isDebug = false, isDevelopment = false, isTest = true)
- }
- },
- dataModule
+/** Exercises the real HTTP client with an in-memory transport, without creating live sessions. */
+class MeldRepositoryTest {
+ @Test
+ fun `Request CryptoQuoteRequest`() = runTest {
+ val engine = MockEngine { request ->
+ assertEquals(HttpMethod.Post, request.method)
+ assertEquals("https://ramps.blockstream.com/payments/crypto/quote", request.url.toString())
+ assertEquals("application/json", request.body.contentType?.toString())
+ assertEquals(
+ Json.parseToJsonElement("""{"countryCode":"US","sourceAmount":"200","sourceCurrencyCode":"USD","destinationCurrencyCode":"BTC"}"""),
+ Json.parseToJsonElement(request.body.toByteArray().decodeToString()),
)
+ respond(quoteResponse, HttpStatusCode.OK, jsonHeaders)
+ }
+ withRepository(engine) { repository ->
+ val quote = repository.createCryptoQuote(CryptoQuoteRequest()).dataOrThrow().quotes!!.single()
+ assertEquals("BTC", quote.destinationCurrencyCode)
+ assertEquals("0.002", quote.destinationAmount)
+ assertEquals("TEST_PROVIDER", quote.serviceProvider)
}
+ assertEquals(1, engine.requestHistory.size)
}
- @AfterTest
- fun afterTest() {
- stopKoin()
+ @Test
+ fun `Request CryptoWidgetRequest`() = runTest {
+ val engine = MockEngine { request ->
+ assertEquals(HttpMethod.Post, request.method)
+ assertEquals("ramps.blockstream.com", request.url.host)
+ when (request.url.encodedPath) {
+ "/payments/crypto/quote" -> respond(quoteResponse, HttpStatusCode.OK, jsonHeaders)
+ "/crypto/session/widget" -> {
+ assertEquals(
+ Json.parseToJsonElement("""{"sessionType":"BUY","externalCustomerId":"test-customer","sessionData":{"countryCode":"US","sourceAmount":"200","sourceCurrencyCode":"USD","destinationCurrencyCode":"BTC","walletAddress":"test-wallet-address","serviceProvider":"TEST_PROVIDER","redirectUrl":"https://green-webhooks.blockstream.com/thank-you"}}"""),
+ Json.parseToJsonElement(request.body.toByteArray().decodeToString()),
+ )
+ respond(
+ """{"id":"test-session","customerId":"test-customer","widgetUrl":"https://example.test/widget","token":"test-token"}""",
+ HttpStatusCode.OK, jsonHeaders,
+ )
+ }
+ else -> error("Unexpected request: ${request.url}")
+ }
+ }
+ withRepository(engine) { repository ->
+ val request = repository.createCryptoQuote(CryptoQuoteRequest()).dataOrThrow().quotes!!.single()
+ .toCryptoWidgetRequest("test-wallet-address", "test-customer")
+ val widget = repository.createCryptoWidget(request).dataOrThrow()
+ assertEquals("test-session", widget.id)
+ assertEquals("https://example.test/widget", widget.widgetUrl)
+ }
+ assertEquals(2, engine.requestHistory.size)
}
@Test
- fun `Request CryptoQuoteRequest`() = runTest {
- meldRepository.createCryptoQuote(CryptoQuoteRequest()).also {
- logger.d { "$it" }
- assertNotNull(it.dataOrThrow().quotes)
+ fun `Request CryptoLimitsRequest`() = runTest {
+ val engine = MockEngine { request ->
+ assertEquals(HttpMethod.Get, request.method)
+ assertEquals("/payments/crypto/limits", request.url.encodedPath)
+ assertEquals("EUR", request.url.parameters["fiatCurrency"])
+ respond(
+ """[{"currencyCode":"EUR","defaultAmount":200,"minAmount":20,"maxAmount":1000}]""",
+ HttpStatusCode.OK, jsonHeaders,
+ )
+ }
+ withRepository(engine) { repository ->
+ val limit = repository.getCryptoLimits("EUR").dataOrThrow().single()
+ assertEquals("EUR", limit.currencyCode)
+ assertEquals(20.0, limit.minAmount)
+ assertEquals(1000.0, limit.maxAmount)
}
+ assertEquals(1, engine.requestHistory.size)
}
@Test
- fun `Request CryptoWidgetRequest`() = runTest {
- meldRepository.createCryptoQuote(CryptoQuoteRequest())
- .dataOrThrow().quotes!!.first().let {
- it.toCryptoWidgetRequest("bc1qcr8ktl3nzwh8xm88225ysynt5zsdydae26thrg")
- }.also {
- meldRepository.createCryptoWidget(it).also {
- logger.d { "$it" }
- assertNotNull(it.dataOrThrow().widgetUrl)
- }
+ fun `Forbidden response remains an error`() = runTest {
+ val engine = MockEngine {
+ respond("""{"message":"Forbidden"}""", HttpStatusCode.Forbidden, jsonHeaders)
+ }
+ withRepository(engine) { repository ->
+ assertEquals(NetworkResponse.Error(403, "Forbidden"), repository.getCryptoLimits("EUR"))
}
}
@Test
- fun `Request CryptoLimitsRequest`() = runTest {
- meldRepository.getCryptoLimits(fiatCurrency = "EUR").also {
- assertNotEquals(0.0, it.dataOrThrow().first().maxAmount)
+ fun `Malformed successful response remains an error`() = runTest {
+ val engine = MockEngine { respond("not json", HttpStatusCode.OK, jsonHeaders) }
+ withRepository(engine) { repository ->
+ assertTrue(repository.getCryptoLimits("EUR") is NetworkResponse.Error)
}
}
+ private suspend fun withRepository(engine: MockEngine, block: suspend (MeldRepository) -> Unit) {
+ val client = MeldHttpClient(AppInfo("test", "0.0.0", isDebug = false, isDevelopment = false, isTest = true), engine)
+ try {
+ block(MeldRepository(MeldRemoteDataSource(client), MeldLocalDataSource()))
+ } finally {
+ client.httpClient.close()
+ engine.close()
+ }
+ }
+
+ companion object {
+ private val jsonHeaders = headersOf(HttpHeaders.ContentType, "application/json")
+ private const val quoteResponse = """{"quotes":[{"transactionType":"BUY","sourceAmount":"200","sourceAmountWithoutFees":"190","fiatAmountWithoutFees":"190","sourceCurrencyCode":"USD","countryCode":"US","totalFee":"10","transactionFee":"10","destinationAmount":"0.002","destinationCurrencyCode":"BTC","exchangeRate":"95000","paymentMethodType":"CARD","customerScore":"1","serviceProvider":"TEST_PROVIDER"}]}"""
+ }
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index c60471d51..300321ca7 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -93,6 +93,7 @@ coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil" }
constraintlayout-compose-multiplatform = { module = "tech.annexflow.compose:constraintlayout-compose-multiplatform", version.ref = "constraintlayoutComposeMultiplatform" }
kotlin-multiplatform-appdirs = { module = "ca.gosyer:kotlin-multiplatform-appdirs", version.ref = "kotlinMultiplatformAppdirs" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
+ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
ktor-client-resources = { module = "io.ktor:ktor-client-resources", version.ref = "ktor" }
diff --git a/network/src/commonMain/kotlin/com/blockstream/network/AppHttpClient.kt b/network/src/commonMain/kotlin/com/blockstream/network/AppHttpClient.kt
index 8a008b05a..5c2e2d23a 100644
--- a/network/src/commonMain/kotlin/com/blockstream/network/AppHttpClient.kt
+++ b/network/src/commonMain/kotlin/com/blockstream/network/AppHttpClient.kt
@@ -3,6 +3,7 @@ package com.blockstream.network
import co.touchlab.kermit.Logger
import io.ktor.client.HttpClient
import io.ktor.client.HttpClientConfig
+import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.call.body
import io.ktor.client.plugins.ClientRequestException
import io.ktor.client.plugins.ServerResponseException
@@ -65,9 +66,11 @@ inline fun NetworkResponse.exception(): Exception {
}
abstract class AppHttpClient(
- enableLogging: Boolean = false, configBlock: HttpClientConfig<*>.() -> Unit = {}
+ enableLogging: Boolean = false,
+ configBlock: HttpClientConfig<*>.() -> Unit = {},
+ engine: HttpClientEngine? = null,
) {
- val httpClient: HttpClient = defaultHttpClient(enableLogging, configBlock)
+ val httpClient: HttpClient = defaultHttpClient(enableLogging, configBlock, engine)
suspend inline fun get(
path: String, block: HttpRequestBuilder.() -> Unit = {}
@@ -150,9 +153,11 @@ abstract class AppHttpClient(
companion object {
private fun defaultHttpClient(
- enableLogging: Boolean = false, configBlock: HttpClientConfig<*>.() -> Unit = {}
+ enableLogging: Boolean,
+ configBlock: HttpClientConfig<*>.() -> Unit,
+ engine: HttpClientEngine?,
): HttpClient {
- return HttpClient {
+ val configure: HttpClientConfig<*>.() -> Unit = {
install(Resources)
if (enableLogging) {
install(Logging) {
@@ -180,6 +185,7 @@ abstract class AppHttpClient(
}
configBlock()
}
+ return if (engine == null) HttpClient(configure) else HttpClient(engine, configure)
}
}
@@ -189,4 +195,4 @@ private suspend fun HttpResponse.bodyAsTextOrNull(): String? = try {
this.bodyAsText()
} catch (_: Exception) {
null
-}
\ No newline at end of file
+}