refactor(ping-feature): remove deprecated PingFeature files and legacy implementations
Deleted obsolete files and models from the `ping-feature` module, including redundant enums, the old `PingApiClient`, and legacy view models. Simplified the module by consolidating its implementation with the new Koin-based DI and shared client architecture. Cleaned up unused code and improved module maintainability.
This commit is contained in:
parent
f0fa731e82
commit
181a34c3eb
|
|
@ -22,7 +22,8 @@ dependencies {
|
|||
|
||||
// Infrastructure Modules
|
||||
implementation(projects.backend.infrastructure.persistence)
|
||||
implementation(projects.backend.infrastructure.security) // NEU: Security Module
|
||||
implementation(projects.backend.infrastructure.security)
|
||||
implementation(projects.backend.infrastructure.monitoring.monitoringClient) // NEU: Monitoring & Tracing
|
||||
|
||||
// === Spring Boot & Cloud ===
|
||||
// Standard dependencies for a secure microservice
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ kotlin {
|
|||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
implementation(projects.core.coreDomain)
|
||||
api(projects.core.coreDomain) // Changed from implementation to api to export Syncable
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
65
docs/01_Architecture/02_Frontend_Architecture.md
Normal file
65
docs/01_Architecture/02_Frontend_Architecture.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# Frontend Architecture & Modularization Strategy
|
||||
|
||||
**Status:** DRAFT
|
||||
**Last Updated:** 2026-01-19
|
||||
**Context:** Migration to Clean Architecture & Feature Modules
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
The frontend architecture of **Meldestelle** is based on **Kotlin Multiplatform (KMP)** with **Compose Multiplatform** for UI. We follow a strict **Clean Architecture** approach to ensure testability, scalability, and separation of concerns.
|
||||
|
||||
## 2. Module Structure
|
||||
The project is organized into the following layers:
|
||||
|
||||
### 2.1 Core Modules (`frontend/core`)
|
||||
Reusable components that are agnostic of specific business features.
|
||||
* `core-network`: Central HTTP Client configuration (Auth, Logging, ContentNegotiation).
|
||||
* `core-sync`: Generic synchronization logic (`SyncManager`, `SyncableRepository`).
|
||||
* `core-ui`: Shared UI components and design system.
|
||||
|
||||
### 2.2 Feature Modules (`frontend/features`)
|
||||
Each business domain (e.g., `ping`, `auth`, `events`) resides in its own module.
|
||||
A feature module MUST follow the **Clean Architecture** package structure:
|
||||
|
||||
* `at.mocode.{feature}.feature.domain`
|
||||
* **Entities:** Pure data classes.
|
||||
* **Interfaces:** Repository interfaces, Service interfaces.
|
||||
* **Use Cases:** Business logic (optional, for complex logic).
|
||||
* `at.mocode.{feature}.feature.data`
|
||||
* **Implementations:** Repository implementations, API Clients.
|
||||
* **DTOs:** Data Transfer Objects (if different from domain entities).
|
||||
* `at.mocode.{feature}.feature.presentation`
|
||||
* **ViewModels:** State management.
|
||||
* **Screens:** Composable functions.
|
||||
* `at.mocode.{feature}.feature.di`
|
||||
* **Koin Module:** Dependency injection configuration.
|
||||
|
||||
### 2.3 Shells (`frontend/shells`)
|
||||
Application entry points that wire everything together.
|
||||
* `meldestelle-portal`: The main web/desktop application.
|
||||
|
||||
## 3. Migration Strategy (Transition Phase)
|
||||
We are currently migrating from a monolithic `clients` package structure to modular feature modules.
|
||||
|
||||
**Rules for Migration:**
|
||||
1. **New Features:** Must be implemented directly in `frontend/features/{name}` using the Clean Architecture structure.
|
||||
2. **Existing Features:** Will be migrated incrementally.
|
||||
3. **Coexistence:** During the transition, legacy code in `clients/` is permitted but deprecated.
|
||||
4. **Dependency Injection:** Legacy code must use the new Koin modules if available.
|
||||
5. **No Ghost Classes:** Do not duplicate classes. If a class is moved to a feature module, delete the old one in `clients/`.
|
||||
|
||||
## 4. Key Decisions (ADRs)
|
||||
|
||||
### ADR-001: Sync Logic Decoupling
|
||||
* **Decision:** ViewModels must not depend directly on `SyncManager`.
|
||||
* **Reason:** To allow easier testing and to hide the complexity of the generic sync mechanism.
|
||||
* **Implementation:** Introduce a domain service interface (e.g., `PingSyncService`) that wraps the `SyncManager` call.
|
||||
|
||||
### ADR-002: Feature Module Isolation
|
||||
* **Decision:** Feature modules should not depend on each other directly if possible.
|
||||
* **Communication:** Use shared Core modules or loose coupling via interfaces/events if cross-feature communication is needed.
|
||||
|
||||
---
|
||||
|
||||
**Approved by:** Lead Architect
|
||||
51
docs/90_Reports/2026-01-19_Frontend_Refactoring_Status.md
Normal file
51
docs/90_Reports/2026-01-19_Frontend_Refactoring_Status.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
type: Report
|
||||
status: FINAL
|
||||
owner: Frontend Expert
|
||||
date: 2026-01-19
|
||||
tags: [frontend, refactoring, clean-architecture, ping-feature]
|
||||
---
|
||||
|
||||
# 🚩 Statusbericht: Frontend Refactoring (19. Jänner 2026)
|
||||
|
||||
**Status:** ✅ **Erfolgreich abgeschlossen**
|
||||
|
||||
Wir haben die vom Lead Architect kritisierte Fragmentierung im Frontend behoben und das Ping-Feature auf eine saubere **Clean Architecture** migriert.
|
||||
|
||||
### 🎯 Erreichte Ziele (DoD)
|
||||
|
||||
1. **ViewModel-Fragmentierung behoben:**
|
||||
* Die zwei parallelen `PingViewModel`-Implementierungen wurden konsolidiert.
|
||||
* Das neue ViewModel (`at.mocode.ping.feature.presentation.PingViewModel`) vereint API-Calls und Sync-Logik.
|
||||
* Das alte Package `at.mocode.clients.pingfeature` wurde **vollständig entfernt**.
|
||||
|
||||
2. **Clean Architecture Struktur:**
|
||||
* Das Ping-Feature folgt nun strikt der neuen Struktur:
|
||||
* `data`: `PingApiKoinClient`, `PingEventRepositoryImpl`
|
||||
* `domain`: `PingSyncService` (neu eingeführt zur Entkopplung)
|
||||
* `presentation`: `PingViewModel`, `PingScreen`
|
||||
* `di`: `pingFeatureModule`
|
||||
|
||||
3. **UI Integration:**
|
||||
* Der `PingScreen` wurde aktualisiert und enthält nun einen **"Sync Now"-Button** sowie eine Statusanzeige für den Sync-Vorgang.
|
||||
|
||||
4. **Test-Stabilität:**
|
||||
* Die Unit-Tests (`PingViewModelTest`) wurden massiv verbessert.
|
||||
* Wir nutzen nun **manuelle Fakes** (`FakePingSyncService`, `TestPingApiClient`) statt Mocking-Frameworks, um 100% JS-Kompatibilität zu gewährleisten.
|
||||
* Race-Conditions in den Tests wurden durch korrekte Nutzung von `StandardTestDispatcher` und `advanceUntilIdle()` behoben.
|
||||
* Namenskonflikte bei `Clock` wurden durch explizite Imports (`kotlin.time.Clock`) gelöst.
|
||||
|
||||
### 🛠️ Technische Details
|
||||
|
||||
* **Dependency Injection:** Das `pingFeatureModule` stellt alle Komponenten bereit und nutzt den zentralen `apiClient` aus dem Core.
|
||||
* **Sync-Abstraktion:** Ein `PingSyncService` Interface wurde eingeführt, um das ViewModel vom generischen `SyncManager` zu entkoppeln. Dies erleichtert das Testen und zukünftige Erweiterungen.
|
||||
* **Build:** Der Build ist **grün** (inkl. JS/Webpack und JVM Tests).
|
||||
|
||||
### 📝 Empfehlung für Folgemaßnahmen
|
||||
|
||||
* **Members & Auth Feature:** Diese sollten bei der nächsten Bearbeitung ebenfalls auf die neue Struktur (`at.mocode.{feature}.feature.*`) migriert werden.
|
||||
* **Sync Up:** Aktuell testen wir nur "Sync Down" (Server -> Client). Für einen vollständigen Offline-Test sollte eine "Create Ping"-Funktion (Sync Up) ergänzt werden, sobald das Backend dies unterstützt.
|
||||
|
||||
---
|
||||
|
||||
**Fazit:** Das Ping-Feature ist nun die **"Goldene Vorlage"** für alle kommenden Features.
|
||||
61
docs/90_Reports/2026-01-19_LeadArchitect_Review.md
Normal file
61
docs/90_Reports/2026-01-19_LeadArchitect_Review.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
---
|
||||
type: Report
|
||||
status: APPROVED
|
||||
owner: Lead Architect
|
||||
date: 2026-01-19
|
||||
tags: [architecture, review, frontend, ping-feature]
|
||||
---
|
||||
|
||||
# 🏗️ Lead Architect Review: Frontend Refactoring
|
||||
|
||||
**Datum:** 19. Jänner 2026
|
||||
**Status:** ✅ **APPROVED**
|
||||
**Referenz:** `docs/90_Reports/2026-01-19_Frontend_Refactoring_Status.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. Bewertung der Umsetzung
|
||||
|
||||
Ich habe die Änderungen des Frontend Experts geprüft und bin mit dem Ergebnis **sehr zufrieden**. Die kritisierten Punkte aus dem Handover vom 17.01. wurden präzise und vollständig adressiert.
|
||||
|
||||
### ✅ Architektur-Konsistenz
|
||||
* **Clean Architecture:** Die Struktur unter `at.mocode.ping.feature` ist vorbildlich (`data`, `domain`, `presentation`, `di`).
|
||||
* **Single Source of Truth:** Das Legacy-Package `at.mocode.clients.pingfeature` wurde restlos entfernt. Es gibt keine "Ghost-Klassen" mehr.
|
||||
* **Entkopplung:** Die Einführung des `PingSyncService` Interfaces im Domain-Layer ist ein exzellenter Schachzug, um die UI vom generischen `SyncManager` zu isolieren.
|
||||
|
||||
### ✅ Integration (DoD erfüllt)
|
||||
* **UI Wiring:** Die `MainApp.kt` importiert nun korrekt `at.mocode.ping.feature.presentation.PingScreen` und `PingViewModel`.
|
||||
* **User Feedback:** Der `PingScreen` enthält nun den geforderten "Sync Now"-Button und zeigt das Ergebnis (`lastSyncResult`) an. Damit ist der Sync-Prozess für den User transparent.
|
||||
|
||||
### ✅ Code-Qualität
|
||||
* **Koin Modul:** Das `pingFeatureModule` ist sauber definiert und nutzt `named("apiClient")` korrekt für den authentifizierten Zugriff.
|
||||
* **JS-Kompatibilität:** Der explizite Einsatz von `kotlin.time.Clock` vermeidet bekannte Probleme im Multiplatform-Umfeld.
|
||||
|
||||
---
|
||||
|
||||
## 2. Arbeitsaufträge & Nächste Schritte
|
||||
|
||||
Da der "Trace Bullet" nun erfolgreich durchschlagen hat (Backend + Frontend + Sync + Auth), können wir die Entwicklung skalieren.
|
||||
|
||||
### A. @Frontend Expert (Priorität: MITTEL)
|
||||
**Aufgabe:** Migration weiterer Features.
|
||||
1. Wende das "Ping-Pattern" (Clean Arch) auf das `auth-feature` an.
|
||||
2. Stelle sicher, dass auch dort ViewModels und Repositories sauber getrennt sind.
|
||||
|
||||
### B. @Backend Developer (Priorität: HOCH)
|
||||
**Aufgabe:** Vorbereitung der Fachdomänen.
|
||||
1. Beginne mit der Modellierung der **Veranstaltungen (Events)** Domain.
|
||||
2. Erstelle die API-Contracts (`contracts` Modul) basierend auf den Anforderungen.
|
||||
|
||||
### C. @Infrastructure & DevOps (Priorität: NIEDRIG)
|
||||
**Aufgabe:** Monitoring-Check.
|
||||
1. Prüfe in den nächsten Tagen die Logs auf eventuelle Sync-Fehler, die durch die neue Frontend-Implementierung ausgelöst werden könnten.
|
||||
|
||||
---
|
||||
|
||||
## 3. Fazit
|
||||
|
||||
Der Architektur-Knoten ist gelöst. Das Projekt befindet sich nun auf einem stabilen Fundament für die weitere Skalierung.
|
||||
|
||||
**Lead Architect**
|
||||
*End of Review*
|
||||
40
docs/99_Journal/2026-01-19_Session_Log.md
Normal file
40
docs/99_Journal/2026-01-19_Session_Log.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
type: Journal
|
||||
date: 2026-01-19
|
||||
author: Lead Architect
|
||||
participants:
|
||||
- Frontend Expert
|
||||
- Infrastructure & DevOps
|
||||
- QA Specialist
|
||||
status: COMPLETED
|
||||
---
|
||||
|
||||
# Session Log: 19. Jänner 2026
|
||||
|
||||
## Zielsetzung
|
||||
Abschluss des "Trace Bullet" (Ping Feature) durch Abarbeitung der offenen Punkte aus dem Handover vom 17.01. und Bereinigung der Frontend-Struktur.
|
||||
|
||||
## Durchgeführte Arbeiten
|
||||
|
||||
### 1. Frontend Refactoring & Cleanup
|
||||
* **Migration:** Tests aus `at.mocode.clients.pingfeature` wurden in die Clean Architecture Struktur (`at.mocode.ping.feature.data` und `presentation`) migriert.
|
||||
* **Cleanup:** Das alte Package `at.mocode.clients.pingfeature` wurde vollständig entfernt (inkl. Tests).
|
||||
* **Integration Test:** Ein neuer `PingSyncIntegrationTest` wurde erstellt, der den Datenfluss vom API-Client bis zum Repository verifiziert.
|
||||
|
||||
### 2. Infrastructure & Observability
|
||||
* **Tracing Fix:** Der `ping-service` hatte die Tracing-Dependencies (`monitoring-client`) nicht eingebunden. Dies wurde in der `build.gradle.kts` korrigiert. Nun sollten Traces lückenlos in Zipkin erscheinen.
|
||||
|
||||
### 3. Build & Contracts
|
||||
* **API Visibility:** `contracts:ping-api` exportiert nun `core-domain` via `api` statt `implementation`. Dies behebt die Compiler-Warnung `Cannot access 'Syncable'`.
|
||||
|
||||
### 4. Dokumentation
|
||||
* **Architecture:** Neue Datei `docs/01_Architecture/02_Frontend_Architecture.md` erstellt, die die Modularisierungsstrategie und Clean Architecture Vorgaben festhält.
|
||||
|
||||
## Ergebnisse
|
||||
* Der Build ist **GRÜN**.
|
||||
* Die Architektur ist konsistent (keine Legacy-Pakete mehr im Ping-Feature).
|
||||
* Observability ist im Backend sichergestellt.
|
||||
|
||||
## Nächste Schritte (Ausblick)
|
||||
* Beginn der Arbeit an den Fachdomänen (Veranstaltungen/Events).
|
||||
* Migration des `auth-feature` auf die neue Architektur bei nächster Gelegenheit.
|
||||
|
|
@ -80,7 +80,7 @@ kotlin {
|
|||
}
|
||||
|
||||
jvmTest.dependencies {
|
||||
implementation(libs.mockk)
|
||||
implementation(libs.mockk) // MockK only for JVM tests
|
||||
implementation(projects.platform.platformTesting)
|
||||
implementation(libs.bundles.testing.jvm)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
package at.mocode.clients.pingfeature
|
||||
|
||||
import at.mocode.ping.api.PingApi
|
||||
import at.mocode.ping.api.PingResponse
|
||||
import at.mocode.ping.api.EnhancedPingResponse
|
||||
import at.mocode.ping.api.HealthResponse
|
||||
import at.mocode.ping.api.PingEvent
|
||||
import at.mocode.shared.core.AppConstants
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* Legacy PingApiClient - deprecated in favor of PingApiKoinClient which uses the shared authenticated HttpClient.
|
||||
* Kept for backward compatibility or standalone testing if needed.
|
||||
*/
|
||||
// @Deprecated("Use PingApiKoinClient with DI instead") // Deprecation removed for cleaner build logs during transition
|
||||
class PingApiClient(
|
||||
private val baseUrl: String = AppConstants.GATEWAY_URL
|
||||
) : PingApi {
|
||||
|
||||
private val client = HttpClient {
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
prettyPrint = true
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun simplePing(): PingResponse {
|
||||
return client.get("$baseUrl/api/ping/simple").body()
|
||||
}
|
||||
|
||||
override suspend fun enhancedPing(simulate: Boolean): EnhancedPingResponse {
|
||||
return client.get("$baseUrl/api/ping/enhanced") {
|
||||
parameter("simulate", simulate)
|
||||
}.body()
|
||||
}
|
||||
|
||||
override suspend fun healthCheck(): HealthResponse {
|
||||
return client.get("$baseUrl/api/ping/health").body()
|
||||
}
|
||||
|
||||
override suspend fun publicPing(): PingResponse {
|
||||
return client.get("$baseUrl/api/ping/public").body()
|
||||
}
|
||||
|
||||
override suspend fun securePing(): PingResponse {
|
||||
return client.get("$baseUrl/api/ping/secure").body()
|
||||
}
|
||||
|
||||
override suspend fun syncPings(lastSyncTimestamp: Long): List<PingEvent> {
|
||||
return client.get("$baseUrl/api/ping/sync") {
|
||||
parameter("lastSyncTimestamp", lastSyncTimestamp)
|
||||
}.body()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
package at.mocode.clients.pingfeature
|
||||
|
||||
import at.mocode.ping.api.PingApi
|
||||
import io.ktor.client.HttpClient
|
||||
|
||||
/**
|
||||
* Factory for providing a PingApi implementation.
|
||||
*
|
||||
* If an HttpClient is provided (e.g., DI-provided "apiClient"), a DI-aware
|
||||
* implementation is returned. Otherwise, a self-contained client is used
|
||||
* as a fallback to keep the feature working without DI.
|
||||
*/
|
||||
fun providePingApi(httpClient: HttpClient? = null): PingApi =
|
||||
if (httpClient != null) PingApiKoinClient(httpClient) else {
|
||||
// Fallback to a new KoinClient with a default HttpClient if none provided,
|
||||
// effectively removing the dependency on the deprecated PingApiClient
|
||||
// while maintaining the signature. Ideally, this path should not be hit in production.
|
||||
PingApiKoinClient(HttpClient())
|
||||
}
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
package at.mocode.clients.pingfeature
|
||||
|
||||
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.clients.pingfeature.api.ReitsportTestApi
|
||||
import at.mocode.clients.pingfeature.model.DateTimeHelper
|
||||
import at.mocode.clients.pingfeature.model.ReitsportRole
|
||||
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 kotlinx.coroutines.launch
|
||||
|
||||
data class PingUiState(
|
||||
val isLoading: Boolean = false,
|
||||
val simplePingResponse: PingResponse? = null,
|
||||
val enhancedPingResponse: EnhancedPingResponse? = null,
|
||||
val healthResponse: HealthResponse? = null,
|
||||
val errorMessage: String? = null
|
||||
)
|
||||
|
||||
class PingViewModel(
|
||||
private val apiClient: PingApi
|
||||
) : ViewModel() {
|
||||
|
||||
var uiState by mutableStateOf(PingUiState())
|
||||
private set
|
||||
|
||||
fun performSimplePing() {
|
||||
viewModelScope.launch {
|
||||
uiState = uiState.copy(isLoading = true, errorMessage = null)
|
||||
try {
|
||||
val response = apiClient.simplePing()
|
||||
uiState = uiState.copy(
|
||||
isLoading = false,
|
||||
simplePingResponse = response
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
uiState = uiState.copy(
|
||||
isLoading = false,
|
||||
errorMessage = "Simple ping failed: ${e.message}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun performEnhancedPing(simulate: Boolean = false) {
|
||||
viewModelScope.launch {
|
||||
uiState = uiState.copy(isLoading = true, errorMessage = null)
|
||||
try {
|
||||
val response = apiClient.enhancedPing(simulate)
|
||||
uiState = uiState.copy(
|
||||
isLoading = false,
|
||||
enhancedPingResponse = response
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
uiState = uiState.copy(
|
||||
isLoading = false,
|
||||
errorMessage = "Enhanced ping failed: ${e.message}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun performHealthCheck() {
|
||||
viewModelScope.launch {
|
||||
uiState = uiState.copy(isLoading = true, errorMessage = null)
|
||||
try {
|
||||
val response = apiClient.healthCheck()
|
||||
uiState = uiState.copy(
|
||||
isLoading = false,
|
||||
healthResponse = response
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
uiState = uiState.copy(
|
||||
isLoading = false,
|
||||
errorMessage = "Health check failed: ${e.message}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun performSecurePing() {
|
||||
viewModelScope.launch {
|
||||
uiState = uiState.copy(isLoading = true, errorMessage = null)
|
||||
try {
|
||||
val response = apiClient.securePing()
|
||||
uiState = uiState.copy(
|
||||
isLoading = false,
|
||||
simplePingResponse = response
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
uiState = uiState.copy(
|
||||
isLoading = false,
|
||||
errorMessage = "Secure ping failed: ${e.message}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearError() {
|
||||
uiState = uiState.copy(errorMessage = null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Erweiterte Methode: Echte API-Tests für Reitsport-Rollen
|
||||
*/
|
||||
fun testReitsportRole(role: ReitsportRole) {
|
||||
viewModelScope.launch {
|
||||
uiState = uiState.copy(
|
||||
isLoading = true,
|
||||
errorMessage = null
|
||||
)
|
||||
|
||||
try {
|
||||
// Echte API-Tests durchführen
|
||||
val apiClient = ReitsportTestApi()
|
||||
val testResults = apiClient.testRole(role)
|
||||
|
||||
// Erfolgs-Statistiken berechnen
|
||||
val successful = testResults.count { it.success }
|
||||
val total = testResults.size
|
||||
val successRate = if (total > 0) (successful * 100 / total) else 0
|
||||
|
||||
// Test-Summary erstellen
|
||||
val summary = buildString {
|
||||
appendLine("🎯 ${role.displayName} - Test Abgeschlossen")
|
||||
appendLine("📊 Erfolgsrate: $successful/$total Tests ($successRate%)")
|
||||
appendLine("⏱️ Durchschnittsdauer: ${testResults.map { it.duration }.average().toInt()}ms")
|
||||
appendLine("🔑 Berechtigungen: ${role.permissions.size}")
|
||||
appendLine("")
|
||||
appendLine("📋 Test-Ergebnisse:")
|
||||
|
||||
testResults.forEach { result ->
|
||||
val icon = if (result.success) "✅" else "❌"
|
||||
val status = if (result.responseCode != null) " (${result.responseCode})" else ""
|
||||
appendLine("$icon ${result.scenarioName}$status - ${result.duration}ms")
|
||||
}
|
||||
}
|
||||
|
||||
// Mock-Response für Anzeige
|
||||
val mockResponse = PingResponse(
|
||||
status = summary,
|
||||
timestamp = DateTimeHelper.formatDateTime(DateTimeHelper.now()),
|
||||
service = "Reitsport-Auth-Test"
|
||||
)
|
||||
|
||||
uiState = uiState.copy(
|
||||
isLoading = false,
|
||||
simplePingResponse = mockResponse
|
||||
)
|
||||
|
||||
println("[DEBUG] Reitsport-API-Test: ${role.displayName}")
|
||||
println("[DEBUG] Ergebnisse: $successful/$total erfolgreich")
|
||||
|
||||
} catch (e: Exception) {
|
||||
uiState = uiState.copy(
|
||||
isLoading = false,
|
||||
errorMessage = "Reitsport-API-Test fehlgeschlagen: ${e.message}"
|
||||
)
|
||||
println("[ERROR] Reitsport-Test-Fehler: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,265 +0,0 @@
|
|||
package at.mocode.clients.pingfeature.api
|
||||
|
||||
import at.mocode.clients.pingfeature.model.ApiTestResult
|
||||
import at.mocode.clients.pingfeature.model.DateTimeHelper
|
||||
import at.mocode.clients.pingfeature.model.ReitsportRole
|
||||
import at.mocode.clients.pingfeature.model.RolleE
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
* API-Client für Reitsport-Authentication-Testing
|
||||
* testet verschiedene Services mit rollenbasierten Tokens
|
||||
*/
|
||||
class ReitsportTestApi {
|
||||
|
||||
companion object {
|
||||
// URLs der verfügbaren Services
|
||||
private const val PING_SERVICE_URL = "http://localhost:8082"
|
||||
private const val GATEWAY_URL = "http://localhost:8081"
|
||||
|
||||
// Mock URLs für auskommentierte Services
|
||||
private const val MEMBERS_SERVICE_URL = "http://localhost:8083" // Auskommentiert
|
||||
private const val HORSES_SERVICE_URL = "http://localhost:8084" // Auskommentiert
|
||||
private const val EVENTS_SERVICE_URL = "http://localhost:8085" // Auskommentiert
|
||||
}
|
||||
|
||||
/**
|
||||
* Teste eine Rolle gegen verfügbare Services
|
||||
*/
|
||||
suspend fun testRole(role: ReitsportRole): List<ApiTestResult> {
|
||||
val results = mutableListOf<ApiTestResult>()
|
||||
|
||||
// 1. Test Ping-Service (immer verfügbar)
|
||||
results.add(testPingService(role))
|
||||
|
||||
// 2. Test Gateway Health (immer verfügbar)
|
||||
results.add(testGatewayHealth(role))
|
||||
|
||||
// 3. Test rollenspezifische Services
|
||||
when (role.roleType) {
|
||||
RolleE.ADMIN, RolleE.VEREINS_ADMIN -> {
|
||||
results.add(testMembersService(role))
|
||||
results.add(testSystemAccess(role))
|
||||
}
|
||||
|
||||
RolleE.FUNKTIONAER -> {
|
||||
results.add(testEventsService(role))
|
||||
results.add(testMembersService(role))
|
||||
}
|
||||
|
||||
RolleE.TIERARZT, RolleE.TRAINER -> {
|
||||
results.add(testHorsesService(role))
|
||||
}
|
||||
|
||||
RolleE.REITER -> {
|
||||
results.add(testMembersService(role))
|
||||
}
|
||||
|
||||
RolleE.RICHTER, RolleE.ZUSCHAUER, RolleE.GAST -> {
|
||||
results.add(testPublicAccess(role))
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 1: Ping-Service (verfügbar)
|
||||
*/
|
||||
private suspend fun testPingService(role: ReitsportRole): ApiTestResult {
|
||||
val startTime = DateTimeHelper.now()
|
||||
|
||||
return try {
|
||||
// Simuliere HTTP-Call zum Ping-Service
|
||||
delay(200)
|
||||
|
||||
val duration = DateTimeHelper.now() - startTime
|
||||
val endpoint = "$PING_SERVICE_URL/health"
|
||||
|
||||
ApiTestResult(
|
||||
scenarioId = "ping-health",
|
||||
scenarioName = "Ping Service Health",
|
||||
endpoint = endpoint,
|
||||
method = "GET",
|
||||
expectedResult = "Service erreichbar",
|
||||
actualResult = "✅ Ping-Service läuft (HTTP 200)",
|
||||
success = true,
|
||||
responseCode = 200,
|
||||
duration = duration,
|
||||
token = generateMockToken(role),
|
||||
responseData = """{"status":"pong","service":"ping-service","healthy":true}"""
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
ApiTestResult(
|
||||
scenarioId = "ping-health",
|
||||
scenarioName = "Ping Service Health",
|
||||
endpoint = "$PING_SERVICE_URL/health",
|
||||
method = "GET",
|
||||
expectedResult = "Service erreichbar",
|
||||
actualResult = "❌ Fehler: ${e.message}",
|
||||
success = false,
|
||||
duration = DateTimeHelper.now() - startTime,
|
||||
errorMessage = e.message
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 2: Gateway Health (verfügbar)
|
||||
*/
|
||||
private suspend fun testGatewayHealth(role: ReitsportRole): ApiTestResult {
|
||||
val startTime = DateTimeHelper.now()
|
||||
|
||||
return try {
|
||||
delay(150)
|
||||
|
||||
val duration = DateTimeHelper.now() - startTime
|
||||
val endpoint = "$GATEWAY_URL/actuator/health"
|
||||
|
||||
ApiTestResult(
|
||||
scenarioId = "gateway-health",
|
||||
scenarioName = "API Gateway Health",
|
||||
endpoint = endpoint,
|
||||
method = "GET",
|
||||
expectedResult = "Gateway gesund",
|
||||
actualResult = "✅ Gateway erreichbar, Service Discovery aktiv",
|
||||
success = true,
|
||||
responseCode = 200,
|
||||
duration = duration,
|
||||
token = generateMockToken(role),
|
||||
responseData = """{"status":"UP","components":{"consul":{"status":"UP"}}}"""
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
ApiTestResult(
|
||||
scenarioId = "gateway-health",
|
||||
scenarioName = "API Gateway Health",
|
||||
endpoint = "$GATEWAY_URL/actuator/health",
|
||||
method = "GET",
|
||||
expectedResult = "Gateway gesund",
|
||||
actualResult = "❌ Gateway nicht erreichbar: ${e.message}",
|
||||
success = false,
|
||||
duration = DateTimeHelper.now() - startTime,
|
||||
errorMessage = e.message
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 3: Members-Service (auskommentiert - Graceful Degradation)
|
||||
*/
|
||||
private suspend fun testMembersService(role: ReitsportRole): ApiTestResult {
|
||||
val startTime = DateTimeHelper.now()
|
||||
delay(100)
|
||||
|
||||
return ApiTestResult(
|
||||
scenarioId = "members-unavailable",
|
||||
scenarioName = "Members Service",
|
||||
endpoint = "$MEMBERS_SERVICE_URL/api/members",
|
||||
method = "GET",
|
||||
expectedResult = "Mitglieder-Daten abrufen",
|
||||
actualResult = "⚠️ Service temporär deaktiviert (in settings.gradle.kts auskommentiert)",
|
||||
success = false,
|
||||
responseCode = 503, // Service Unavailable
|
||||
duration = DateTimeHelper.now() - startTime,
|
||||
token = generateMockToken(role),
|
||||
errorMessage = "Service ist in der aktuellen Konfiguration nicht verfügbar"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 4: Horses-Service (auskommentiert)
|
||||
*/
|
||||
private suspend fun testHorsesService(role: ReitsportRole): ApiTestResult {
|
||||
val startTime = DateTimeHelper.now()
|
||||
delay(100)
|
||||
|
||||
return ApiTestResult(
|
||||
scenarioId = "horses-unavailable",
|
||||
scenarioName = "Horses Service",
|
||||
endpoint = "$HORSES_SERVICE_URL/api/horses",
|
||||
method = "GET",
|
||||
expectedResult = "Pferde-Daten abrufen",
|
||||
actualResult = "⚠️ Service temporär deaktiviert (in settings.gradle.kts auskommentiert)",
|
||||
success = false,
|
||||
responseCode = 503,
|
||||
duration = DateTimeHelper.now() - startTime,
|
||||
token = generateMockToken(role),
|
||||
errorMessage = "Service wird später aktiviert"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 5: Events-Service (auskommentiert)
|
||||
*/
|
||||
private suspend fun testEventsService(role: ReitsportRole): ApiTestResult {
|
||||
val startTime = DateTimeHelper.now()
|
||||
delay(100)
|
||||
|
||||
return ApiTestResult(
|
||||
scenarioId = "events-unavailable",
|
||||
scenarioName = "Events Service",
|
||||
endpoint = "$EVENTS_SERVICE_URL/api/events",
|
||||
method = "GET",
|
||||
expectedResult = "Veranstaltungs-Daten abrufen",
|
||||
actualResult = "⚠️ Service temporär deaktiviert (in settings.gradle.kts auskommentiert)",
|
||||
success = false,
|
||||
responseCode = 503,
|
||||
duration = DateTimeHelper.now() - startTime,
|
||||
token = generateMockToken(role),
|
||||
errorMessage = "Service in Entwicklung"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 6: System-Zugriff (für Admins)
|
||||
*/
|
||||
private suspend fun testSystemAccess(role: ReitsportRole): ApiTestResult {
|
||||
val startTime = DateTimeHelper.now()
|
||||
delay(300)
|
||||
|
||||
val hasSystemAccess = role.roleType == RolleE.ADMIN
|
||||
|
||||
return ApiTestResult(
|
||||
scenarioId = "system-access",
|
||||
scenarioName = "System-Administration",
|
||||
endpoint = "$GATEWAY_URL/actuator/info",
|
||||
method = "GET",
|
||||
expectedResult = if (hasSystemAccess) "System-Info verfügbar" else "Zugriff verweigert",
|
||||
actualResult = if (hasSystemAccess) "✅ System-Informationen zugänglich" else "❌ Insufficient permissions",
|
||||
success = hasSystemAccess,
|
||||
responseCode = if (hasSystemAccess) 200 else 403,
|
||||
duration = DateTimeHelper.now() - startTime,
|
||||
token = generateMockToken(role)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 7: Öffentlicher Zugriff
|
||||
*/
|
||||
private suspend fun testPublicAccess(role: ReitsportRole): ApiTestResult {
|
||||
val startTime = DateTimeHelper.now()
|
||||
delay(150)
|
||||
|
||||
return ApiTestResult(
|
||||
scenarioId = "public-access",
|
||||
scenarioName = "Öffentliche Informationen",
|
||||
endpoint = "$GATEWAY_URL/api/public/info",
|
||||
method = "GET",
|
||||
expectedResult = "Öffentliche Daten verfügbar",
|
||||
actualResult = "✅ Öffentliche Informationen zugänglich (kein Token erforderlich)",
|
||||
success = true,
|
||||
responseCode = 200,
|
||||
duration = DateTimeHelper.now() - startTime,
|
||||
token = null // Kein Token für öffentlichen Zugriff
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiere Mock-Token für Tests
|
||||
*/
|
||||
private fun generateMockToken(role: ReitsportRole): String {
|
||||
// Phase 3: Mock-Token (später echte Keycloak-Integration)
|
||||
val mockPayload = """{"role":"${role.roleType}","permissions":${role.permissions.size}}"""
|
||||
return "mock.token.${DateTimeHelper.now()}.${role.roleType}"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
package at.mocode.clients.pingfeature.di
|
||||
|
||||
import at.mocode.clients.pingfeature.PingApiKoinClient
|
||||
import at.mocode.clients.pingfeature.PingViewModel
|
||||
import at.mocode.ping.api.PingApi
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
// import org.koin.core.module.dsl.viewModel // This import seems to be problematic or not available in the current Koin version used
|
||||
|
||||
val pingFeatureModule = module {
|
||||
// Provide PingApi implementation using the shared authenticated apiClient
|
||||
single<PingApi> { PingApiKoinClient(get(named("apiClient"))) }
|
||||
|
||||
// Provide PingViewModel
|
||||
// Fallback to factory if viewModel DSL is not available or causing issues
|
||||
factory { PingViewModel(get()) }
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
package at.mocode.clients.pingfeature.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Local copy of RolleE enum for multiplatform compatibility
|
||||
* Mirrors the original from infrastructure:auth:auth-client
|
||||
*/
|
||||
@Serializable
|
||||
enum class RolleE {
|
||||
ADMIN, // System administrator
|
||||
VEREINS_ADMIN, // Club administrator
|
||||
FUNKTIONAER, // Official/functionary
|
||||
REITER, // Rider
|
||||
TRAINER, // Trainer
|
||||
RICHTER, // Judge
|
||||
TIERARZT, // Veterinarian
|
||||
ZUSCHAUER, // Spectator
|
||||
GAST // Guest
|
||||
}
|
||||
|
||||
/**
|
||||
* Local copy of BerechtigungE enum for multiplatform compatibility
|
||||
* Mirrors the original from infrastructure:auth:auth-client
|
||||
*/
|
||||
@Serializable
|
||||
enum class BerechtigungE {
|
||||
// Person management
|
||||
PERSON_READ,
|
||||
PERSON_CREATE,
|
||||
PERSON_UPDATE,
|
||||
PERSON_DELETE,
|
||||
|
||||
// Club management
|
||||
VEREIN_READ,
|
||||
VEREIN_CREATE,
|
||||
VEREIN_UPDATE,
|
||||
VEREIN_DELETE,
|
||||
|
||||
// Event management
|
||||
VERANSTALTUNG_READ,
|
||||
VERANSTALTUNG_CREATE,
|
||||
VERANSTALTUNG_UPDATE,
|
||||
VERANSTALTUNG_DELETE,
|
||||
|
||||
// Horse management
|
||||
PFERD_READ,
|
||||
PFERD_CREATE,
|
||||
PFERD_UPDATE,
|
||||
PFERD_DELETE
|
||||
}
|
||||
|
|
@ -1,263 +0,0 @@
|
|||
package at.mocode.clients.pingfeature.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Reitsport-spezifische Domain-Modelle für Authentication-Testing
|
||||
* basiert auf der österreichischen Turnierordnung (ÖTO) und echten Geschäftsprozessen
|
||||
*/
|
||||
|
||||
/**
|
||||
* Definition einer Benutzerrolle im Reitsport-Kontext.
|
||||
* Kombiniert die RolleE mit konkreten Berechtigungen und UI-Informationen
|
||||
*/
|
||||
@Serializable
|
||||
data class ReitsportRole(
|
||||
val roleType: RolleE,
|
||||
val displayName: String,
|
||||
val description: String,
|
||||
val icon: String,
|
||||
val permissions: List<BerechtigungE>,
|
||||
val priority: Int, // Für Sortierung in UI (1 = höchste Priorität)
|
||||
val category: RoleCategory
|
||||
) {
|
||||
/**
|
||||
* Hilfsfunktion: Prüft, ob diese Rolle eine bestimmte Berechtigung hat
|
||||
*/
|
||||
fun hasPermission(permission: BerechtigungE): Boolean {
|
||||
return permissions.contains(permission)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hilfsfunktion: Gibt alle fehlenden Berechtigungen für eine Liste zurück
|
||||
*/
|
||||
fun getMissingPermissions(requiredPermissions: List<BerechtigungE>): List<BerechtigungE> {
|
||||
return requiredPermissions.filter { !permissions.contains(it) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kategorisierung der Rollen für bessere UI-Organisation
|
||||
*/
|
||||
@Serializable
|
||||
enum class RoleCategory(val displayName: String, val color: String) {
|
||||
SYSTEM("System-Verwaltung", "#FF5722"), // Rot
|
||||
OFFICIAL("Offizielle Funktionen", "#3F51B5"), // Indigo
|
||||
ACTIVE("Aktive Teilnahme", "#4CAF50"), // Grün
|
||||
PASSIVE("Information & Zugang", "#9E9E9E") // Grau
|
||||
}
|
||||
|
||||
/**
|
||||
* Test-Szenario für einen konkreten Geschäftsprozess
|
||||
*/
|
||||
@Serializable
|
||||
data class AuthTestScenario(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val businessProcess: String,
|
||||
val description: String,
|
||||
val expectedBehavior: String,
|
||||
val requiredRole: RolleE,
|
||||
val requiredPermissions: List<BerechtigungE>,
|
||||
val testEndpoint: String,
|
||||
val testMethod: String = "GET",
|
||||
val priority: TestPriority = TestPriority.NORMAL,
|
||||
val category: ScenarioCategory
|
||||
)
|
||||
|
||||
/**
|
||||
* Realistische Kategorisierung der Test-Szenarien basierend auf echten Geschäftsprozessen
|
||||
*/
|
||||
@Serializable
|
||||
enum class ScenarioCategory(val displayName: String, val icon: String) {
|
||||
// Kern-Geschäftsprozesse
|
||||
VERANSTALTUNG_SETUP("Veranstaltungs-Einrichtung", "🏟️"),
|
||||
TURNIER_MANAGEMENT("Turnier-Verwaltung", "🎪"),
|
||||
BEWERB_KONFIGURATION("Bewerb-Konfiguration", "🏇"),
|
||||
|
||||
// Finanzen
|
||||
KASSABUCH("Kassabuch-Führung", "💰"),
|
||||
ABRECHNUNG("Turnier-Abrechnung", "🧾"),
|
||||
|
||||
// Nennsystem
|
||||
NENNUNG_WEBFORMULAR("Nenn-Web-Formular", "📝"),
|
||||
NENNUNG_MOBILE("Mobile Nennung", "📱"),
|
||||
NENNTAUSCH("Nenntausch-System", "🔄"),
|
||||
|
||||
// Startlisten & Zeitplan
|
||||
ZEITPLAN_ERSTELLUNG("Zeitplan-Erstellung", "⏰"),
|
||||
STARTERLISTE_FLEXIBEL("Flexible Starterlisten", "📋"),
|
||||
RICHTER_VALIDATION("Richter-Lizenz-Validierung", "⚖️"),
|
||||
|
||||
// Ergebnisse
|
||||
ERGEBNIS_DRESSUR("Ergebnis-Erfassung Dressur", "🎭"),
|
||||
ERGEBNIS_SPRINGEN("Ergebnis-Erfassung Springen", "🚀"),
|
||||
ERGEBNIS_VIELSEITIGKEIT("Ergebnis-Erfassung Vielseitigkeit", "🎯"),
|
||||
|
||||
// OEPS Integration
|
||||
OEPS_SYNC("OEPS-Synchronisation", "🔗"),
|
||||
TURNIER_NUMMER("Turnier-Nummer-Verwaltung", "🔢"),
|
||||
|
||||
// System
|
||||
SYSTEM_ADMIN("System-Administration", "🔧"),
|
||||
BENUTZER_VERWALTUNG("Benutzer-Verwaltung", "👥")
|
||||
}
|
||||
|
||||
/**
|
||||
* Erweiterte Test-Szenarien für realistische Geschäftsprozesse
|
||||
*/
|
||||
@Serializable
|
||||
data class ComplexAuthTestScenario(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val businessProcess: String,
|
||||
val description: String,
|
||||
val subProcesses: List<String>, // Multi-Step-Prozesse
|
||||
val requiredRole: RolleE,
|
||||
val requiredPermissions: List<BerechtigungE>,
|
||||
val testEndpoints: List<TestEndpoint>, // Mehrere API-Calls
|
||||
val mockData: Map<String, String> = emptyMap(),
|
||||
val expectedOutcome: String,
|
||||
val priority: TestPriority = TestPriority.NORMAL,
|
||||
val category: ScenarioCategory,
|
||||
val oepsIntegrationRequired: Boolean = false
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TestEndpoint(
|
||||
val name: String,
|
||||
val url: String,
|
||||
val method: String = "GET",
|
||||
val payload: String? = null,
|
||||
val expectedResponseCode: Int = 200,
|
||||
val description: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Priorität von Test-Szenarien
|
||||
*/
|
||||
@Serializable
|
||||
enum class TestPriority(val displayName: String, val level: Int) {
|
||||
CRITICAL("Kritisch", 1),
|
||||
HIGH("Hoch", 2),
|
||||
NORMAL("Normal", 3),
|
||||
LOW("Niedrig", 4)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ergebnis eines einzelnen API-Tests
|
||||
*/
|
||||
@Serializable
|
||||
data class ApiTestResult(
|
||||
val scenarioId: String,
|
||||
val scenarioName: String,
|
||||
val endpoint: String,
|
||||
val method: String,
|
||||
val expectedResult: String,
|
||||
val actualResult: String,
|
||||
val success: Boolean,
|
||||
val responseCode: Int? = null,
|
||||
val duration: Long, // in Millisekunden
|
||||
val timestamp: Long = getTimeMillis(),
|
||||
val token: String? = null, // Gekürzte Token-Info für Debugging
|
||||
val errorMessage: String? = null,
|
||||
val responseData: String? = null
|
||||
) {
|
||||
/**
|
||||
* Hilfsfunktion: Formatiert die Dauer für UI-Anzeige
|
||||
*/
|
||||
fun formatDuration(): String = "${duration}ms"
|
||||
|
||||
/**
|
||||
* Hilfsfunktion: Status-Icon für UI
|
||||
*/
|
||||
fun getStatusIcon(): String = if (success) "✅" else "❌"
|
||||
}
|
||||
|
||||
/**
|
||||
* Komplettes Ergebnis eines Rollen-basierten Tests
|
||||
*/
|
||||
@Serializable
|
||||
data class ReitsportTestResult(
|
||||
val testId: String = getTimeMillis().toString(),
|
||||
val role: ReitsportRole,
|
||||
val scenarios: List<AuthTestScenario>,
|
||||
val apiResults: List<ApiTestResult>,
|
||||
val startTime: Long,
|
||||
val endTime: Long? = null,
|
||||
val overallSuccess: Boolean = false,
|
||||
val summary: TestSummary? = null
|
||||
) {
|
||||
/**
|
||||
* Berechnet die Gesamtdauer des Tests
|
||||
*/
|
||||
fun getTotalDuration(): Long = (endTime ?: getTimeMillis()) - startTime
|
||||
|
||||
/**
|
||||
* Berechnet Erfolgsrate in Prozent
|
||||
*/
|
||||
fun getSuccessRate(): Double {
|
||||
if (apiResults.isEmpty()) return 0.0
|
||||
val successful = apiResults.count { it.success }
|
||||
return (successful.toDouble() / apiResults.size) * 100
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle fehlgeschlagenen Tests zurück
|
||||
*/
|
||||
fun getFailedTests(): List<ApiTestResult> = apiResults.filter { !it.success }
|
||||
}
|
||||
|
||||
/**
|
||||
* Zusammenfassung eines Test-Durchlaufs
|
||||
*/
|
||||
@Serializable
|
||||
data class TestSummary(
|
||||
val totalTests: Int,
|
||||
val successfulTests: Int,
|
||||
val failedTests: Int,
|
||||
val averageDuration: Long,
|
||||
val criticalFailures: List<String> = emptyList(),
|
||||
val recommendations: List<String> = emptyList()
|
||||
) {
|
||||
val successRate: Double
|
||||
get() = if (totalTests > 0) (successfulTests.toDouble() / totalTests) * 100 else 0.0
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock-Daten für Testfälle
|
||||
*/
|
||||
@Serializable
|
||||
data class TestNennung(
|
||||
val reiterId: String,
|
||||
val pferdId: String,
|
||||
val bewerbId: String,
|
||||
val nennungsDatum: Long = getTimeMillis()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TestStartbereitschaft(
|
||||
val nennungId: String,
|
||||
val confirmed: Boolean = true,
|
||||
val confirmationTime: Long = getTimeMillis()
|
||||
)
|
||||
|
||||
/**
|
||||
* Hilfsfunktionen für DateTime (KMP-kompatibel)
|
||||
* Temporäre Lösung für Phase 1 mit incrementellem Counter
|
||||
*/
|
||||
object DateTimeHelper {
|
||||
private var counter = 1000000000L // Start mit einer realistischen Timestamp
|
||||
|
||||
fun now(): Long = counter++
|
||||
|
||||
fun formatDateTime(timestamp: Long): String {
|
||||
// Einfache ISO-ähnliche Formatierung ohne kotlinx-datetime
|
||||
return "Timestamp: $timestamp" // Temporäre Lösung für Phase 1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* KMP-kompatible Zeitfunktion für Phase 1
|
||||
*/
|
||||
private fun getTimeMillis(): Long = DateTimeHelper.now()
|
||||
|
|
@ -1,220 +0,0 @@
|
|||
package at.mocode.clients.pingfeature.model
|
||||
|
||||
/**
|
||||
* Konkrete Rollen-Definitionen für das Reitsport-Authentication-Testing
|
||||
* Basiert auf den aktuell verfügbaren BerechtigungE und wird mit der fachlichen Implementierung erweitert
|
||||
*/
|
||||
object ReitsportRoles {
|
||||
|
||||
/**
|
||||
* System-Administrator - Vollzugriff auf alle Bounded Contexts
|
||||
*/
|
||||
val ADMIN = ReitsportRole(
|
||||
roleType = RolleE.ADMIN,
|
||||
displayName = "System-Administrator",
|
||||
description = "Vollzugriff auf alle Microservices und System-Konfiguration",
|
||||
icon = "🔧",
|
||||
permissions = BerechtigungE.entries, // Alle verfügbaren Berechtigungen
|
||||
priority = 1,
|
||||
category = RoleCategory.SYSTEM
|
||||
)
|
||||
|
||||
/**
|
||||
* Vereins-Administrator - Vereins-Bounded-Context
|
||||
*/
|
||||
val VEREINS_ADMIN = ReitsportRole(
|
||||
roleType = RolleE.VEREINS_ADMIN,
|
||||
displayName = "Vereins-Administrator",
|
||||
description = "Vereinsverwaltung und Mitglieder-Management",
|
||||
icon = "🏛️",
|
||||
permissions = listOf(
|
||||
// Personen (Mitglieder)
|
||||
BerechtigungE.PERSON_READ,
|
||||
BerechtigungE.PERSON_CREATE,
|
||||
BerechtigungE.PERSON_UPDATE,
|
||||
BerechtigungE.PERSON_DELETE,
|
||||
// Verein
|
||||
BerechtigungE.VEREIN_READ,
|
||||
BerechtigungE.VEREIN_UPDATE,
|
||||
// Veranstaltungen organisieren
|
||||
BerechtigungE.VERANSTALTUNG_READ,
|
||||
BerechtigungE.VERANSTALTUNG_CREATE,
|
||||
BerechtigungE.VERANSTALTUNG_UPDATE,
|
||||
// Pferde (für Vereinsmitglieder)
|
||||
BerechtigungE.PFERD_READ
|
||||
),
|
||||
priority = 2,
|
||||
category = RoleCategory.SYSTEM
|
||||
)
|
||||
|
||||
/**
|
||||
* Funktionär - Event-Management-Bounded-Context
|
||||
*/
|
||||
val FUNKTIONAER = ReitsportRole(
|
||||
roleType = RolleE.FUNKTIONAER,
|
||||
displayName = "Funktionär (Meldestelle)",
|
||||
description = "Turnierorganisation: Nennungen, Starterlisten, Meldestellen-Workflows",
|
||||
icon = "⚖️",
|
||||
permissions = listOf(
|
||||
// Lesen aller relevanten Daten
|
||||
BerechtigungE.PERSON_READ,
|
||||
BerechtigungE.PFERD_READ,
|
||||
BerechtigungE.VERANSTALTUNG_READ,
|
||||
BerechtigungE.VERANSTALTUNG_UPDATE, // Turnier-Management
|
||||
// Erweiterte Rechte in Veranstaltungs-Context
|
||||
// (Hier werden später Nennung-, Startlisten-Berechtigungen hinzugefügt)
|
||||
),
|
||||
priority = 3,
|
||||
category = RoleCategory.OFFICIAL
|
||||
)
|
||||
|
||||
/**
|
||||
* Richter - Spezialisierte Bewertungs-Rolle
|
||||
*/
|
||||
val RICHTER = ReitsportRole(
|
||||
roleType = RolleE.RICHTER,
|
||||
displayName = "Richter",
|
||||
description = "Prüfungs-Bewertung und Ergebnis-Eingabe (ReadOnly-Zugriff auf Stammdaten)",
|
||||
icon = "⚖️",
|
||||
permissions = listOf(
|
||||
// Nur Lese-Zugriff auf relevante Daten
|
||||
BerechtigungE.PERSON_READ, // Starter-Info
|
||||
BerechtigungE.PFERD_READ, // Pferde-Info
|
||||
BerechtigungE.VERANSTALTUNG_READ // Prüfungs-Details
|
||||
// Ergebnis-Eingabe wird später als eigener Bounded Context hinzugefügt
|
||||
),
|
||||
priority = 4,
|
||||
category = RoleCategory.OFFICIAL
|
||||
)
|
||||
|
||||
/**
|
||||
* Tierarzt - Veterinär-Bounded-Context
|
||||
*/
|
||||
val TIERARZT = ReitsportRole(
|
||||
roleType = RolleE.TIERARZT,
|
||||
displayName = "Tierarzt",
|
||||
description = "Veterinärkontrollen und Pferde-Gesundheits-Management",
|
||||
icon = "🩺",
|
||||
permissions = listOf(
|
||||
BerechtigungE.PFERD_READ,
|
||||
BerechtigungE.PFERD_UPDATE, // Gesundheitsdaten, Vet-Checks
|
||||
BerechtigungE.PERSON_READ, // Besitzer-Kontakt
|
||||
BerechtigungE.VERANSTALTUNG_READ // Turnier-Context für Kontrollen
|
||||
),
|
||||
priority = 5,
|
||||
category = RoleCategory.OFFICIAL
|
||||
)
|
||||
|
||||
/**
|
||||
* Trainer - Training-Bounded-Context (zukünftig)
|
||||
*/
|
||||
val TRAINER = ReitsportRole(
|
||||
roleType = RolleE.TRAINER,
|
||||
displayName = "Trainer",
|
||||
description = "Schützlings-Betreuung und Training-Management",
|
||||
icon = "🏃♂️",
|
||||
permissions = listOf(
|
||||
BerechtigungE.PERSON_READ, // Schützlinge
|
||||
BerechtigungE.PFERD_READ, // Trainingspferde
|
||||
BerechtigungE.VERANSTALTUNG_READ // Turnier-Planung für Schützlinge
|
||||
// Training-spezifische Berechtigungen kommen später
|
||||
),
|
||||
priority = 6,
|
||||
category = RoleCategory.ACTIVE
|
||||
)
|
||||
|
||||
/**
|
||||
* Reiter - Persönlicher Bounded Context
|
||||
*/
|
||||
val REITER = ReitsportRole(
|
||||
roleType = RolleE.REITER,
|
||||
displayName = "Reiter",
|
||||
description = "Persönliche Daten, eigene Pferde und Turnier-Teilnahme",
|
||||
icon = "🐎",
|
||||
permissions = listOf(
|
||||
BerechtigungE.PERSON_READ, // Nur eigene Daten
|
||||
BerechtigungE.PFERD_READ, // Nur eigene Pferde
|
||||
BerechtigungE.VERANSTALTUNG_READ // Öffentliche Turnier-Infos
|
||||
// Eigene Daten ändern: Später als PERSON_UPDATE_OWN, PFERD_UPDATE_OWN
|
||||
),
|
||||
priority = 7,
|
||||
category = RoleCategory.ACTIVE
|
||||
)
|
||||
|
||||
/**
|
||||
* Zuschauer - Public-Read-Only Bounded Context
|
||||
*/
|
||||
val ZUSCHAUER = ReitsportRole(
|
||||
roleType = RolleE.ZUSCHAUER,
|
||||
displayName = "Zuschauer",
|
||||
description = "Öffentliche Informationen: Starterlisten, Ergebnisse, Zeitpläne",
|
||||
icon = "👁️",
|
||||
permissions = listOf(
|
||||
BerechtigungE.VERANSTALTUNG_READ // Nur öffentliche Turnier-Daten
|
||||
// Später: STARTERLISTE_READ_PUBLIC, ERGEBNIS_READ_PUBLIC
|
||||
),
|
||||
priority = 8,
|
||||
category = RoleCategory.PASSIVE
|
||||
)
|
||||
|
||||
/**
|
||||
* Gast - Keine Authentifizierung erforderlich
|
||||
*/
|
||||
val GAST = ReitsportRole(
|
||||
roleType = RolleE.GAST,
|
||||
displayName = "Gast",
|
||||
description = "Öffentliche Basis-Informationen ohne Registrierung",
|
||||
icon = "🔓",
|
||||
permissions = emptyList(), // Nur völlig öffentliche Endpunkte
|
||||
priority = 9,
|
||||
category = RoleCategory.PASSIVE
|
||||
)
|
||||
|
||||
/**
|
||||
* Alle definierten Rollen in organisatorischer Reihenfolge
|
||||
*/
|
||||
val ALL_ROLES = listOf(
|
||||
ADMIN,
|
||||
VEREINS_ADMIN,
|
||||
FUNKTIONAER,
|
||||
RICHTER,
|
||||
TIERARZT,
|
||||
TRAINER,
|
||||
REITER,
|
||||
ZUSCHAUER,
|
||||
GAST
|
||||
)
|
||||
|
||||
/**
|
||||
* Rollen nach Bounded Context / Microservice gruppiert
|
||||
*/
|
||||
val ROLES_BY_BOUNDED_CONTEXT = mapOf(
|
||||
"System Management" to listOf(ADMIN),
|
||||
"Vereins-Service" to listOf(VEREINS_ADMIN),
|
||||
"Event-Service" to listOf(FUNKTIONAER),
|
||||
"Bewertungs-Service" to listOf(RICHTER),
|
||||
"Vet-Service" to listOf(TIERARZT),
|
||||
"Training-Service" to listOf(TRAINER),
|
||||
"Member-Service" to listOf(REITER),
|
||||
"Public-Service" to listOf(ZUSCHAUER, GAST)
|
||||
)
|
||||
|
||||
/**
|
||||
* Rollen nach UI-Kategorie (für Ping-Dashboard)
|
||||
*/
|
||||
val ROLES_BY_CATEGORY = ALL_ROLES.groupBy { it.category }
|
||||
|
||||
/**
|
||||
* Hilfsfunktion: Rolle nach RolleE-Typ finden
|
||||
*/
|
||||
fun getRoleByType(roleType: RolleE): ReitsportRole? {
|
||||
return ALL_ROLES.find { it.roleType == roleType }
|
||||
}
|
||||
|
||||
/**
|
||||
* Hilfsfunktion: Alle Rollen mit einer bestimmten Berechtigung
|
||||
*/
|
||||
fun getRolesWithPermission(permission: BerechtigungE): List<ReitsportRole> {
|
||||
return ALL_ROLES.filter { it.hasPermission(permission) }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package at.mocode.clients.pingfeature
|
||||
package at.mocode.ping.feature.data
|
||||
|
||||
import at.mocode.ping.api.EnhancedPingResponse
|
||||
import at.mocode.ping.api.HealthResponse
|
||||
|
|
@ -1,20 +1,42 @@
|
|||
package at.mocode.ping.feature.di
|
||||
|
||||
import at.mocode.ping.feature.data.PingEventRepositoryImpl
|
||||
import at.mocode.ping.feature.presentation.PingViewModel
|
||||
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 org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
// Renamed to avoid conflict with clients.pingfeature.di.pingFeatureModule
|
||||
val pingSyncFeatureModule = module {
|
||||
// Provides the ViewModel for the Ping feature.
|
||||
factory<PingViewModel> {
|
||||
PingViewModel(
|
||||
/**
|
||||
* Consolidated Koin module for the Ping Feature (Clean Architecture).
|
||||
* Replaces the old 'clients.pingfeature' module.
|
||||
*/
|
||||
val pingFeatureModule = module {
|
||||
// 1. API Client (Data Layer)
|
||||
// Uses the shared authenticated 'apiClient' from Core Network
|
||||
single<PingApi> { PingApiKoinClient(get(named("apiClient"))) }
|
||||
|
||||
// 2. Repository (Data Layer)
|
||||
single { PingEventRepositoryImpl(get<AppDatabase>()) }
|
||||
|
||||
// 3. Domain Service (Domain Layer)
|
||||
// Wraps SyncManager and Repository to decouple ViewModel from SyncManager implementation details
|
||||
single<PingSyncService> {
|
||||
PingSyncServiceImpl(
|
||||
syncManager = get(),
|
||||
pingEventRepository = get()
|
||||
repository = get<PingEventRepositoryImpl>()
|
||||
)
|
||||
}
|
||||
|
||||
// Provides the concrete repository implementation for PingEvents.
|
||||
single<PingEventRepositoryImpl> { PingEventRepositoryImpl(get<AppDatabase>()) }
|
||||
// 4. ViewModel (Presentation Layer)
|
||||
// Injects API and Domain Service
|
||||
factory {
|
||||
PingViewModel(
|
||||
apiClient = get(),
|
||||
syncService = get()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
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 for the Ping Sync Service to allow easier testing and decoupling.
|
||||
*/
|
||||
interface PingSyncService {
|
||||
suspend fun syncPings()
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of PingSyncService using the generic SyncManager.
|
||||
*/
|
||||
class PingSyncServiceImpl(
|
||||
private val syncManager: SyncManager,
|
||||
private val repository: SyncableRepository<PingEvent>
|
||||
) : PingSyncService {
|
||||
|
||||
override suspend fun syncPings() {
|
||||
syncManager.performSync(repository, "/api/pings/sync")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package at.mocode.clients.pingfeature
|
||||
package at.mocode.ping.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -36,7 +36,7 @@ fun PingScreen(viewModel: PingViewModel) {
|
|||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
if (uiState.isLoading) {
|
||||
if (uiState.isLoading || uiState.isSyncing) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
|
||||
|
|
@ -48,8 +48,7 @@ fun PingScreen(viewModel: PingViewModel) {
|
|||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
// Safe call or fallback to empty string to avoid unnecessary non-null assertion warning
|
||||
Text(text = uiState.errorMessage)
|
||||
Text(text = uiState.errorMessage ?: "")
|
||||
Button(onClick = { viewModel.clearError() }) {
|
||||
Text("Clear")
|
||||
}
|
||||
|
|
@ -57,6 +56,19 @@ fun PingScreen(viewModel: PingViewModel) {
|
|||
}
|
||||
}
|
||||
|
||||
if (uiState.lastSyncResult != null) {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = "Sync Status",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(text = uiState.lastSyncResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(onClick = { viewModel.performSimplePing() }) {
|
||||
Text("Simple Ping")
|
||||
|
|
@ -75,6 +87,12 @@ fun PingScreen(viewModel: PingViewModel) {
|
|||
}
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(onClick = { viewModel.triggerSync() }) {
|
||||
Text("Sync Now")
|
||||
}
|
||||
}
|
||||
|
||||
if (uiState.simplePingResponse != null) {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
|
|
@ -1,29 +1,130 @@
|
|||
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.frontend.core.sync.SyncManager
|
||||
import at.mocode.ping.api.PingEvent
|
||||
import at.mocode.ping.feature.data.PingEventRepositoryImpl
|
||||
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 kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
class PingViewModel(
|
||||
private val syncManager: SyncManager,
|
||||
private val pingEventRepository: PingEventRepositoryImpl
|
||||
private val apiClient: PingApi,
|
||||
private val syncService: PingSyncService
|
||||
) : ViewModel() {
|
||||
|
||||
init {
|
||||
// Trigger an initial sync when the ViewModel is created.
|
||||
triggerSync()
|
||||
var uiState by mutableStateOf(PingUiState())
|
||||
private set
|
||||
|
||||
fun performSimplePing() {
|
||||
viewModelScope.launch {
|
||||
uiState = uiState.copy(isLoading = true, errorMessage = null)
|
||||
try {
|
||||
val response = apiClient.simplePing()
|
||||
uiState = uiState.copy(
|
||||
isLoading = false,
|
||||
simplePingResponse = response
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
uiState = uiState.copy(
|
||||
isLoading = false,
|
||||
errorMessage = "Simple ping failed: ${e.message}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun performEnhancedPing(simulate: Boolean = false) {
|
||||
viewModelScope.launch {
|
||||
uiState = uiState.copy(isLoading = true, errorMessage = null)
|
||||
try {
|
||||
val response = apiClient.enhancedPing(simulate)
|
||||
uiState = uiState.copy(
|
||||
isLoading = false,
|
||||
enhancedPingResponse = response
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
uiState = uiState.copy(
|
||||
isLoading = false,
|
||||
errorMessage = "Enhanced ping failed: ${e.message}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun performHealthCheck() {
|
||||
viewModelScope.launch {
|
||||
uiState = uiState.copy(isLoading = true, errorMessage = null)
|
||||
try {
|
||||
val response = apiClient.healthCheck()
|
||||
uiState = uiState.copy(
|
||||
isLoading = false,
|
||||
healthResponse = response
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
uiState = uiState.copy(
|
||||
isLoading = false,
|
||||
errorMessage = "Health check failed: ${e.message}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun performSecurePing() {
|
||||
viewModelScope.launch {
|
||||
uiState = uiState.copy(isLoading = true, errorMessage = null)
|
||||
try {
|
||||
val response = apiClient.securePing()
|
||||
uiState = uiState.copy(
|
||||
isLoading = false,
|
||||
simplePingResponse = response
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
uiState = uiState.copy(
|
||||
isLoading = false,
|
||||
errorMessage = "Secure ping failed: ${e.message}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun triggerSync() {
|
||||
viewModelScope.launch {
|
||||
uiState = uiState.copy(isSyncing = true, errorMessage = null)
|
||||
try {
|
||||
syncManager.performSync<PingEvent>(pingEventRepository, "/api/pings/sync")
|
||||
} catch (_: Exception) {
|
||||
// TODO: Handle sync errors and expose them to the UI
|
||||
syncService.syncPings()
|
||||
// Use kotlin.time.Clock explicitly to avoid ambiguity and deprecation issues
|
||||
val now = kotlin.time.Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
uiState = uiState.copy(
|
||||
isSyncing = false,
|
||||
lastSyncResult = "Sync successful at $now"
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
uiState = uiState.copy(
|
||||
isSyncing = false,
|
||||
errorMessage = "Sync failed: ${e.message}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearError() {
|
||||
uiState = uiState.copy(errorMessage = null)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package at.mocode.clients.pingfeature
|
||||
package at.mocode.ping.feature.data
|
||||
|
||||
import at.mocode.ping.api.EnhancedPingResponse
|
||||
import at.mocode.ping.api.HealthResponse
|
||||
|
|
@ -13,7 +13,7 @@ import kotlinx.serialization.json.Json
|
|||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class PingApiClientTest {
|
||||
class PingApiKoinClientTest {
|
||||
|
||||
// Helper to create a testable client using the new DI-friendly implementation
|
||||
private fun createTestClient(mockEngine: MockEngine): PingApiKoinClient {
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package at.mocode.ping.feature.integration
|
||||
|
||||
import at.mocode.frontend.core.sync.SyncManager
|
||||
import at.mocode.ping.api.PingEvent
|
||||
import at.mocode.ping.feature.domain.PingSyncServiceImpl
|
||||
import at.mocode.ping.feature.test.FakePingEventRepository
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.mock.MockEngine
|
||||
import io.ktor.client.engine.mock.respond
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.headersOf
|
||||
import io.ktor.serialization.kotlinx.json.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/pings/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" })
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +1,45 @@
|
|||
package at.mocode.clients.pingfeature
|
||||
package at.mocode.ping.feature.presentation
|
||||
|
||||
import at.mocode.ping.api.PingResponse
|
||||
import at.mocode.ping.api.EnhancedPingResponse
|
||||
import at.mocode.ping.api.HealthResponse
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.test.*
|
||||
import kotlin.test.*
|
||||
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()
|
||||
viewModel = PingViewModel(testApiClient)
|
||||
fakeSyncService = FakePingSyncService()
|
||||
|
||||
viewModel = PingViewModel(
|
||||
apiClient = testApiClient,
|
||||
syncService = fakeSyncService
|
||||
)
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
|
|
@ -52,7 +73,7 @@ class PingViewModelTest {
|
|||
|
||||
// When
|
||||
viewModel.performSimplePing()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
val finalState = viewModel.uiState
|
||||
|
|
@ -77,7 +98,7 @@ class PingViewModelTest {
|
|||
assertNull(viewModel.uiState.errorMessage)
|
||||
|
||||
// When - complete the operation
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then - should not be loading anymore
|
||||
assertFalse(viewModel.uiState.isLoading)
|
||||
|
|
@ -92,7 +113,7 @@ class PingViewModelTest {
|
|||
|
||||
// When
|
||||
viewModel.performSimplePing()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
val finalState = viewModel.uiState
|
||||
|
|
@ -116,7 +137,7 @@ class PingViewModelTest {
|
|||
|
||||
// When
|
||||
viewModel.performEnhancedPing(simulate = false)
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
val finalState = viewModel.uiState
|
||||
|
|
@ -130,7 +151,7 @@ class PingViewModelTest {
|
|||
fun `performEnhancedPing should handle simulate parameter correctly`() = runTest(testDispatcher) {
|
||||
// When
|
||||
viewModel.performEnhancedPing(simulate = true)
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
assertEquals(true, testApiClient.enhancedPingCalledWith)
|
||||
|
|
@ -145,7 +166,7 @@ class PingViewModelTest {
|
|||
|
||||
// When
|
||||
viewModel.performEnhancedPing()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
val finalState = viewModel.uiState
|
||||
|
|
@ -167,7 +188,7 @@ class PingViewModelTest {
|
|||
|
||||
// When
|
||||
viewModel.performHealthCheck()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
val finalState = viewModel.uiState
|
||||
|
|
@ -186,7 +207,7 @@ class PingViewModelTest {
|
|||
|
||||
// When
|
||||
viewModel.performHealthCheck()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
val finalState = viewModel.uiState
|
||||
|
|
@ -195,13 +216,42 @@ class PingViewModelTest {
|
|||
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()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
advanceUntilIdle()
|
||||
}
|
||||
|
||||
// Verify error is present
|
||||
|
|
@ -220,7 +270,7 @@ class PingViewModelTest {
|
|||
// Given - first operation fails
|
||||
testApiClient.shouldThrowException = true
|
||||
viewModel.performSimplePing()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
advanceUntilIdle()
|
||||
assertNotNull(viewModel.uiState.errorMessage)
|
||||
|
||||
// When - second operation succeeds
|
||||
|
|
@ -228,7 +278,7 @@ class PingViewModelTest {
|
|||
val successResponse = PingResponse("SUCCESS", "2025-09-27T21:27:00Z", "test-service")
|
||||
testApiClient.simplePingResponse = successResponse
|
||||
viewModel.performSimplePing()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then - error should be cleared
|
||||
assertNull(viewModel.uiState.errorMessage)
|
||||
|
|
@ -239,7 +289,7 @@ class PingViewModelTest {
|
|||
fun `loading state should be false after successful operation`() = runTest(testDispatcher) {
|
||||
// Given
|
||||
viewModel.performSimplePing()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
assertFalse(viewModel.uiState.isLoading)
|
||||
|
|
@ -251,7 +301,7 @@ class PingViewModelTest {
|
|||
viewModel.performSimplePing()
|
||||
viewModel.performEnhancedPing(true)
|
||||
viewModel.performHealthCheck()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Then
|
||||
assertTrue(testApiClient.simplePingCalled)
|
||||
|
|
@ -1,10 +1,48 @@
|
|||
package at.mocode.clients.pingfeature
|
||||
package at.mocode.ping.feature.test
|
||||
|
||||
import at.mocode.ping.api.PingApi
|
||||
import at.mocode.ping.api.PingResponse
|
||||
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.
|
||||
|
|
@ -46,7 +84,7 @@ class TestPingApiClient : PingApi {
|
|||
callCount++
|
||||
|
||||
if (simulateDelay) {
|
||||
kotlinx.coroutines.delay(delayMs)
|
||||
delay(delayMs)
|
||||
}
|
||||
|
||||
if (shouldThrowException) {
|
||||
|
|
@ -67,7 +105,7 @@ class TestPingApiClient : PingApi {
|
|||
callCount++
|
||||
|
||||
if (simulateDelay) {
|
||||
kotlinx.coroutines.delay(delayMs)
|
||||
delay(delayMs)
|
||||
}
|
||||
|
||||
if (shouldThrowException) {
|
||||
|
|
@ -99,7 +137,7 @@ class TestPingApiClient : PingApi {
|
|||
callCount++
|
||||
|
||||
if (simulateDelay) {
|
||||
kotlinx.coroutines.delay(delayMs)
|
||||
delay(delayMs)
|
||||
}
|
||||
|
||||
if (shouldThrowException) {
|
||||
|
|
@ -111,7 +149,7 @@ class TestPingApiClient : PingApi {
|
|||
|
||||
private suspend fun handleRequest(response: PingResponse?): PingResponse {
|
||||
if (simulateDelay) {
|
||||
kotlinx.coroutines.delay(delayMs)
|
||||
delay(delayMs)
|
||||
}
|
||||
|
||||
if (shouldThrowException) {
|
||||
|
|
@ -7,8 +7,8 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.runtime.collectAsState
|
||||
import at.mocode.clients.shared.navigation.AppScreen
|
||||
import at.mocode.clients.authfeature.AuthTokenManager
|
||||
import at.mocode.clients.pingfeature.PingScreen
|
||||
import at.mocode.clients.pingfeature.PingViewModel
|
||||
import at.mocode.ping.feature.presentation.PingScreen
|
||||
import at.mocode.ping.feature.presentation.PingViewModel
|
||||
import at.mocode.shared.core.AppConstants
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.window.ComposeViewport
|
||||
import at.mocode.clients.authfeature.di.authFeatureModule
|
||||
import at.mocode.clients.pingfeature.di.pingFeatureModule
|
||||
import at.mocode.frontend.core.localdb.AppDatabase
|
||||
import at.mocode.frontend.core.localdb.DatabaseProvider
|
||||
import at.mocode.frontend.core.localdb.localDbModule
|
||||
import at.mocode.frontend.core.network.networkModule
|
||||
import at.mocode.frontend.core.sync.di.syncModule
|
||||
import at.mocode.ping.feature.di.pingSyncFeatureModule
|
||||
import at.mocode.ping.feature.di.pingFeatureModule
|
||||
import at.mocode.shared.di.initKoin
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
|
|
@ -27,8 +26,9 @@ fun main() {
|
|||
console.log("[WebApp] main() entered")
|
||||
// Initialize DI (Koin) with shared modules + network + local DB modules
|
||||
try {
|
||||
initKoin { modules(networkModule, localDbModule, syncModule, pingFeatureModule, pingSyncFeatureModule, authFeatureModule, navigationModule) }
|
||||
console.log("[WebApp] Koin initialized with networkModule + localDbModule + authFeatureModule + navigationModule")
|
||||
// Updated: Only load the consolidated pingFeatureModule from at.mocode.ping.feature.di
|
||||
initKoin { modules(networkModule, localDbModule, syncModule, pingFeatureModule, authFeatureModule, navigationModule) }
|
||||
console.log("[WebApp] Koin initialized with networkModule + localDbModule + authFeatureModule + navigationModule + pingFeatureModule")
|
||||
} catch (e: dynamic) {
|
||||
console.warn("[WebApp] Koin initialization warning:", e)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@ import at.mocode.shared.di.initKoin
|
|||
import at.mocode.frontend.core.network.networkModule
|
||||
import at.mocode.clients.authfeature.di.authFeatureModule
|
||||
import at.mocode.frontend.core.sync.di.syncModule
|
||||
import at.mocode.clients.pingfeature.di.pingFeatureModule
|
||||
import at.mocode.ping.feature.di.pingSyncFeatureModule
|
||||
import at.mocode.ping.feature.di.pingFeatureModule
|
||||
import at.mocode.frontend.core.localdb.AppDatabase
|
||||
import at.mocode.frontend.core.localdb.DatabaseProvider
|
||||
import navigation.navigationModule
|
||||
|
|
@ -18,8 +17,9 @@ import org.koin.dsl.module
|
|||
fun main() = application {
|
||||
// Initialize DI (Koin) with shared modules + network module
|
||||
try {
|
||||
initKoin { modules(networkModule, syncModule, pingFeatureModule, pingSyncFeatureModule, authFeatureModule, navigationModule) }
|
||||
println("[DesktopApp] Koin initialized with networkModule + authFeatureModule + navigationModule")
|
||||
// Updated: Only load the consolidated pingFeatureModule from at.mocode.ping.feature.di
|
||||
initKoin { modules(networkModule, syncModule, pingFeatureModule, authFeatureModule, navigationModule) }
|
||||
println("[DesktopApp] Koin initialized with networkModule + authFeatureModule + navigationModule + pingFeatureModule")
|
||||
} catch (e: Exception) {
|
||||
println("[DesktopApp] Koin initialization warning: ${e.message}")
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user