chore: entferne veraltete turnier-feature Artefakte und ViewModels nach Migration auf Module Structure Blueprint
This commit is contained in:
+5
-5
@@ -2,11 +2,11 @@ package at.mocode.frontend.features.ping.di
|
||||
|
||||
import at.mocode.frontend.core.localdb.AppDatabase
|
||||
import at.mocode.ping.api.PingApi
|
||||
import at.mocode.ping.feature.data.PingApiKoinClient
|
||||
import at.mocode.ping.feature.data.PingEventRepositoryImpl
|
||||
import at.mocode.ping.feature.domain.PingSyncService
|
||||
import at.mocode.ping.feature.domain.PingSyncServiceImpl
|
||||
import at.mocode.ping.feature.presentation.PingViewModel
|
||||
import at.mocode.frontend.features.ping.data.PingApiKoinClient
|
||||
import at.mocode.frontend.features.ping.data.PingEventRepositoryImpl
|
||||
import at.mocode.frontend.features.ping.domain.PingSyncService
|
||||
import at.mocode.frontend.features.ping.domain.PingSyncServiceImpl
|
||||
import at.mocode.frontend.features.ping.presentation.PingViewModel
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
package at.mocode.ping.feature.data
|
||||
|
||||
import at.mocode.ping.api.*
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
|
||||
/**
|
||||
* PingApi-Implementierung, die einen bereitgestellten HttpClient verwendet (z. B. den per Dependency Injection
|
||||
* bereitgestellten "apiClient").
|
||||
*/
|
||||
class PingApiKoinClient(private val client: HttpClient) : PingApi {
|
||||
|
||||
override suspend fun simplePing(): PingResponse {
|
||||
return client.get("/api/ping/simple").body()
|
||||
}
|
||||
|
||||
override suspend fun enhancedPing(simulate: Boolean): EnhancedPingResponse {
|
||||
return client.get("/api/ping/enhanced") {
|
||||
url.parameters.append("simulate", simulate.toString())
|
||||
}.body()
|
||||
}
|
||||
|
||||
override suspend fun healthCheck(): HealthResponse {
|
||||
return client.get("/api/ping/health").body()
|
||||
}
|
||||
|
||||
override suspend fun publicPing(): PingResponse {
|
||||
return client.get("/api/ping/public").body()
|
||||
}
|
||||
|
||||
override suspend fun securePing(): PingResponse {
|
||||
return client.get("/api/ping/secure").body()
|
||||
}
|
||||
|
||||
override suspend fun syncPings(since: Long): List<PingEvent> {
|
||||
return client.get("/api/ping/sync") {
|
||||
url.parameters.append("since", since.toString())
|
||||
}.body()
|
||||
}
|
||||
}
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
package at.mocode.ping.feature.data
|
||||
|
||||
import at.mocode.frontend.core.localdb.AppDatabase
|
||||
import at.mocode.frontend.core.sync.SyncableRepository
|
||||
import at.mocode.ping.api.PingEvent
|
||||
import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull
|
||||
|
||||
/**
|
||||
** ARCH-BLUEPRINT: Dieses Repository implementiert das generische Syncable Repository
|
||||
** für eine bestimmte Entität und überbrückt so die Lücke zwischen dem Sync-Core und der
|
||||
** lokalen Datenbank.
|
||||
*/
|
||||
class PingEventRepositoryImpl(
|
||||
private val db: AppDatabase
|
||||
) : SyncableRepository<PingEvent> {
|
||||
|
||||
// Der `since`-Parameter für unsere Synchronisierung ist der Zeitstempel des letzten Ereignisses.
|
||||
// Das Backend erwartet einen Long (Timestamp), keinen String (UUID).
|
||||
override suspend fun getLatestSince(): String? {
|
||||
println("PingEventRepositoryImpl: getLatestSince called - fetching latest timestamp")
|
||||
// Wir holen den letzten Timestamp aus der DB.
|
||||
val lastModified = db.appDatabaseQueries.selectLatestPingEventTimestamp().awaitAsOneOrNull()
|
||||
|
||||
// Wir geben ihn als String zurück, da das Interface String? erwartet.
|
||||
// Der SyncManager wird ihn als Parameter "since" an den Request hängen.
|
||||
// Das Backend erwartet "since" als Long, aber HTTP Parameter sind Strings.
|
||||
// Spring Boot konvertiert "123456789" automatisch in Long 123456789.
|
||||
return lastModified?.toString()
|
||||
}
|
||||
|
||||
override suspend fun upsert(items: List<PingEvent>) {
|
||||
// Führen Sie Massenoperationen immer innerhalb einer Transaktion durch.
|
||||
db.transaction {
|
||||
items.forEach { event ->
|
||||
db.appDatabaseQueries.upsertPingEvent(
|
||||
id = event.id,
|
||||
message = event.message,
|
||||
last_modified = event.lastModified
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
package at.mocode.ping.feature.domain
|
||||
|
||||
import at.mocode.frontend.core.sync.SyncManager
|
||||
import at.mocode.frontend.core.sync.SyncableRepository
|
||||
import at.mocode.ping.api.PingEvent
|
||||
|
||||
/**
|
||||
* Interface für den Ping-Sync-Dienst zur einfacheren Prüfung und Entkopplung.
|
||||
*/
|
||||
interface PingSyncService {
|
||||
suspend fun syncPings()
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementierung des PingSyncService unter Verwendung des generischen SyncManager.
|
||||
*/
|
||||
class PingSyncServiceImpl(
|
||||
private val syncManager: SyncManager,
|
||||
private val repository: SyncableRepository<PingEvent>
|
||||
) : PingSyncService {
|
||||
|
||||
override suspend fun syncPings() {
|
||||
// Corrected endpoint: /api/ping/sync (singular)
|
||||
syncManager.performSync(repository, "/api/ping/sync")
|
||||
}
|
||||
}
|
||||
-102
@@ -1,102 +0,0 @@
|
||||
package at.mocode.ping.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.HealthAndSafety
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.NetworkCheck
|
||||
import androidx.compose.material.icons.filled.Sync
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||
|
||||
/**
|
||||
* Eine modulare Gruppe von Test-Buttons für die Konnektivitäts-Diagnose.
|
||||
* Plug-and-Play fähig für Ping-Screen oder Sidebar.
|
||||
*/
|
||||
@Composable
|
||||
fun PingActionGroup(
|
||||
viewModel: PingViewModel,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val uiState = viewModel.uiState
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(Dimens.SpacingS)
|
||||
) {
|
||||
Text(
|
||||
text = "DIAGNOSE-TESTS",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = Dimens.SpacingXS)
|
||||
)
|
||||
|
||||
// Grid-ähnliches Layout für die Buttons
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS)) {
|
||||
PingTestButton(
|
||||
text = "Simple Ping",
|
||||
icon = Icons.Default.NetworkCheck,
|
||||
onClick = { viewModel.performSimplePing() },
|
||||
isLoading = uiState.isLoading,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
PingTestButton(
|
||||
text = "Secure Ping",
|
||||
icon = Icons.Default.Lock,
|
||||
onClick = { viewModel.performSecurePing() },
|
||||
isLoading = uiState.isLoading,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS)) {
|
||||
PingTestButton(
|
||||
text = "Health Check",
|
||||
icon = Icons.Default.HealthAndSafety,
|
||||
onClick = { viewModel.performHealthCheck() },
|
||||
isLoading = uiState.isLoading,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
PingTestButton(
|
||||
text = "Delta Sync",
|
||||
icon = Icons.Default.Sync,
|
||||
onClick = { viewModel.triggerSync() },
|
||||
isLoading = uiState.isSyncing,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
// Zusätzlicher Button für Enhanced Ping (Circuit Breaker Test)
|
||||
OutlinedButton(
|
||||
onClick = { viewModel.performEnhancedPing() },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = !uiState.isLoading
|
||||
) {
|
||||
Text("Enhanced Ping (Simulation)", fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PingTestButton(
|
||||
text: String,
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
onClick: () -> Unit,
|
||||
isLoading: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = modifier.height(48.dp),
|
||||
enabled = !isLoading,
|
||||
contentPadding = PaddingValues(horizontal = Dimens.SpacingS)
|
||||
) {
|
||||
Icon(icon, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(Dimens.SpacingXS))
|
||||
Text(text, fontSize = 12.sp, maxLines = 1)
|
||||
}
|
||||
}
|
||||
-237
@@ -1,237 +0,0 @@
|
||||
package at.mocode.ping.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.core.auth.presentation.AuthStatusCard
|
||||
import at.mocode.frontend.core.auth.presentation.LoginViewModel
|
||||
import at.mocode.frontend.core.designsystem.components.MsCard
|
||||
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@Composable
|
||||
fun PingScreen(
|
||||
viewModel: PingViewModel,
|
||||
onBack: () -> Unit = {},
|
||||
onNavigateToLogin: () -> Unit = {}
|
||||
) {
|
||||
val uiState = viewModel.uiState
|
||||
val authViewModel: LoginViewModel = koinInject()
|
||||
|
||||
// Wir nutzen jetzt das globale Theme (Hintergrund kommt vom Theme)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(Dimens.SpacingS) // Globales Spacing
|
||||
) {
|
||||
// 1. Header
|
||||
PingHeader(
|
||||
onBack = onBack,
|
||||
isSyncing = uiState.isSyncing,
|
||||
isLoading = uiState.isLoading
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(Dimens.SpacingS))
|
||||
|
||||
// 2. Auth Status Area (Plug-and-Play)
|
||||
AuthStatusCard(
|
||||
viewModel = authViewModel,
|
||||
onLoginClick = onNavigateToLogin
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(Dimens.SpacingS))
|
||||
|
||||
// 3. Main Dashboard Area (Split View)
|
||||
Row(modifier = Modifier.weight(1f)) {
|
||||
// Left Panel: Controls & Status Grid (60%)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(0.6f)
|
||||
.fillMaxHeight()
|
||||
.padding(end = Dimens.SpacingS)
|
||||
) {
|
||||
PingActionGroup(viewModel)
|
||||
Spacer(Modifier.height(Dimens.SpacingS))
|
||||
StatusGrid(uiState)
|
||||
}
|
||||
|
||||
// Right Panel: Terminal Log (40%)
|
||||
TerminalConsole(
|
||||
logs = uiState.logs,
|
||||
onClear = { viewModel.clearLogs() },
|
||||
modifier = Modifier
|
||||
.weight(0.4f)
|
||||
.fillMaxHeight()
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(Dimens.SpacingXS))
|
||||
|
||||
// 4. Footer
|
||||
PingStatusBar(uiState.lastSyncResult)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PingHeader(
|
||||
onBack: () -> Unit,
|
||||
isSyncing: Boolean,
|
||||
isLoading: Boolean
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth().height(40.dp)
|
||||
) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back", tint = MaterialTheme.colorScheme.onBackground)
|
||||
}
|
||||
Text(
|
||||
"KONNEKTIVITÄTS-DIAGNOSE // DASHBOARD",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.weight(1f).padding(start = Dimens.SpacingS)
|
||||
)
|
||||
|
||||
if (isLoading) {
|
||||
StatusBadge("BUSY", Color(0xFFFFA000)) // Amber
|
||||
Spacer(Modifier.width(Dimens.SpacingS))
|
||||
}
|
||||
|
||||
if (isSyncing) {
|
||||
StatusBadge("SYNCING", MaterialTheme.colorScheme.primary)
|
||||
Spacer(Modifier.width(Dimens.SpacingS))
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(16.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
} else {
|
||||
StatusBadge("IDLE", Color(0xFF388E3C)) // Green
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatusBadge(text: String, color: Color) {
|
||||
Surface(
|
||||
color = color.copy(alpha = 0.1f),
|
||||
contentColor = color,
|
||||
shape = MaterialTheme.shapes.small,
|
||||
border = androidx.compose.foundation.BorderStroke(1.dp, color)
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatusGrid(uiState: PingUiState) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(Dimens.SpacingS)) {
|
||||
// Row 1
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS)) {
|
||||
MsCard(modifier = Modifier.weight(1f)) {
|
||||
StatusHeader("SIMPLE / SECURE PING")
|
||||
if (uiState.simplePingResponse != null) {
|
||||
KeyValueRow("Status", uiState.simplePingResponse.status)
|
||||
KeyValueRow("Service", uiState.simplePingResponse.service)
|
||||
KeyValueRow("Time", uiState.simplePingResponse.timestamp)
|
||||
} else {
|
||||
EmptyStateText()
|
||||
}
|
||||
}
|
||||
|
||||
MsCard(modifier = Modifier.weight(1f)) {
|
||||
StatusHeader("HEALTH CHECK")
|
||||
if (uiState.healthResponse != null) {
|
||||
KeyValueRow("Status", uiState.healthResponse.status)
|
||||
KeyValueRow("Healthy", uiState.healthResponse.healthy.toString())
|
||||
KeyValueRow("Service", uiState.healthResponse.service)
|
||||
} else {
|
||||
EmptyStateText()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Row 2
|
||||
MsCard(modifier = Modifier.fillMaxWidth()) {
|
||||
StatusHeader("ENHANCED PING (RESILIENCE)")
|
||||
if (uiState.enhancedPingResponse != null) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
KeyValueRow("Status", uiState.enhancedPingResponse.status)
|
||||
KeyValueRow("Timestamp", uiState.enhancedPingResponse.timestamp)
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
KeyValueRow("Circuit Breaker", uiState.enhancedPingResponse.circuitBreakerState)
|
||||
KeyValueRow("Latency", "${uiState.enhancedPingResponse.responseTime}ms")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
EmptyStateText()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatusHeader(title: String) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = Dimens.SpacingXS)
|
||||
)
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant, thickness = 1.dp)
|
||||
Spacer(Modifier.height(Dimens.SpacingXS))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyStateText() {
|
||||
Text("No Data", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KeyValueRow(key: String, value: String) {
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp)) {
|
||||
Text(
|
||||
text = "$key:",
|
||||
modifier = Modifier.width(100.dp),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PingStatusBar(lastSync: String?) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.primaryContainer,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = lastSync ?: "Ready",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
modifier = Modifier.padding(horizontal = Dimens.SpacingS, vertical = 2.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
-156
@@ -1,156 +0,0 @@
|
||||
package at.mocode.ping.feature.presentation
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import at.mocode.ping.api.EnhancedPingResponse
|
||||
import at.mocode.ping.api.HealthResponse
|
||||
import at.mocode.ping.api.PingApi
|
||||
import at.mocode.ping.api.PingResponse
|
||||
import at.mocode.ping.feature.domain.PingSyncService
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Clock
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
|
||||
data class LogEntry(
|
||||
val timestamp: String,
|
||||
val source: String,
|
||||
val message: String,
|
||||
val isError: Boolean = false
|
||||
)
|
||||
|
||||
data class PingUiState(
|
||||
val isLoading: Boolean = false,
|
||||
val simplePingResponse: PingResponse? = null,
|
||||
val enhancedPingResponse: EnhancedPingResponse? = null,
|
||||
val healthResponse: HealthResponse? = null,
|
||||
val errorMessage: String? = null,
|
||||
val isSyncing: Boolean = false,
|
||||
val lastSyncResult: String? = null,
|
||||
val logs: List<LogEntry> = emptyList()
|
||||
)
|
||||
|
||||
open class PingViewModel(
|
||||
private val apiClient: PingApi,
|
||||
private val syncService: PingSyncService
|
||||
) : ViewModel() {
|
||||
|
||||
var uiState by mutableStateOf(PingUiState())
|
||||
internal set
|
||||
|
||||
private fun addLog(source: String, message: String, isError: Boolean = false) {
|
||||
val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
val timeString = "${now.hour.toString().padStart(2, '0')}:${now.minute.toString().padStart(2, '0')}:${
|
||||
now.second.toString().padStart(2, '0')
|
||||
}"
|
||||
val entry = LogEntry(timeString, source, message, isError)
|
||||
uiState = uiState.copy(logs = listOf(entry) + uiState.logs) // Prepend for newest first
|
||||
}
|
||||
|
||||
fun performSimplePing() {
|
||||
viewModelScope.launch {
|
||||
uiState = uiState.copy(isLoading = true, errorMessage = null)
|
||||
addLog("SimplePing", "Sending request...")
|
||||
try {
|
||||
val response = apiClient.simplePing()
|
||||
uiState = uiState.copy(
|
||||
isLoading = false,
|
||||
simplePingResponse = response
|
||||
)
|
||||
addLog("SimplePing", "Success: ${response.status} from ${response.service}")
|
||||
} catch (e: Exception) {
|
||||
val msg = "Simple ping failed: ${e.message}"
|
||||
uiState = uiState.copy(isLoading = false, errorMessage = msg)
|
||||
addLog("SimplePing", "Failed: ${e.message}", isError = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun performEnhancedPing(simulate: Boolean = false) {
|
||||
viewModelScope.launch {
|
||||
uiState = uiState.copy(isLoading = true, errorMessage = null)
|
||||
addLog("EnhancedPing", "Sending request (simulate=$simulate)...")
|
||||
try {
|
||||
val response = apiClient.enhancedPing(simulate)
|
||||
uiState = uiState.copy(
|
||||
isLoading = false,
|
||||
enhancedPingResponse = response
|
||||
)
|
||||
addLog("EnhancedPing", "Success: CB=${response.circuitBreakerState}, Time=${response.responseTime}ms")
|
||||
} catch (e: Exception) {
|
||||
val msg = "Enhanced ping failed: ${e.message}"
|
||||
uiState = uiState.copy(isLoading = false, errorMessage = msg)
|
||||
addLog("EnhancedPing", "Failed: ${e.message}", isError = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun performHealthCheck() {
|
||||
viewModelScope.launch {
|
||||
uiState = uiState.copy(isLoading = true, errorMessage = null)
|
||||
addLog("HealthCheck", "Checking system health...")
|
||||
try {
|
||||
val response = apiClient.healthCheck()
|
||||
uiState = uiState.copy(
|
||||
isLoading = false,
|
||||
healthResponse = response
|
||||
)
|
||||
addLog("HealthCheck", "Status: ${response.status}, Healthy: ${response.healthy}")
|
||||
} catch (e: Exception) {
|
||||
val msg = "Health check failed: ${e.message}"
|
||||
uiState = uiState.copy(isLoading = false, errorMessage = msg)
|
||||
addLog("HealthCheck", "Failed: ${e.message}", isError = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun performSecurePing() {
|
||||
viewModelScope.launch {
|
||||
uiState = uiState.copy(isLoading = true, errorMessage = null)
|
||||
addLog("SecurePing", "Sending authenticated request...")
|
||||
try {
|
||||
val response = apiClient.securePing()
|
||||
uiState = uiState.copy(
|
||||
isLoading = false,
|
||||
simplePingResponse = response
|
||||
)
|
||||
addLog("SecurePing", "Success: Authorized access granted.")
|
||||
} catch (e: Exception) {
|
||||
val msg = "Secure ping failed: ${e.message}"
|
||||
uiState = uiState.copy(isLoading = false, errorMessage = msg)
|
||||
addLog("SecurePing", "Access Denied/Error: ${e.message}", isError = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun triggerSync() {
|
||||
viewModelScope.launch {
|
||||
uiState = uiState.copy(isSyncing = true, errorMessage = null)
|
||||
addLog("Sync", "Starting delta sync...")
|
||||
try {
|
||||
syncService.syncPings()
|
||||
val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
uiState = uiState.copy(
|
||||
isSyncing = false,
|
||||
lastSyncResult = "Sync successful at $now"
|
||||
)
|
||||
addLog("Sync", "Sync completed successfully.")
|
||||
} catch (e: Exception) {
|
||||
val msg = "Sync failed: ${e.message}"
|
||||
uiState = uiState.copy(isSyncing = false, errorMessage = msg)
|
||||
addLog("Sync", "Sync failed: ${e.message}", isError = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearLogs() {
|
||||
uiState = uiState.copy(logs = emptyList())
|
||||
}
|
||||
|
||||
fun clearError() {
|
||||
uiState = uiState.copy(errorMessage = null)
|
||||
}
|
||||
}
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
package at.mocode.ping.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||
|
||||
/**
|
||||
* Eine universelle Terminal-Konsole zur Anzeige von Log-Einträgen.
|
||||
* Plug-and-Play ist fähig für verschiedene Features (Ping, Sync, Auth-Logs).
|
||||
*/
|
||||
@Composable
|
||||
fun TerminalConsole(
|
||||
logs: List<LogEntry>,
|
||||
modifier: Modifier = Modifier,
|
||||
onClear: () -> Unit = {}
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = Dimens.SpacingXS),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text("EVENT LOG", style = MaterialTheme.typography.labelSmall, fontWeight = FontWeight.Bold)
|
||||
TextButton(
|
||||
onClick = onClear,
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
modifier = Modifier.height(24.dp)
|
||||
) {
|
||||
Text("CLEAR", style = MaterialTheme.typography.labelSmall)
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFF1E1E1E)) // Terminallook (Dunkel)
|
||||
.padding(Dimens.SpacingXS)
|
||||
) {
|
||||
items(logs) { log ->
|
||||
val color = if (log.isError) Color(0xFFFF5555) else Color(0xFF55FF55)
|
||||
Text(
|
||||
text = "[${log.timestamp}] [${log.source}] ${log.message}",
|
||||
color = color,
|
||||
fontSize = 11.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
lineHeight = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-173
@@ -1,173 +0,0 @@
|
||||
package at.mocode.ping.feature.data
|
||||
|
||||
import at.mocode.ping.api.EnhancedPingResponse
|
||||
import at.mocode.ping.api.HealthResponse
|
||||
import at.mocode.ping.api.PingResponse
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.mock.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class PingApiKoinClientTest {
|
||||
|
||||
// Hilfe zur Erstellung eines testbaren Clients mithilfe der neuen DI-freundlichen Implementierung
|
||||
private fun createTestClient(mockEngine: MockEngine): PingApiKoinClient {
|
||||
val client = HttpClient(mockEngine) {
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
prettyPrint = true
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
})
|
||||
}
|
||||
}
|
||||
return PingApiKoinClient(client)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `simplePing should return correct response`() = runTest {
|
||||
// Given
|
||||
val expectedResponse = PingResponse(
|
||||
status = "OK",
|
||||
timestamp = "2025-09-27T21:27:00Z",
|
||||
service = "ping-service"
|
||||
)
|
||||
|
||||
val mockEngine = MockEngine { request ->
|
||||
assertEquals("/api/ping/simple", request.url.encodedPath)
|
||||
assertEquals(HttpMethod.Get, request.method)
|
||||
|
||||
respond(
|
||||
content = Json.encodeToString(PingResponse.serializer(), expectedResponse),
|
||||
status = HttpStatusCode.OK,
|
||||
headers = headersOf(HttpHeaders.ContentType, "application/json")
|
||||
)
|
||||
}
|
||||
|
||||
// When
|
||||
val apiClient = createTestClient(mockEngine)
|
||||
val response = apiClient.simplePing()
|
||||
|
||||
// Then
|
||||
assertEquals(expectedResponse, response)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `enhancedPing should include simulate parameter`() = runTest {
|
||||
// Given
|
||||
val expectedResponse = EnhancedPingResponse(
|
||||
status = "OK",
|
||||
timestamp = "2025-09-27T21:27:00Z",
|
||||
service = "ping-service",
|
||||
circuitBreakerState = "CLOSED",
|
||||
responseTime = 42L
|
||||
)
|
||||
|
||||
val mockEngine = MockEngine { request ->
|
||||
assertEquals("/api/ping/enhanced", request.url.encodedPath)
|
||||
assertEquals("true", request.url.parameters["simulate"])
|
||||
assertEquals(HttpMethod.Get, request.method)
|
||||
|
||||
respond(
|
||||
content = Json.encodeToString(EnhancedPingResponse.serializer(), expectedResponse),
|
||||
status = HttpStatusCode.OK,
|
||||
headers = headersOf(HttpHeaders.ContentType, "application/json")
|
||||
)
|
||||
}
|
||||
|
||||
// When
|
||||
val apiClient = createTestClient(mockEngine)
|
||||
val response = apiClient.enhancedPing(simulate = true)
|
||||
|
||||
// Then
|
||||
assertEquals(expectedResponse, response)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `healthCheck should return health response`() = runTest {
|
||||
// Given
|
||||
val expectedResponse = HealthResponse(
|
||||
status = "UP",
|
||||
timestamp = "2025-09-27T21:27:00Z",
|
||||
service = "ping-service",
|
||||
healthy = true
|
||||
)
|
||||
|
||||
val mockEngine = MockEngine { request ->
|
||||
assertEquals("/api/ping/health", request.url.encodedPath)
|
||||
assertEquals(HttpMethod.Get, request.method)
|
||||
|
||||
respond(
|
||||
content = Json.encodeToString(HealthResponse.serializer(), expectedResponse),
|
||||
status = HttpStatusCode.OK,
|
||||
headers = headersOf(HttpHeaders.ContentType, "application/json")
|
||||
)
|
||||
}
|
||||
|
||||
// When
|
||||
val apiClient = createTestClient(mockEngine)
|
||||
val response = apiClient.healthCheck()
|
||||
|
||||
// Then
|
||||
assertEquals(expectedResponse, response)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `JSON serialization should work correctly`() {
|
||||
// Given
|
||||
val pingResponse = PingResponse(
|
||||
status = "OK",
|
||||
timestamp = "2025-09-27T21:27:00Z",
|
||||
service = "test-service"
|
||||
)
|
||||
|
||||
// When
|
||||
val json = Json.encodeToString(PingResponse.serializer(), pingResponse)
|
||||
val deserializedResponse = Json.decodeFromString(PingResponse.serializer(), json)
|
||||
|
||||
// Then
|
||||
assertEquals(pingResponse, deserializedResponse)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Enhanced ping response serialization should work correctly`() {
|
||||
// Given
|
||||
val enhancedResponse = EnhancedPingResponse(
|
||||
status = "OK",
|
||||
timestamp = "2025-09-27T21:27:00Z",
|
||||
service = "test-service",
|
||||
circuitBreakerState = "CLOSED",
|
||||
responseTime = 123L
|
||||
)
|
||||
|
||||
// When
|
||||
val json = Json.encodeToString(EnhancedPingResponse.serializer(), enhancedResponse)
|
||||
val deserializedResponse = Json.decodeFromString(EnhancedPingResponse.serializer(), json)
|
||||
|
||||
// Then
|
||||
assertEquals(enhancedResponse, deserializedResponse)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Health response serialization should work correctly`() {
|
||||
// Given
|
||||
val healthResponse = HealthResponse(
|
||||
status = "UP",
|
||||
timestamp = "2025-09-27T21:27:00Z",
|
||||
service = "test-service",
|
||||
healthy = true
|
||||
)
|
||||
|
||||
// When
|
||||
val json = Json.encodeToString(HealthResponse.serializer(), healthResponse)
|
||||
val deserializedResponse = Json.decodeFromString(HealthResponse.serializer(), json)
|
||||
|
||||
// Then
|
||||
assertEquals(healthResponse, deserializedResponse)
|
||||
}
|
||||
}
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
package at.mocode.ping.feature.integration
|
||||
|
||||
import at.mocode.frontend.core.sync.SyncManager
|
||||
import at.mocode.ping.feature.domain.PingSyncServiceImpl
|
||||
import at.mocode.ping.feature.test.FakePingEventRepository
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.mock.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class PingSyncIntegrationTest {
|
||||
|
||||
@Test
|
||||
fun `syncPings should fetch data from API and store in repository`() = runTest {
|
||||
// Given
|
||||
val fakeRepo = FakePingEventRepository()
|
||||
|
||||
// Mock API Response
|
||||
val mockEngine = MockEngine { request ->
|
||||
assertEquals("/api/ping/sync", request.url.encodedPath)
|
||||
|
||||
val responseBody = """
|
||||
[
|
||||
{
|
||||
"id": "event-1",
|
||||
"message": "Ping 1",
|
||||
"lastModified": 1000
|
||||
},
|
||||
{
|
||||
"id": "event-2",
|
||||
"message": "Ping 2",
|
||||
"lastModified": 2000
|
||||
}
|
||||
]
|
||||
""".trimIndent()
|
||||
|
||||
respond(
|
||||
content = responseBody,
|
||||
status = HttpStatusCode.OK,
|
||||
headers = headersOf(HttpHeaders.ContentType, "application/json")
|
||||
)
|
||||
}
|
||||
|
||||
val httpClient = HttpClient(mockEngine) {
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
ignoreUnknownKeys = true
|
||||
isLenient = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
val syncManager = SyncManager(httpClient)
|
||||
val syncService = PingSyncServiceImpl(syncManager, fakeRepo)
|
||||
|
||||
// When
|
||||
syncService.syncPings()
|
||||
|
||||
// Then
|
||||
assertEquals(2, fakeRepo.storedEvents.size)
|
||||
assertTrue(fakeRepo.storedEvents.any { it.id == "event-1" && it.message == "Ping 1" })
|
||||
assertTrue(fakeRepo.storedEvents.any { it.id == "event-2" && it.message == "Ping 2" })
|
||||
}
|
||||
}
|
||||
-312
@@ -1,312 +0,0 @@
|
||||
package at.mocode.ping.feature.presentation
|
||||
|
||||
import at.mocode.ping.api.EnhancedPingResponse
|
||||
import at.mocode.ping.api.HealthResponse
|
||||
import at.mocode.ping.api.PingResponse
|
||||
import at.mocode.ping.feature.test.FakePingSyncService
|
||||
import at.mocode.ping.feature.test.TestPingApiClient
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class PingViewModelTest {
|
||||
|
||||
private lateinit var viewModel: PingViewModel
|
||||
private lateinit var testApiClient: TestPingApiClient
|
||||
private lateinit var fakeSyncService: FakePingSyncService
|
||||
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
|
||||
@BeforeTest
|
||||
fun setup() {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
testApiClient = TestPingApiClient()
|
||||
fakeSyncService = FakePingSyncService()
|
||||
|
||||
viewModel = PingViewModel(
|
||||
apiClient = testApiClient,
|
||||
syncService = fakeSyncService
|
||||
)
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
fun tearDown() {
|
||||
Dispatchers.resetMain()
|
||||
testApiClient.reset()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be empty`() {
|
||||
// Given & When - initial state
|
||||
val initialState = viewModel.uiState
|
||||
|
||||
// Then
|
||||
assertFalse(initialState.isLoading)
|
||||
assertNull(initialState.simplePingResponse)
|
||||
assertNull(initialState.enhancedPingResponse)
|
||||
assertNull(initialState.healthResponse)
|
||||
assertNull(initialState.errorMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `performSimplePing should update state with success response`() = runTest(testDispatcher) {
|
||||
// Given
|
||||
val expectedResponse = PingResponse(
|
||||
status = "OK",
|
||||
timestamp = "2025-09-27T21:27:00Z",
|
||||
service = "test-service"
|
||||
)
|
||||
testApiClient.simplePingResponse = expectedResponse
|
||||
|
||||
// When
|
||||
viewModel.performSimplePing()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
val finalState = viewModel.uiState
|
||||
assertFalse(finalState.isLoading)
|
||||
assertEquals(expectedResponse, finalState.simplePingResponse)
|
||||
assertNull(finalState.errorMessage)
|
||||
assertTrue(testApiClient.simplePingCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `performSimplePing should set loading state during execution`() = runTest(testDispatcher) {
|
||||
// Given
|
||||
testApiClient.simulateDelay = true
|
||||
testApiClient.delayMs = 100
|
||||
|
||||
// When
|
||||
viewModel.performSimplePing()
|
||||
testDispatcher.scheduler.advanceTimeBy(1) // Allow the coroutine to start
|
||||
|
||||
// Then - should be loading during execution
|
||||
assertTrue(viewModel.uiState.isLoading)
|
||||
assertNull(viewModel.uiState.errorMessage)
|
||||
|
||||
// When - complete the operation
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then - should not be loading anymore
|
||||
assertFalse(viewModel.uiState.isLoading)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `performSimplePing should handle error and update state`() = runTest(testDispatcher) {
|
||||
// Given
|
||||
val errorMessage = "Network error"
|
||||
testApiClient.shouldThrowException = true
|
||||
testApiClient.exceptionMessage = errorMessage
|
||||
|
||||
// When
|
||||
viewModel.performSimplePing()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
val finalState = viewModel.uiState
|
||||
assertFalse(finalState.isLoading)
|
||||
assertNull(finalState.simplePingResponse)
|
||||
assertEquals("Simple ping failed: $errorMessage", finalState.errorMessage)
|
||||
assertTrue(testApiClient.simplePingCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `performEnhancedPing should update state with success response`() = runTest(testDispatcher) {
|
||||
// Given
|
||||
val expectedResponse = EnhancedPingResponse(
|
||||
status = "OK",
|
||||
timestamp = "2025-09-27T21:27:00Z",
|
||||
service = "test-service",
|
||||
circuitBreakerState = "CLOSED",
|
||||
responseTime = 42L
|
||||
)
|
||||
testApiClient.enhancedPingResponse = expectedResponse
|
||||
|
||||
// When
|
||||
viewModel.performEnhancedPing(simulate = false)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
val finalState = viewModel.uiState
|
||||
assertFalse(finalState.isLoading)
|
||||
assertEquals(expectedResponse, finalState.enhancedPingResponse)
|
||||
assertNull(finalState.errorMessage)
|
||||
assertEquals(false, testApiClient.enhancedPingCalledWith)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `performEnhancedPing should handle simulate parameter correctly`() = runTest(testDispatcher) {
|
||||
// When
|
||||
viewModel.performEnhancedPing(simulate = true)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
assertEquals(true, testApiClient.enhancedPingCalledWith)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `performEnhancedPing should handle error and update state`() = runTest(testDispatcher) {
|
||||
// Given
|
||||
val errorMessage = "Enhanced ping error"
|
||||
testApiClient.shouldThrowException = true
|
||||
testApiClient.exceptionMessage = errorMessage
|
||||
|
||||
// When
|
||||
viewModel.performEnhancedPing()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
val finalState = viewModel.uiState
|
||||
assertFalse(finalState.isLoading)
|
||||
assertNull(finalState.enhancedPingResponse)
|
||||
assertEquals("Enhanced ping failed: $errorMessage", finalState.errorMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `performHealthCheck should update state with success response`() = runTest(testDispatcher) {
|
||||
// Given
|
||||
val expectedResponse = HealthResponse(
|
||||
status = "UP",
|
||||
timestamp = "2025-09-27T21:27:00Z",
|
||||
service = "test-service",
|
||||
healthy = true
|
||||
)
|
||||
testApiClient.healthResponse = expectedResponse
|
||||
|
||||
// When
|
||||
viewModel.performHealthCheck()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
val finalState = viewModel.uiState
|
||||
assertFalse(finalState.isLoading)
|
||||
assertEquals(expectedResponse, finalState.healthResponse)
|
||||
assertNull(finalState.errorMessage)
|
||||
assertTrue(testApiClient.healthCheckCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `performHealthCheck should handle error and update state`() = runTest(testDispatcher) {
|
||||
// Given
|
||||
val errorMessage = "Health check error"
|
||||
testApiClient.shouldThrowException = true
|
||||
testApiClient.exceptionMessage = errorMessage
|
||||
|
||||
// When
|
||||
viewModel.performHealthCheck()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
val finalState = viewModel.uiState
|
||||
assertFalse(finalState.isLoading)
|
||||
assertNull(finalState.healthResponse)
|
||||
assertEquals("Health check failed: $errorMessage", finalState.errorMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `triggerSync should call syncService and update state`() = runTest(testDispatcher) {
|
||||
// When
|
||||
viewModel.triggerSync()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
assertTrue(fakeSyncService.syncPingsCalled)
|
||||
assertFalse(viewModel.uiState.isSyncing)
|
||||
assertNotNull(viewModel.uiState.lastSyncResult)
|
||||
assertNull(viewModel.uiState.errorMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `triggerSync should handle error and update state`() = runTest(testDispatcher) {
|
||||
// Given
|
||||
fakeSyncService.shouldThrowException = true
|
||||
fakeSyncService.exceptionMessage = "Sync failed"
|
||||
|
||||
// When
|
||||
viewModel.triggerSync()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
assertTrue(fakeSyncService.syncPingsCalled)
|
||||
assertFalse(viewModel.uiState.isSyncing)
|
||||
assertEquals("Sync failed: Sync failed", viewModel.uiState.errorMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clearError should remove error message from state`() {
|
||||
// Given - set up an error state by simulating an error
|
||||
testApiClient.shouldThrowException = true
|
||||
runTest(testDispatcher) {
|
||||
viewModel.performSimplePing()
|
||||
advanceUntilIdle()
|
||||
}
|
||||
|
||||
// Verify error is present
|
||||
assertNotNull(viewModel.uiState.errorMessage)
|
||||
|
||||
// When
|
||||
viewModel.clearError()
|
||||
|
||||
// Then
|
||||
assertNull(viewModel.uiState.errorMessage)
|
||||
assertFalse(viewModel.uiState.isLoading)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `multiple operations should clear previous error messages`() = runTest(testDispatcher) {
|
||||
// Given - first operation fails
|
||||
testApiClient.shouldThrowException = true
|
||||
viewModel.performSimplePing()
|
||||
advanceUntilIdle()
|
||||
assertNotNull(viewModel.uiState.errorMessage)
|
||||
|
||||
// When - second operation succeeds
|
||||
testApiClient.shouldThrowException = false
|
||||
val successResponse = PingResponse("SUCCESS", "2025-09-27T21:27:00Z", "test-service")
|
||||
testApiClient.simplePingResponse = successResponse
|
||||
viewModel.performSimplePing()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then - error should be cleared
|
||||
assertNull(viewModel.uiState.errorMessage)
|
||||
assertEquals(successResponse, viewModel.uiState.simplePingResponse)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loading state should be false after successful operation`() = runTest(testDispatcher) {
|
||||
// Given
|
||||
viewModel.performSimplePing()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
assertFalse(viewModel.uiState.isLoading)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all operations should call respective API methods`() = runTest(testDispatcher) {
|
||||
// When
|
||||
viewModel.performSimplePing()
|
||||
viewModel.performEnhancedPing(true)
|
||||
viewModel.performHealthCheck()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
assertTrue(testApiClient.simplePingCalled)
|
||||
assertEquals(true, testApiClient.enhancedPingCalledWith)
|
||||
assertTrue(testApiClient.healthCheckCalled)
|
||||
assertEquals(3, testApiClient.callCount)
|
||||
}
|
||||
}
|
||||
-186
@@ -1,186 +0,0 @@
|
||||
package at.mocode.ping.feature.test
|
||||
|
||||
import at.mocode.frontend.core.sync.SyncableRepository
|
||||
import at.mocode.ping.api.EnhancedPingResponse
|
||||
import at.mocode.ping.api.HealthResponse
|
||||
import at.mocode.ping.api.PingApi
|
||||
import at.mocode.ping.api.PingEvent
|
||||
import at.mocode.ping.api.PingResponse
|
||||
import at.mocode.ping.feature.domain.PingSyncService
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
* Fake implementation of PingSyncService for testing.
|
||||
*/
|
||||
class FakePingSyncService : PingSyncService {
|
||||
var syncPingsCalled = false
|
||||
var shouldThrowException = false
|
||||
var exceptionMessage = "Sync failed"
|
||||
|
||||
override suspend fun syncPings() {
|
||||
syncPingsCalled = true
|
||||
if (shouldThrowException) {
|
||||
throw Exception(exceptionMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake implementation of PingEventRepository for testing.
|
||||
*/
|
||||
class FakePingEventRepository : SyncableRepository<PingEvent> {
|
||||
var storedEvents = mutableListOf<PingEvent>()
|
||||
var latestSince: String? = null
|
||||
|
||||
override suspend fun getLatestSince(): String? {
|
||||
return latestSince
|
||||
}
|
||||
|
||||
override suspend fun upsert(items: List<PingEvent>) {
|
||||
// Simple upsert logic: remove existing with same ID, add new
|
||||
val ids = items.map { it.id }.toSet()
|
||||
storedEvents.removeAll { it.id in ids }
|
||||
storedEvents.addAll(items)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test double implementation of PingApi for testing purposes.
|
||||
* This allows us to test ViewModel behavior without needing MockK.
|
||||
*/
|
||||
class TestPingApiClient : PingApi {
|
||||
|
||||
// Test configuration properties
|
||||
var shouldThrowException = false
|
||||
var exceptionMessage = "Test exception"
|
||||
var simulateDelay = false
|
||||
var delayMs = 100L
|
||||
|
||||
// Response configuration
|
||||
var simplePingResponse: PingResponse? = null
|
||||
var enhancedPingResponse: EnhancedPingResponse? = null
|
||||
var healthResponse: HealthResponse? = null
|
||||
var publicPingResponse: PingResponse? = null
|
||||
var securePingResponse: PingResponse? = null
|
||||
var syncPingsResponse: List<PingEvent> = emptyList()
|
||||
|
||||
// Call tracking
|
||||
var simplePingCalled = false
|
||||
var enhancedPingCalledWith: Boolean? = null
|
||||
var healthCheckCalled = false
|
||||
var publicPingCalled = false
|
||||
var securePingCalled = false
|
||||
var syncPingsCalledWith: Long? = null
|
||||
var callCount = 0
|
||||
|
||||
override suspend fun simplePing(): PingResponse {
|
||||
simplePingCalled = true
|
||||
callCount++
|
||||
return handleRequest(simplePingResponse)
|
||||
}
|
||||
|
||||
override suspend fun enhancedPing(simulate: Boolean): EnhancedPingResponse {
|
||||
enhancedPingCalledWith = simulate
|
||||
callCount++
|
||||
|
||||
if (simulateDelay) {
|
||||
delay(delayMs)
|
||||
}
|
||||
|
||||
if (shouldThrowException) {
|
||||
throw Exception(exceptionMessage)
|
||||
}
|
||||
|
||||
return enhancedPingResponse ?: EnhancedPingResponse(
|
||||
status = "OK",
|
||||
timestamp = "2025-09-27T21:27:00Z",
|
||||
service = "test-ping-service",
|
||||
circuitBreakerState = "CLOSED",
|
||||
responseTime = 42L
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun healthCheck(): HealthResponse {
|
||||
healthCheckCalled = true
|
||||
callCount++
|
||||
|
||||
if (simulateDelay) {
|
||||
delay(delayMs)
|
||||
}
|
||||
|
||||
if (shouldThrowException) {
|
||||
throw Exception(exceptionMessage)
|
||||
}
|
||||
|
||||
return healthResponse ?: HealthResponse(
|
||||
status = "UP",
|
||||
timestamp = "2025-09-27T21:27:00Z",
|
||||
service = "test-ping-service",
|
||||
healthy = true
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun publicPing(): PingResponse {
|
||||
publicPingCalled = true
|
||||
callCount++
|
||||
return handleRequest(publicPingResponse)
|
||||
}
|
||||
|
||||
override suspend fun securePing(): PingResponse {
|
||||
securePingCalled = true
|
||||
callCount++
|
||||
return handleRequest(securePingResponse)
|
||||
}
|
||||
|
||||
override suspend fun syncPings(since: Long): List<PingEvent> {
|
||||
syncPingsCalledWith = since
|
||||
callCount++
|
||||
|
||||
if (simulateDelay) {
|
||||
delay(delayMs)
|
||||
}
|
||||
|
||||
if (shouldThrowException) {
|
||||
throw Exception(exceptionMessage)
|
||||
}
|
||||
|
||||
return syncPingsResponse
|
||||
}
|
||||
|
||||
private suspend fun handleRequest(response: PingResponse?): PingResponse {
|
||||
if (simulateDelay) {
|
||||
delay(delayMs)
|
||||
}
|
||||
|
||||
if (shouldThrowException) {
|
||||
throw Exception(exceptionMessage)
|
||||
}
|
||||
|
||||
return response ?: PingResponse(
|
||||
status = "OK",
|
||||
timestamp = "2025-09-27T21:27:00Z",
|
||||
service = "test-ping-service"
|
||||
)
|
||||
}
|
||||
|
||||
// Test utilities
|
||||
fun reset() {
|
||||
shouldThrowException = false
|
||||
exceptionMessage = "Test exception"
|
||||
simulateDelay = false
|
||||
delayMs = 100L
|
||||
simplePingResponse = null
|
||||
enhancedPingResponse = null
|
||||
healthResponse = null
|
||||
publicPingResponse = null
|
||||
securePingResponse = null
|
||||
syncPingsResponse = emptyList()
|
||||
simplePingCalled = false
|
||||
enhancedPingCalledWith = null
|
||||
healthCheckCalled = false
|
||||
publicPingCalled = false
|
||||
securePingCalled = false
|
||||
syncPingsCalledWith = null
|
||||
callCount = 0
|
||||
}
|
||||
}
|
||||
-105
@@ -1,105 +0,0 @@
|
||||
package at.mocode.ping.feature.presentation
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import at.mocode.frontend.core.designsystem.preview.ComponentPreview
|
||||
import at.mocode.ping.api.*
|
||||
import at.mocode.ping.feature.domain.PingSyncService
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Fake-Implementierungen für Preview (kein Koin, kein Netzwerk nötig)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
private val fakePingResponse = PingResponse(
|
||||
status = "OK", timestamp = "2026-03-26T12:00:00Z", service = "ping-service"
|
||||
)
|
||||
|
||||
private val fakeEnhancedResponse = EnhancedPingResponse(
|
||||
status = "OK", timestamp = "2026-03-26T12:00:00Z", service = "ping-service",
|
||||
circuitBreakerState = "CLOSED", responseTime = 42L
|
||||
)
|
||||
|
||||
private val fakeHealthResponse = HealthResponse(
|
||||
status = "UP", timestamp = "2026-03-26T12:00:00Z", service = "ping-service", healthy = true
|
||||
)
|
||||
|
||||
private object FakePingApi : PingApi {
|
||||
override suspend fun simplePing() = fakePingResponse
|
||||
override suspend fun enhancedPing(simulate: Boolean) = fakeEnhancedResponse
|
||||
override suspend fun healthCheck() = fakeHealthResponse
|
||||
override suspend fun publicPing() = fakePingResponse
|
||||
override suspend fun securePing() = fakePingResponse
|
||||
override suspend fun syncPings(since: Long): List<PingEvent> = emptyList()
|
||||
}
|
||||
|
||||
private object FakePingSyncService : PingSyncService {
|
||||
override suspend fun syncPings() { /* no-op */
|
||||
}
|
||||
}
|
||||
|
||||
// Subclass um uiState für Preview direkt setzen zu können
|
||||
private class PreviewPingViewModel(state: PingUiState) :
|
||||
PingViewModel(FakePingApi, FakePingSyncService) {
|
||||
init {
|
||||
uiState = state
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Previews
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@ComponentPreview
|
||||
@Composable
|
||||
fun PreviewPingScreen_Empty() {
|
||||
MaterialTheme {
|
||||
PingScreen(
|
||||
viewModel = PreviewPingViewModel(PingUiState()),
|
||||
onBack = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ComponentPreview
|
||||
@Composable
|
||||
fun PreviewPingScreen_WithData() {
|
||||
MaterialTheme {
|
||||
PingScreen(
|
||||
viewModel = PreviewPingViewModel(
|
||||
PingUiState(
|
||||
simplePingResponse = fakePingResponse,
|
||||
healthResponse = fakeHealthResponse,
|
||||
logs = listOf(
|
||||
LogEntry("12:00:01", "SimplePing", "Success: OK from ping-service"),
|
||||
LogEntry("12:00:00", "HealthCheck", "Status: UP, Healthy: true"),
|
||||
)
|
||||
)
|
||||
),
|
||||
onBack = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ComponentPreview
|
||||
@Composable
|
||||
fun PreviewPingScreen_Loading() {
|
||||
MaterialTheme {
|
||||
PingScreen(
|
||||
viewModel = PreviewPingViewModel(PingUiState(isLoading = true, isSyncing = true)),
|
||||
onBack = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ComponentPreview
|
||||
@Composable
|
||||
fun PreviewPingScreen_Error() {
|
||||
MaterialTheme {
|
||||
PingScreen(
|
||||
viewModel = PreviewPingViewModel(
|
||||
PingUiState(errorMessage = "Connection refused: Backend nicht erreichbar")
|
||||
),
|
||||
onBack = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user