diff --git a/backend/services/ping/ping-service/build.gradle.kts b/backend/services/ping/ping-service/build.gradle.kts index 911d14ba..5f831c02 100644 --- a/backend/services/ping/ping-service/build.gradle.kts +++ b/backend/services/ping/ping-service/build.gradle.kts @@ -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 diff --git a/contracts/ping-api/build.gradle.kts b/contracts/ping-api/build.gradle.kts index 85c0fc35..383c5bcd 100644 --- a/contracts/ping-api/build.gradle.kts +++ b/contracts/ping-api/build.gradle.kts @@ -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) } } diff --git a/docs/01_Architecture/02_Frontend_Architecture.md b/docs/01_Architecture/02_Frontend_Architecture.md new file mode 100644 index 00000000..f29152c3 --- /dev/null +++ b/docs/01_Architecture/02_Frontend_Architecture.md @@ -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 diff --git a/docs/90_Reports/2026-01-19_Frontend_Refactoring_Status.md b/docs/90_Reports/2026-01-19_Frontend_Refactoring_Status.md new file mode 100644 index 00000000..a9becdc7 --- /dev/null +++ b/docs/90_Reports/2026-01-19_Frontend_Refactoring_Status.md @@ -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. diff --git a/docs/90_Reports/2026-01-19_LeadArchitect_Review.md b/docs/90_Reports/2026-01-19_LeadArchitect_Review.md new file mode 100644 index 00000000..20b428cd --- /dev/null +++ b/docs/90_Reports/2026-01-19_LeadArchitect_Review.md @@ -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* diff --git a/docs/99_Journal/2026-01-19_Session_Log.md b/docs/99_Journal/2026-01-19_Session_Log.md new file mode 100644 index 00000000..6c0dd102 --- /dev/null +++ b/docs/99_Journal/2026-01-19_Session_Log.md @@ -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. diff --git a/frontend/features/ping-feature/build.gradle.kts b/frontend/features/ping-feature/build.gradle.kts index 05a220a3..6deecd6c 100644 --- a/frontend/features/ping-feature/build.gradle.kts +++ b/frontend/features/ping-feature/build.gradle.kts @@ -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) } diff --git a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/PingApiClient.kt b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/PingApiClient.kt deleted file mode 100644 index 3f1305a3..00000000 --- a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/PingApiClient.kt +++ /dev/null @@ -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 { - return client.get("$baseUrl/api/ping/sync") { - parameter("lastSyncTimestamp", lastSyncTimestamp) - }.body() - } -} diff --git a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/PingApiFactory.kt b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/PingApiFactory.kt deleted file mode 100644 index b1357716..00000000 --- a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/PingApiFactory.kt +++ /dev/null @@ -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()) - } diff --git a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/PingViewModel.kt b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/PingViewModel.kt deleted file mode 100644 index a4abcb23..00000000 --- a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/PingViewModel.kt +++ /dev/null @@ -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}") - } - } - } -} diff --git a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/api/ReitsportTestApi.kt b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/api/ReitsportTestApi.kt deleted file mode 100644 index 07ea9a53..00000000 --- a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/api/ReitsportTestApi.kt +++ /dev/null @@ -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 { - val results = mutableListOf() - - // 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}" - } -} diff --git a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/di/PingFeatureModule.kt b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/di/PingFeatureModule.kt deleted file mode 100644 index 3a875304..00000000 --- a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/di/PingFeatureModule.kt +++ /dev/null @@ -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 { PingApiKoinClient(get(named("apiClient"))) } - - // Provide PingViewModel - // Fallback to factory if viewModel DSL is not available or causing issues - factory { PingViewModel(get()) } -} diff --git a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/model/AuthEnums.kt b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/model/AuthEnums.kt deleted file mode 100644 index f527d7ef..00000000 --- a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/model/AuthEnums.kt +++ /dev/null @@ -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 -} diff --git a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/model/ReitsportDomainModels.kt b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/model/ReitsportDomainModels.kt deleted file mode 100644 index 1d573474..00000000 --- a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/model/ReitsportDomainModels.kt +++ /dev/null @@ -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, - 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): List { - 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, - 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, // Multi-Step-Prozesse - val requiredRole: RolleE, - val requiredPermissions: List, - val testEndpoints: List, // Mehrere API-Calls - val mockData: Map = 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, - val apiResults: List, - 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 = 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 = emptyList(), - val recommendations: List = 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() diff --git a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/model/ReitsportRoles.kt b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/model/ReitsportRoles.kt deleted file mode 100644 index 23c7688d..00000000 --- a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/model/ReitsportRoles.kt +++ /dev/null @@ -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 { - return ALL_ROLES.filter { it.hasPermission(permission) } - } -} diff --git a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/PingApiKoinClient.kt b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/ping/feature/data/PingApiKoinClient.kt similarity index 97% rename from frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/PingApiKoinClient.kt rename to frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/ping/feature/data/PingApiKoinClient.kt index bf18fd0f..d4035355 100644 --- a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/PingApiKoinClient.kt +++ b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/ping/feature/data/PingApiKoinClient.kt @@ -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 diff --git a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/ping/feature/di/PingFeatureModule.kt b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/ping/feature/di/PingFeatureModule.kt index 740ae430..d0e725ad 100644 --- a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/ping/feature/di/PingFeatureModule.kt +++ b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/ping/feature/di/PingFeatureModule.kt @@ -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( +/** + * 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 { PingApiKoinClient(get(named("apiClient"))) } + + // 2. Repository (Data Layer) + single { PingEventRepositoryImpl(get()) } + + // 3. Domain Service (Domain Layer) + // Wraps SyncManager and Repository to decouple ViewModel from SyncManager implementation details + single { + PingSyncServiceImpl( syncManager = get(), - pingEventRepository = get() + repository = get() ) } - // Provides the concrete repository implementation for PingEvents. - single { PingEventRepositoryImpl(get()) } + // 4. ViewModel (Presentation Layer) + // Injects API and Domain Service + factory { + PingViewModel( + apiClient = get(), + syncService = get() + ) + } } diff --git a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/ping/feature/domain/PingSyncService.kt b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/ping/feature/domain/PingSyncService.kt new file mode 100644 index 00000000..58952e53 --- /dev/null +++ b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/ping/feature/domain/PingSyncService.kt @@ -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 +) : PingSyncService { + + override suspend fun syncPings() { + syncManager.performSync(repository, "/api/pings/sync") + } +} diff --git a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/PingScreen.kt b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/ping/feature/presentation/PingScreen.kt similarity index 83% rename from frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/PingScreen.kt rename to frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/ping/feature/presentation/PingScreen.kt index 8b84179e..7e817581 100644 --- a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/clients/pingfeature/PingScreen.kt +++ b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/ping/feature/presentation/PingScreen.kt @@ -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)) { diff --git a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/ping/feature/presentation/PingViewModel.kt b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/ping/feature/presentation/PingViewModel.kt index 080c68db..51a56c66 100644 --- a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/ping/feature/presentation/PingViewModel.kt +++ b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/ping/feature/presentation/PingViewModel.kt @@ -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(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) + } } diff --git a/frontend/features/ping-feature/src/commonTest/kotlin/at/mocode/clients/pingfeature/PingApiClientTest.kt b/frontend/features/ping-feature/src/commonTest/kotlin/at/mocode/ping/feature/data/PingApiKoinClientTest.kt similarity index 98% rename from frontend/features/ping-feature/src/commonTest/kotlin/at/mocode/clients/pingfeature/PingApiClientTest.kt rename to frontend/features/ping-feature/src/commonTest/kotlin/at/mocode/ping/feature/data/PingApiKoinClientTest.kt index cf12e146..0be55b39 100644 --- a/frontend/features/ping-feature/src/commonTest/kotlin/at/mocode/clients/pingfeature/PingApiClientTest.kt +++ b/frontend/features/ping-feature/src/commonTest/kotlin/at/mocode/ping/feature/data/PingApiKoinClientTest.kt @@ -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 { diff --git a/frontend/features/ping-feature/src/commonTest/kotlin/at/mocode/ping/feature/integration/PingSyncIntegrationTest.kt b/frontend/features/ping-feature/src/commonTest/kotlin/at/mocode/ping/feature/integration/PingSyncIntegrationTest.kt new file mode 100644 index 00000000..c3bcea38 --- /dev/null +++ b/frontend/features/ping-feature/src/commonTest/kotlin/at/mocode/ping/feature/integration/PingSyncIntegrationTest.kt @@ -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" }) + } +} diff --git a/frontend/features/ping-feature/src/commonTest/kotlin/at/mocode/clients/pingfeature/PingViewModelTest.kt b/frontend/features/ping-feature/src/commonTest/kotlin/at/mocode/ping/feature/presentation/PingViewModelTest.kt similarity index 77% rename from frontend/features/ping-feature/src/commonTest/kotlin/at/mocode/clients/pingfeature/PingViewModelTest.kt rename to frontend/features/ping-feature/src/commonTest/kotlin/at/mocode/ping/feature/presentation/PingViewModelTest.kt index 6af64ac4..32f0d24d 100644 --- a/frontend/features/ping-feature/src/commonTest/kotlin/at/mocode/clients/pingfeature/PingViewModelTest.kt +++ b/frontend/features/ping-feature/src/commonTest/kotlin/at/mocode/ping/feature/presentation/PingViewModelTest.kt @@ -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) diff --git a/frontend/features/ping-feature/src/commonTest/kotlin/at/mocode/clients/pingfeature/TestPingApiClient.kt b/frontend/features/ping-feature/src/commonTest/kotlin/at/mocode/ping/feature/test/Fakes.kt similarity index 75% rename from frontend/features/ping-feature/src/commonTest/kotlin/at/mocode/clients/pingfeature/TestPingApiClient.kt rename to frontend/features/ping-feature/src/commonTest/kotlin/at/mocode/ping/feature/test/Fakes.kt index 1b502fc3..6a77d0d5 100644 --- a/frontend/features/ping-feature/src/commonTest/kotlin/at/mocode/clients/pingfeature/TestPingApiClient.kt +++ b/frontend/features/ping-feature/src/commonTest/kotlin/at/mocode/ping/feature/test/Fakes.kt @@ -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 { + var storedEvents = mutableListOf() + var latestSince: String? = null + + override suspend fun getLatestSince(): String? { + return latestSince + } + + override suspend fun upsert(items: List) { + // 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) { diff --git a/frontend/shells/meldestelle-portal/src/commonMain/kotlin/MainApp.kt b/frontend/shells/meldestelle-portal/src/commonMain/kotlin/MainApp.kt index 90171f7f..26ae636c 100644 --- a/frontend/shells/meldestelle-portal/src/commonMain/kotlin/MainApp.kt +++ b/frontend/shells/meldestelle-portal/src/commonMain/kotlin/MainApp.kt @@ -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 diff --git a/frontend/shells/meldestelle-portal/src/jsMain/kotlin/main.kt b/frontend/shells/meldestelle-portal/src/jsMain/kotlin/main.kt index 635bcb8c..17c68c05 100644 --- a/frontend/shells/meldestelle-portal/src/jsMain/kotlin/main.kt +++ b/frontend/shells/meldestelle-portal/src/jsMain/kotlin/main.kt @@ -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) } diff --git a/frontend/shells/meldestelle-portal/src/jvmMain/kotlin/main.kt b/frontend/shells/meldestelle-portal/src/jvmMain/kotlin/main.kt index b0c380db..86daca86 100644 --- a/frontend/shells/meldestelle-portal/src/jvmMain/kotlin/main.kt +++ b/frontend/shells/meldestelle-portal/src/jvmMain/kotlin/main.kt @@ -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}") }