(fix) Umbau zu SCS

### API-Gateway erweitern
- Bestehenden API-Gateway-Service mit zusätzlichen Funktionen ausstatten:
    - Rate Limiting implementieren
    - Request/Response Logging verbessern
This commit is contained in:
stefan
2025-07-21 16:25:12 +02:00
parent c551ef63c6
commit 7a64325196
19 changed files with 1719 additions and 1320 deletions
@@ -12,6 +12,17 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.*
import kotlin.test.*
/**
* Comprehensive test suite for the CreatePersonViewModel.
*
* Tests cover:
* - Initial state verification
* - Field update operations
* - Form validation
* - Person creation with various inputs
* - Form reset functionality
* - Error handling
*/
@OptIn(ExperimentalCoroutinesApi::class)
class CreatePersonViewModelTest {
@@ -26,6 +37,25 @@ class CreatePersonViewModelTest {
fun setup() {
Dispatchers.setMain(testDispatcher)
// Initialize mock repositories and services
setupMockRepositories()
// Create the use case with mocks
createPersonUseCase = CreatePersonUseCase(
personRepository = mockPersonRepository,
vereinRepository = mockVereinRepository,
masterDataService = mockMasterDataService
)
// Initialize the view model
viewModel = CreatePersonViewModel(createPersonUseCase)
}
/**
* Sets up all mock repositories and services needed for testing
*/
private fun setupMockRepositories() {
// Mock person repository with in-memory storage
mockPersonRepository = object : PersonRepository {
private val persons = mutableListOf<DomPerson>()
@@ -71,6 +101,7 @@ class CreatePersonViewModelTest {
}
}
// Mock verein repository (minimal implementation)
mockVereinRepository = object : VereinRepository {
override suspend fun findById(id: com.benasher44.uuid.Uuid): at.mocode.members.domain.model.DomVerein? {
return null
@@ -121,6 +152,7 @@ class CreatePersonViewModelTest {
}
}
// Mock master data service (minimal implementation)
mockMasterDataService = object : MasterDataService {
override suspend fun countryExists(countryId: com.benasher44.uuid.Uuid): Boolean {
return true
@@ -146,14 +178,6 @@ class CreatePersonViewModelTest {
return emptyList()
}
}
createPersonUseCase = CreatePersonUseCase(
personRepository = mockPersonRepository,
vereinRepository = mockVereinRepository,
masterDataService = mockMasterDataService
)
viewModel = CreatePersonViewModel(createPersonUseCase)
}
@AfterTest
@@ -161,47 +185,84 @@ class CreatePersonViewModelTest {
Dispatchers.resetMain()
}
//region Initial State Tests
@Test
fun `initial state should be correct`() {
assertEquals("", viewModel.nachname)
assertEquals("", viewModel.vorname)
assertEquals("", viewModel.titel)
assertEquals("", viewModel.oepsSatzNr)
assertEquals("", viewModel.geburtsdatum)
assertNull(viewModel.geschlecht)
assertEquals("", viewModel.telefon)
assertEquals("", viewModel.email)
assertEquals("", viewModel.strasse)
assertEquals("", viewModel.plz)
assertEquals("", viewModel.ort)
assertEquals("", viewModel.adresszusatz)
assertEquals("", viewModel.feiId)
assertEquals("", viewModel.mitgliedsNummer)
assertEquals("", viewModel.notizen)
assertFalse(viewModel.istGesperrt)
assertEquals("", viewModel.sperrGrund)
assertFalse(viewModel.isLoading)
assertNull(viewModel.errorMessage)
assertFalse(viewModel.isSuccess)
// Verify all fields are initialized to empty values
assertEquals("", viewModel.nachname, "Nachname should be empty initially")
assertEquals("", viewModel.vorname, "Vorname should be empty initially")
assertEquals("", viewModel.titel, "Titel should be empty initially")
assertEquals("", viewModel.oepsSatzNr, "OepsSatzNr should be empty initially")
assertEquals("", viewModel.geburtsdatum, "Geburtsdatum should be empty initially")
assertNull(viewModel.geschlecht, "Geschlecht should be null initially")
assertEquals("", viewModel.telefon, "Telefon should be empty initially")
assertEquals("", viewModel.email, "Email should be empty initially")
assertEquals("", viewModel.strasse, "Strasse should be empty initially")
assertEquals("", viewModel.plz, "PLZ should be empty initially")
assertEquals("", viewModel.ort, "Ort should be empty initially")
assertEquals("", viewModel.adresszusatz, "Adresszusatz should be empty initially")
assertEquals("", viewModel.feiId, "FeiId should be empty initially")
assertEquals("", viewModel.mitgliedsNummer, "MitgliedsNummer should be empty initially")
assertEquals("", viewModel.notizen, "Notizen should be empty initially")
// Verify flags are initialized correctly
assertFalse(viewModel.istGesperrt, "IstGesperrt should be false initially")
assertEquals("", viewModel.sperrGrund, "SperrGrund should be empty initially")
assertFalse(viewModel.isLoading, "IsLoading should be false initially")
assertNull(viewModel.errorMessage, "ErrorMessage should be null initially")
assertFalse(viewModel.isSuccess, "IsSuccess should be false initially")
}
//endregion
//region Update Method Tests
@Test
fun `update methods should change state correctly`() {
// When - update multiple fields
viewModel.updateNachname("Mustermann")
viewModel.updateVorname("Max")
viewModel.updateTitel("Dr.")
viewModel.updateGeschlecht(GeschlechtE.M)
viewModel.updateEmail("max@example.com")
viewModel.updateIstGesperrt(true)
viewModel.updateSperrGrund("Test Sperrgrund")
assertEquals("Mustermann", viewModel.nachname)
assertEquals("Max", viewModel.vorname)
assertEquals("Dr.", viewModel.titel)
assertEquals(GeschlechtE.M, viewModel.geschlecht)
assertEquals("max@example.com", viewModel.email)
assertTrue(viewModel.istGesperrt)
// Then - verify all fields were updated correctly
assertEquals("Mustermann", viewModel.nachname, "Nachname should be updated")
assertEquals("Max", viewModel.vorname, "Vorname should be updated")
assertEquals("Dr.", viewModel.titel, "Titel should be updated")
assertEquals(GeschlechtE.M, viewModel.geschlecht, "Geschlecht should be updated")
assertEquals("max@example.com", viewModel.email, "Email should be updated")
assertTrue(viewModel.istGesperrt, "IstGesperrt should be updated")
assertEquals("Test Sperrgrund", viewModel.sperrGrund, "SperrGrund should be updated")
}
@Test
fun `update methods should handle special characters`() {
// When - update with special characters
val nameWithSpecialChars = "Müller-Höß"
viewModel.updateNachname(nameWithSpecialChars)
// Then - verify special characters are preserved
assertEquals(nameWithSpecialChars, viewModel.nachname, "Special characters should be preserved")
}
@Test
fun `update methods should handle very long inputs`() {
// When - update with very long input
val longText = "A".repeat(500)
viewModel.updateNotizen(longText)
// Then - verify long text is preserved
assertEquals(longText, viewModel.notizen, "Long text should be preserved")
}
//endregion
//region Validation Tests
@Test
fun `createPerson should fail with empty nachname`() = runTest {
// Given - empty nachname
@@ -212,9 +273,9 @@ class CreatePersonViewModelTest {
testDispatcher.scheduler.advanceUntilIdle()
// Then
assertEquals("Nachname ist erforderlich", viewModel.errorMessage)
assertFalse(viewModel.isSuccess)
assertFalse(viewModel.isLoading)
assertEquals("Nachname ist erforderlich", viewModel.errorMessage, "Should show error for empty nachname")
assertFalse(viewModel.isSuccess, "Should not be successful with validation error")
assertFalse(viewModel.isLoading, "Loading state should be reset after validation")
}
@Test
@@ -227,14 +288,36 @@ class CreatePersonViewModelTest {
testDispatcher.scheduler.advanceUntilIdle()
// Then
assertEquals("Vorname ist erforderlich", viewModel.errorMessage)
assertFalse(viewModel.isSuccess)
assertFalse(viewModel.isLoading)
assertEquals("Vorname ist erforderlich", viewModel.errorMessage, "Should show error for empty vorname")
assertFalse(viewModel.isSuccess, "Should not be successful with validation error")
assertFalse(viewModel.isLoading, "Loading state should be reset after validation")
}
@Test
fun `createPerson should handle invalid date format`() = runTest {
// Given - invalid date format
viewModel.updateNachname("Mustermann")
viewModel.updateVorname("Max")
viewModel.updateGeburtsdatum("invalid-date")
// When
viewModel.createPerson()
testDispatcher.scheduler.advanceUntilIdle()
// Then
assertEquals("Ungültiges Datumsformat. Verwenden Sie YYYY-MM-DD", viewModel.errorMessage,
"Should show error for invalid date format")
assertFalse(viewModel.isSuccess, "Should not be successful with validation error")
assertFalse(viewModel.isLoading, "Loading state should be reset after validation")
}
//endregion
//region Success Tests
@Test
fun `createPerson should succeed with valid data`() = runTest {
// Given
// Given - valid data
viewModel.updateNachname("Mustermann")
viewModel.updateVorname("Max")
viewModel.updateGeschlecht(GeschlechtE.M)
@@ -245,31 +328,14 @@ class CreatePersonViewModelTest {
testDispatcher.scheduler.advanceUntilIdle()
// Then
assertTrue(viewModel.isSuccess)
assertNull(viewModel.errorMessage)
assertFalse(viewModel.isLoading)
}
@Test
fun `createPerson should handle invalid date format`() = runTest {
// Given
viewModel.updateNachname("Mustermann")
viewModel.updateVorname("Max")
viewModel.updateGeburtsdatum("invalid-date")
// When
viewModel.createPerson()
testDispatcher.scheduler.advanceUntilIdle()
// Then
assertEquals("Ungültiges Datumsformat. Verwenden Sie YYYY-MM-DD", viewModel.errorMessage)
assertFalse(viewModel.isSuccess)
assertFalse(viewModel.isLoading)
assertTrue(viewModel.isSuccess, "Should be successful with valid data")
assertNull(viewModel.errorMessage, "Should not have error message")
assertFalse(viewModel.isLoading, "Loading state should be reset after success")
}
@Test
fun `createPerson should handle valid date format`() = runTest {
// Given
// Given - valid date format
viewModel.updateNachname("Mustermann")
viewModel.updateVorname("Max")
viewModel.updateGeburtsdatum("1990-05-15")
@@ -279,11 +345,31 @@ class CreatePersonViewModelTest {
testDispatcher.scheduler.advanceUntilIdle()
// Then
assertTrue(viewModel.isSuccess)
assertNull(viewModel.errorMessage)
assertFalse(viewModel.isLoading)
assertTrue(viewModel.isSuccess, "Should be successful with valid date")
assertNull(viewModel.errorMessage, "Should not have error message")
assertFalse(viewModel.isLoading, "Loading state should be reset after success")
}
@Test
fun `createPerson should succeed with minimal required data`() = runTest {
// Given - only required fields
viewModel.updateNachname("Mustermann")
viewModel.updateVorname("Max")
// When
viewModel.createPerson()
testDispatcher.scheduler.advanceUntilIdle()
// Then
assertTrue(viewModel.isSuccess, "Should be successful with minimal required data")
assertNull(viewModel.errorMessage, "Should not have error message")
assertFalse(viewModel.isLoading, "Loading state should be reset after success")
}
//endregion
//region Form Management Tests
@Test
fun `resetForm should clear all fields`() {
// Given - set some values
@@ -291,18 +377,22 @@ class CreatePersonViewModelTest {
viewModel.updateVorname("Max")
viewModel.updateEmail("max@example.com")
viewModel.updateIstGesperrt(true)
viewModel.updateSperrGrund("Test Sperrgrund")
// When
viewModel.resetForm()
// Then
assertEquals("", viewModel.nachname)
assertEquals("", viewModel.vorname)
assertEquals("", viewModel.email)
assertFalse(viewModel.istGesperrt)
assertFalse(viewModel.isLoading)
assertNull(viewModel.errorMessage)
assertFalse(viewModel.isSuccess)
// Then - verify all fields are reset
assertEquals("", viewModel.nachname, "Nachname should be reset")
assertEquals("", viewModel.vorname, "Vorname should be reset")
assertEquals("", viewModel.email, "Email should be reset")
assertFalse(viewModel.istGesperrt, "IstGesperrt should be reset")
assertEquals("", viewModel.sperrGrund, "SperrGrund should be reset")
// Verify state flags are reset
assertFalse(viewModel.isLoading, "IsLoading should be reset")
assertNull(viewModel.errorMessage, "ErrorMessage should be reset")
assertFalse(viewModel.isSuccess, "IsSuccess should be reset")
}
@Test
@@ -310,18 +400,33 @@ class CreatePersonViewModelTest {
// Given - simulate an error
viewModel.updateNachname("") // This will cause validation error
viewModel.updateVorname("Max")
// When
viewModel.createPerson()
testDispatcher.scheduler.advanceUntilIdle()
// Then - verify error message exists
assertNotNull(viewModel.errorMessage)
assertNotNull(viewModel.errorMessage, "Should have error message")
// When - clear the error
viewModel.clearError()
// Then - verify error message is cleared
assertNull(viewModel.errorMessage)
assertNull(viewModel.errorMessage, "Error message should be cleared")
}
@Test
fun `loading state should be reset after createPerson completes`() = runTest {
// Given
viewModel.updateNachname("Mustermann")
viewModel.updateVorname("Max")
// When - start creation and complete the operation
viewModel.createPerson()
testDispatcher.scheduler.advanceUntilIdle()
// Then - verify loading state is reset after completion
assertFalse(viewModel.isLoading, "Loading state should be reset after operation completes")
assertTrue(viewModel.isSuccess, "Operation should complete successfully")
}
//endregion
}
@@ -4,13 +4,22 @@ import at.mocode.members.domain.model.DomPerson
import at.mocode.members.domain.repository.PersonRepository
import at.mocode.enums.GeschlechtE
import at.mocode.enums.DatenQuelleE
import com.benasher44.uuid.Uuid
import com.benasher44.uuid.uuid4
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.*
import kotlinx.datetime.LocalDate
import kotlin.test.*
/**
* Comprehensive test suite for the PersonListViewModel.
*
* Tests cover:
* - Initial state verification
* - Loading and refreshing person data
* - Error handling
* - Loading state management
*/
@OptIn(ExperimentalCoroutinesApi::class)
class PersonListViewModelTest {
@@ -21,17 +30,30 @@ class PersonListViewModelTest {
@BeforeTest
fun setup() {
Dispatchers.setMain(testDispatcher)
setupMockRepository()
}
/**
* Sets up the mock repository with test data
*/
private fun setupMockRepository() {
val persons = mutableListOf<DomPerson>()
mockPersonRepository = object : PersonRepository {
private val persons = mutableListOf<DomPerson>()
override suspend fun save(person: DomPerson): DomPerson {
val savedPerson = person.copy(personId = uuid4())
// Remove existing person with same OEPS number if exists
val existingIndex = persons.indexOfFirst { it.oepsSatzNr == person.oepsSatzNr }
if (existingIndex >= 0) {
persons.removeAt(existingIndex)
}
persons.add(savedPerson)
return savedPerson
}
override suspend fun findById(id: com.benasher44.uuid.Uuid): DomPerson? {
override suspend fun findById(id: Uuid): DomPerson? {
return persons.find { it.personId == id }
}
@@ -39,7 +61,7 @@ class PersonListViewModelTest {
return persons.find { it.oepsSatzNr == oepsSatzNr }
}
override suspend fun findByStammVereinId(vereinId: com.benasher44.uuid.Uuid): List<DomPerson> {
override suspend fun findByStammVereinId(vereinId: Uuid): List<DomPerson> {
return persons.filter { it.stammVereinId == vereinId }
}
@@ -62,86 +84,213 @@ class PersonListViewModelTest {
return persons.any { it.oepsSatzNr == oepsSatzNr }
}
override suspend fun delete(id: com.benasher44.uuid.Uuid): Boolean {
return persons.removeAll { it.personId == id }
override suspend fun delete(id: Uuid): Boolean {
val initialSize = persons.size
persons.removeAll { it.personId == id }
return persons.size < initialSize
}
}
}
/**
* Adds test persons to the repository
*/
private suspend fun addTestPersons() {
// Create and add test persons
val testPersons = listOf(
createTestPerson("123456", "Müller", "Hans", GeschlechtE.M),
createTestPerson("234567", "Schmidt", "Anna", GeschlechtE.W),
createTestPerson("345678", "Weber", "Thomas", GeschlechtE.M)
)
testPersons.forEach { mockPersonRepository.save(it) }
}
/**
* Creates a test person with the given data
*/
private fun createTestPerson(
oepsSatzNr: String,
nachname: String,
vorname: String,
geschlecht: GeschlechtE,
isActive: Boolean = true
): DomPerson {
return DomPerson(
personId = uuid4(), // Generate a new UUID
oepsSatzNr = oepsSatzNr,
nachname = nachname,
vorname = vorname,
geschlechtE = geschlecht,
datenQuelle = DatenQuelleE.MANUELL,
istAktiv = isActive
)
}
@AfterTest
fun tearDown() {
Dispatchers.resetMain()
}
//region Initial State Tests
@Test
fun `initial state should be correct`() {
// When - create view model with empty repository
viewModel = PersonListViewModel(mockPersonRepository)
assertTrue(viewModel.persons.isEmpty())
assertFalse(viewModel.isLoading)
assertNull(viewModel.errorMessage)
// Then - verify initial state
assertTrue(viewModel.persons.isEmpty(), "Persons list should be empty initially")
assertFalse(viewModel.isLoading, "Loading state should be false initially")
assertNull(viewModel.errorMessage, "Error message should be null initially")
}
//endregion
//region Data Loading Tests
@Test
fun `loadPersons should update persons list`() = runTest {
// Given
val testPerson = DomPerson(
oepsSatzNr = "123456",
nachname = "Test",
vorname = "User",
geschlechtE = GeschlechtE.M,
datenQuelle = DatenQuelleE.MANUELL
)
mockPersonRepository.save(testPerson)
// Given - repository with test data
addTestPersons()
// When
// When - initialize view model (which triggers loadPersons)
viewModel = PersonListViewModel(mockPersonRepository)
testDispatcher.scheduler.advanceUntilIdle()
// Then
assertEquals(1, viewModel.persons.size)
assertEquals("Test", viewModel.persons.first().nachname)
assertEquals("User", viewModel.persons.first().vorname)
assertFalse(viewModel.isLoading)
assertNull(viewModel.errorMessage)
// Then - verify persons list is populated
assertEquals(3, viewModel.persons.size, "Should load all test persons")
assertTrue(
viewModel.persons.any { it.nachname == "Müller" && it.vorname == "Hans" },
"Should contain person Müller Hans"
)
assertTrue(
viewModel.persons.any { it.nachname == "Schmidt" && it.vorname == "Anna" },
"Should contain person Schmidt Anna"
)
assertTrue(
viewModel.persons.any { it.nachname == "Weber" && it.vorname == "Thomas" },
"Should contain person Weber Thomas"
)
assertFalse(viewModel.isLoading, "Loading state should be reset after loading")
assertNull(viewModel.errorMessage, "Should not have error message after successful loading")
}
@Test
fun `refreshPersons should reload data`() = runTest {
// Given
// Given - view model with initial data loaded
addTestPersons()
viewModel = PersonListViewModel(mockPersonRepository)
testDispatcher.scheduler.advanceUntilIdle()
val initialCount = viewModel.persons.size
// Add a new person to repository
val newPerson = DomPerson(
oepsSatzNr = "789012",
nachname = "New",
vorname = "Person",
geschlechtE = GeschlechtE.W,
datenQuelle = DatenQuelleE.MANUELL
// When - add a new person and refresh
val newPerson = createTestPerson(
"999999",
"New",
"Person",
GeschlechtE.D
)
mockPersonRepository.save(newPerson)
// When
viewModel.refreshPersons()
testDispatcher.scheduler.advanceUntilIdle()
// Then
assertEquals(initialCount + 1, viewModel.persons.size)
assertTrue(viewModel.persons.any { it.nachname == "New" })
// Then - verify new person is included
assertEquals(initialCount + 1, viewModel.persons.size, "Should have one more person after refresh")
assertTrue(
viewModel.persons.any { it.nachname == "New" && it.vorname == "Person" },
"Should contain newly added person after refresh"
)
assertFalse(viewModel.isLoading, "Loading state should be reset after refresh")
}
@Test
fun `clearError should reset error message`() {
fun `loadPersons should handle empty repository`() = runTest {
// Given - empty repository (already set up in setup())
// When - initialize view model
viewModel = PersonListViewModel(mockPersonRepository)
testDispatcher.scheduler.advanceUntilIdle()
// Then - verify empty list is handled correctly
assertTrue(viewModel.persons.isEmpty(), "Persons list should be empty with empty repository")
assertFalse(viewModel.isLoading, "Loading state should be reset even with empty result")
assertNull(viewModel.errorMessage, "Should not have error with empty repository")
}
@Test
fun `loading state should be reset after operations complete`() = runTest {
// Given
viewModel = PersonListViewModel(mockPersonRepository)
// Simulate an error (this would normally happen in a real error scenario)
// For testing, we can't easily simulate repository errors with our mock
// but we can test the clearError functionality
// Add some test data to verify operation works
addTestPersons()
viewModel.clearError()
assertNull(viewModel.errorMessage)
// When - refresh and complete the operation
viewModel.refreshPersons()
testDispatcher.scheduler.advanceUntilIdle()
// Then - verify loading state is reset after completion
assertFalse(viewModel.isLoading, "Loading state should be reset after operation completes")
assertTrue(viewModel.persons.isNotEmpty(), "Persons list should be populated after successful refresh")
}
//endregion
//region Error Handling Tests
@Test
fun `clearError should reset error message`() {
// Given - view model
viewModel = PersonListViewModel(mockPersonRepository)
// When - clear error (even when no error exists)
viewModel.clearError()
// Then - verify no error message
assertNull(viewModel.errorMessage, "Error message should be null after clearError")
}
@Test
fun `error handling should be robust`() = runTest {
// Given - view model with initial data loaded
addTestPersons()
viewModel = PersonListViewModel(mockPersonRepository)
testDispatcher.scheduler.advanceUntilIdle()
// Capture initial state
val initialPersons = viewModel.persons.toList()
// When - simulate a refresh operation that might cause errors
viewModel.refreshPersons()
testDispatcher.scheduler.advanceUntilIdle()
// Then - verify data is still intact regardless of potential errors
assertEquals(initialPersons.size, viewModel.persons.size,
"Person list size should be maintained even with potential errors")
// And error handling mechanism works
viewModel.clearError()
assertNull(viewModel.errorMessage, "Should be able to clear any potential errors")
}
//endregion
//region Search Tests
@Test
fun `repository search should work correctly`() = runTest {
// Given - repository with test data
addTestPersons()
// When - search for a specific person
val searchResults = mockPersonRepository.findByName("Müller", 10)
// Then - verify correct results
assertEquals(1, searchResults.size, "Should find one person with name Müller")
assertEquals("Müller", searchResults.first().nachname, "Should find person with correct last name")
assertEquals("Hans", searchResults.first().vorname, "Should find person with correct first name")
}
//endregion
}