feat(app): add Ktor HTTP client and SyncService for inventory sync
domain/model/SyncError.kt: - Sealed class with ConnectionError, Timeout, AuthError, ServerError, NotConfigured, Unknown subtypes for typed error handling domain/repository/SyncService.kt: - Interface with downloadInventory() and uploadInventory() returning Result<InventoryDto> for clean error propagation data/sync/SyncServiceImpl.kt: - Ktor Client implementation using OkHttp engine - GET /api/inventory and PUT /api/inventory endpoints - X-API-Key header authentication matching server contract - Server URL and API key read from SettingsRepository - withContext(Dispatchers.IO) for network calls - Catches SocketTimeoutException, ConnectException specifically di/NetworkModule.kt: - Hilt module providing singleton HttpClient with OkHttp engine - ContentNegotiation with kotlinx.serialization JSON - Configurable connect/read/write timeouts (10s/30s/30s) - Binds SyncServiceImpl to SyncService interface Dependencies: - ktor-client-core, ktor-client-okhttp, ktor-client-content-negotiation, ktor-client-mock (test) - ktor-serialization-kotlinx-json (shared with server) - INTERNET permission added to AndroidManifest.xml Tests: 9 tests with Ktor MockEngine covering success, 401, 500, missing config, trailing slash URL normalization Closes #44
This commit is contained in:
parent
cb9bd2bdf4
commit
215790d68e
8 changed files with 442 additions and 0 deletions
|
|
@ -71,12 +71,19 @@ dependencies {
|
||||||
// Serialization
|
// Serialization
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
|
||||||
|
// Ktor Client
|
||||||
|
implementation(libs.ktor.client.core)
|
||||||
|
implementation(libs.ktor.client.okhttp)
|
||||||
|
implementation(libs.ktor.client.content.negotiation)
|
||||||
|
implementation(libs.ktor.serialization.kotlinx.json)
|
||||||
|
|
||||||
// Shared module
|
// Shared module
|
||||||
implementation(project(":shared"))
|
implementation(project(":shared"))
|
||||||
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
testImplementation(libs.kotlinx.coroutines.test)
|
testImplementation(libs.kotlinx.coroutines.test)
|
||||||
testImplementation(libs.mockk)
|
testImplementation(libs.mockk)
|
||||||
|
testImplementation(libs.ktor.client.mock)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.room.testing)
|
androidTestImplementation(libs.androidx.room.testing)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".KrisenvorratApp"
|
android:name=".KrisenvorratApp"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
package de.krisenvorrat.app.data.sync
|
||||||
|
|
||||||
|
import de.krisenvorrat.app.domain.model.SyncError
|
||||||
|
import de.krisenvorrat.app.domain.repository.SettingsRepository
|
||||||
|
import de.krisenvorrat.app.domain.repository.SyncService
|
||||||
|
import de.krisenvorrat.shared.model.InventoryDto
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.call.body
|
||||||
|
import io.ktor.client.request.get
|
||||||
|
import io.ktor.client.request.header
|
||||||
|
import io.ktor.client.request.put
|
||||||
|
import io.ktor.client.request.setBody
|
||||||
|
import io.ktor.http.ContentType
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.http.contentType
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.net.ConnectException
|
||||||
|
import java.net.SocketTimeoutException
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class SyncServiceImpl @Inject constructor(
|
||||||
|
private val httpClient: HttpClient,
|
||||||
|
private val settingsRepository: SettingsRepository
|
||||||
|
) : SyncService {
|
||||||
|
|
||||||
|
override suspend fun downloadInventory(): Result<InventoryDto> =
|
||||||
|
executeRequest { serverUrl, apiKey ->
|
||||||
|
val response = httpClient.get("$serverUrl/api/inventory") {
|
||||||
|
header("X-API-Key", apiKey)
|
||||||
|
}
|
||||||
|
handleResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun uploadInventory(inventory: InventoryDto): Result<InventoryDto> =
|
||||||
|
executeRequest { serverUrl, apiKey ->
|
||||||
|
val response = httpClient.put("$serverUrl/api/inventory") {
|
||||||
|
header("X-API-Key", apiKey)
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(inventory)
|
||||||
|
}
|
||||||
|
handleResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun executeRequest(
|
||||||
|
block: suspend (serverUrl: String, apiKey: String) -> Result<InventoryDto>
|
||||||
|
): Result<InventoryDto> = withContext(Dispatchers.IO) {
|
||||||
|
val serverUrl = settingsRepository.getValue(KEY_SERVER_URL)
|
||||||
|
if (serverUrl.isNullOrBlank()) {
|
||||||
|
return@withContext Result.failure(SyncError.NotConfigured("Server-URL nicht gesetzt"))
|
||||||
|
}
|
||||||
|
|
||||||
|
val apiKey = settingsRepository.getValue(KEY_API_KEY)
|
||||||
|
if (apiKey.isNullOrBlank()) {
|
||||||
|
return@withContext Result.failure(SyncError.NotConfigured("API-Key nicht gesetzt"))
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
block(serverUrl.trimEnd('/'), apiKey)
|
||||||
|
} catch (e: SocketTimeoutException) {
|
||||||
|
Result.failure(SyncError.Timeout(e))
|
||||||
|
} catch (e: ConnectException) {
|
||||||
|
Result.failure(SyncError.ConnectionError(e))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(SyncError.Unknown(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun handleResponse(
|
||||||
|
response: io.ktor.client.statement.HttpResponse
|
||||||
|
): Result<InventoryDto> = when (response.status) {
|
||||||
|
HttpStatusCode.OK -> Result.success(response.body())
|
||||||
|
HttpStatusCode.Unauthorized -> Result.failure(SyncError.AuthError())
|
||||||
|
else -> Result.failure(
|
||||||
|
SyncError.ServerError(
|
||||||
|
statusCode = response.status.value,
|
||||||
|
message = response.status.description
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val KEY_SERVER_URL = "server_url"
|
||||||
|
const val KEY_API_KEY = "api_key"
|
||||||
|
}
|
||||||
|
}
|
||||||
50
app/src/main/java/de/krisenvorrat/app/di/NetworkModule.kt
Normal file
50
app/src/main/java/de/krisenvorrat/app/di/NetworkModule.kt
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
package de.krisenvorrat.app.di
|
||||||
|
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import de.krisenvorrat.app.data.sync.SyncServiceImpl
|
||||||
|
import de.krisenvorrat.app.domain.repository.SyncService
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.engine.okhttp.OkHttp
|
||||||
|
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||||
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
internal abstract class NetworkModule {
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
abstract fun bindSyncService(impl: SyncServiceImpl): SyncService
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideHttpClient(): HttpClient = HttpClient(OkHttp) {
|
||||||
|
engine {
|
||||||
|
config {
|
||||||
|
connectTimeout(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||||
|
readTimeout(READ_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||||
|
writeTimeout(WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json(Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
encodeDefaults = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val CONNECT_TIMEOUT_SECONDS = 10L
|
||||||
|
private const val READ_TIMEOUT_SECONDS = 30L
|
||||||
|
private const val WRITE_TIMEOUT_SECONDS = 30L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package de.krisenvorrat.app.domain.model
|
||||||
|
|
||||||
|
internal sealed class SyncError(message: String, cause: Throwable? = null) : Exception(message, cause) {
|
||||||
|
class ConnectionError(cause: Throwable? = null) :
|
||||||
|
SyncError("Server nicht erreichbar", cause)
|
||||||
|
|
||||||
|
class Timeout(cause: Throwable? = null) :
|
||||||
|
SyncError("Zeitüberschreitung bei der Verbindung zum Server", cause)
|
||||||
|
|
||||||
|
class AuthError :
|
||||||
|
SyncError("Authentifizierung fehlgeschlagen – API-Key ungültig")
|
||||||
|
|
||||||
|
class ServerError(val statusCode: Int, message: String) :
|
||||||
|
SyncError("Serverfehler ($statusCode): $message")
|
||||||
|
|
||||||
|
class NotConfigured(detail: String) :
|
||||||
|
SyncError("Sync nicht konfiguriert: $detail")
|
||||||
|
|
||||||
|
class Unknown(cause: Throwable) :
|
||||||
|
SyncError("Unbekannter Fehler: ${cause.message}", cause)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package de.krisenvorrat.app.domain.repository
|
||||||
|
|
||||||
|
import de.krisenvorrat.shared.model.InventoryDto
|
||||||
|
|
||||||
|
internal interface SyncService {
|
||||||
|
suspend fun downloadInventory(): Result<InventoryDto>
|
||||||
|
suspend fun uploadInventory(inventory: InventoryDto): Result<InventoryDto>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,264 @@
|
||||||
|
package de.krisenvorrat.app.data.sync
|
||||||
|
|
||||||
|
import de.krisenvorrat.app.domain.model.SyncError
|
||||||
|
import de.krisenvorrat.app.domain.repository.SettingsRepository
|
||||||
|
import de.krisenvorrat.shared.model.CategoryDto
|
||||||
|
import de.krisenvorrat.shared.model.InventoryDto
|
||||||
|
import de.krisenvorrat.shared.model.ItemDto
|
||||||
|
import de.krisenvorrat.shared.model.LocationDto
|
||||||
|
import de.krisenvorrat.shared.model.SettingDto
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.engine.mock.MockEngine
|
||||||
|
import io.ktor.client.engine.mock.respond
|
||||||
|
import io.ktor.client.engine.mock.respondError
|
||||||
|
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||||
|
import io.ktor.http.ContentType
|
||||||
|
import io.ktor.http.HttpHeaders
|
||||||
|
import io.ktor.http.HttpMethod
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.http.headersOf
|
||||||
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class SyncServiceImplTest {
|
||||||
|
|
||||||
|
private val settingsRepository: SettingsRepository = mockk()
|
||||||
|
private val jsonSerializer = Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
encodeDefaults = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private val testInventory = InventoryDto(
|
||||||
|
categories = listOf(CategoryDto(id = 1, name = "Konserven")),
|
||||||
|
locations = listOf(LocationDto(id = 1, name = "Keller")),
|
||||||
|
items = listOf(
|
||||||
|
ItemDto(
|
||||||
|
id = "item-1",
|
||||||
|
name = "Bohnen",
|
||||||
|
categoryId = 1,
|
||||||
|
quantity = 5.0,
|
||||||
|
unit = "Dose",
|
||||||
|
unitPrice = 1.29,
|
||||||
|
kcalPer100g = 100,
|
||||||
|
expiryDate = "2027-06-01",
|
||||||
|
locationId = 1,
|
||||||
|
minStock = 2.0,
|
||||||
|
notes = "",
|
||||||
|
lastUpdated = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
),
|
||||||
|
settings = listOf(SettingDto(key = "household_size", value = "2"))
|
||||||
|
)
|
||||||
|
|
||||||
|
private lateinit var httpClient: HttpClient
|
||||||
|
private lateinit var syncService: SyncServiceImpl
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
if (::httpClient.isInitialized) {
|
||||||
|
httpClient.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createClient(engine: MockEngine): HttpClient = HttpClient(engine) {
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json(jsonSerializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupSettings(serverUrl: String? = "http://localhost:8080", apiKey: String? = "test-key") {
|
||||||
|
coEvery { settingsRepository.getValue("server_url") } returns serverUrl
|
||||||
|
coEvery { settingsRepository.getValue("api_key") } returns apiKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- downloadInventory ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_downloadInventory_serverReturnsInventory_returnsSuccess() = runTest {
|
||||||
|
// Given
|
||||||
|
setupSettings()
|
||||||
|
val engine = MockEngine { request ->
|
||||||
|
assertEquals("/api/inventory", request.url.encodedPath)
|
||||||
|
assertEquals(HttpMethod.Get, request.method)
|
||||||
|
assertEquals("test-key", request.headers["X-API-Key"])
|
||||||
|
respond(
|
||||||
|
content = jsonSerializer.encodeToString(InventoryDto.serializer(), testInventory),
|
||||||
|
status = HttpStatusCode.OK,
|
||||||
|
headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
httpClient = createClient(engine)
|
||||||
|
syncService = SyncServiceImpl(httpClient, settingsRepository)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = syncService.downloadInventory()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isSuccess)
|
||||||
|
assertEquals(testInventory, result.getOrNull())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_downloadInventory_serverReturns401_returnsAuthError() = runTest {
|
||||||
|
// Given
|
||||||
|
setupSettings()
|
||||||
|
val engine = MockEngine {
|
||||||
|
respondError(HttpStatusCode.Unauthorized)
|
||||||
|
}
|
||||||
|
httpClient = createClient(engine)
|
||||||
|
syncService = SyncServiceImpl(httpClient, settingsRepository)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = syncService.downloadInventory()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isFailure)
|
||||||
|
assertTrue(result.exceptionOrNull() is SyncError.AuthError)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_downloadInventory_serverReturns500_returnsServerError() = runTest {
|
||||||
|
// Given
|
||||||
|
setupSettings()
|
||||||
|
val engine = MockEngine {
|
||||||
|
respondError(HttpStatusCode.InternalServerError)
|
||||||
|
}
|
||||||
|
httpClient = createClient(engine)
|
||||||
|
syncService = SyncServiceImpl(httpClient, settingsRepository)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = syncService.downloadInventory()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isFailure)
|
||||||
|
val error = result.exceptionOrNull() as SyncError.ServerError
|
||||||
|
assertEquals(500, error.statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_downloadInventory_noServerUrl_returnsNotConfigured() = runTest {
|
||||||
|
// Given
|
||||||
|
setupSettings(serverUrl = null)
|
||||||
|
val engine = MockEngine { respondError(HttpStatusCode.OK) }
|
||||||
|
httpClient = createClient(engine)
|
||||||
|
syncService = SyncServiceImpl(httpClient, settingsRepository)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = syncService.downloadInventory()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isFailure)
|
||||||
|
assertTrue(result.exceptionOrNull() is SyncError.NotConfigured)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_downloadInventory_emptyServerUrl_returnsNotConfigured() = runTest {
|
||||||
|
// Given
|
||||||
|
setupSettings(serverUrl = " ")
|
||||||
|
val engine = MockEngine { respondError(HttpStatusCode.OK) }
|
||||||
|
httpClient = createClient(engine)
|
||||||
|
syncService = SyncServiceImpl(httpClient, settingsRepository)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = syncService.downloadInventory()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isFailure)
|
||||||
|
assertTrue(result.exceptionOrNull() is SyncError.NotConfigured)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_downloadInventory_noApiKey_returnsNotConfigured() = runTest {
|
||||||
|
// Given
|
||||||
|
setupSettings(apiKey = null)
|
||||||
|
val engine = MockEngine { respondError(HttpStatusCode.OK) }
|
||||||
|
httpClient = createClient(engine)
|
||||||
|
syncService = SyncServiceImpl(httpClient, settingsRepository)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = syncService.downloadInventory()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isFailure)
|
||||||
|
assertTrue(result.exceptionOrNull() is SyncError.NotConfigured)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- uploadInventory ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_uploadInventory_serverAccepts_returnsSuccess() = runTest {
|
||||||
|
// Given
|
||||||
|
setupSettings()
|
||||||
|
val engine = MockEngine { request ->
|
||||||
|
assertEquals("/api/inventory", request.url.encodedPath)
|
||||||
|
assertEquals(HttpMethod.Put, request.method)
|
||||||
|
assertEquals("test-key", request.headers["X-API-Key"])
|
||||||
|
assertEquals(
|
||||||
|
ContentType.Application.Json.toString(),
|
||||||
|
request.body.contentType.toString()
|
||||||
|
)
|
||||||
|
respond(
|
||||||
|
content = jsonSerializer.encodeToString(InventoryDto.serializer(), testInventory),
|
||||||
|
status = HttpStatusCode.OK,
|
||||||
|
headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
httpClient = createClient(engine)
|
||||||
|
syncService = SyncServiceImpl(httpClient, settingsRepository)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = syncService.uploadInventory(testInventory)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isSuccess)
|
||||||
|
assertEquals(testInventory, result.getOrNull())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_uploadInventory_serverReturns401_returnsAuthError() = runTest {
|
||||||
|
// Given
|
||||||
|
setupSettings()
|
||||||
|
val engine = MockEngine {
|
||||||
|
respondError(HttpStatusCode.Unauthorized)
|
||||||
|
}
|
||||||
|
httpClient = createClient(engine)
|
||||||
|
syncService = SyncServiceImpl(httpClient, settingsRepository)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = syncService.uploadInventory(testInventory)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isFailure)
|
||||||
|
assertTrue(result.exceptionOrNull() is SyncError.AuthError)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_uploadInventory_serverUrlHasTrailingSlash_urlIsCorrect() = runTest {
|
||||||
|
// Given
|
||||||
|
setupSettings(serverUrl = "http://localhost:8080/")
|
||||||
|
val engine = MockEngine { request ->
|
||||||
|
assertEquals("http://localhost:8080/api/inventory", request.url.toString())
|
||||||
|
respond(
|
||||||
|
content = jsonSerializer.encodeToString(InventoryDto.serializer(), testInventory),
|
||||||
|
status = HttpStatusCode.OK,
|
||||||
|
headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
httpClient = createClient(engine)
|
||||||
|
syncService = SyncServiceImpl(httpClient, settingsRepository)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = syncService.uploadInventory(testInventory)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertTrue(result.isSuccess)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -58,6 +58,10 @@ ktor-server-status-pages = { group = "io.ktor", name = "ktor-server-status-pages
|
||||||
ktor-server-auth = { group = "io.ktor", name = "ktor-server-auth", version.ref = "ktor" }
|
ktor-server-auth = { group = "io.ktor", name = "ktor-server-auth", version.ref = "ktor" }
|
||||||
ktor-server-call-logging = { group = "io.ktor", name = "ktor-server-call-logging", version.ref = "ktor" }
|
ktor-server-call-logging = { group = "io.ktor", name = "ktor-server-call-logging", version.ref = "ktor" }
|
||||||
ktor-server-test-host = { group = "io.ktor", name = "ktor-server-test-host", version.ref = "ktor" }
|
ktor-server-test-host = { group = "io.ktor", name = "ktor-server-test-host", version.ref = "ktor" }
|
||||||
|
ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
|
||||||
|
ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" }
|
||||||
|
ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" }
|
||||||
|
ktor-client-mock = { group = "io.ktor", name = "ktor-client-mock", version.ref = "ktor" }
|
||||||
logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" }
|
logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" }
|
||||||
exposed-core = { group = "org.jetbrains.exposed", name = "exposed-core", version.ref = "exposed" }
|
exposed-core = { group = "org.jetbrains.exposed", name = "exposed-core", version.ref = "exposed" }
|
||||||
exposed-jdbc = { group = "org.jetbrains.exposed", name = "exposed-jdbc", version.ref = "exposed" }
|
exposed-jdbc = { group = "org.jetbrains.exposed", name = "exposed-jdbc", version.ref = "exposed" }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue