Add unit tests for data file rotation utility functions
- Implement tests for writing data files with rotation, ensuring backups are created only on changes. - Verify that old backups are rotated correctly and the maximum number of backups is maintained. - Test restoration of backups while preserving the current state as a backup. - Utilize Vitest for testing framework and manage temporary file storage during tests.
This commit is contained in:
@@ -37,6 +37,12 @@ data class SpielplanResponse(
|
||||
val seasons: List<SeasonDto> = emptyList(),
|
||||
)
|
||||
data class SeasonDto(val slug: String = "", val label: String = "")
|
||||
data class MannschaftenSeasonsResponse(
|
||||
val success: Boolean = false,
|
||||
val seasons: List<SeasonDto> = emptyList(),
|
||||
val currentSeason: String = "",
|
||||
val defaultSeason: String = "",
|
||||
)
|
||||
data class SpielDto(
|
||||
@param:Json(name = "Termin") val termin: String = "",
|
||||
@param:Json(name = "HeimMannschaft") val heimMannschaft: String = "",
|
||||
@@ -584,6 +590,9 @@ interface ApiService {
|
||||
@GET("/api/mannschaften")
|
||||
suspend fun mannschaften(@Query("season") season: String? = null): Response<ResponseBody>
|
||||
|
||||
@GET("/api/mannschaften/seasons")
|
||||
suspend fun mannschaftenSeasons(): Response<MannschaftenSeasonsResponse>
|
||||
|
||||
@GET("/api/config")
|
||||
suspend fun config(): Response<ConfigResponse>
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package de.harheimertc.repositories
|
||||
|
||||
import de.harheimertc.data.ApiService
|
||||
import de.harheimertc.data.MannschaftenSeasonsResponse
|
||||
import de.harheimertc.data.SeasonDto
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@@ -29,6 +31,12 @@ class MannschaftenRepository @Inject constructor(private val api: ApiService) {
|
||||
parseCsv(response.body()?.string().orEmpty())
|
||||
}
|
||||
|
||||
suspend fun fetchSeasons(): Result<MannschaftenSeasonsResponse> = runCatching {
|
||||
val response = api.mannschaftenSeasons()
|
||||
if (!response.isSuccessful) error("Saisons konnten nicht geladen werden.")
|
||||
response.body() ?: error("Saisons konnten nicht geladen werden.")
|
||||
}
|
||||
|
||||
private fun parseCsv(csv: String): List<Mannschaft> = csv.lineSequence()
|
||||
.filter(String::isNotBlank)
|
||||
.drop(1)
|
||||
|
||||
@@ -18,7 +18,10 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -27,6 +30,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -34,6 +38,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavController
|
||||
@@ -43,6 +48,7 @@ import de.harheimertc.repositories.Mannschaft
|
||||
import de.harheimertc.ui.navigation.Destinations
|
||||
import de.harheimertc.ui.theme.Accent100
|
||||
import de.harheimertc.ui.theme.Accent500
|
||||
import de.harheimertc.ui.theme.Accent700
|
||||
import de.harheimertc.ui.theme.Accent900
|
||||
import de.harheimertc.ui.theme.Primary100
|
||||
import de.harheimertc.ui.theme.Primary600
|
||||
@@ -64,8 +70,17 @@ fun MannschaftenScreen(
|
||||
BackLink(navController, showBackNavigation)
|
||||
Text("Unsere Mannschaften", style = MaterialTheme.typography.displayLarge, color = Accent900, modifier = Modifier.padding(top = 18.dp))
|
||||
Text("Unsere aktiven Mannschaften in der aktuellen Saison", color = Accent500, modifier = Modifier.padding(top = 8.dp))
|
||||
if (state.seasons.isNotEmpty()) {
|
||||
SeasonSelector(
|
||||
seasons = state.seasons,
|
||||
selectedSeason = state.selectedSeason,
|
||||
onSeasonSelected = viewModel::selectSeason,
|
||||
modifier = Modifier.padding(top = 14.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
when {
|
||||
state.seasonsLoading -> item { Loading() }
|
||||
state.loading -> item { Loading() }
|
||||
state.error != null -> item { ErrorPanel(state.error.orEmpty(), viewModel::load) }
|
||||
state.teams.isEmpty() -> item { Text("Keine Mannschaftsdaten geladen", color = Accent500) }
|
||||
@@ -88,6 +103,38 @@ fun MannschaftenScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SeasonSelector(
|
||||
seasons: List<de.harheimertc.data.SeasonDto>,
|
||||
selectedSeason: String,
|
||||
onSeasonSelected: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var open by remember { mutableStateOf(false) }
|
||||
val selectedLabel = seasons.firstOrNull { it.slug == selectedSeason }?.label ?: selectedSeason
|
||||
|
||||
Column(modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("Saison", color = Accent700, style = MaterialTheme.typography.labelSmall)
|
||||
BoxWithConstraints {
|
||||
OutlinedButton(onClick = { open = true }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(selectedLabel.ifBlank { "-" }, modifier = Modifier.weight(1f), textAlign = TextAlign.Start)
|
||||
Text("v")
|
||||
}
|
||||
DropdownMenu(expanded = open, onDismissRequest = { open = false }) {
|
||||
seasons.forEach { season ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(season.label.ifBlank { season.slug }) },
|
||||
onClick = {
|
||||
open = false
|
||||
onSeasonSelected(season.slug)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TeamCard(team: Mannschaft, onOpen: () -> Unit) {
|
||||
Surface(
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import de.harheimertc.data.SpielDto
|
||||
import de.harheimertc.data.LeagueTableRowDto
|
||||
import de.harheimertc.data.SeasonDto
|
||||
import de.harheimertc.repositories.Mannschaft
|
||||
import de.harheimertc.repositories.MannschaftenRepository
|
||||
import de.harheimertc.repositories.SpielplanRepository
|
||||
@@ -17,6 +18,9 @@ data class MannschaftenUiState(
|
||||
val loading: Boolean = true,
|
||||
val error: String? = null,
|
||||
val teams: List<Mannschaft> = emptyList(),
|
||||
val seasons: List<SeasonDto> = emptyList(),
|
||||
val selectedSeason: String = "",
|
||||
val seasonsLoading: Boolean = false,
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
@@ -27,17 +31,69 @@ class MannschaftenViewModel @Inject constructor(
|
||||
val state: StateFlow<MannschaftenUiState> = _state
|
||||
|
||||
init {
|
||||
load()
|
||||
loadSeasonsAndMannschaften()
|
||||
}
|
||||
|
||||
fun load() {
|
||||
viewModelScope.launch {
|
||||
_state.value = MannschaftenUiState(loading = true)
|
||||
repository.fetchMannschaften()
|
||||
.onSuccess { _state.value = MannschaftenUiState(loading = false, teams = it) }
|
||||
.onFailure { _state.value = MannschaftenUiState(loading = false, error = "Mannschaften konnten nicht geladen werden.") }
|
||||
val season = _state.value.selectedSeason.ifBlank { null }
|
||||
_state.value = _state.value.copy(loading = true, error = null)
|
||||
repository.fetchMannschaften(season)
|
||||
.onSuccess { teams -> _state.value = _state.value.copy(loading = false, teams = teams) }
|
||||
.onFailure { _state.value = _state.value.copy(loading = false, error = "Mannschaften konnten nicht geladen werden.") }
|
||||
}
|
||||
}
|
||||
|
||||
fun selectSeason(season: String) {
|
||||
if (season == _state.value.selectedSeason) return
|
||||
_state.value = _state.value.copy(selectedSeason = season)
|
||||
load()
|
||||
}
|
||||
|
||||
private fun loadSeasonsAndMannschaften() {
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(seasonsLoading = true, error = null)
|
||||
repository.fetchSeasons()
|
||||
.onSuccess { response ->
|
||||
val currentSeason = getCurrentSeasonSlug()
|
||||
val seasons = response.seasons.ifEmpty { listOf(SeasonDto(slug = response.currentSeason.ifBlank { currentSeason }, label = formatSeasonLabel(response.currentSeason.ifBlank { currentSeason }))) }
|
||||
val selectedSeason = when {
|
||||
response.defaultSeason.isNotBlank() -> response.defaultSeason
|
||||
seasons.any { it.slug == currentSeason } -> currentSeason
|
||||
seasons.isNotEmpty() -> seasons.first().slug
|
||||
else -> currentSeason
|
||||
}
|
||||
_state.value = _state.value.copy(
|
||||
seasonsLoading = false,
|
||||
seasons = seasons,
|
||||
selectedSeason = selectedSeason,
|
||||
)
|
||||
load()
|
||||
}
|
||||
.onFailure {
|
||||
val currentSeason = getCurrentSeasonSlug()
|
||||
_state.value = _state.value.copy(
|
||||
seasonsLoading = false,
|
||||
seasons = listOf(SeasonDto(slug = currentSeason, label = formatSeasonLabel(currentSeason))),
|
||||
selectedSeason = currentSeason,
|
||||
)
|
||||
load()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCurrentSeasonSlug(): String {
|
||||
val now = java.util.Calendar.getInstance()
|
||||
val year = now.get(java.util.Calendar.YEAR)
|
||||
val startYear = if (now.get(java.util.Calendar.MONTH) >= 6) year else year - 1
|
||||
val endYear = startYear + 1
|
||||
return "%02d--%02d".format(startYear % 100, endYear % 100)
|
||||
}
|
||||
|
||||
private fun formatSeasonLabel(seasonSlug: String): String {
|
||||
val match = Regex("^(\\d{2})--(\\d{2})$").matchEntire(seasonSlug.trim()) ?: return seasonSlug
|
||||
return "20${match.groupValues[1]}/${match.groupValues[2]}"
|
||||
}
|
||||
}
|
||||
|
||||
data class MannschaftDetailUiState(
|
||||
|
||||
Reference in New Issue
Block a user