(fix) Umbau zu SCS

This commit is contained in:
stefan
2025-07-21 12:08:20 +02:00
parent 83d0d81193
commit 62b5e71427
34 changed files with 3403 additions and 20 deletions
+142
View File
@@ -0,0 +1,142 @@
# Cleanup Implementation Plan
This document outlines the plan for cleaning up the codebase, improving its structure, and enhancing maintainability as requested.
## 1. Directory Structure Standardization
### 1.1 Fix api-gateway Module Inconsistency
The api-gateway module currently has an inconsistent directory structure with both `src/main` and `src/jvmMain` directories, which causes confusion and potential maintenance issues.
**Actions:**
1. Compare files in both directories to identify duplicates and differences
2. Consolidate all code into the `src/jvmMain` directory to be consistent with other modules
3. Update any references to the old directory structure
4. Remove the `src/main` directory after ensuring all functionality is preserved
### 1.2 Standardize Module Structure
Ensure all modules follow the same directory structure pattern:
- `src/jvmMain/kotlin` for backend code
- `src/jsMain/kotlin` for frontend code
- `src/commonMain/kotlin` for shared code
- `src/jvmTest/kotlin` for backend tests
- `src/jsTest/kotlin` for frontend tests
- `src/commonTest/kotlin` for shared tests
## 2. Test Files Organization
### 2.1 Move Standalone Test Scripts
Move the following standalone test scripts from the root directory to appropriate test directories:
- `test_authentication.kt``member-management/src/jvmTest/kotlin/at/mocode/members/test/`
- `test_authentication_authorization.kt``api-gateway/src/jvmTest/kotlin/at/mocode/gateway/test/`
- `test_validation.kt``shared-kernel/src/jvmTest/kotlin/at/mocode/validation/test/`
- `database-integration-test.kt``shared-kernel/src/jvmTest/kotlin/at/mocode/shared/database/test/`
### 2.2 Convert to Proper Unit Tests
Convert the standalone test scripts to proper unit tests using the Kotlin test framework:
1. Add appropriate test annotations
2. Organize tests into test classes
3. Use proper assertions
4. Set up test dependencies
## 3. Documentation Cleanup
### 3.1 Update Outdated Documentation
Update the following documentation files to reflect the current project structure:
- `README_CODE_ORGANIZATION.md` - Update to reflect actual code organization
- `README_API_Implementation.md` - Verify and update if needed
- Other README files - Review and update as needed
### 3.2 Consolidate Documentation
Consolidate fragmented documentation into a more organized structure:
1. Create a `docs` directory with subdirectories for different topics
2. Move documentation files from the root directory to appropriate subdirectories
3. Create an index document that links to all documentation
4. Update the main README.md to point to the new documentation structure
Suggested structure:
```
docs/
├── architecture/
│ ├── code-organization.md
│ └── system-overview.md
├── api/
│ ├── api-implementation.md
│ └── validation.md
├── database/
│ ├── setup.md
│ └── migrations.md
├── security/
│ ├── authentication.md
│ └── authorization.md
└── development/
├── getting-started.md
└── testing.md
```
### 3.3 Remove Redundant Documentation
Identify and remove any redundant or obsolete documentation files after consolidation.
## 4. Code Cleanup
### 4.1 Remove Duplicate Code
Identify and remove duplicate code, particularly in the api-gateway module where files exist in both src/main and src/jvmMain.
### 4.2 Standardize Naming Conventions
Ensure consistent naming conventions across the codebase:
- Repository interfaces: `EntityRepository`
- Repository implementations: `EntityRepositoryImpl`
- Service interfaces: `EntityService`
- Service implementations: `EntityServiceImpl`
- Controllers/Routes: `EntityRoutes`
- Data classes: `EntityDto` for DTOs, `Entity` for domain models
### 4.3 Improve Separation of Concerns
Ensure proper separation of concerns:
- Domain logic in domain layer
- Infrastructure concerns in infrastructure layer
- API endpoints in presentation layer
- Shared utilities in appropriate utility classes
## 5. Implementation Approach
### 5.1 Phase 1: Directory Structure and Documentation
1. Fix api-gateway module directory structure
2. Move standalone test scripts
3. Update and consolidate documentation
### 5.2 Phase 2: Code Cleanup
1. Remove duplicate code
2. Standardize naming conventions
3. Improve separation of concerns
### 5.3 Phase 3: Verification
1. Ensure the project builds correctly
2. Run tests to verify functionality
3. Manual testing of key features
## 6. Success Criteria
The cleanup will be considered successful when:
1. All modules follow a consistent directory structure
2. All test files are properly organized in test directories
3. Documentation is accurate, up-to-date, and well-organized
4. No duplicate code or files exist
5. Naming conventions are consistent
6. The project builds and all tests pass
## Last Updated
2025-07-21
+164
View File
@@ -0,0 +1,164 @@
# Client-Side Validation Implementation
## Übersicht
Dieses Dokument beschreibt die Implementierung der Client-seitigen Validierung für die Meldestelle-Anwendung. Die Validierung wurde gemäß der Anforderung "Implementiere Client-seitige Validierung" umgesetzt.
## Implementierungsansatz
Die Client-seitige Validierung nutzt die bereits vorhandenen Validierungsklassen aus dem `shared-kernel`-Modul, die in `commonMain` definiert sind und somit sowohl auf dem Server (JVM) als auch im Client (JS) verfügbar sind:
1. **ApiValidationUtils**: Enthält spezifische Validierungsmethoden für API-Anfragen
2. **ValidationUtils**: Enthält allgemeine Validierungsmethoden
3. **ValidationError**: Repräsentiert einen einzelnen Validierungsfehler
4. **ValidationResult**: Repräsentiert das Ergebnis einer Validierung
Durch die Nutzung dieser gemeinsamen Klassen wird sichergestellt, dass die Validierungslogik auf Client- und Serverseite konsistent ist.
## Beispielimplementierung
Als Beispiel wurde eine `LoginForm`-Komponente implementiert, die Client-seitige Validierung für Benutzername und Passwort durchführt, bevor die Daten an den Server gesendet werden.
### Validierungsablauf
1. Benutzer gibt Daten in das Formular ein
2. Bei Klick auf den Login-Button werden die Eingaben mit `ApiValidationUtils.validateLoginRequest()` validiert
3. Bei Validierungsfehlern werden diese angezeigt, ohne dass eine Serveranfrage gesendet wird
4. Nur bei erfolgreicher Validierung wird die Anfrage an den Server gesendet
### Code-Beispiel
```kotlin
// Perform client-side validation
val errors = ApiValidationUtils.validateLoginRequest(username, password)
if (errors.isNotEmpty()) {
// If validation fails, update the validationErrors state
validationErrors = errors
} else {
// If validation passes, submit the form
// ... API call code ...
}
```
### Fehleranzeige
Validierungsfehler werden benutzerfreundlich angezeigt:
```kotlin
// Display validation error for username if any
getFieldError("username")?.let {
p {
css {
"color" to "#e74c3c"
"fontSize" to "12px"
"margin" to "5px 0 0 0"
}
+it
}
}
```
## Vorteile der Client-seitigen Validierung
1. **Bessere Benutzererfahrung**: Sofortiges Feedback ohne Wartezeit auf Serverantwort
2. **Reduzierte Serverlast**: Ungültige Anfragen werden bereits im Client abgefangen
3. **Konsistente Validierung**: Gleiche Validierungslogik auf Client und Server
4. **Offline-Fähigkeit**: Grundlegende Validierung funktioniert auch ohne Serververbindung
5. **Erhöhte Sicherheit**: Doppelte Validierungsschicht (Client und Server)
## Implementierte Komponenten
### LoginForm
Eine React-Komponente, die als Web-Komponente registriert ist und in HTML verwendet werden kann:
```html
<login-form onloginsuccess="handleLoginSuccess"></login-form>
```
Die Komponente validiert:
- Benutzername/E-Mail (Pflichtfeld, Länge, E-Mail-Format wenn @ enthalten)
- Passwort (Pflichtfeld, Mindestlänge)
## Anleitung zur Implementierung weiterer Validierungen
### 1. Nutzung vorhandener Validierungsmethoden
Für die meisten Anwendungsfälle können die vorhandenen Methoden in `ApiValidationUtils` verwendet werden:
```kotlin
// Validierung von Login-Daten
ApiValidationUtils.validateLoginRequest(username, password)
// Validierung von Länder-Daten
ApiValidationUtils.validateCountryRequest(isoAlpha2Code, isoAlpha3Code, nameDeutsch, nameEnglisch)
// Validierung von Pferde-Daten
ApiValidationUtils.validateHorseRequest(pferdeName, lebensnummer, chipNummer, oepsNummer, feiNummer)
// Validierung von Veranstaltungs-Daten
ApiValidationUtils.validateEventRequest(name, ort, startDatum, endDatum, maxTeilnehmer)
// Validierung von Query-Parametern
ApiValidationUtils.validateQueryParameters(limit, offset, startDate, endDate, search, q)
// Validierung von UUID-Strings
ApiValidationUtils.validateUuidString(uuidString)
```
### 2. Implementierung eigener Validierungen
Für spezifische Validierungen können die Basismethoden in `ValidationUtils` verwendet werden:
```kotlin
// Prüfung auf nicht-leere Eingabe
ValidationUtils.validateNotBlank(value, fieldName)
// Längenvalidierung
ValidationUtils.validateLength(value, fieldName, maxLength, minLength)
// E-Mail-Validierung
ValidationUtils.validateEmail(email, fieldName)
// Telefonnummer-Validierung
ValidationUtils.validatePhoneNumber(phone, fieldName)
// Postleitzahl-Validierung
ValidationUtils.validatePostalCode(postalCode, fieldName)
// Ländercode-Validierung
ValidationUtils.validateCountryCode(countryCode, fieldName)
// Geburtsdatum-Validierung
ValidationUtils.validateBirthDate(birthDate, fieldName)
// Jahres-Validierung
ValidationUtils.validateYear(year, fieldName, minYear)
```
### 3. Anzeige von Validierungsfehlern
Validierungsfehler sollten benutzerfreundlich angezeigt werden:
```kotlin
// Hilfsfunktion zum Abrufen eines Fehlers für ein bestimmtes Feld
val getFieldError = { fieldName: String ->
validationErrors.find { it.field == fieldName }?.message
}
// Anzeige des Fehlers
getFieldError("fieldName")?.let {
// Fehleranzeige-Code
}
```
## Fazit
Die Client-seitige Validierung wurde erfolgreich implementiert und kann als Grundlage für weitere Formularvalidierungen dienen. Durch die Nutzung der gemeinsamen Validierungsklassen wird eine konsistente Benutzererfahrung und erhöhte Datensicherheit gewährleistet.
---
**Implementiert am**: 2025-07-21
**Status**: ✅ Vollständig implementiert
**Dokumentation**: ✅ Vollständig
+50
View File
@@ -0,0 +1,50 @@
# Datenbank-Installation Vervollständigt
## Überblick
Dieses Dokument beschreibt die Änderungen, die vorgenommen wurden, um die Datenbank-Installation zu vervollständigen.
## Vorgenommene Änderungen
### 1. PgAdmin-Service aktiviert
**Problem:** Der PgAdmin-Service war in der docker-compose.yml-Datei auskommentiert, was die Verwaltung der Datenbank erschwerte.
**Lösung:**
- Auskommentierung des PgAdmin-Service in docker-compose.yml entfernt
- Standard-Passwort auf den Wert aus der .env-Datei angepasst (`admin_password_change_me`)
- pgadmin_data-Volume aktiviert, um PgAdmin-Daten zu persistieren
### 2. Überprüfung der Konfiguration
Die folgenden Konfigurationen wurden überprüft und sind korrekt eingerichtet:
- **Umgebungsvariablen in .env-Datei:** Alle erforderlichen Variablen (DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD) sind vorhanden und korrekt konfiguriert.
- **Datenbank-Konfiguration:** DatabaseConfig.kt liest die Umgebungsvariablen korrekt ein und erstellt die JDBC-URL entsprechend.
- **Datenbank-Initialisierung:** DatabaseFactory.kt initialisiert die Datenbankverbindung korrekt mit HikariCP.
- **Migrations-System:** MigrationSetup.kt registriert und führt alle Migrationen aus, die in den entsprechenden Modulen definiert sind.
- **Anwendungsstart:** Application.kt initialisiert die Datenbank und führt Migrationen beim Start aus.
## Nächste Schritte
1. Starten Sie die Anwendung mit Docker Compose:
```
docker-compose up -d
```
2. Zugriff auf PgAdmin:
- Öffnen Sie http://localhost:5050 im Browser
- Melden Sie sich mit den Zugangsdaten aus der .env-Datei an:
- E-Mail: admin@example.com (oder Wert von PGADMIN_DEFAULT_EMAIL)
- Passwort: admin_password_change_me (oder Wert von PGADMIN_DEFAULT_PASSWORD)
3. Verbindung zur Datenbank in PgAdmin einrichten:
- Rechtsklick auf "Servers" > "Create" > "Server..."
- Name: Meldestelle
- Connection-Tab:
- Host: db
- Port: 5432
- Maintenance database: meldestelle_db
- Username: meldestelle_user
- Password: secure_password_change_me (oder Wert von DB_PASSWORD)
4. Überprüfen Sie, ob die Tabellen korrekt erstellt wurden, einschließlich der _migrations-Tabelle.
## Datum
2025-07-21 10:14
+10
View File
@@ -12,6 +12,7 @@ The project follows Domain-Driven Design (DDD) principles with clearly separated
* **`master-data`** - Master data management (countries, regions, age classes, venues)
* **`member-management`** - Person and club/association management
* **`horse-registry`** - Horse registration and management
* **`event-management`** - Event and tournament management
* **`api-gateway`** - Central API gateway aggregating all services
### Module Dependencies
@@ -21,6 +22,11 @@ api-gateway
├── shared-kernel
├── master-data
├── member-management
├── horse-registry
└── event-management
event-management
├── shared-kernel
└── horse-registry
horse-registry
@@ -63,3 +69,7 @@ master-data
## Documentation
See the `docs/` directory for detailed architecture documentation and diagrams.
## Last Updated
2025-07-21
+4
View File
@@ -244,3 +244,7 @@ The server module now provides a **production-ready RESTful API** that:
- Can be easily extended with additional features
The API serves as a solid foundation for the Meldestelle system and can support web applications, mobile apps, and third-party integrations.
## Last Updated
2025-07-21
+4
View File
@@ -222,3 +222,7 @@ val data = call.safeReceive<Artikel>() ?: return
6. **Performance**: Lazy loading and efficient resource management
This reorganization provides a solid foundation for future development while maintaining backward compatibility and improving code quality.
## Last Updated
2025-07-21
+9
View File
@@ -158,6 +158,11 @@ services:
- DB_NAME=${POSTGRES_DB}
- DB_USER=${POSTGRES_USER}
- DB_PASSWORD=${POSTGRES_PASSWORD}
pgadmin:
environment:
- PGADMIN_DEFAULT_EMAIL=${PGADMIN_DEFAULT_EMAIL:-admin@example.com}
- PGADMIN_DEFAULT_PASSWORD=${PGADMIN_DEFAULT_PASSWORD:-admin_password_change_me}
```
## Beste Praktiken
@@ -171,3 +176,7 @@ services:
4. **Validierung**: Validieren Sie kritische Konfigurationen beim Anwendungsstart, um Fehler frühzeitig zu erkennen.
5. **Dokumentation**: Halten Sie die Dokumentation der Konfigurationsparameter aktuell, damit neue Teammitglieder die Anwendung leicht konfigurieren können.
## Letztes Update
2025-07-21
+36
View File
@@ -28,6 +28,11 @@ POSTGRES_USER=meldestelle_user
POSTGRES_PASSWORD=secure_password_change_me
POSTGRES_DB=meldestelle_db
# PgAdmin Konfiguration
PGADMIN_DEFAULT_EMAIL=admin@example.com
PGADMIN_DEFAULT_PASSWORD=admin_password_change_me
PGADMIN_PORT=5050
# API Gateway Konfiguration
API_PORT=8081
```
@@ -77,6 +82,33 @@ Die Datenbankstruktur ist in verschiedene Bereiche unterteilt, die den Modulen d
2. Führen Sie `docker-compose up -d db` aus, um nur die Datenbank zu starten
3. Alternativ `docker-compose up -d` für das gesamte System
### PgAdmin verwenden
Das Projekt enthält einen PgAdmin-Service für die einfache Verwaltung der Datenbank über eine Web-Oberfläche.
1. Starten Sie die Anwendung mit Docker Compose:
```
docker-compose up -d
```
2. Zugriff auf PgAdmin:
- Öffnen Sie http://localhost:5050 im Browser
- Melden Sie sich mit den Zugangsdaten aus der .env-Datei an:
- E-Mail: admin@example.com (oder Wert von PGADMIN_DEFAULT_EMAIL)
- Passwort: admin_password_change_me (oder Wert von PGADMIN_DEFAULT_PASSWORD)
3. Verbindung zur Datenbank in PgAdmin einrichten:
- Rechtsklick auf "Servers" > "Create" > "Server..."
- Name: Meldestelle
- Connection-Tab:
- Host: db
- Port: 5432
- Maintenance database: meldestelle_db
- Username: meldestelle_user
- Password: secure_password_change_me (oder Wert von DB_PASSWORD)
4. Überprüfen Sie, ob die Tabellen korrekt erstellt wurden, einschließlich der _migrations-Tabelle.
### Manuell
1. Installieren Sie PostgreSQL auf Ihrem System
@@ -97,3 +129,7 @@ Die Datenbankstruktur ist in verschiedene Bereiche unterteilt, die den Modulen d
- Prüfen Sie die Logs auf detaillierte Fehlermeldungen
- Migrationen werden nur einmal ausgeführt - Änderungen an bestehenden Migrationen haben keine Auswirkung
- Bei schwerwiegenden Problemen kann die `_migrations`-Tabelle manuell bearbeitet werden (nur für Fortgeschrittene)
## Letztes Update
2025-07-21
+91
View File
@@ -0,0 +1,91 @@
# API Gateway Consolidation Plan
This document outlines the plan for consolidating the duplicate directory structure in the api-gateway module, specifically merging the `src/main` and `src/jvmMain` directories.
## 1. File Analysis
### 1.1 Duplicate Files
The following files exist in both directories:
| File | Action | Reasoning |
|------|--------|-----------|
| Application.kt | Merge into src/jvmMain | src/jvmMain version has better configuration handling, but src/main has more complete module setup |
| config/AuthorizationConfig.kt | Keep src/jvmMain version | Assuming identical content |
| config/DatabaseConfig.kt | Keep src/jvmMain version | Assuming identical content |
| config/MonitoringConfig.kt | Keep src/jvmMain version | Confirmed identical content |
| config/OpenApiConfig.kt | Keep src/jvmMain version | Assuming identical content |
| config/SecurityConfig.kt | Keep src/jvmMain version | Assuming identical content |
| config/SerializationConfig.kt | Keep src/jvmMain version | Assuming identical content |
| routing/AuthRoutes.kt | Keep src/jvmMain version | Assuming identical content |
### 1.2 Files Unique to src/main
The following files exist only in src/main:
| File | Action | Reasoning |
|------|--------|-----------|
| auth/AuthorizationHelper.kt | Move to src/jvmMain | Contains important authorization functionality not present in src/jvmMain |
| routing/RoutingConfig.kt | Move to src/jvmMain | Contains critical routing configuration not present in src/jvmMain |
| config/configureSwagger.kt | Check if needed | Not referenced in src/jvmMain, but referenced in src/main Application.kt |
### 1.3 Files Unique to src/jvmMain
The following files exist only in src/jvmMain:
| File | Action | Reasoning |
|------|--------|-----------|
| auth/ApiKeyAuth.kt | Keep | Provides API key authentication |
| auth/JwtAuth.kt | Keep and enhance | Provides JWT authentication, but should be enhanced with functionality from AuthorizationHelper.kt |
| config/MigrationSetup.kt | Keep | Handles database migrations |
| migrations/* (4 files) | Keep | Handle migrations for different contexts |
| module.kt | Merge with Application.kt | Contains module definition but needs to be enhanced with functionality from src/main |
| validation/RequestValidator.kt | Keep | Provides request validation |
## 2. Implementation Steps
### 2.1 Merge Application.kt and module.kt
1. Start with the src/jvmMain Application.kt
2. Incorporate the module configuration from src/main Application.kt
3. Ensure all necessary components are configured:
- Database
- Serialization
- Monitoring
- Security
- OpenAPI/Swagger
- Routing
### 2.2 Move Unique Files from src/main to src/jvmMain
1. Move AuthorizationHelper.kt to src/jvmMain/kotlin/at/mocode/gateway/auth/
2. Move RoutingConfig.kt to src/jvmMain/kotlin/at/mocode/gateway/routing/
3. Check if configureSwagger.kt is needed and move if necessary
### 2.3 Update References
1. Update imports in all files to reflect the new structure
2. Ensure all configuration functions are called in the module function
### 2.4 Remove src/main Directory
After all files have been consolidated and the application has been verified to work correctly, remove the src/main directory.
## 3. Testing
After consolidation, the following tests should be performed:
1. Build the project to ensure there are no compilation errors
2. Run the application to ensure it starts correctly
3. Test key functionality to ensure it works as expected:
- Authentication
- Authorization
- API endpoints
- Database operations
## 4. Documentation Update
Update the project documentation to reflect the new structure:
1. Update README.md if it references the old structure
2. Update any other documentation that mentions the directory structure
+1
View File
@@ -17,6 +17,7 @@ kotlin {
implementation(project(":master-data"))
implementation(project(":member-management"))
implementation(project(":horse-registry"))
implementation(project(":event-management"))
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.serialization.json)
@@ -0,0 +1,330 @@
package at.mocode.gateway.config
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.response.*
import io.ktor.http.*
import io.ktor.server.routing.*
import io.ktor.util.pipeline.*
import at.mocode.enums.RolleE
import at.mocode.enums.BerechtigungE
/**
* Authorization configuration and middleware for role-based access control.
*
* Provides utilities for checking user roles and permissions on protected endpoints.
*/
/**
* Enum representing user roles in the system.
*/
enum class UserRole {
ADMIN,
VEREINS_ADMIN,
FUNKTIONAER,
REITER,
TRAINER,
RICHTER,
TIERARZT,
ZUSCHAUER,
GAST
}
/**
* Enum representing permissions in the system.
*/
enum class Permission {
// 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,
// Master data management
STAMMDATEN_READ,
STAMMDATEN_UPDATE,
// System administration
SYSTEM_ADMIN,
BENUTZER_VERWALTEN,
ROLLEN_VERWALTEN
}
/**
* Data class representing user authorization context.
*/
data class UserAuthContext(
val userId: String,
val username: String,
val roles: List<UserRole>,
val permissions: List<Permission>
)
/**
* Maps domain role enum to authorization role enum.
*/
private fun mapDomainRoleToUserRole(domainRole: RolleE): UserRole {
return when (domainRole) {
RolleE.ADMIN -> UserRole.ADMIN
RolleE.VEREINS_ADMIN -> UserRole.VEREINS_ADMIN
RolleE.FUNKTIONAER -> UserRole.FUNKTIONAER
RolleE.REITER -> UserRole.REITER
RolleE.TRAINER -> UserRole.TRAINER
RolleE.RICHTER -> UserRole.RICHTER
RolleE.TIERARZT -> UserRole.TIERARZT
RolleE.ZUSCHAUER -> UserRole.ZUSCHAUER
RolleE.GAST -> UserRole.GAST
}
}
/**
* Maps domain permission enum to authorization permission enum.
*/
private fun mapDomainPermissionToPermission(domainPermission: BerechtigungE): Permission {
return when (domainPermission) {
BerechtigungE.PERSON_READ -> Permission.PERSON_READ
BerechtigungE.PERSON_CREATE -> Permission.PERSON_CREATE
BerechtigungE.PERSON_UPDATE -> Permission.PERSON_UPDATE
BerechtigungE.PERSON_DELETE -> Permission.PERSON_DELETE
BerechtigungE.VEREIN_READ -> Permission.VEREIN_READ
BerechtigungE.VEREIN_CREATE -> Permission.VEREIN_CREATE
BerechtigungE.VEREIN_UPDATE -> Permission.VEREIN_UPDATE
BerechtigungE.VEREIN_DELETE -> Permission.VEREIN_DELETE
BerechtigungE.VERANSTALTUNG_READ -> Permission.VERANSTALTUNG_READ
BerechtigungE.VERANSTALTUNG_CREATE -> Permission.VERANSTALTUNG_CREATE
BerechtigungE.VERANSTALTUNG_UPDATE -> Permission.VERANSTALTUNG_UPDATE
BerechtigungE.VERANSTALTUNG_DELETE -> Permission.VERANSTALTUNG_DELETE
BerechtigungE.PFERD_READ -> Permission.PFERD_READ
BerechtigungE.PFERD_CREATE -> Permission.PFERD_CREATE
BerechtigungE.PFERD_UPDATE -> Permission.PFERD_UPDATE
BerechtigungE.PFERD_DELETE -> Permission.PFERD_DELETE
BerechtigungE.STAMMDATEN_READ -> Permission.STAMMDATEN_READ
BerechtigungE.STAMMDATEN_UPDATE -> Permission.STAMMDATEN_UPDATE
BerechtigungE.SYSTEM_ADMIN -> Permission.SYSTEM_ADMIN
BerechtigungE.BENUTZER_VERWALTEN -> Permission.BENUTZER_VERWALTEN
BerechtigungE.ROLLEN_VERWALTEN -> Permission.ROLLEN_VERWALTEN
}
}
/**
* Extension function to get user authorization context from JWT principal.
*/
fun JWTPrincipal.getUserAuthContext(): UserAuthContext? {
val userId = getClaim("userId", String::class) ?: return null
val username = getClaim("username", String::class) ?: return null
// Get roles and permissions from JWT token
val domainRoles = getClaim("roles", Array<RolleE>::class)?.toList() ?: emptyList()
val domainPermissions = getClaim("permissions", Array<BerechtigungE>::class)?.toList() ?: emptyList()
// Map domain enums to authorization enums
val roles = domainRoles.map { mapDomainRoleToUserRole(it) }
val permissions = domainPermissions.map { mapDomainPermissionToPermission(it) }
return UserAuthContext(
userId = userId,
username = username,
roles = roles,
permissions = permissions
)
}
/**
* Maps roles to their corresponding permissions.
*/
private fun getRolePermissions(roles: List<UserRole>): List<Permission> {
val permissions = mutableSetOf<Permission>()
roles.forEach { role ->
when (role) {
UserRole.ADMIN -> {
permissions.addAll(Permission.values())
}
UserRole.VEREINS_ADMIN -> {
permissions.addAll(listOf(
Permission.PERSON_READ, Permission.PERSON_CREATE, Permission.PERSON_UPDATE,
Permission.VEREIN_READ, Permission.VEREIN_UPDATE,
Permission.PFERD_READ, Permission.PFERD_CREATE, Permission.PFERD_UPDATE,
Permission.STAMMDATEN_READ
))
}
UserRole.FUNKTIONAER -> {
permissions.addAll(listOf(
Permission.PERSON_READ,
Permission.VEREIN_READ,
Permission.VERANSTALTUNG_READ, Permission.VERANSTALTUNG_CREATE, Permission.VERANSTALTUNG_UPDATE,
Permission.PFERD_READ,
Permission.STAMMDATEN_READ
))
}
UserRole.TRAINER -> {
permissions.addAll(listOf(
Permission.PERSON_READ,
Permission.VEREIN_READ,
Permission.VERANSTALTUNG_READ,
Permission.PFERD_READ,
Permission.STAMMDATEN_READ
))
}
UserRole.REITER -> {
permissions.addAll(listOf(
Permission.PERSON_READ,
Permission.VEREIN_READ,
Permission.VERANSTALTUNG_READ,
Permission.PFERD_READ,
Permission.STAMMDATEN_READ
))
}
UserRole.RICHTER -> {
permissions.addAll(listOf(
Permission.PERSON_READ,
Permission.VEREIN_READ,
Permission.VERANSTALTUNG_READ,
Permission.PFERD_READ,
Permission.STAMMDATEN_READ
))
}
UserRole.TIERARZT -> {
permissions.addAll(listOf(
Permission.PERSON_READ,
Permission.PFERD_READ,
Permission.STAMMDATEN_READ
))
}
UserRole.ZUSCHAUER -> {
permissions.addAll(listOf(
Permission.VERANSTALTUNG_READ,
Permission.STAMMDATEN_READ
))
}
UserRole.GAST -> {
permissions.addAll(listOf(
Permission.STAMMDATEN_READ
))
}
}
}
return permissions.toList()
}
/**
* Route extension function to require specific roles.
*/
fun Route.requireRoles(vararg roles: UserRole, build: Route.() -> Unit): Route {
val route = createChild(object : RouteSelector() {
override suspend fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation {
return RouteSelectorEvaluation.Constant
}
override fun toString(): String = "requireRoles(${roles.joinToString()})"
})
route.intercept(ApplicationCallPipeline.Call) {
val principal = call.principal<JWTPrincipal>()
val authContext = principal?.getUserAuthContext()
if (authContext == null) {
call.respond(HttpStatusCode.Unauthorized, "Authentication required")
finish()
return@intercept
}
val hasRequiredRole = roles.any { requiredRole ->
authContext.roles.contains(requiredRole)
}
if (!hasRequiredRole) {
call.respond(
HttpStatusCode.Forbidden,
"Access denied. Required roles: ${roles.joinToString()}"
)
finish()
return@intercept
}
}
route.build()
return route
}
/**
* Route extension function to require specific permissions.
*/
fun Route.requirePermissions(vararg permissions: Permission, build: Route.() -> Unit): Route {
val route = createChild(object : RouteSelector() {
override suspend fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation {
return RouteSelectorEvaluation.Constant
}
override fun toString(): String = "requirePermissions(${permissions.joinToString()})"
})
route.intercept(ApplicationCallPipeline.Call) {
val principal = call.principal<JWTPrincipal>()
val authContext = principal?.getUserAuthContext()
if (authContext == null) {
call.respond(HttpStatusCode.Unauthorized, "Authentication required")
finish()
return@intercept
}
val hasAllPermissions = permissions.all { requiredPermission ->
authContext.permissions.contains(requiredPermission)
}
if (!hasAllPermissions) {
call.respond(
HttpStatusCode.Forbidden,
"Access denied. Required permissions: ${permissions.joinToString()}"
)
finish()
return@intercept
}
}
route.build()
return route
}
/**
* Pipeline context extension to get current user authorization context.
*/
val PipelineContext<Unit, ApplicationCall>.userAuthContext: UserAuthContext?
get() = call.principal<JWTPrincipal>()?.getUserAuthContext()
/**
* Application call extension to check if user has specific role.
*/
fun ApplicationCall.hasRole(role: UserRole): Boolean {
val authContext = principal<JWTPrincipal>()?.getUserAuthContext()
return authContext?.roles?.contains(role) == true
}
/**
* Application call extension to check if user has specific permission.
*/
fun ApplicationCall.hasPermission(permission: Permission): Boolean {
val authContext = principal<JWTPrincipal>()?.getUserAuthContext()
return authContext?.permissions?.contains(permission) == true
}
@@ -0,0 +1,62 @@
package at.mocode.gateway.config
import io.ktor.server.application.*
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
import org.slf4j.LoggerFactory
/**
* Database configuration for the API Gateway.
*
* Sets up database connections and schema initialization for all bounded contexts.
*/
fun Application.configureDatabase() {
val log = LoggerFactory.getLogger("DatabaseConfig")
val databaseUrl = environment.config.propertyOrNull("database.url")?.getString()
?: "jdbc:postgresql://localhost:5432/meldestelle"
val databaseUser = environment.config.propertyOrNull("database.user")?.getString()
?: "meldestelle_user"
val databasePassword = environment.config.propertyOrNull("database.password")?.getString()
?: "meldestelle_password"
// Initialize database connection
Database.connect(
url = databaseUrl,
driver = "org.postgresql.Driver",
user = databaseUser,
password = databasePassword
)
// Initialize database schemas for all contexts
transaction {
// Import table definitions from all contexts
try {
// Master Data Context tables
SchemaUtils.createMissingTablesAndColumns(
at.mocode.masterdata.infrastructure.repository.LandTable
)
// Member Management Context tables
SchemaUtils.createMissingTablesAndColumns(
at.mocode.members.infrastructure.repository.PersonTable,
at.mocode.members.infrastructure.repository.VereinTable
)
// Horse Registry Context tables
SchemaUtils.createMissingTablesAndColumns(
at.mocode.horses.infrastructure.repository.HorseTable
)
// Event Management Context tables
SchemaUtils.createMissingTablesAndColumns(
at.mocode.events.infrastructure.repository.VeranstaltungTable
)
log.info("Database schemas initialized successfully")
} catch (e: Exception) {
log.error("Failed to initialize database schemas: ${e.message}")
// In production, you might want to fail fast here
}
}
}
@@ -0,0 +1,59 @@
package at.mocode.gateway.config
import io.ktor.server.application.*
import io.ktor.server.plugins.calllogging.*
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.request.*
import io.ktor.http.*
import io.ktor.server.response.*
import at.mocode.dto.base.ApiResponse
import org.slf4j.event.Level
/**
* Monitoring and logging configuration for the API Gateway.
*
* Configures request logging, error handling, and status pages.
*/
fun Application.configureMonitoring() {
install(CallLogging) {
level = Level.INFO
filter { call -> call.request.path().startsWith("/api") }
format { call ->
val status = call.response.status()
val httpMethod = call.request.httpMethod.value
val userAgent = call.request.headers["User-Agent"]
"$status: $httpMethod ${call.request.path()} - $userAgent"
}
}
install(StatusPages) {
exception<Throwable> { call, cause ->
call.application.log.error("Unhandled exception", cause)
call.respond(
HttpStatusCode.InternalServerError,
ApiResponse.error<Any>("Internal server error: ${cause.message}")
)
}
status(HttpStatusCode.NotFound) { call, status ->
call.respond(
status,
ApiResponse.error<Any>("Endpoint not found: ${call.request.path()}")
)
}
status(HttpStatusCode.Unauthorized) { call, status ->
call.respond(
status,
ApiResponse.error<Any>("Authentication required")
)
}
status(HttpStatusCode.Forbidden) { call, status ->
call.respond(
status,
ApiResponse.error<Any>("Access forbidden")
)
}
}
}
@@ -0,0 +1,35 @@
package at.mocode.gateway.config
import io.ktor.server.application.*
import io.ktor.server.plugins.openapi.*
import io.ktor.server.plugins.swagger.*
import io.ktor.server.routing.*
/**
* Configuration for OpenAPI/Swagger documentation.
*
* This module configures the OpenAPI specification generation and Swagger UI
* for the API Gateway, providing comprehensive API documentation.
*/
fun Application.configureOpenApi() {
// Configure OpenAPI using a static file
routing {
// Serve the OpenAPI specification from a file
openAPI(path = "openapi", swaggerFile = "openapi/documentation.yaml") {
// Additional configuration can be added here if needed
}
}
}
/**
* Configuration for Swagger UI.
*
* Provides an interactive web interface for exploring and testing the API.
*/
fun Application.configureSwagger() {
routing {
swaggerUI(path = "swagger", swaggerFile = "openapi/documentation.yaml") {
version = "4.15.5"
}
}
}
@@ -0,0 +1,85 @@
package at.mocode.gateway.config
import io.ktor.server.application.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.http.*
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import io.ktor.server.response.respond
/**
* Security configuration for the API Gateway.
*
* Configures CORS, JWT authentication, and other security-related settings.
*/
fun Application.configureSecurity() {
install(CORS) {
allowMethod(HttpMethod.Options)
allowMethod(HttpMethod.Put)
allowMethod(HttpMethod.Delete)
allowMethod(HttpMethod.Patch)
allowHeader(HttpHeaders.Authorization)
allowHeader(HttpHeaders.ContentType)
allowHeader("X-Requested-With")
// Allow requests from common development origins
allowHost("localhost:3000")
allowHost("localhost:8080")
allowHost("127.0.0.1:3000")
allowHost("127.0.0.1:8080")
// In production, configure specific allowed origins
anyHost() // This should be restricted in production
}
// JWT Configuration
val jwtConfig = JwtConfig.fromEnvironment()
install(Authentication) {
jwt("auth-jwt") {
realm = jwtConfig.realm
verifier(
JWT
.require(Algorithm.HMAC512(jwtConfig.secret))
.withAudience(jwtConfig.audience)
.withIssuer(jwtConfig.issuer)
.build()
)
validate { credential ->
if (credential.payload.getClaim("userId").asString() != null) {
JWTPrincipal(credential.payload)
} else {
null
}
}
challenge { defaultScheme, realm ->
call.respond(HttpStatusCode.Unauthorized, "Token is not valid or has expired")
}
}
}
}
/**
* JWT Configuration data class.
*/
data class JwtConfig(
val secret: String,
val issuer: String,
val audience: String,
val realm: String,
val expirationTime: Long = 3600000L // 1 hour in milliseconds
) {
companion object {
fun fromEnvironment(): JwtConfig {
return JwtConfig(
secret = System.getenv("JWT_SECRET") ?: "default-secret-key-change-in-production",
issuer = System.getenv("JWT_ISSUER") ?: "meldestelle-api",
audience = System.getenv("JWT_AUDIENCE") ?: "meldestelle-users",
realm = System.getenv("JWT_REALM") ?: "Meldestelle API",
expirationTime = System.getenv("JWT_EXPIRATION")?.toLongOrNull() ?: 3600000L
)
}
}
}
@@ -0,0 +1,23 @@
package at.mocode.gateway.config
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import kotlinx.serialization.json.Json
/**
* Serialization configuration for the API Gateway.
*
* Configures JSON serialization settings that are consistent across all bounded contexts.
*/
fun Application.configureSerialization() {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
encodeDefaults = true
explicitNulls = false
})
}
}
@@ -0,0 +1,276 @@
package at.mocode.gateway.test
import kotlinx.coroutines.runBlocking
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.net.URI
import java.time.Duration
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
/**
* Test class for authentication and authorization functionality.
*
* This test verifies the complete authentication and authorization flow:
* 1. User registration
* 2. User login
* 3. Access to protected endpoints
* 4. Token refresh
* 5. Password change
* 6. Logout
*/
class AuthenticationAuthorizationTest {
private val baseUrl = "http://localhost:8080"
private val client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build()
@Test
fun testAuthenticationFlow() = runBlocking {
println("🚀 Starting Authentication and Authorization Tests")
println("=" * 60)
try {
// Test 1: Health Check
println("\n📋 Test 1: API Health Check")
testHealthCheck()
// Test 2: User Registration
println("\n📝 Test 2: User Registration")
testUserRegistration()
// Test 3: User Login
println("\n🔐 Test 3: User Login")
val token = testUserLogin()
if (token != null) {
// Test 4: Access Protected Profile Endpoint
println("\n👤 Test 4: Access Protected Profile")
testProtectedProfile(token)
// Test 5: Token Refresh
println("\n🔄 Test 5: Token Refresh")
val newToken = testTokenRefresh(token)
// Test 6: Change Password
println("\n🔑 Test 6: Change Password")
testChangePassword(newToken ?: token)
// Test 7: Logout
println("\n👋 Test 7: Logout")
testLogout(newToken ?: token)
}
println("\n✅ All tests completed!")
} catch (e: Exception) {
println("\n❌ Test failed with error: ${e.message}")
e.printStackTrace()
throw e
}
}
private suspend fun testHealthCheck() {
val request = HttpRequest.newBuilder()
.uri(URI.create("$baseUrl/health"))
.GET()
.build()
val response = client.send(request, HttpResponse.BodyHandlers.ofString())
if (response.statusCode() == 200) {
println("✅ Health check passed")
println(" Response: ${response.body()}")
assertEquals(200, response.statusCode(), "Health check should return 200 OK")
} else {
println("❌ Health check failed: ${response.statusCode()}")
println(" Response: ${response.body()}")
assertEquals(200, response.statusCode(), "Health check should return 200 OK")
}
}
private suspend fun testUserRegistration() {
val registrationData = """
{
"personId": "550e8400-e29b-41d4-a716-446655440000",
"username": "testuser_${System.currentTimeMillis()}",
"email": "test_${System.currentTimeMillis()}@example.com",
"password": "SecurePassword123!"
}
""".trimIndent()
val request = HttpRequest.newBuilder()
.uri(URI.create("$baseUrl/auth/register"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(registrationData))
.build()
val response = client.send(request, HttpResponse.BodyHandlers.ofString())
if (response.statusCode() == 201) {
println("✅ User registration successful")
println(" Response: ${response.body()}")
assertEquals(201, response.statusCode(), "User registration should return 201 Created")
} else {
println("⚠️ User registration response: ${response.statusCode()}")
println(" Response: ${response.body()}")
println(" Note: This might be expected if registration requires existing person ID")
// Don't assert here as registration might fail for valid reasons in test environment
}
}
private suspend fun testUserLogin(): String? {
// Try to login with a test user (this assumes there's already a user in the system)
val loginData = """
{
"usernameOrEmail": "admin",
"password": "admin123"
}
""".trimIndent()
val request = HttpRequest.newBuilder()
.uri(URI.create("$baseUrl/auth/login"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(loginData))
.build()
val response = client.send(request, HttpResponse.BodyHandlers.ofString())
if (response.statusCode() == 200) {
println("✅ User login successful")
println(" Response: ${response.body()}")
// Extract token from response (simplified - in real scenario, parse JSON)
val responseBody = response.body()
val tokenStart = responseBody.indexOf("\"token\":\"") + 9
val tokenEnd = responseBody.indexOf("\"", tokenStart)
return if (tokenStart > 8 && tokenEnd > tokenStart) {
val token = responseBody.substring(tokenStart, tokenEnd)
println(" Token extracted: ${token.take(20)}...")
assertNotNull(token, "Token should not be null")
assertTrue(token.isNotEmpty(), "Token should not be empty")
token
} else {
println(" Could not extract token from response")
null
}
} else {
println("⚠️ User login failed: ${response.statusCode()}")
println(" Response: ${response.body()}")
println(" Note: This is expected if no test user exists in the database")
return null
}
}
private suspend fun testProtectedProfile(token: String) {
val request = HttpRequest.newBuilder()
.uri(URI.create("$baseUrl/auth/profile"))
.header("Authorization", "Bearer $token")
.GET()
.build()
val response = client.send(request, HttpResponse.BodyHandlers.ofString())
if (response.statusCode() == 200) {
println("✅ Protected profile access successful")
println(" Response: ${response.body()}")
assertEquals(200, response.statusCode(), "Protected profile access should return 200 OK")
} else {
println("❌ Protected profile access failed: ${response.statusCode()}")
println(" Response: ${response.body()}")
assertEquals(200, response.statusCode(), "Protected profile access should return 200 OK")
}
}
private suspend fun testTokenRefresh(token: String): String? {
val request = HttpRequest.newBuilder()
.uri(URI.create("$baseUrl/auth/refresh"))
.header("Authorization", "Bearer $token")
.POST(HttpRequest.BodyPublishers.noBody())
.build()
val response = client.send(request, HttpResponse.BodyHandlers.ofString())
if (response.statusCode() == 200) {
println("✅ Token refresh successful")
println(" Response: ${response.body()}")
// Extract new token from response (simplified)
val responseBody = response.body()
val tokenStart = responseBody.indexOf("\"token\":\"") + 9
val tokenEnd = responseBody.indexOf("\"", tokenStart)
return if (tokenStart > 8 && tokenEnd > tokenStart) {
val newToken = responseBody.substring(tokenStart, tokenEnd)
println(" New token extracted: ${newToken.take(20)}...")
assertNotNull(newToken, "New token should not be null")
assertTrue(newToken.isNotEmpty(), "New token should not be empty")
newToken
} else {
println(" Could not extract new token from response")
null
}
} else {
println("❌ Token refresh failed: ${response.statusCode()}")
println(" Response: ${response.body()}")
return null
}
}
private suspend fun testChangePassword(token: String) {
val changePasswordData = """
{
"currentPassword": "admin123",
"newPassword": "NewSecurePassword123!"
}
""".trimIndent()
val request = HttpRequest.newBuilder()
.uri(URI.create("$baseUrl/auth/change-password"))
.header("Authorization", "Bearer $token")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(changePasswordData))
.build()
val response = client.send(request, HttpResponse.BodyHandlers.ofString())
if (response.statusCode() == 200) {
println("✅ Password change successful")
println(" Response: ${response.body()}")
assertEquals(200, response.statusCode(), "Password change should return 200 OK")
} else {
println("⚠️ Password change response: ${response.statusCode()}")
println(" Response: ${response.body()}")
println(" Note: This might fail if current password is incorrect")
// Don't assert here as password change might fail for valid reasons in test environment
}
}
private suspend fun testLogout(token: String) {
val request = HttpRequest.newBuilder()
.uri(URI.create("$baseUrl/auth/logout"))
.header("Authorization", "Bearer $token")
.POST(HttpRequest.BodyPublishers.noBody())
.build()
val response = client.send(request, HttpResponse.BodyHandlers.ofString())
if (response.statusCode() == 200) {
println("✅ Logout successful")
println(" Response: ${response.body()}")
assertEquals(200, response.statusCode(), "Logout should return 200 OK")
} else {
println("❌ Logout failed: ${response.statusCode()}")
println(" Response: ${response.body()}")
assertEquals(200, response.statusCode(), "Logout should return 200 OK")
}
}
}
// Extension function for string repetition
operator fun String.times(n: Int): String = this.repeat(n)
+121
View File
@@ -0,0 +1,121 @@
# Meldestelle Codebase Cleanup Summary
This document summarizes the cleanup tasks identified for the Meldestelle project and provides a comprehensive plan for implementation.
## 1. Issues Identified
### 1.1 Directory Structure Inconsistencies
- The api-gateway module has inconsistent directory structure with both `src/main` and `src/jvmMain` directories
- Duplicate files exist in both directories with varying levels of completeness
- Some functionality exists only in one directory or the other
### 1.2 Test File Organization
- Standalone test scripts exist in the root directory instead of proper test directories
- Test scripts use ad-hoc testing approaches rather than proper unit test frameworks
- Test naming and organization is inconsistent
### 1.3 Documentation Issues
- Documentation is fragmented across multiple files
- Some documentation is outdated or inaccurate
- Redundant documentation exists for the same topics
- Inconsistent naming conventions for documentation files
- Documentation is scattered between root directory and docs directory
### 1.4 Code Quality Issues
- Potential unused or redundant code
- Inconsistent naming conventions
- Possible separation of concerns issues
## 2. Implementation Plans
Detailed implementation plans have been created for each area:
### 2.1 API Gateway Consolidation Plan
The [API Gateway Consolidation Plan](api-gateway-consolidation-plan.md) outlines:
- Analysis of duplicate and unique files in src/main and src/jvmMain
- Strategy for merging Application.kt and module.kt
- Plan for moving unique files from src/main to src/jvmMain
- Steps for updating references and removing redundant directories
### 2.2 Test Scripts Conversion Plan
The [Test Scripts Conversion Plan](test-scripts-conversion-plan.md) outlines:
- Identification of standalone test scripts and their target directories
- Guidelines for converting scripts to proper unit tests
- Implementation steps for each test script with sample code structures
- Verification steps to ensure converted tests work correctly
### 2.3 Documentation Consolidation Plan
The [Documentation Consolidation Plan](documentation-consolidation-plan.md) outlines:
- Analysis of current documentation files and issues
- Strategy for consolidating documentation into a clear, hierarchical structure
- Content consolidation approach for each topic area
- Implementation steps and verification process
## 3. Implementation Approach
The implementation will follow a phased approach:
### 3.1 Phase 1: Directory Structure and Test Organization
1. Consolidate api-gateway module directory structure
- Merge Application.kt and module.kt
- Move unique files from src/main to src/jvmMain
- Update references
- Remove src/main directory
2. Organize test files
- Move standalone test scripts to appropriate test directories
- Convert scripts to proper unit tests
- Ensure consistent test naming and organization
### 3.2 Phase 2: Documentation and Code Cleanup
1. Consolidate and update documentation
- Create new directory structure in docs directory
- Consolidate content from existing files
- Update main README.md
- Remove redundant documentation files
2. Clean up code
- Remove unused or redundant code
- Standardize naming conventions
- Improve separation of concerns
### 3.3 Phase 3: Verification
1. Build the project to ensure there are no compilation errors
2. Run tests to verify functionality
3. Review documentation for accuracy and completeness
4. Final check against requirements in the issue description
## 4. Benefits
Implementing these cleanup tasks will result in:
1. **Improved Maintainability**: Consistent directory structure, better organized tests, and clear documentation make the codebase easier to maintain
2. **Enhanced Readability**: Standardized naming conventions and improved separation of concerns make the code easier to understand
3. **Better Developer Experience**: Consolidated documentation and proper test organization improve the developer experience
4. **Reduced Technical Debt**: Removing redundant code and fixing inconsistencies reduces technical debt
5. **Easier Onboarding**: Clear structure and documentation make it easier for new developers to understand the project
## 5. Next Steps
1. Review and approve the implementation plans
2. Prioritize tasks based on impact and dependencies
3. Begin implementation following the phased approach
4. Regularly verify changes to ensure they meet requirements
5. Update this summary as implementation progresses
## Last Updated
2025-07-21
+20 -20
View File
@@ -42,29 +42,29 @@ services:
ports: # Nur bei Bedarf freigeben, z.B. für lokalen Zugriff
- "127.0.0.1:54321:5432" # Host-Port 54321 → Container-Port 5432
# Optional: PgAdmin Service
# pgadmin:
# image: dpage/pgadmin4:latest # Oder spezifische Version
# container_name: meldestelle-pgadmin
# restart: unless-stopped
# environment:
# # Werte aus .env lesen (oder Defaults nutzen)
# PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-admin@example.com}
# PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-DeinSicheresPgAdminPasswort!} # UNBEDINGT IN .env SETZEN!
# PGADMIN_CONFIG_SERVER_MODE: 'False'
# volumes:
# - pgadmin_data:/var/lib/pgadmin # Benanntes Volume
# ports:
# # Port 5050 auf dem Host (nur localhost) → Port 80 im Container
# - "${PGADMIN_PORT:-127.0.0.1:5050}:80"
# networks:
# - meldestelle-net # <--- Muss zum Netzwerk-Namen passen
# depends_on: # PgAdmin braucht die DB
# - db
# PgAdmin Service
pgadmin:
image: dpage/pgadmin4:latest # Oder spezifische Version
container_name: meldestelle-pgadmin
restart: unless-stopped
environment:
# Werte aus .env lesen (oder Defaults nutzen)
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-admin@example.com}
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin_password_change_me} # UNBEDINGT IN .env SETZEN!
PGADMIN_CONFIG_SERVER_MODE: 'False'
volumes:
- pgadmin_data:/var/lib/pgadmin # Benanntes Volume
ports:
# Port 5050 auf dem Host (nur localhost) → Port 80 im Container
- "${PGADMIN_PORT:-127.0.0.1:5050}:80"
networks:
- meldestelle-net # <--- Muss zum Netzwerk-Namen passen
depends_on: # PgAdmin braucht die DB
- db
networks:
meldestelle-net:
driver: bridge
volumes:
postgres_data: # <--- Konsistenter Name
# pgadmin_data: # <--- Konsistenter Name
pgadmin_data: # <--- Konsistenter Name
+180
View File
@@ -0,0 +1,180 @@
# Documentation Consolidation Plan
This document outlines the plan for consolidating and updating the documentation in the project to improve clarity, accuracy, and maintainability.
## 1. Documentation Analysis
### 1.1 Current Documentation Files
The project contains numerous documentation files, many of which appear to be redundant or fragmented:
#### Root Directory Documentation
- API_VALIDATION_IMPLEMENTATION.md
- AUTHENTICATION_AUTHORIZATION_IMPLEMENTATION_SUMMARY.md
- AUTHENTICATION_AUTHORIZATION_SUMMARY.md
- CLEANUP_IMPLEMENTATION_PLAN.md
- CLIENT_VALIDATION_IMPLEMENTATION.md
- DATABASE_INSTALLATION_COMPLETED.md
- DATABASE_SETUP_FIXES.md
- README_API_Implementation.md
- README_CODE_ORGANIZATION.md
- README_CONFIG.md
- README_DATABASE_SETUP.md
- README.md
- fixes_implemented.md
- issues_found.md
#### Docs Directory Documentation
- API_Documentation.md
- API_DOCUMENTATION.md (duplicate with different case)
- API_IMPLEMENTATION_SUMMARY.md
- API_VERSIONING.md
- bounded-contexts-design.md
- module-structure-design.md
- scs-implementation-completed.md
- scs-implementation-summary.md
- SWAGGER_DOCUMENTATION.md
- Various diagram files
### 1.2 Documentation Issues
Based on initial examination, the following issues have been identified:
1. **Outdated Content**: Some documentation (e.g., README_CODE_ORGANIZATION.md) refers to paths and patterns that don't match the current codebase structure.
2. **Fragmentation**: Related information is spread across multiple files (e.g., multiple files about API implementation).
3. **Redundancy**: Some topics are covered in multiple files with overlapping content.
4. **Inconsistent Naming**: Inconsistent file naming conventions (e.g., mix of uppercase, lowercase, and snake_case).
5. **Lack of Organization**: Documentation is scattered between the root directory and the docs directory.
## 2. Consolidation Strategy
### 2.1 Documentation Structure
Consolidate documentation into a clear, hierarchical structure in the docs directory:
```
docs/
├── architecture/
│ ├── bounded-contexts.md
│ ├── module-structure.md
│ └── system-overview.md
├── api/
│ ├── implementation.md
│ ├── validation.md
│ └── versioning.md
├── database/
│ ├── setup.md
│ └── migrations.md
├── security/
│ ├── authentication.md
│ └── authorization.md
├── development/
│ ├── getting-started.md
│ ├── code-organization.md
│ └── testing.md
├── diagrams/
│ └── [existing diagram files]
└── README.md (index document)
```
### 2.2 Content Consolidation
1. **Architecture Documentation**:
- Consolidate bounded-contexts-design.md and module-structure-design.md into the architecture directory
- Create a new system-overview.md that provides a high-level overview of the system
2. **API Documentation**:
- Merge API_Documentation.md, API_DOCUMENTATION.md, API_IMPLEMENTATION_SUMMARY.md, and README_API_Implementation.md into api/implementation.md
- Move API_VALIDATION_IMPLEMENTATION.md and CLIENT_VALIDATION_IMPLEMENTATION.md to api/validation.md
- Move API_VERSIONING.md to api/versioning.md
3. **Database Documentation**:
- Merge DATABASE_INSTALLATION_COMPLETED.md, DATABASE_SETUP_FIXES.md, and README_DATABASE_SETUP.md into database/setup.md
- Create database/migrations.md for database migration information
4. **Security Documentation**:
- Merge AUTHENTICATION_AUTHORIZATION_IMPLEMENTATION_SUMMARY.md and AUTHENTICATION_AUTHORIZATION_SUMMARY.md into security/authentication.md and security/authorization.md
5. **Development Documentation**:
- Create development/getting-started.md with setup instructions
- Update and move README_CODE_ORGANIZATION.md to development/code-organization.md
- Create development/testing.md with testing guidelines
6. **Main README.md**:
- Update to provide a clear overview of the project
- Include links to the detailed documentation in the docs directory
- Keep it concise and focused on getting started quickly
### 2.3 Content Updates
For each consolidated document:
1. **Verify Accuracy**:
- Check that all information is current and accurate
- Update any outdated references to file paths, class names, etc.
- Ensure examples reflect the current codebase
2. **Improve Clarity**:
- Use consistent terminology throughout
- Add explanatory diagrams where helpful
- Include code examples for common tasks
3. **Ensure Completeness**:
- Cover all important aspects of each topic
- Include troubleshooting sections for common issues
- Add references to related documentation
## 3. Implementation Steps
### 3.1 Create New Directory Structure
1. Create the new directory structure in the docs directory
2. Create placeholder files for each new document
### 3.2 Consolidate Content
For each topic area:
1. Review all related existing documentation
2. Extract relevant, current information
3. Organize into the new document structure
4. Update references, examples, and paths
5. Add missing information as needed
### 3.3 Update Main README.md
1. Create a new version of README.md that:
- Provides a clear project overview
- Explains the project structure
- Includes quick start instructions
- Links to detailed documentation
### 3.4 Remove Redundant Files
After consolidation is complete and verified:
1. Create a list of files to be removed
2. Verify that all valuable content has been preserved
3. Remove the redundant files
## 4. Verification
After consolidation:
1. Review all new documentation for accuracy and completeness
2. Verify that all links between documents work correctly
3. Check that code examples are correct and up-to-date
4. Ensure the documentation accurately reflects the current codebase
## 5. Future Maintenance
Establish guidelines for future documentation:
1. **Single Source of Truth**: Each topic should be documented in exactly one place
2. **Consistent Structure**: Follow the established directory structure
3. **Regular Updates**: Documentation should be updated whenever related code changes
4. **Clear Ownership**: Assign responsibility for maintaining each section of documentation
## Last Updated
2025-07-21
+11
View File
@@ -39,11 +39,22 @@ kotlin {
jsMain.dependencies {
// Kotlin React dependencies with explicit stable versions
implementation("org.jetbrains.kotlin-wrappers:kotlin-react:18.2.0-pre.467")
implementation("org.jetbrains.kotlin-wrappers:kotlin-react-dom:18.2.0-pre.467")
implementation("org.jetbrains.kotlin-wrappers:kotlin-emotion:11.10.5-pre.467")
// Ktor client dependencies for API calls
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.js)
implementation(libs.ktor.client.contentNegotiation)
implementation(libs.ktor.client.serializationKotlinxJson)
// Coroutines for async operations
implementation(libs.kotlinx.coroutines.core)
// NPM dependencies
implementation(npm("react", "18.2.0"))
implementation(npm("react-dom", "18.2.0"))
implementation(npm("@r2wc/react-to-web-component", "2.0.4"))
}
}
}
+24
View File
@@ -0,0 +1,24 @@
import at.mocode.horses.ui.components.PferdeListe
import react.create
/**
* Main entry point for the Horse Registry JavaScript build.
*
* This function serves as the entry point for the Kotlin/JS application.
* It registers the React component as a web component using r2wc.
*/
fun main() {
console.log("Horse Registry JS module loaded successfully!")
// Import r2wc function from @r2wc/react-to-web-component npm package
val r2wc = js("require('@r2wc/react-to-web-component')")
// Convert React component to Web Component using r2wc
val PferdeListeWebComponent = r2wc(PferdeListe, js("{}"))
// Register the new component with a custom HTML tag
js("customElements.define('pferde-liste', arguments[0])")(PferdeListeWebComponent)
console.log("Web component 'pferde-liste' registered successfully!")
console.log("You can now use <pferde-liste></pferde-liste> in your HTML")
}
@@ -0,0 +1,308 @@
package at.mocode.horses.ui.components
import at.mocode.horses.domain.model.DomPferd
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.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import react.*
import react.dom.html.ReactHTML.div
import react.dom.html.ReactHTML.h1
import react.dom.html.ReactHTML.h3
import react.dom.html.ReactHTML.p
import react.dom.html.ReactHTML.span
import emotion.react.css
/**
* Props for the PferdeListe component
*/
external interface PferdeListeProps : Props
// Create Ktor client for API calls
private val apiClient = HttpClient {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
})
}
}
/**
* React component that displays a list of horses (Pferde).
*
* This component loads horse data from the API and renders it as HTML.
* Uses useState for state management and useEffectOnce for data loading.
*/
val PferdeListe = FC<PferdeListeProps> { _ ->
// State management with useState
var horses by useState<List<DomPferd>>(emptyList())
var loading by useState(true)
var error by useState<String?>(null)
// Data loading with useEffectOnce hook
useEffectOnce {
val scope = MainScope()
scope.launch {
try {
loading = true
error = null
// Load data with Ktor client
val response = apiClient.get("http://localhost:8080/api/horses")
val loadedHorses: List<DomPferd> = response.body()
horses = loadedHorses
} catch (e: Exception) {
error = "Fehler beim Laden der Pferde: ${e.message}"
console.error("Error loading horses:", e)
} finally {
loading = false
}
}
}
// Render HTML with React DOM elements
div {
css {
// Basic styling for the main container
"padding" to "20px"
"fontFamily" to "Arial, sans-serif"
"maxWidth" to "1200px"
"margin" to "0 auto"
}
h1 {
css {
"color" to "#2c3e50"
"borderBottom" to "2px solid #3498db"
"paddingBottom" to "10px"
"marginBottom" to "20px"
}
+"Pferde-Register"
}
when {
loading -> {
div {
css {
"padding" to "20px"
"textAlign" to "center"
"color" to "#666"
"fontSize" to "18px"
}
+"Lade Pferde..."
}
}
error != null -> {
div {
css {
"padding" to "20px"
"textAlign" to "center"
"color" to "#e74c3c"
"backgroundColor" to "#fdeaea"
"border" to "1px solid #e74c3c"
"borderRadius" to "8px"
"margin" to "20px 0"
}
+error!!
}
}
horses.isEmpty() -> {
div {
css {
"padding" to "20px"
"textAlign" to "center"
"color" to "#666"
"backgroundColor" to "#f8f9fa"
"border" to "1px solid #e0e0e0"
"borderRadius" to "8px"
"margin" to "20px 0"
}
+"Keine Pferde verfügbar"
}
}
else -> {
div {
css {
"display" to "grid"
"gridTemplateColumns" to "repeat(auto-fill, minmax(300px, 1fr))"
"gap" to "20px"
}
horses.forEach { horse ->
div {
css {
"border" to "1px solid #e0e0e0"
"borderRadius" to "8px"
"padding" to "15px"
"backgroundColor" to "#f9f9f9"
"boxShadow" to "0 2px 4px rgba(0,0,0,0.1)"
"transition" to "transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out"
"hover" to {
"transform" to "translateY(-5px)"
"boxShadow" to "0 5px 15px rgba(0,0,0,0.1)"
}
}
h3 {
css {
"color" to "#3498db"
"marginTop" to "0"
"marginBottom" to "10px"
"borderBottom" to "1px solid #e0e0e0"
"paddingBottom" to "5px"
}
+horse.getDisplayName()
}
// Basic information
p {
span {
+"🐎"
}
+" Geschlecht: ${horse.geschlecht.name}"
}
horse.geburtsdatum?.let { birthDate ->
p {
span {
+"📅"
}
+" Geburtsdatum: $birthDate"
horse.getAge()?.let { age ->
+" (${age} Jahre alt)"
}
}
}
horse.rasse?.let { breed ->
p {
span {
+"🏇"
}
+" Rasse: $breed"
}
}
horse.farbe?.let { color ->
p {
span {
+"🎨"
}
+" Farbe: $color"
}
}
horse.stockmass?.let { height ->
p {
span {
+"📏"
}
+" Stockmaß: ${height} cm"
}
}
// Identification numbers
val identificationNumbers = mutableListOf<String>()
horse.lebensnummer?.let { identificationNumbers.add("Lebensnummer: $it") }
horse.chipNummer?.let { identificationNumbers.add("Chip: $it") }
horse.passNummer?.let { identificationNumbers.add("Pass: $it") }
horse.oepsNummer?.let { identificationNumbers.add("OEPS: $it") }
horse.feiNummer?.let { identificationNumbers.add("FEI: $it") }
if (identificationNumbers.isNotEmpty()) {
p {
span {
+"🆔"
}
+" Identifikation: ${identificationNumbers.joinToString(", ")}"
}
}
// Pedigree information
val pedigreeInfo = mutableListOf<String>()
horse.vaterName?.let { pedigreeInfo.add("Vater: $it") }
horse.mutterName?.let { pedigreeInfo.add("Mutter: $it") }
horse.mutterVaterName?.let { pedigreeInfo.add("Muttervater: $it") }
if (pedigreeInfo.isNotEmpty()) {
p {
span {
+"🧬"
}
+" Abstammung: ${pedigreeInfo.joinToString(", ")}"
}
}
// Breeding information
horse.zuechterName?.let { breeder ->
p {
span {
+"👨‍🌾"
}
+" Züchter: $breeder"
}
}
horse.zuchtbuchNummer?.let { studbook ->
p {
span {
+"📖"
}
+" Zuchtbuchnummer: $studbook"
}
}
// Status indicators
val statusList = mutableListOf<String>()
if (horse.istAktiv) statusList.add("Aktiv") else statusList.add("Inaktiv")
if (horse.hasCompleteIdentification()) statusList.add("Vollständig identifiziert")
if (horse.isOepsRegistered()) statusList.add("OEPS registriert")
if (horse.isFeiRegistered()) statusList.add("FEI registriert")
p {
span {
+""
}
+" Status: ${statusList.joinToString(", ")}"
}
// Data source
p {
span {
+"📊"
}
+" Datenquelle: ${horse.datenQuelle.name}"
}
// Notes
horse.bemerkungen?.let { notes ->
p {
span {
+"📝"
}
+" Bemerkungen: $notes"
}
}
// Creation and update dates
p {
span {
+"📅"
}
+" Erstellt am: ${horse.createdAt}"
}
p {
span {
+"🔄"
}
+" Zuletzt geändert: ${horse.updatedAt}"
}
}
}
}
}
}
}
}
+11
View File
@@ -37,11 +37,22 @@ kotlin {
jsMain.dependencies {
// Kotlin React dependencies with explicit stable versions
implementation("org.jetbrains.kotlin-wrappers:kotlin-react:18.2.0-pre.467")
implementation("org.jetbrains.kotlin-wrappers:kotlin-react-dom:18.2.0-pre.467")
implementation("org.jetbrains.kotlin-wrappers:kotlin-emotion:11.10.5-pre.467")
// Ktor client dependencies for API calls
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.js)
implementation(libs.ktor.client.contentNegotiation)
implementation(libs.ktor.client.serializationKotlinxJson)
// Coroutines for async operations
implementation(libs.kotlinx.coroutines.core)
// NPM dependencies
implementation(npm("react", "18.2.0"))
implementation(npm("react-dom", "18.2.0"))
implementation(npm("@r2wc/react-to-web-component", "2.0.4"))
}
}
}
+24
View File
@@ -0,0 +1,24 @@
import at.mocode.masterdata.ui.components.StammdatenListe
import react.create
/**
* Main entry point for the Master Data JavaScript build.
*
* This function serves as the entry point for the Kotlin/JS application.
* It registers the React component as a web component using r2wc.
*/
fun main() {
console.log("Master Data JS module loaded successfully!")
// Import r2wc function from @r2wc/react-to-web-component npm package
val r2wc = js("require('@r2wc/react-to-web-component')")
// Convert React component to Web Component using r2wc
val StammdatenListeWebComponent = r2wc(StammdatenListe, js("{}"))
// Register the new component with a custom HTML tag
js("customElements.define('stammdaten-liste', arguments[0])")(StammdatenListeWebComponent)
console.log("Web component 'stammdaten-liste' registered successfully!")
console.log("You can now use <stammdaten-liste></stammdaten-liste> in your HTML")
}
@@ -0,0 +1,257 @@
package at.mocode.masterdata.ui.components
import at.mocode.masterdata.domain.model.LandDefinition
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.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import react.*
import react.dom.html.ReactHTML.div
import react.dom.html.ReactHTML.h1
import react.dom.html.ReactHTML.h2
import react.dom.html.ReactHTML.h3
import react.dom.html.ReactHTML.p
import react.dom.html.ReactHTML.span
import emotion.react.css
/**
* Props for the StammdatenListe component
*/
external interface StammdatenListeProps : Props
// Create Ktor client for API calls
private val apiClient = HttpClient {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
})
}
}
/**
* React component that displays master data (Stammdaten).
*
* This component loads master data from the API and renders it as HTML.
* Currently focuses on countries (LandDefinition) but can be extended for other master data types.
* Uses useState for state management and useEffectOnce for data loading.
*/
val StammdatenListe = FC<StammdatenListeProps> { _ ->
// State management with useState
var countries by useState<List<LandDefinition>>(emptyList())
var loading by useState(true)
var error by useState<String?>(null)
// Data loading with useEffectOnce hook
useEffectOnce {
val scope = MainScope()
scope.launch {
try {
loading = true
error = null
// Load data with Ktor client
val response = apiClient.get("http://localhost:8080/api/masterdata/countries")
val loadedCountries: List<LandDefinition> = response.body()
countries = loadedCountries
} catch (e: Exception) {
error = "Fehler beim Laden der Stammdaten: ${e.message}"
console.error("Error loading master data:", e)
} finally {
loading = false
}
}
}
// Render HTML with React DOM elements
div {
css {
// Basic styling for the main container
"padding" to "20px"
"fontFamily" to "Arial, sans-serif"
"maxWidth" to "1200px"
"margin" to "0 auto"
}
h1 {
css {
"color" to "#2c3e50"
"borderBottom" to "2px solid #3498db"
"paddingBottom" to "10px"
"marginBottom" to "20px"
}
+"Stammdaten"
}
h2 {
css {
"color" to "#34495e"
"marginTop" to "20px"
"marginBottom" to "15px"
"fontSize" to "1.5em"
}
+"Länder"
}
when {
loading -> {
div {
css {
"padding" to "20px"
"textAlign" to "center"
"color" to "#666"
"fontSize" to "18px"
}
+"Lade Stammdaten..."
}
}
error != null -> {
div {
css {
"padding" to "20px"
"textAlign" to "center"
"color" to "#e74c3c"
"backgroundColor" to "#fdeaea"
"border" to "1px solid #e74c3c"
"borderRadius" to "8px"
"margin" to "20px 0"
}
+error!!
}
}
countries.isEmpty() -> {
div {
css {
"padding" to "20px"
"textAlign" to "center"
"color" to "#666"
"backgroundColor" to "#f8f9fa"
"border" to "1px solid #e0e0e0"
"borderRadius" to "8px"
"margin" to "20px 0"
}
+"Keine Länder verfügbar"
}
}
else -> {
div {
css {
"display" to "grid"
"gridTemplateColumns" to "repeat(auto-fill, minmax(300px, 1fr))"
"gap" to "20px"
}
countries.forEach { country ->
div {
css {
"border" to "1px solid #e0e0e0"
"borderRadius" to "8px"
"padding" to "15px"
"backgroundColor" to "#f9f9f9"
"boxShadow" to "0 2px 4px rgba(0,0,0,0.1)"
"transition" to "transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out"
"hover" to {
"transform" to "translateY(-5px)"
"boxShadow" to "0 5px 15px rgba(0,0,0,0.1)"
}
}
h3 {
css {
"color" to "#3498db"
"marginTop" to "0"
"marginBottom" to "10px"
"borderBottom" to "1px solid #e0e0e0"
"paddingBottom" to "5px"
}
+country.nameDeutsch
}
// ISO codes
p {
span {
+"🌍"
}
+" ISO-Codes: ${country.isoAlpha2Code} / ${country.isoAlpha3Code}"
country.isoNumerischerCode?.let { numCode ->
+" / $numCode"
}
}
// English name if available
country.nameEnglisch?.let { englishName ->
p {
span {
+"🇬🇧"
}
+" Englischer Name: $englishName"
}
}
// EU/EWR membership
val membershipInfo = mutableListOf<String>()
country.istEuMitglied?.let { isEuMember ->
if (isEuMember) membershipInfo.add("EU-Mitglied")
}
country.istEwrMitglied?.let { isEwrMember ->
if (isEwrMember) membershipInfo.add("EWR-Mitglied")
}
if (membershipInfo.isNotEmpty()) {
p {
span {
+"🇪🇺"
}
+" Mitgliedschaft: ${membershipInfo.joinToString(", ")}"
}
}
// Status
p {
span {
+""
}
+" Status: ${if (country.istAktiv) "Aktiv" else "Inaktiv"}"
}
// Sort order if available
country.sortierReihenfolge?.let { sortOrder ->
p {
span {
+"🔢"
}
+" Sortierreihenfolge: $sortOrder"
}
}
// Coat of arms/flag URL if available
country.wappenUrl?.let { flagUrl ->
p {
span {
+"🏴"
}
+" Wappen/Flagge: $flagUrl"
}
}
// Creation and update dates
p {
span {
+"📅"
}
+" Erstellt am: ${country.createdAt}"
}
p {
span {
+"🔄"
}
+" Zuletzt geändert: ${country.updatedAt}"
}
}
}
}
}
}
}
}
+11
View File
@@ -38,11 +38,22 @@ kotlin {
jsMain.dependencies {
// Kotlin React dependencies with explicit stable versions
implementation("org.jetbrains.kotlin-wrappers:kotlin-react:18.2.0-pre.467")
implementation("org.jetbrains.kotlin-wrappers:kotlin-react-dom:18.2.0-pre.467")
implementation("org.jetbrains.kotlin-wrappers:kotlin-emotion:11.10.5-pre.467")
// Ktor client dependencies for API calls
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.js)
implementation(libs.ktor.client.contentNegotiation)
implementation(libs.ktor.client.serializationKotlinxJson)
// Coroutines for async operations
implementation(libs.kotlinx.coroutines.core)
// NPM dependencies
implementation(npm("react", "18.2.0"))
implementation(npm("react-dom", "18.2.0"))
implementation(npm("@r2wc/react-to-web-component", "2.0.4"))
}
}
}
@@ -0,0 +1,37 @@
import at.mocode.members.ui.components.MitgliederListe
import at.mocode.members.ui.components.LoginForm
import react.create
/**
* Main entry point for the Member Management JavaScript build.
*
* This function serves as the entry point for the Kotlin/JS application.
* It registers the React components as web components using r2wc.
*/
fun main() {
console.log("Member Management JS module loaded successfully!")
// Import r2wc function from @r2wc/react-to-web-component npm package
val r2wc = js("require('@r2wc/react-to-web-component')")
// Convert MitgliederListe React component to Web Component using r2wc
val MitgliederListeWebComponent = r2wc(MitgliederListe, js("{}"))
// Register the MitgliederListe component with a custom HTML tag
js("customElements.define('mitglieder-liste', arguments[0])")(MitgliederListeWebComponent)
console.log("Web component 'mitglieder-liste' registered successfully!")
// Convert LoginForm React component to Web Component using r2wc
// Define props configuration for the LoginForm component
val loginFormProps = js("{}")
js("loginFormProps.onLoginSuccess = { type: Function }")
val LoginFormWebComponent = r2wc(LoginForm, loginFormProps)
// Register the LoginForm component with a custom HTML tag
js("customElements.define('login-form', arguments[0])")(LoginFormWebComponent)
console.log("Web component 'login-form' registered successfully!")
console.log("You can now use <mitglieder-liste></mitglieder-liste> and <login-form></login-form> in your HTML")
}
@@ -0,0 +1,284 @@
package at.mocode.members.ui.components
import at.mocode.validation.ApiValidationUtils
import at.mocode.validation.ValidationError
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import react.*
import react.dom.html.InputType
import react.dom.html.ReactHTML.button
import react.dom.html.ReactHTML.div
import react.dom.html.ReactHTML.form
import react.dom.html.ReactHTML.h2
import react.dom.html.ReactHTML.input
import react.dom.html.ReactHTML.label
import react.dom.html.ReactHTML.p
import react.dom.html.ReactHTML.span
import emotion.react.css
/**
* Props for the LoginForm component
*/
external interface LoginFormProps : Props {
var onLoginSuccess: (String) -> Unit
}
/**
* Request body for login API
*/
@Serializable
private data class LoginRequest(
val username: String,
val password: String
)
/**
* Response from login API
*/
@Serializable
private data class LoginResponse(
val token: String,
val username: String
)
/**
* Error response from API
*/
@Serializable
private data class ErrorResponse(
val message: String,
val status: String
)
// Create Ktor client for API calls
private val apiClient = HttpClient {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
})
}
}
/**
* React component that displays a login form with client-side validation.
*
* This component demonstrates how to use the existing validation utilities
* for client-side validation before submitting the form to the server.
*/
val LoginForm = FC<LoginFormProps> { props ->
// State management with useState
var username by useState("")
var password by useState("")
var validationErrors by useState<List<ValidationError>>(emptyList())
var serverError by useState<String?>(null)
var isLoading by useState(false)
// Function to handle login
val handleLogin = {
// Clear previous errors
validationErrors = emptyList()
serverError = null
// Perform client-side validation
val errors = ApiValidationUtils.validateLoginRequest(username, password)
if (errors.isNotEmpty()) {
// If validation fails, update the validationErrors state
validationErrors = errors
} else {
// If validation passes, submit the form
isLoading = true
val scope = MainScope()
scope.launch {
try {
val response = apiClient.post("http://localhost:8080/auth/login") {
contentType(ContentType.Application.Json)
setBody(LoginRequest(username, password))
}
if (response.status.isSuccess()) {
val loginResponse: LoginResponse = response.body()
props.onLoginSuccess(loginResponse.token)
} else {
val errorResponse: ErrorResponse = response.body()
serverError = errorResponse.message
}
} catch (e: Exception) {
serverError = "Login failed: ${e.message}"
console.error("Login error:", e)
} finally {
isLoading = false
}
}
}
}
// Helper function to get validation error for a field
val getFieldError = { fieldName: String ->
validationErrors.find { it.field == fieldName }?.message
}
// Render the form
div {
css {
"maxWidth" to "400px"
"margin" to "0 auto"
"padding" to "20px"
"backgroundColor" to "#f9f9f9"
"borderRadius" to "8px"
"boxShadow" to "0 2px 4px rgba(0,0,0,0.1)"
}
h2 {
css {
"textAlign" to "center"
"color" to "#2c3e50"
"marginBottom" to "20px"
}
+"Login"
}
// Display server error if any
serverError?.let {
div {
css {
"backgroundColor" to "#fdeaea"
"color" to "#e74c3c"
"padding" to "10px"
"borderRadius" to "4px"
"marginBottom" to "15px"
"textAlign" to "center"
}
+it
}
}
form {
// No onSubmit handler, using button click instead
// Username field
div {
css {
"marginBottom" to "15px"
}
label {
css {
"display" to "block"
"marginBottom" to "5px"
"fontWeight" to "bold"
}
htmlFor = "username"
+"Username or Email"
}
input {
css {
"width" to "100%"
"padding" to "8px"
"borderRadius" to "4px"
"border" to if (getFieldError("username") != null) "1px solid #e74c3c" else "1px solid #ddd"
}
type = InputType.text
id = "username"
value = username
onChange = { event -> username = event.target.value }
disabled = isLoading
required = true
}
// Display validation error for username if any
getFieldError("username")?.let {
p {
css {
"color" to "#e74c3c"
"fontSize" to "12px"
"margin" to "5px 0 0 0"
}
+it
}
}
}
// Password field
div {
css {
"marginBottom" to "20px"
}
label {
css {
"display" to "block"
"marginBottom" to "5px"
"fontWeight" to "bold"
}
htmlFor = "password"
+"Password"
}
input {
css {
"width" to "100%"
"padding" to "8px"
"borderRadius" to "4px"
"border" to if (getFieldError("password") != null) "1px solid #e74c3c" else "1px solid #ddd"
}
type = InputType.password
id = "password"
value = password
onChange = { event -> password = event.target.value }
disabled = isLoading
required = true
}
// Display validation error for password if any
getFieldError("password")?.let {
p {
css {
"color" to "#e74c3c"
"fontSize" to "12px"
"margin" to "5px 0 0 0"
}
+it
}
}
}
// Submit button
button {
css {
"width" to "100%"
"padding" to "10px"
"backgroundColor" to "#3498db"
"color" to "white"
"border" to "none"
"borderRadius" to "4px"
"cursor" to if (isLoading) "not-allowed" else "pointer"
"opacity" to if (isLoading) "0.7" else "1"
"transition" to "background-color 0.3s"
"hover" to {
"backgroundColor" to if (!isLoading) "#2980b9" else "#3498db"
}
}
type = react.dom.html.ButtonType.button
disabled = isLoading
onClick = { handleLogin() }
if (isLoading) {
+"Logging in..."
} else {
+"Login"
}
}
}
}
}
@@ -0,0 +1,227 @@
package at.mocode.members.ui.components
import at.mocode.members.domain.model.DomUser
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.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import react.*
import react.dom.html.ReactHTML.div
import react.dom.html.ReactHTML.h1
import react.dom.html.ReactHTML.h3
import react.dom.html.ReactHTML.p
import react.dom.html.ReactHTML.span
import emotion.react.css
/**
* Props for the MitgliederListe component
*/
external interface MitgliederListeProps : Props
// Create Ktor client for API calls
private val apiClient = HttpClient {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
})
}
}
/**
* React component that displays a list of members (Mitglieder).
*
* This component loads member data from the API and renders it as HTML.
* Uses useState for state management and useEffectOnce for data loading.
*/
val MitgliederListe = FC<MitgliederListeProps> { _ ->
// State management with useState
var members by useState<List<DomUser>>(emptyList())
var loading by useState(true)
var error by useState<String?>(null)
// Data loading with useEffectOnce hook
useEffectOnce {
val scope = MainScope()
scope.launch {
try {
loading = true
error = null
// Load data with Ktor client
val response = apiClient.get("http://localhost:8080/api/members")
val loadedMembers: List<DomUser> = response.body()
members = loadedMembers
} catch (e: Exception) {
error = "Fehler beim Laden der Mitglieder: ${e.message}"
console.error("Error loading members:", e)
} finally {
loading = false
}
}
}
// Render HTML with React DOM elements
div {
css {
// Basic styling for the main container
"padding" to "20px"
"fontFamily" to "Arial, sans-serif"
"maxWidth" to "1200px"
"margin" to "0 auto"
}
h1 {
css {
"color" to "#2c3e50"
"borderBottom" to "2px solid #3498db"
"paddingBottom" to "10px"
"marginBottom" to "20px"
}
+"Mitglieder"
}
when {
loading -> {
div {
css {
"padding" to "20px"
"textAlign" to "center"
"color" to "#666"
"fontSize" to "18px"
}
+"Lade Mitglieder..."
}
}
error != null -> {
div {
css {
"padding" to "20px"
"textAlign" to "center"
"color" to "#e74c3c"
"backgroundColor" to "#fdeaea"
"border" to "1px solid #e74c3c"
"borderRadius" to "8px"
"margin" to "20px 0"
}
+error!!
}
}
members.isEmpty() -> {
div {
css {
"padding" to "20px"
"textAlign" to "center"
"color" to "#666"
"backgroundColor" to "#f8f9fa"
"border" to "1px solid #e0e0e0"
"borderRadius" to "8px"
"margin" to "20px 0"
}
+"Keine Mitglieder verfügbar"
}
}
else -> {
div {
css {
"display" to "grid"
"gridTemplateColumns" to "repeat(auto-fill, minmax(300px, 1fr))"
"gap" to "20px"
}
members.forEach { member ->
div {
css {
"border" to "1px solid #e0e0e0"
"borderRadius" to "8px"
"padding" to "15px"
"backgroundColor" to "#f9f9f9"
"boxShadow" to "0 2px 4px rgba(0,0,0,0.1)"
"transition" to "transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out"
"hover" to {
"transform" to "translateY(-5px)"
"boxShadow" to "0 5px 15px rgba(0,0,0,0.1)"
}
}
h3 {
css {
"color" to "#3498db"
"marginTop" to "0"
"marginBottom" to "10px"
"borderBottom" to "1px solid #e0e0e0"
"paddingBottom" to "5px"
}
+member.username
}
p {
span {
+"📧"
}
+" E-Mail: ${member.email}"
}
p {
span {
+"🆔"
}
+" Person-ID: ${member.personId}"
}
// Status indicators
val statusList = mutableListOf<String>()
if (member.istAktiv) statusList.add("Aktiv") else statusList.add("Inaktiv")
if (member.istEmailVerifiziert) statusList.add("E-Mail verifiziert")
if (member.isLocked()) statusList.add("Gesperrt")
if (member.canLogin()) statusList.add("Kann sich anmelden")
p {
span {
+""
}
+" Status: ${statusList.joinToString(", ")}"
}
// Failed login attempts
if (member.fehlgeschlageneAnmeldungen > 0) {
p {
span {
+"⚠️"
}
+" Fehlgeschlagene Anmeldungen: ${member.fehlgeschlageneAnmeldungen}"
}
}
// Last login
member.letzteAnmeldung?.let { lastLogin ->
p {
span {
+"🔐"
}
+" Letzte Anmeldung: $lastLogin"
}
}
// Creation date
p {
span {
+"📅"
}
+" Erstellt am: ${member.createdAt}"
}
// Last update
p {
span {
+"🔄"
}
+" Zuletzt geändert: ${member.updatedAt}"
}
}
}
}
}
}
}
}
@@ -0,0 +1,75 @@
package at.mocode.members.test
import at.mocode.members.domain.service.UserAuthorizationService
import at.mocode.members.domain.service.JwtService
import at.mocode.members.domain.service.AuthenticationService
import at.mocode.members.infrastructure.repository.*
import com.benasher44.uuid.uuid4
import kotlin.test.Test
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlinx.coroutines.runBlocking
/**
* Test class for the authentication system.
*
* This test verifies that the authentication services can be created
* and basic authentication operations work correctly.
*/
class AuthenticationTest {
@Test
fun testAuthenticationSystem() = runBlocking {
println("[DEBUG_LOG] Testing Authentication System")
try {
// Try to create the services
val userRepository = UserRepositoryImpl()
val personRolleRepository = PersonRolleRepositoryImpl()
val rolleRepository = RolleRepositoryImpl()
val rolleBerechtigungRepository = RolleBerechtigungRepositoryImpl()
val berechtigungRepository = BerechtigungRepositoryImpl()
val userAuthorizationService = UserAuthorizationService(
userRepository,
personRolleRepository,
rolleRepository,
rolleBerechtigungRepository,
berechtigungRepository
)
val jwtService = JwtService(userAuthorizationService)
println("[DEBUG_LOG] Services created successfully")
// Try to get user auth info for a test user
val testUsers = userRepository.getAllUsers()
println("[DEBUG_LOG] Found ${testUsers.size} test users")
if (testUsers.isNotEmpty()) {
val testUser = testUsers.first()
println("[DEBUG_LOG] Testing with user: ${testUser.username}")
val authInfo = userAuthorizationService.getUserAuthInfo(testUser.userId)
println("[DEBUG_LOG] Auth info for test user: $authInfo")
assertNotNull(authInfo, "Auth info should not be null")
// Test JWT token generation
val token = jwtService.createToken(testUser)
println("[DEBUG_LOG] Generated JWT token: ${token}")
assertNotNull(token, "JWT token should not be null")
assertTrue(token.isNotEmpty(), "JWT token should not be empty")
// Test token validation
val payload = jwtService.validateToken(token)
println("[DEBUG_LOG] Token validation result: $payload")
assertNotNull(payload, "Token validation payload should not be null")
}
} catch (e: Exception) {
println("[DEBUG_LOG] Error testing authentication system: ${e.message}")
e.printStackTrace()
throw e
}
}
}
+18
View File
@@ -32,6 +32,24 @@ kotlin {
implementation(libs.postgresql.driver)
}
jvmTest.dependencies {
// Ktor server dependencies
implementation(libs.ktor.server.core)
implementation(libs.ktor.server.netty)
implementation(libs.ktor.server.tests)
// H2 database for testing
implementation(libs.h2.driver)
// Dependencies on other modules
implementation(project(":api-gateway"))
implementation(project(":master-data"))
implementation(project(":event-management"))
// Coroutines testing
implementation(libs.kotlinx.coroutines.test)
}
jsMain.dependencies {
// Kotlin React dependencies with explicit stable versions (for shared components)
implementation("org.jetbrains.kotlin-wrappers:kotlin-react:18.2.0-pre.467")
@@ -0,0 +1,105 @@
package at.mocode.validation.test
import at.mocode.validation.ApiValidationUtils
import at.mocode.validation.ValidationError
import kotlin.test.Test
import kotlin.test.assertTrue
import kotlin.test.assertFalse
import kotlinx.datetime.LocalDate
/**
* Test class for API validation utilities.
*
* This test verifies that the validation implementation works correctly
* for all API endpoints.
*/
class ValidationTest {
@Test
fun testQueryParameterValidation() {
// Test valid parameters
val validErrors = ApiValidationUtils.validateQueryParameters(
limit = "50",
offset = "0",
search = "test"
)
assertTrue(ApiValidationUtils.isValid(validErrors), "Valid query parameters should pass validation")
// Test invalid limit
val invalidLimitErrors = ApiValidationUtils.validateQueryParameters(
limit = "invalid"
)
assertFalse(ApiValidationUtils.isValid(invalidLimitErrors), "Invalid limit parameter should fail validation")
// Test limit out of range
val outOfRangeLimitErrors = ApiValidationUtils.validateQueryParameters(
limit = "2000"
)
assertFalse(ApiValidationUtils.isValid(outOfRangeLimitErrors), "Out of range limit should fail validation")
// Test invalid offset
val invalidOffsetErrors = ApiValidationUtils.validateQueryParameters(
offset = "-1"
)
assertFalse(ApiValidationUtils.isValid(invalidOffsetErrors), "Invalid offset parameter should fail validation")
}
@Test
fun testLoginRequestValidation() {
// Test valid login
val validErrors = ApiValidationUtils.validateLoginRequest("user@example.com", "password123")
assertTrue(ApiValidationUtils.isValid(validErrors), "Valid login request should pass validation")
// Test missing username
val missingUsernameErrors = ApiValidationUtils.validateLoginRequest(null, "password123")
assertFalse(ApiValidationUtils.isValid(missingUsernameErrors), "Missing username should fail validation")
// Test missing password
val missingPasswordErrors = ApiValidationUtils.validateLoginRequest("user@example.com", null)
assertFalse(ApiValidationUtils.isValid(missingPasswordErrors), "Missing password should fail validation")
}
@Test
fun testCountryRequestValidation() {
// Test valid country request
val validErrors = ApiValidationUtils.validateCountryRequest("AT", "AUT", "Österreich", "Austria")
assertTrue(ApiValidationUtils.isValid(validErrors), "Valid country request should pass validation")
// Test missing required fields
val missingFieldsErrors = ApiValidationUtils.validateCountryRequest(null, null, null, null)
assertFalse(ApiValidationUtils.isValid(missingFieldsErrors), "Missing required fields should fail validation")
// Test invalid ISO codes
val invalidIsoErrors = ApiValidationUtils.validateCountryRequest("INVALID", "INVALID", "Test", "Test")
assertFalse(ApiValidationUtils.isValid(invalidIsoErrors), "Invalid ISO codes should fail validation")
}
@Test
fun testHorseRequestValidation() {
// Test valid horse request
val validErrors = ApiValidationUtils.validateHorseRequest("Thunder", "123456789", "987654321", "OEPS123", "FEI456")
assertTrue(ApiValidationUtils.isValid(validErrors), "Valid horse request should pass validation")
// Test missing horse name
val missingNameErrors = ApiValidationUtils.validateHorseRequest(null, "123456789", "987654321", "OEPS123", "FEI456")
assertFalse(ApiValidationUtils.isValid(missingNameErrors), "Missing horse name should fail validation")
}
@Test
fun testEventRequestValidation() {
val startDate = LocalDate(2024, 6, 1)
val endDate = LocalDate(2024, 6, 3)
// Test valid event request
val validErrors = ApiValidationUtils.validateEventRequest("Test Event", "Vienna", startDate, endDate, 100)
assertTrue(ApiValidationUtils.isValid(validErrors), "Valid event request should pass validation")
// Test missing event name
val missingNameErrors = ApiValidationUtils.validateEventRequest(null, "Vienna", startDate, endDate, 100)
assertFalse(ApiValidationUtils.isValid(missingNameErrors), "Missing event name should fail validation")
// Test invalid date range (end before start)
val invalidDateErrors = ApiValidationUtils.validateEventRequest("Test Event", "Vienna", endDate, startDate, 100)
assertFalse(ApiValidationUtils.isValid(invalidDateErrors), "Invalid date range should fail validation")
}
}
+309
View File
@@ -0,0 +1,309 @@
# Test Scripts Conversion Plan
This document outlines the plan for moving standalone test scripts from the root directory to appropriate test directories and converting them to proper unit tests.
## 1. Standalone Test Scripts
The following standalone test scripts have been identified in the root directory:
| File | Target Directory | Test Class Name |
|------|-----------------|-----------------|
| test_authentication.kt | member-management/src/jvmTest/kotlin/at/mocode/members/test/ | AuthenticationTest |
| test_authentication_authorization.kt | api-gateway/src/jvmTest/kotlin/at/mocode/gateway/test/ | AuthenticationAuthorizationTest |
| test_validation.kt | shared-kernel/src/jvmTest/kotlin/at/mocode/validation/test/ | ValidationTest |
| database-integration-test.kt | shared-kernel/src/jvmTest/kotlin/at/mocode/shared/database/test/ | DatabaseIntegrationTest |
## 2. Conversion Guidelines
When converting the standalone scripts to proper unit tests, the following guidelines should be followed:
1. **Add proper test annotations**:
- Use `@Test` for test methods
- Use `@BeforeTest` for setup methods
- Use `@AfterTest` for teardown methods
2. **Organize tests into test classes**:
- Create a test class with a descriptive name
- Group related tests into methods within the class
- Use descriptive method names that explain what is being tested
3. **Use proper assertions**:
- Replace `println` statements with proper assertions
- Use `kotlin.test.assertEquals`, `kotlin.test.assertTrue`, etc.
- Add meaningful error messages to assertions
4. **Set up test dependencies properly**:
- Initialize dependencies in setup methods
- Use mocks or test doubles where appropriate
- Clean up resources in teardown methods
5. **Add proper package declarations**:
- Use the package that corresponds to the target directory
## 3. Implementation Steps
### 3.1 test_authentication.kt → AuthenticationTest
1. Create the target directory if it doesn't exist
2. Create a new file AuthenticationTest.kt with the following structure:
```kotlin
package at.mocode.members.test
import at.mocode.members.domain.service.UserAuthorizationService
import at.mocode.members.domain.service.JwtService
import at.mocode.members.domain.service.AuthenticationService
import at.mocode.members.infrastructure.repository.*
import kotlin.test.*
class AuthenticationTest {
private lateinit var userRepository: UserRepositoryImpl
private lateinit var userAuthorizationService: UserAuthorizationService
private lateinit var jwtService: JwtService
@BeforeTest
fun setup() {
userRepository = UserRepositoryImpl()
val personRolleRepository = PersonRolleRepositoryImpl()
val rolleRepository = RolleRepositoryImpl()
val rolleBerechtigungRepository = RolleBerechtigungRepositoryImpl()
val berechtigungRepository = BerechtigungRepositoryImpl()
userAuthorizationService = UserAuthorizationService(
userRepository,
personRolleRepository,
rolleRepository,
rolleBerechtigungRepository,
berechtigungRepository
)
jwtService = JwtService(userAuthorizationService)
}
@Test
fun testUserAuthInfo() {
val testUsers = userRepository.getAllUsers()
assertNotEquals(0, testUsers.size, "Should have at least one test user")
if (testUsers.isNotEmpty()) {
val testUser = testUsers.first()
val authInfo = userAuthorizationService.getUserAuthInfo(testUser.userId)
assertNotNull(authInfo, "Auth info should not be null")
}
}
@Test
fun testJwtTokenGeneration() {
val testUsers = userRepository.getAllUsers()
if (testUsers.isNotEmpty()) {
val testUser = testUsers.first()
val tokenInfo = jwtService.generateToken(testUser)
assertNotNull(tokenInfo.token, "Token should not be null")
assertNotNull(tokenInfo.expiresAt, "Expiration date should not be null")
}
}
@Test
fun testTokenValidation() {
val testUsers = userRepository.getAllUsers()
if (testUsers.isNotEmpty()) {
val testUser = testUsers.first()
val tokenInfo = jwtService.generateToken(testUser)
val payload = jwtService.validateToken(tokenInfo.token)
assertNotNull(payload, "Payload should not be null")
}
}
}
```
### 3.2 test_authentication_authorization.kt → AuthenticationAuthorizationTest
1. Create the target directory if it doesn't exist
2. Create a new file AuthenticationAuthorizationTest.kt with a similar structure to AuthenticationTest.kt
3. Convert the main function to test methods with proper assertions
4. Add setup and teardown methods as needed
### 3.3 test_validation.kt → ValidationTest
1. Create the target directory if it doesn't exist
2. Create a new file ValidationTest.kt with a similar structure
3. Convert the main function to test methods with proper assertions
4. Add setup and teardown methods as needed
### 3.4 database-integration-test.kt → DatabaseIntegrationTest
1. Create the target directory if it doesn't exist
2. Create a new file DatabaseIntegrationTest.kt with the following structure:
```kotlin
package at.mocode.shared.database.test
import at.mocode.gateway.config.configureDatabase
import at.mocode.masterdata.domain.model.LandDefinition
import at.mocode.masterdata.infrastructure.repository.LandRepositoryImpl
import at.mocode.events.domain.model.Veranstaltung
import at.mocode.events.infrastructure.repository.VeranstaltungRepositoryImpl
import at.mocode.enums.SparteE
import com.benasher44.uuid.uuid4
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import kotlinx.coroutines.runBlocking
import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDate
import org.jetbrains.exposed.sql.transactions.transaction
import kotlin.test.*
class DatabaseIntegrationTest {
private lateinit var application: Application
private lateinit var landRepository: LandRepositoryImpl
private lateinit var eventRepository: VeranstaltungRepositoryImpl
@BeforeTest
fun setup() {
val environment = applicationEngineEnvironment {
config = MapApplicationConfig(
"database.url" to "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE",
"database.user" to "sa",
"database.password" to ""
)
}
application = Application(environment)
application.configureDatabase()
landRepository = LandRepositoryImpl()
eventRepository = VeranstaltungRepositoryImpl()
}
@Test
fun testMasterDataRepository() = runBlocking {
transaction {
// Create a test country
val testCountry = LandDefinition(
landId = uuid4(),
isoAlpha2Code = "TS",
isoAlpha3Code = "TST",
isoNumerischerCode = "999",
nameDeutsch = "Testland",
nameEnglisch = "Testland",
wappenUrl = null,
istEuMitglied = false,
istEwrMitglied = false,
istAktiv = true,
sortierReihenfolge = 999,
createdAt = Clock.System.now(),
updatedAt = Clock.System.now()
)
// Save the test country
val savedCountry = landRepository.save(testCountry)
assertEquals("Testland", savedCountry.nameDeutsch, "Country name should match")
// Retrieve the test country
val retrievedCountry = landRepository.findByIsoAlpha2Code("TS")
assertNotNull(retrievedCountry, "Retrieved country should not be null")
assertEquals("Testland", retrievedCountry.nameDeutsch, "Retrieved country name should match")
// Count active countries
val activeCount = landRepository.countActive()
assertTrue(activeCount > 0, "Should have at least one active country")
// Clean up
landRepository.delete(testCountry.landId)
}
}
@Test
fun testEventManagementRepository() = runBlocking {
transaction {
// Create a test event
val testEvent = Veranstaltung(
name = "Test Veranstaltung",
beschreibung = "Eine Test-Veranstaltung für die Integration",
startDatum = LocalDate(2024, 8, 15),
endDatum = LocalDate(2024, 8, 17),
ort = "Test-Ort",
veranstalterVereinId = uuid4(),
sparten = listOf(SparteE.DRESSUR, SparteE.SPRINGEN),
istAktiv = true,
istOeffentlich = true,
maxTeilnehmer = 100,
anmeldeschluss = LocalDate(2024, 8, 1)
)
// Save the test event
val savedEvent = eventRepository.save(testEvent)
assertEquals("Test Veranstaltung", savedEvent.name, "Event name should match")
// Retrieve the test event
val retrievedEvent = eventRepository.findById(savedEvent.veranstaltungId)
assertNotNull(retrievedEvent, "Retrieved event should not be null")
assertEquals("Test Veranstaltung", retrievedEvent.name, "Retrieved event name should match")
assertEquals(3, retrievedEvent.getDurationInDays(), "Event duration should be 3 days")
assertTrue(retrievedEvent.isMultiDay(), "Event should be multi-day")
// Test search functionality
val searchResults = eventRepository.findByName("Test", 10)
assertTrue(searchResults.isNotEmpty(), "Search should return at least one result")
// Test public events
val publicEvents = eventRepository.findPublicEvents(true)
assertTrue(publicEvents.isNotEmpty(), "Should have at least one public event")
// Count active events
val activeEventCount = eventRepository.countActive()
assertTrue(activeEventCount > 0, "Should have at least one active event")
// Clean up event
eventRepository.delete(savedEvent.veranstaltungId)
}
}
}
/**
* Simple map-based application config for testing
*/
class MapApplicationConfig(private val map: Map<String, String>) : ApplicationConfig {
constructor(vararg pairs: Pair<String, String>) : this(pairs.toMap())
override fun property(path: String): ApplicationConfigValue {
return MapApplicationConfigValue(map[path])
}
override fun propertyOrNull(path: String): ApplicationConfigValue? {
return map[path]?.let { MapApplicationConfigValue(it) }
}
override fun config(path: String): ApplicationConfig {
return this
}
override fun configList(path: String): List<ApplicationConfig> {
return emptyList()
}
override fun keys(): Set<String> {
return map.keys
}
}
class MapApplicationConfigValue(private val value: String?) : ApplicationConfigValue {
override fun getString(): String = value ?: ""
override fun getList(): List<String> = value?.split(",") ?: emptyList()
}
```
## 4. Verification
After converting each test script:
1. Build the project to ensure there are no compilation errors
2. Run the tests to ensure they pass
3. Verify that the tests provide the same coverage as the original scripts
4. Remove the original scripts from the root directory
## 5. Documentation Update
Update the project documentation to reflect the new test organization:
1. Update README.md if it references the standalone test scripts
2. Update any other documentation that mentions the test scripts