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:
@@ -22,7 +22,8 @@ dependencies {
|
|||||||
|
|
||||||
// Infrastructure Modules
|
// Infrastructure Modules
|
||||||
implementation(projects.backend.infrastructure.persistence)
|
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 ===
|
// === Spring Boot & Cloud ===
|
||||||
// Standard dependencies for a secure microservice
|
// Standard dependencies for a secure microservice
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.core.coreDomain)
|
api(projects.core.coreDomain) // Changed from implementation to api to export Syncable
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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*
|
||||||
@@ -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 {
|
jvmTest.dependencies {
|
||||||
implementation(libs.mockk)
|
implementation(libs.mockk) // MockK only for JVM tests
|
||||||
implementation(projects.platform.platformTesting)
|
implementation(projects.platform.platformTesting)
|
||||||
implementation(libs.bundles.testing.jvm)
|
implementation(libs.bundles.testing.jvm)
|
||||||
}
|
}
|
||||||
|
|||||||
-62
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-19
@@ -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())
|
|
||||||
}
|
|
||||||
-168
@@ -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}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-265
@@ -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}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-17
@@ -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()) }
|
|
||||||
}
|
|
||||||
-51
@@ -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
|
|
||||||
}
|
|
||||||
-263
@@ -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()
|
|
||||||
-220
@@ -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
-1
@@ -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.EnhancedPingResponse
|
||||||
import at.mocode.ping.api.HealthResponse
|
import at.mocode.ping.api.HealthResponse
|
||||||
+32
-10
@@ -1,20 +1,42 @@
|
|||||||
package at.mocode.ping.feature.di
|
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.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
|
import org.koin.dsl.module
|
||||||
|
|
||||||
// Renamed to avoid conflict with clients.pingfeature.di.pingFeatureModule
|
/**
|
||||||
val pingSyncFeatureModule = module {
|
* Consolidated Koin module for the Ping Feature (Clean Architecture).
|
||||||
// Provides the ViewModel for the Ping feature.
|
* Replaces the old 'clients.pingfeature' module.
|
||||||
factory<PingViewModel> {
|
*/
|
||||||
PingViewModel(
|
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(),
|
syncManager = get(),
|
||||||
pingEventRepository = get()
|
repository = get<PingEventRepositoryImpl>()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provides the concrete repository implementation for PingEvents.
|
// 4. ViewModel (Presentation Layer)
|
||||||
single<PingEventRepositoryImpl> { PingEventRepositoryImpl(get<AppDatabase>()) }
|
// Injects API and Domain Service
|
||||||
|
factory {
|
||||||
|
PingViewModel(
|
||||||
|
apiClient = get(),
|
||||||
|
syncService = get()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+25
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
-4
@@ -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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -36,7 +36,7 @@ fun PingScreen(viewModel: PingViewModel) {
|
|||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
|
|
||||||
if (uiState.isLoading) {
|
if (uiState.isLoading || uiState.isSyncing) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,8 +48,7 @@ fun PingScreen(viewModel: PingViewModel) {
|
|||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = MaterialTheme.colorScheme.error
|
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() }) {
|
Button(onClick = { viewModel.clearError() }) {
|
||||||
Text("Clear")
|
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)) {
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Button(onClick = { viewModel.performSimplePing() }) {
|
Button(onClick = { viewModel.performSimplePing() }) {
|
||||||
Text("Simple Ping")
|
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) {
|
if (uiState.simplePingResponse != null) {
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
+112
-11
@@ -1,29 +1,130 @@
|
|||||||
package at.mocode.ping.feature.presentation
|
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.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import at.mocode.frontend.core.sync.SyncManager
|
import at.mocode.ping.api.EnhancedPingResponse
|
||||||
import at.mocode.ping.api.PingEvent
|
import at.mocode.ping.api.HealthResponse
|
||||||
import at.mocode.ping.feature.data.PingEventRepositoryImpl
|
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.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(
|
class PingViewModel(
|
||||||
private val syncManager: SyncManager,
|
private val apiClient: PingApi,
|
||||||
private val pingEventRepository: PingEventRepositoryImpl
|
private val syncService: PingSyncService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
init {
|
var uiState by mutableStateOf(PingUiState())
|
||||||
// Trigger an initial sync when the ViewModel is created.
|
private set
|
||||||
triggerSync()
|
|
||||||
|
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() {
|
fun triggerSync() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
uiState = uiState.copy(isSyncing = true, errorMessage = null)
|
||||||
try {
|
try {
|
||||||
syncManager.performSync<PingEvent>(pingEventRepository, "/api/pings/sync")
|
syncService.syncPings()
|
||||||
} catch (_: Exception) {
|
// Use kotlin.time.Clock explicitly to avoid ambiguity and deprecation issues
|
||||||
// TODO: Handle sync errors and expose them to the UI
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -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.EnhancedPingResponse
|
||||||
import at.mocode.ping.api.HealthResponse
|
import at.mocode.ping.api.HealthResponse
|
||||||
@@ -13,7 +13,7 @@ import kotlinx.serialization.json.Json
|
|||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class PingApiClientTest {
|
class PingApiKoinClientTest {
|
||||||
|
|
||||||
// Helper to create a testable client using the new DI-friendly implementation
|
// Helper to create a testable client using the new DI-friendly implementation
|
||||||
private fun createTestClient(mockEngine: MockEngine): PingApiKoinClient {
|
private fun createTestClient(mockEngine: MockEngine): PingApiKoinClient {
|
||||||
+74
@@ -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" })
|
||||||
|
}
|
||||||
|
}
|
||||||
+69
-19
@@ -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.EnhancedPingResponse
|
||||||
import at.mocode.ping.api.HealthResponse
|
import at.mocode.ping.api.HealthResponse
|
||||||
import kotlinx.coroutines.*
|
import at.mocode.ping.api.PingResponse
|
||||||
import kotlinx.coroutines.test.*
|
import at.mocode.ping.feature.test.FakePingSyncService
|
||||||
import kotlin.test.*
|
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)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class PingViewModelTest {
|
class PingViewModelTest {
|
||||||
|
|
||||||
private lateinit var viewModel: PingViewModel
|
private lateinit var viewModel: PingViewModel
|
||||||
private lateinit var testApiClient: TestPingApiClient
|
private lateinit var testApiClient: TestPingApiClient
|
||||||
|
private lateinit var fakeSyncService: FakePingSyncService
|
||||||
|
|
||||||
private val testDispatcher = StandardTestDispatcher()
|
private val testDispatcher = StandardTestDispatcher()
|
||||||
|
|
||||||
@BeforeTest
|
@BeforeTest
|
||||||
fun setup() {
|
fun setup() {
|
||||||
Dispatchers.setMain(testDispatcher)
|
Dispatchers.setMain(testDispatcher)
|
||||||
testApiClient = TestPingApiClient()
|
testApiClient = TestPingApiClient()
|
||||||
viewModel = PingViewModel(testApiClient)
|
fakeSyncService = FakePingSyncService()
|
||||||
|
|
||||||
|
viewModel = PingViewModel(
|
||||||
|
apiClient = testApiClient,
|
||||||
|
syncService = fakeSyncService
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterTest
|
@AfterTest
|
||||||
@@ -52,7 +73,7 @@ class PingViewModelTest {
|
|||||||
|
|
||||||
// When
|
// When
|
||||||
viewModel.performSimplePing()
|
viewModel.performSimplePing()
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
val finalState = viewModel.uiState
|
val finalState = viewModel.uiState
|
||||||
@@ -77,7 +98,7 @@ class PingViewModelTest {
|
|||||||
assertNull(viewModel.uiState.errorMessage)
|
assertNull(viewModel.uiState.errorMessage)
|
||||||
|
|
||||||
// When - complete the operation
|
// When - complete the operation
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
// Then - should not be loading anymore
|
// Then - should not be loading anymore
|
||||||
assertFalse(viewModel.uiState.isLoading)
|
assertFalse(viewModel.uiState.isLoading)
|
||||||
@@ -92,7 +113,7 @@ class PingViewModelTest {
|
|||||||
|
|
||||||
// When
|
// When
|
||||||
viewModel.performSimplePing()
|
viewModel.performSimplePing()
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
val finalState = viewModel.uiState
|
val finalState = viewModel.uiState
|
||||||
@@ -116,7 +137,7 @@ class PingViewModelTest {
|
|||||||
|
|
||||||
// When
|
// When
|
||||||
viewModel.performEnhancedPing(simulate = false)
|
viewModel.performEnhancedPing(simulate = false)
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
val finalState = viewModel.uiState
|
val finalState = viewModel.uiState
|
||||||
@@ -130,7 +151,7 @@ class PingViewModelTest {
|
|||||||
fun `performEnhancedPing should handle simulate parameter correctly`() = runTest(testDispatcher) {
|
fun `performEnhancedPing should handle simulate parameter correctly`() = runTest(testDispatcher) {
|
||||||
// When
|
// When
|
||||||
viewModel.performEnhancedPing(simulate = true)
|
viewModel.performEnhancedPing(simulate = true)
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
assertEquals(true, testApiClient.enhancedPingCalledWith)
|
assertEquals(true, testApiClient.enhancedPingCalledWith)
|
||||||
@@ -145,7 +166,7 @@ class PingViewModelTest {
|
|||||||
|
|
||||||
// When
|
// When
|
||||||
viewModel.performEnhancedPing()
|
viewModel.performEnhancedPing()
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
val finalState = viewModel.uiState
|
val finalState = viewModel.uiState
|
||||||
@@ -167,7 +188,7 @@ class PingViewModelTest {
|
|||||||
|
|
||||||
// When
|
// When
|
||||||
viewModel.performHealthCheck()
|
viewModel.performHealthCheck()
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
val finalState = viewModel.uiState
|
val finalState = viewModel.uiState
|
||||||
@@ -186,7 +207,7 @@ class PingViewModelTest {
|
|||||||
|
|
||||||
// When
|
// When
|
||||||
viewModel.performHealthCheck()
|
viewModel.performHealthCheck()
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
val finalState = viewModel.uiState
|
val finalState = viewModel.uiState
|
||||||
@@ -195,13 +216,42 @@ class PingViewModelTest {
|
|||||||
assertEquals("Health check failed: $errorMessage", finalState.errorMessage)
|
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
|
@Test
|
||||||
fun `clearError should remove error message from state`() {
|
fun `clearError should remove error message from state`() {
|
||||||
// Given - set up an error state by simulating an error
|
// Given - set up an error state by simulating an error
|
||||||
testApiClient.shouldThrowException = true
|
testApiClient.shouldThrowException = true
|
||||||
runTest(testDispatcher) {
|
runTest(testDispatcher) {
|
||||||
viewModel.performSimplePing()
|
viewModel.performSimplePing()
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify error is present
|
// Verify error is present
|
||||||
@@ -220,7 +270,7 @@ class PingViewModelTest {
|
|||||||
// Given - first operation fails
|
// Given - first operation fails
|
||||||
testApiClient.shouldThrowException = true
|
testApiClient.shouldThrowException = true
|
||||||
viewModel.performSimplePing()
|
viewModel.performSimplePing()
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
assertNotNull(viewModel.uiState.errorMessage)
|
assertNotNull(viewModel.uiState.errorMessage)
|
||||||
|
|
||||||
// When - second operation succeeds
|
// When - second operation succeeds
|
||||||
@@ -228,7 +278,7 @@ class PingViewModelTest {
|
|||||||
val successResponse = PingResponse("SUCCESS", "2025-09-27T21:27:00Z", "test-service")
|
val successResponse = PingResponse("SUCCESS", "2025-09-27T21:27:00Z", "test-service")
|
||||||
testApiClient.simplePingResponse = successResponse
|
testApiClient.simplePingResponse = successResponse
|
||||||
viewModel.performSimplePing()
|
viewModel.performSimplePing()
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
// Then - error should be cleared
|
// Then - error should be cleared
|
||||||
assertNull(viewModel.uiState.errorMessage)
|
assertNull(viewModel.uiState.errorMessage)
|
||||||
@@ -239,7 +289,7 @@ class PingViewModelTest {
|
|||||||
fun `loading state should be false after successful operation`() = runTest(testDispatcher) {
|
fun `loading state should be false after successful operation`() = runTest(testDispatcher) {
|
||||||
// Given
|
// Given
|
||||||
viewModel.performSimplePing()
|
viewModel.performSimplePing()
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
assertFalse(viewModel.uiState.isLoading)
|
assertFalse(viewModel.uiState.isLoading)
|
||||||
@@ -251,7 +301,7 @@ class PingViewModelTest {
|
|||||||
viewModel.performSimplePing()
|
viewModel.performSimplePing()
|
||||||
viewModel.performEnhancedPing(true)
|
viewModel.performEnhancedPing(true)
|
||||||
viewModel.performHealthCheck()
|
viewModel.performHealthCheck()
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
assertTrue(testApiClient.simplePingCalled)
|
assertTrue(testApiClient.simplePingCalled)
|
||||||
+45
-7
@@ -1,10 +1,48 @@
|
|||||||
package at.mocode.clients.pingfeature
|
package at.mocode.ping.feature.test
|
||||||
|
|
||||||
import at.mocode.ping.api.PingApi
|
import at.mocode.frontend.core.sync.SyncableRepository
|
||||||
import at.mocode.ping.api.PingResponse
|
|
||||||
import at.mocode.ping.api.EnhancedPingResponse
|
import at.mocode.ping.api.EnhancedPingResponse
|
||||||
import at.mocode.ping.api.HealthResponse
|
import at.mocode.ping.api.HealthResponse
|
||||||
|
import at.mocode.ping.api.PingApi
|
||||||
import at.mocode.ping.api.PingEvent
|
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.
|
* Test double implementation of PingApi for testing purposes.
|
||||||
@@ -46,7 +84,7 @@ class TestPingApiClient : PingApi {
|
|||||||
callCount++
|
callCount++
|
||||||
|
|
||||||
if (simulateDelay) {
|
if (simulateDelay) {
|
||||||
kotlinx.coroutines.delay(delayMs)
|
delay(delayMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldThrowException) {
|
if (shouldThrowException) {
|
||||||
@@ -67,7 +105,7 @@ class TestPingApiClient : PingApi {
|
|||||||
callCount++
|
callCount++
|
||||||
|
|
||||||
if (simulateDelay) {
|
if (simulateDelay) {
|
||||||
kotlinx.coroutines.delay(delayMs)
|
delay(delayMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldThrowException) {
|
if (shouldThrowException) {
|
||||||
@@ -99,7 +137,7 @@ class TestPingApiClient : PingApi {
|
|||||||
callCount++
|
callCount++
|
||||||
|
|
||||||
if (simulateDelay) {
|
if (simulateDelay) {
|
||||||
kotlinx.coroutines.delay(delayMs)
|
delay(delayMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldThrowException) {
|
if (shouldThrowException) {
|
||||||
@@ -111,7 +149,7 @@ class TestPingApiClient : PingApi {
|
|||||||
|
|
||||||
private suspend fun handleRequest(response: PingResponse?): PingResponse {
|
private suspend fun handleRequest(response: PingResponse?): PingResponse {
|
||||||
if (simulateDelay) {
|
if (simulateDelay) {
|
||||||
kotlinx.coroutines.delay(delayMs)
|
delay(delayMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldThrowException) {
|
if (shouldThrowException) {
|
||||||
@@ -7,8 +7,8 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import at.mocode.clients.shared.navigation.AppScreen
|
import at.mocode.clients.shared.navigation.AppScreen
|
||||||
import at.mocode.clients.authfeature.AuthTokenManager
|
import at.mocode.clients.authfeature.AuthTokenManager
|
||||||
import at.mocode.clients.pingfeature.PingScreen
|
import at.mocode.ping.feature.presentation.PingScreen
|
||||||
import at.mocode.clients.pingfeature.PingViewModel
|
import at.mocode.ping.feature.presentation.PingViewModel
|
||||||
import at.mocode.shared.core.AppConstants
|
import at.mocode.shared.core.AppConstants
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.window.ComposeViewport
|
import androidx.compose.ui.window.ComposeViewport
|
||||||
import at.mocode.clients.authfeature.di.authFeatureModule
|
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.AppDatabase
|
||||||
import at.mocode.frontend.core.localdb.DatabaseProvider
|
import at.mocode.frontend.core.localdb.DatabaseProvider
|
||||||
import at.mocode.frontend.core.localdb.localDbModule
|
import at.mocode.frontend.core.localdb.localDbModule
|
||||||
import at.mocode.frontend.core.network.networkModule
|
import at.mocode.frontend.core.network.networkModule
|
||||||
import at.mocode.frontend.core.sync.di.syncModule
|
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 at.mocode.shared.di.initKoin
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.call.*
|
import io.ktor.client.call.*
|
||||||
@@ -27,8 +26,9 @@ fun main() {
|
|||||||
console.log("[WebApp] main() entered")
|
console.log("[WebApp] main() entered")
|
||||||
// Initialize DI (Koin) with shared modules + network + local DB modules
|
// Initialize DI (Koin) with shared modules + network + local DB modules
|
||||||
try {
|
try {
|
||||||
initKoin { modules(networkModule, localDbModule, syncModule, pingFeatureModule, pingSyncFeatureModule, authFeatureModule, navigationModule) }
|
// Updated: Only load the consolidated pingFeatureModule from at.mocode.ping.feature.di
|
||||||
console.log("[WebApp] Koin initialized with networkModule + localDbModule + authFeatureModule + navigationModule")
|
initKoin { modules(networkModule, localDbModule, syncModule, pingFeatureModule, authFeatureModule, navigationModule) }
|
||||||
|
console.log("[WebApp] Koin initialized with networkModule + localDbModule + authFeatureModule + navigationModule + pingFeatureModule")
|
||||||
} catch (e: dynamic) {
|
} catch (e: dynamic) {
|
||||||
console.warn("[WebApp] Koin initialization warning:", e)
|
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.frontend.core.network.networkModule
|
||||||
import at.mocode.clients.authfeature.di.authFeatureModule
|
import at.mocode.clients.authfeature.di.authFeatureModule
|
||||||
import at.mocode.frontend.core.sync.di.syncModule
|
import at.mocode.frontend.core.sync.di.syncModule
|
||||||
import at.mocode.clients.pingfeature.di.pingFeatureModule
|
import at.mocode.ping.feature.di.pingFeatureModule
|
||||||
import at.mocode.ping.feature.di.pingSyncFeatureModule
|
|
||||||
import at.mocode.frontend.core.localdb.AppDatabase
|
import at.mocode.frontend.core.localdb.AppDatabase
|
||||||
import at.mocode.frontend.core.localdb.DatabaseProvider
|
import at.mocode.frontend.core.localdb.DatabaseProvider
|
||||||
import navigation.navigationModule
|
import navigation.navigationModule
|
||||||
@@ -18,8 +17,9 @@ import org.koin.dsl.module
|
|||||||
fun main() = application {
|
fun main() = application {
|
||||||
// Initialize DI (Koin) with shared modules + network module
|
// Initialize DI (Koin) with shared modules + network module
|
||||||
try {
|
try {
|
||||||
initKoin { modules(networkModule, syncModule, pingFeatureModule, pingSyncFeatureModule, authFeatureModule, navigationModule) }
|
// Updated: Only load the consolidated pingFeatureModule from at.mocode.ping.feature.di
|
||||||
println("[DesktopApp] Koin initialized with networkModule + authFeatureModule + navigationModule")
|
initKoin { modules(networkModule, syncModule, pingFeatureModule, authFeatureModule, navigationModule) }
|
||||||
|
println("[DesktopApp] Koin initialized with networkModule + authFeatureModule + navigationModule + pingFeatureModule")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("[DesktopApp] Koin initialization warning: ${e.message}")
|
println("[DesktopApp] Koin initialization warning: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user