refactor(settings): type-safe Settings-Keys mit SettingsKey sealed class
Neue SettingsKey<T> sealed class mit typisierten StringKey-Objekten, Default-Werten und Compile-Time-Checks auf gueltige Keys. Alte String- basierte API bleibt fuer Import/Export-Filter (SENSITIVE_KEYS) erhalten. SettingsRepository: Neue typisierte Methoden getString(), getStringOrNull(), setString(), observeString() mit SettingsKey.StringKey-Parameter. Alle Caller migriert: - SettingsViewModel: KEY_*-Companion entfernt, StringKey.* direkt genutzt - CameraViewModel, DashboardViewModel, InventoryPickerViewModel: analog - SyncServiceImpl: KEY_*-Companion entfernt, getString()/setString() - ItemRepositoryImpl, MessageRepositoryImpl: getString()/getStringOrNull() Tests: Alle FakeSettingsRepository-Klassen um typisierte Methoden erweitert, Mock-Calls in CameraViewModelTest und SyncServiceImplTest angepasst, 5 neue Tests fuer typisierte API in SettingsRepositoryImplTest. 72 Tests gruen. Closes #82
This commit is contained in:
parent
d81acfbb4f
commit
ec41a64b5e
19 changed files with 288 additions and 132 deletions
|
|
@ -7,7 +7,7 @@ import de.krisenvorrat.app.data.db.entity.PendingSyncOpEntity
|
|||
import de.krisenvorrat.app.data.sync.WebSocketClient
|
||||
import de.krisenvorrat.app.data.sync.WebSocketEvent
|
||||
import de.krisenvorrat.app.di.ApplicationScope
|
||||
import de.krisenvorrat.app.domain.model.SettingsKeys
|
||||
import de.krisenvorrat.app.domain.model.SettingsKey.StringKey
|
||||
import de.krisenvorrat.app.domain.model.SyncError
|
||||
import de.krisenvorrat.app.domain.repository.ItemRepository
|
||||
import de.krisenvorrat.app.domain.repository.SettingsRepository
|
||||
|
|
@ -90,8 +90,8 @@ internal class ItemRepositoryImpl @Inject constructor(
|
|||
withContext(Dispatchers.IO) { dao.getLastUsedLocationId() }
|
||||
|
||||
private suspend fun attemptPatch(itemId: String, item: ItemDto) {
|
||||
val token = settingsRepository.getValue(SettingsKeys.AUTH_ACCESS_TOKEN)
|
||||
if (token.isNullOrBlank()) return
|
||||
val token = settingsRepository.getString(StringKey.AuthAccessToken)
|
||||
if (token.isBlank()) return
|
||||
val result = syncService.patchItem(itemId, item)
|
||||
val error = result.exceptionOrNull()
|
||||
if (error is SyncError.Timeout || error is SyncError.ConnectionError || error is SyncError.Unknown) {
|
||||
|
|
@ -108,8 +108,8 @@ internal class ItemRepositoryImpl @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun attemptDelete(itemId: String) {
|
||||
val token = settingsRepository.getValue(SettingsKeys.AUTH_ACCESS_TOKEN)
|
||||
if (token.isNullOrBlank()) return
|
||||
val token = settingsRepository.getString(StringKey.AuthAccessToken)
|
||||
if (token.isBlank()) return
|
||||
val result = syncService.deleteItem(itemId)
|
||||
val error = result.exceptionOrNull()
|
||||
if (error is SyncError.Timeout || error is SyncError.ConnectionError || error is SyncError.Unknown) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import de.krisenvorrat.app.data.db.entity.MessageEntity
|
|||
import de.krisenvorrat.app.data.sync.WebSocketClient
|
||||
import de.krisenvorrat.app.data.sync.WebSocketEvent
|
||||
import de.krisenvorrat.app.di.ApplicationScope
|
||||
import de.krisenvorrat.app.domain.model.SettingsKeys
|
||||
import de.krisenvorrat.app.domain.model.SettingsKey.StringKey
|
||||
import de.krisenvorrat.app.domain.model.SyncError
|
||||
import de.krisenvorrat.app.domain.repository.MessageRepository
|
||||
import de.krisenvorrat.app.domain.repository.SettingsRepository
|
||||
|
|
@ -77,8 +77,8 @@ internal class MessageRepositoryImpl @Inject constructor(
|
|||
dao.getConversation(myId, otherId)
|
||||
|
||||
override suspend fun sendMessage(recipientId: String, body: String) {
|
||||
val myId = settingsRepository.getValue(SettingsKeys.AUTH_USER_ID) ?: return
|
||||
val myUsername = settingsRepository.getValue(SettingsKeys.AUTH_USERNAME) ?: ""
|
||||
val myId = settingsRepository.getStringOrNull(StringKey.AuthUserId) ?: return
|
||||
val myUsername = settingsRepository.getString(StringKey.AuthUsername)
|
||||
val localId = UUID.randomUUID().toString()
|
||||
val sentAt = System.currentTimeMillis()
|
||||
|
||||
|
|
@ -102,9 +102,9 @@ internal class MessageRepositoryImpl @Inject constructor(
|
|||
|
||||
override suspend fun fetchUsers(): Result<List<UserListItemDto>> =
|
||||
withContext(Dispatchers.IO) {
|
||||
val serverUrl = settingsRepository.getValue(SettingsKeys.SERVER_URL)
|
||||
val serverUrl = settingsRepository.getStringOrNull(StringKey.ServerUrl)
|
||||
?: return@withContext Result.failure(SyncError.NotConfigured("Server-URL nicht gesetzt"))
|
||||
val token = settingsRepository.getValue(SettingsKeys.AUTH_ACCESS_TOKEN)
|
||||
val token = settingsRepository.getStringOrNull(StringKey.AuthAccessToken)
|
||||
?: return@withContext Result.failure(SyncError.NotConfigured("Nicht angemeldet"))
|
||||
try {
|
||||
val response = httpClient.get("${serverUrl.trimEnd('/')}/api/users") {
|
||||
|
|
@ -125,7 +125,7 @@ internal class MessageRepositoryImpl @Inject constructor(
|
|||
}
|
||||
|
||||
override suspend fun getMyUserId(): String? =
|
||||
settingsRepository.getValue(SettingsKeys.AUTH_USER_ID)
|
||||
settingsRepository.getStringOrNull(StringKey.AuthUserId)
|
||||
|
||||
override suspend fun drainPendingMessages() {
|
||||
val pending = withContext(Dispatchers.IO) { dao.getPendingMessages() }
|
||||
|
|
@ -143,9 +143,9 @@ internal class MessageRepositoryImpl @Inject constructor(
|
|||
body: String,
|
||||
sentAt: Long
|
||||
): Result<Unit> = withContext(Dispatchers.IO) {
|
||||
val serverUrl = settingsRepository.getValue(SettingsKeys.SERVER_URL)
|
||||
val serverUrl = settingsRepository.getStringOrNull(StringKey.ServerUrl)
|
||||
?: return@withContext Result.failure(SyncError.NotConfigured("Server-URL nicht gesetzt"))
|
||||
val token = settingsRepository.getValue(SettingsKeys.AUTH_ACCESS_TOKEN)
|
||||
val token = settingsRepository.getStringOrNull(StringKey.AuthAccessToken)
|
||||
?: return@withContext Result.failure(SyncError.NotConfigured("Nicht angemeldet"))
|
||||
try {
|
||||
val response = httpClient.post("${serverUrl.trimEnd('/')}/api/messages") {
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@ package de.krisenvorrat.app.data.repository
|
|||
import de.krisenvorrat.app.data.db.dao.SettingsDao
|
||||
import de.krisenvorrat.app.data.db.entity.SettingsEntity
|
||||
import de.krisenvorrat.app.data.security.SecureTokenStorage
|
||||
import de.krisenvorrat.app.domain.model.SettingsKey
|
||||
import de.krisenvorrat.app.domain.model.SettingsKeys
|
||||
import de.krisenvorrat.app.domain.repository.SettingsRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -38,4 +40,16 @@ internal class SettingsRepositoryImpl @Inject constructor(
|
|||
override fun observeValue(key: String): Flow<String?> = dao.observeValue(key)
|
||||
|
||||
override fun getAll(): Flow<List<SettingsEntity>> = dao.getAll()
|
||||
|
||||
override suspend fun getString(key: SettingsKey.StringKey): String =
|
||||
getValue(key.key) ?: key.defaultValue
|
||||
|
||||
override suspend fun getStringOrNull(key: SettingsKey.StringKey): String? =
|
||||
getValue(key.key)
|
||||
|
||||
override suspend fun setString(key: SettingsKey.StringKey, value: String) =
|
||||
setValue(key.key, value)
|
||||
|
||||
override fun observeString(key: SettingsKey.StringKey): Flow<String> =
|
||||
observeValue(key.key).map { it ?: key.defaultValue }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package de.krisenvorrat.app.data.sync
|
||||
|
||||
import de.krisenvorrat.app.domain.model.SettingsKeys
|
||||
import de.krisenvorrat.app.domain.model.SettingsKey.StringKey
|
||||
import de.krisenvorrat.app.domain.model.SyncError
|
||||
import de.krisenvorrat.app.domain.repository.SettingsRepository
|
||||
import de.krisenvorrat.app.domain.repository.SyncService
|
||||
|
|
@ -85,16 +85,16 @@ internal class SyncServiceImpl @Inject constructor(
|
|||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
val loginResponse: LoginResponse = response.body()
|
||||
settingsRepository.setValue(KEY_SERVER_URL, serverUrl)
|
||||
settingsRepository.setValue(KEY_ACCESS_TOKEN, loginResponse.accessToken)
|
||||
settingsRepository.setValue(KEY_REFRESH_TOKEN, loginResponse.refreshToken)
|
||||
settingsRepository.setValue(KEY_AUTH_USERNAME, username)
|
||||
settingsRepository.setValue(KEY_AUTH_USER_ID, loginResponse.userId)
|
||||
settingsRepository.setString(StringKey.ServerUrl, serverUrl)
|
||||
settingsRepository.setString(StringKey.AuthAccessToken, loginResponse.accessToken)
|
||||
settingsRepository.setString(StringKey.AuthRefreshToken, loginResponse.refreshToken)
|
||||
settingsRepository.setString(StringKey.AuthUsername, username)
|
||||
settingsRepository.setString(StringKey.AuthUserId, loginResponse.userId)
|
||||
loginResponse.inventoryId?.let {
|
||||
settingsRepository.setValue(KEY_ACTIVE_INVENTORY_ID, it)
|
||||
settingsRepository.setString(StringKey.ActiveInventoryId, it)
|
||||
}
|
||||
loginResponse.inventoryName?.let {
|
||||
settingsRepository.setValue(KEY_ACTIVE_INVENTORY_NAME, it)
|
||||
settingsRepository.setString(StringKey.ActiveInventoryName, it)
|
||||
}
|
||||
Result.success(Unit)
|
||||
}
|
||||
|
|
@ -113,20 +113,20 @@ internal class SyncServiceImpl @Inject constructor(
|
|||
}
|
||||
|
||||
override suspend fun logout() {
|
||||
settingsRepository.setValue(KEY_ACCESS_TOKEN, "")
|
||||
settingsRepository.setValue(KEY_REFRESH_TOKEN, "")
|
||||
settingsRepository.setValue(KEY_AUTH_USERNAME, "")
|
||||
settingsRepository.setString(StringKey.AuthAccessToken, "")
|
||||
settingsRepository.setString(StringKey.AuthRefreshToken, "")
|
||||
settingsRepository.setString(StringKey.AuthUsername, "")
|
||||
}
|
||||
|
||||
private suspend fun executeRequest(
|
||||
block: suspend (serverUrl: String, token: String) -> Result<InventoryDto>
|
||||
): Result<InventoryDto> = withContext(Dispatchers.IO) {
|
||||
val serverUrl = settingsRepository.getValue(KEY_SERVER_URL)
|
||||
if (serverUrl.isNullOrBlank()) {
|
||||
val serverUrl = settingsRepository.getString(StringKey.ServerUrl)
|
||||
if (serverUrl.isBlank()) {
|
||||
return@withContext Result.failure(SyncError.NotConfigured("Server-URL nicht gesetzt"))
|
||||
}
|
||||
val token = settingsRepository.getValue(KEY_ACCESS_TOKEN)
|
||||
if (token.isNullOrBlank()) {
|
||||
val token = settingsRepository.getString(StringKey.AuthAccessToken)
|
||||
if (token.isBlank()) {
|
||||
return@withContext Result.failure(SyncError.NotConfigured("Nicht angemeldet"))
|
||||
}
|
||||
try {
|
||||
|
|
@ -135,8 +135,8 @@ internal class SyncServiceImpl @Inject constructor(
|
|||
if (result.exceptionOrNull() is SyncError.AuthError) {
|
||||
val refreshed = refreshToken(serverUrl.trimEnd('/'))
|
||||
if (refreshed) {
|
||||
val newToken = settingsRepository.getValue(KEY_ACCESS_TOKEN)
|
||||
?: return@withContext result
|
||||
val newToken = settingsRepository.getString(StringKey.AuthAccessToken)
|
||||
if (newToken.isBlank()) return@withContext result
|
||||
block(serverUrl.trimEnd('/'), newToken)
|
||||
} else {
|
||||
result
|
||||
|
|
@ -156,12 +156,12 @@ internal class SyncServiceImpl @Inject constructor(
|
|||
private suspend fun executeItemRequest(
|
||||
block: suspend (serverUrl: String, token: String) -> Result<Unit>
|
||||
): Result<Unit> = withContext(Dispatchers.IO) {
|
||||
val serverUrl = settingsRepository.getValue(KEY_SERVER_URL)
|
||||
if (serverUrl.isNullOrBlank()) {
|
||||
val serverUrl = settingsRepository.getString(StringKey.ServerUrl)
|
||||
if (serverUrl.isBlank()) {
|
||||
return@withContext Result.failure(SyncError.NotConfigured("Server-URL nicht gesetzt"))
|
||||
}
|
||||
val token = settingsRepository.getValue(KEY_ACCESS_TOKEN)
|
||||
if (token.isNullOrBlank()) {
|
||||
val token = settingsRepository.getString(StringKey.AuthAccessToken)
|
||||
if (token.isBlank()) {
|
||||
return@withContext Result.failure(SyncError.NotConfigured("Nicht angemeldet"))
|
||||
}
|
||||
try {
|
||||
|
|
@ -176,8 +176,8 @@ internal class SyncServiceImpl @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun refreshToken(serverUrl: String): Boolean {
|
||||
val refreshToken = settingsRepository.getValue(KEY_REFRESH_TOKEN)
|
||||
if (refreshToken.isNullOrBlank()) return false
|
||||
val refreshToken = settingsRepository.getString(StringKey.AuthRefreshToken)
|
||||
if (refreshToken.isBlank()) return false
|
||||
return try {
|
||||
val response = httpClient.post("$serverUrl/api/auth/refresh") {
|
||||
contentType(ContentType.Application.Json)
|
||||
|
|
@ -185,8 +185,8 @@ internal class SyncServiceImpl @Inject constructor(
|
|||
}
|
||||
if (response.status == HttpStatusCode.OK) {
|
||||
val loginResponse: LoginResponse = response.body()
|
||||
settingsRepository.setValue(KEY_ACCESS_TOKEN, loginResponse.accessToken)
|
||||
settingsRepository.setValue(KEY_REFRESH_TOKEN, loginResponse.refreshToken)
|
||||
settingsRepository.setString(StringKey.AuthAccessToken, loginResponse.accessToken)
|
||||
settingsRepository.setString(StringKey.AuthRefreshToken, loginResponse.refreshToken)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
|
|
@ -270,12 +270,12 @@ internal class SyncServiceImpl @Inject constructor(
|
|||
private suspend fun executeInventoryInfoRequest(
|
||||
block: suspend (serverUrl: String, token: String) -> Result<InventoryInfoDto>
|
||||
): Result<InventoryInfoDto> = withContext(Dispatchers.IO) {
|
||||
val serverUrl = settingsRepository.getValue(KEY_SERVER_URL)
|
||||
if (serverUrl.isNullOrBlank()) {
|
||||
val serverUrl = settingsRepository.getString(StringKey.ServerUrl)
|
||||
if (serverUrl.isBlank()) {
|
||||
return@withContext Result.failure(SyncError.NotConfigured("Server-URL nicht gesetzt"))
|
||||
}
|
||||
val token = settingsRepository.getValue(KEY_ACCESS_TOKEN)
|
||||
if (token.isNullOrBlank()) {
|
||||
val token = settingsRepository.getString(StringKey.AuthAccessToken)
|
||||
if (token.isBlank()) {
|
||||
return@withContext Result.failure(SyncError.NotConfigured("Nicht angemeldet"))
|
||||
}
|
||||
try {
|
||||
|
|
@ -292,12 +292,12 @@ internal class SyncServiceImpl @Inject constructor(
|
|||
private suspend fun executeInventoryInfoListRequest(
|
||||
block: suspend (serverUrl: String, token: String) -> Result<List<InventoryInfoDto>>
|
||||
): Result<List<InventoryInfoDto>> = withContext(Dispatchers.IO) {
|
||||
val serverUrl = settingsRepository.getValue(KEY_SERVER_URL)
|
||||
if (serverUrl.isNullOrBlank()) {
|
||||
val serverUrl = settingsRepository.getString(StringKey.ServerUrl)
|
||||
if (serverUrl.isBlank()) {
|
||||
return@withContext Result.failure(SyncError.NotConfigured("Server-URL nicht gesetzt"))
|
||||
}
|
||||
val token = settingsRepository.getValue(KEY_ACCESS_TOKEN)
|
||||
if (token.isNullOrBlank()) {
|
||||
val token = settingsRepository.getString(StringKey.AuthAccessToken)
|
||||
if (token.isBlank()) {
|
||||
return@withContext Result.failure(SyncError.NotConfigured("Nicht angemeldet"))
|
||||
}
|
||||
try {
|
||||
|
|
@ -310,14 +310,4 @@ internal class SyncServiceImpl @Inject constructor(
|
|||
Result.failure(SyncError.Unknown(e))
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val KEY_SERVER_URL = SettingsKeys.SERVER_URL
|
||||
val KEY_ACCESS_TOKEN = SettingsKeys.AUTH_ACCESS_TOKEN
|
||||
val KEY_REFRESH_TOKEN = SettingsKeys.AUTH_REFRESH_TOKEN
|
||||
val KEY_AUTH_USERNAME = SettingsKeys.AUTH_USERNAME
|
||||
val KEY_AUTH_USER_ID = SettingsKeys.AUTH_USER_ID
|
||||
val KEY_ACTIVE_INVENTORY_ID = SettingsKeys.ACTIVE_INVENTORY_ID
|
||||
val KEY_ACTIVE_INVENTORY_NAME = SettingsKeys.ACTIVE_INVENTORY_NAME
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
package de.krisenvorrat.app.domain.model
|
||||
|
||||
/**
|
||||
* Type-safe Settings-Key-Definition.
|
||||
* Jeder Key hat einen festen DB-Schlüssel, einen Default-Wert und einen Typ.
|
||||
* Compile-Time-Fehler bei ungültigen Keys sind garantiert.
|
||||
*/
|
||||
internal sealed class SettingsKey<T>(val key: String, val defaultValue: T) {
|
||||
|
||||
sealed class StringKey(key: String, defaultValue: String = "") : SettingsKey<String>(key, defaultValue) {
|
||||
data object HouseholdSize : StringKey("household_size")
|
||||
data object DailyKcalPerPerson : StringKey("daily_kcal_per_person")
|
||||
data object AgeGroups : StringKey("age_groups")
|
||||
data object ServerUrl : StringKey("server_url")
|
||||
data object AuthAccessToken : StringKey("auth_access_token")
|
||||
data object AuthRefreshToken : StringKey("auth_refresh_token")
|
||||
data object AuthUsername : StringKey("auth_username")
|
||||
data object AuthUserId : StringKey("auth_user_id")
|
||||
data object SyncLastTimestamp : StringKey("sync_last_timestamp")
|
||||
data object OpenAiApiKey : StringKey("openai_api_key")
|
||||
data object ActiveInventoryId : StringKey("active_inventory_id")
|
||||
data object ActiveInventoryName : StringKey("active_inventory_name")
|
||||
}
|
||||
|
||||
companion object {
|
||||
val SENSITIVE_KEYS: Set<StringKey> = setOf(
|
||||
StringKey.AuthAccessToken,
|
||||
StringKey.AuthRefreshToken,
|
||||
StringKey.AuthUsername,
|
||||
StringKey.AuthUserId,
|
||||
StringKey.OpenAiApiKey
|
||||
)
|
||||
|
||||
val SENSITIVE_KEY_STRINGS: Set<String> = SENSITIVE_KEYS.map { it.key }.toSet()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,10 @@
|
|||
package de.krisenvorrat.app.domain.model
|
||||
|
||||
/**
|
||||
* Kompatibilitätsschicht: leitet alle Konstanten an [SettingsKey] weiter.
|
||||
* Bestehender Code, der nur den String-Key braucht (Import/Export-Filter),
|
||||
* kann weiterhin [SettingsKeys.SENSITIVE_KEYS] nutzen.
|
||||
*/
|
||||
internal object SettingsKeys {
|
||||
const val HOUSEHOLD_SIZE = "household_size"
|
||||
const val DAILY_KCAL_PER_PERSON = "daily_kcal_per_person"
|
||||
|
|
@ -14,11 +19,5 @@ internal object SettingsKeys {
|
|||
const val ACTIVE_INVENTORY_ID = "active_inventory_id"
|
||||
const val ACTIVE_INVENTORY_NAME = "active_inventory_name"
|
||||
|
||||
val SENSITIVE_KEYS: Set<String> = setOf(
|
||||
AUTH_ACCESS_TOKEN,
|
||||
AUTH_REFRESH_TOKEN,
|
||||
AUTH_USERNAME,
|
||||
AUTH_USER_ID,
|
||||
OPENAI_API_KEY
|
||||
)
|
||||
val SENSITIVE_KEYS: Set<String> = SettingsKey.SENSITIVE_KEY_STRINGS
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package de.krisenvorrat.app.domain.repository
|
||||
|
||||
import de.krisenvorrat.app.data.db.entity.SettingsEntity
|
||||
import de.krisenvorrat.app.domain.model.SettingsKey
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal interface SettingsRepository {
|
||||
|
|
@ -8,4 +9,9 @@ internal interface SettingsRepository {
|
|||
suspend fun setValue(key: String, value: String)
|
||||
fun observeValue(key: String): Flow<String?>
|
||||
fun getAll(): Flow<List<SettingsEntity>>
|
||||
|
||||
suspend fun getString(key: SettingsKey.StringKey): String
|
||||
suspend fun getStringOrNull(key: SettingsKey.StringKey): String?
|
||||
suspend fun setString(key: SettingsKey.StringKey, value: String)
|
||||
fun observeString(key: SettingsKey.StringKey): Flow<String>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||
import de.krisenvorrat.app.data.remote.OpenAiVisionService
|
||||
import de.krisenvorrat.app.di.IoDispatcher
|
||||
import de.krisenvorrat.app.domain.model.ItemFormPrefill
|
||||
import de.krisenvorrat.app.domain.model.SettingsKeys
|
||||
import de.krisenvorrat.app.domain.model.SettingsKey.StringKey
|
||||
import de.krisenvorrat.app.domain.repository.SettingsRepository
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -42,7 +42,7 @@ internal class CameraViewModel @Inject constructor(
|
|||
|
||||
private fun checkApiKey() {
|
||||
viewModelScope.launch {
|
||||
val apiKey = settingsRepository.getValue(SettingsKeys.OPENAI_API_KEY) ?: ""
|
||||
val apiKey = settingsRepository.getString(StringKey.OpenAiApiKey)
|
||||
_uiState.update { it.copy(hasApiKey = apiKey.isNotBlank()) }
|
||||
}
|
||||
}
|
||||
|
|
@ -78,7 +78,7 @@ internal class CameraViewModel @Inject constructor(
|
|||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(isAnalyzing = true, errorMessage = null) }
|
||||
try {
|
||||
val apiKey = settingsRepository.getValue(SettingsKeys.OPENAI_API_KEY) ?: ""
|
||||
val apiKey = settingsRepository.getString(StringKey.OpenAiApiKey)
|
||||
if (apiKey.isBlank()) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import de.krisenvorrat.app.domain.model.parseAgeGroupsFromJson
|
||||
import de.krisenvorrat.app.domain.model.SettingsKeys
|
||||
import de.krisenvorrat.app.domain.model.SettingsKey.StringKey
|
||||
import de.krisenvorrat.app.domain.model.totalDailyKcal
|
||||
import de.krisenvorrat.app.domain.repository.ItemRepository
|
||||
import de.krisenvorrat.app.domain.repository.SettingsRepository
|
||||
|
|
@ -40,7 +40,7 @@ internal class DashboardViewModel @Inject constructor(
|
|||
combine(
|
||||
itemRepository.getAll(),
|
||||
categoryRepository.getAll(),
|
||||
settingsRepository.observeValue(SettingsKeys.AGE_GROUPS)
|
||||
settingsRepository.observeString(StringKey.AgeGroups)
|
||||
) { items, categories, ageGroupsJson ->
|
||||
val ageGroups = parseAgeGroupsFromJson(ageGroupsJson)
|
||||
val totalDailyKcal = ageGroups.totalDailyKcal().let {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package de.krisenvorrat.app.ui.inventory
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import de.krisenvorrat.app.domain.model.SettingsKeys
|
||||
import de.krisenvorrat.app.domain.model.SettingsKey.StringKey
|
||||
import de.krisenvorrat.app.domain.repository.ImportExportRepository
|
||||
import de.krisenvorrat.app.domain.repository.SettingsRepository
|
||||
import de.krisenvorrat.app.domain.repository.SyncService
|
||||
|
|
@ -30,7 +30,7 @@ internal class InventoryPickerViewModel @Inject constructor(
|
|||
|
||||
private fun loadActiveInventoryName() {
|
||||
viewModelScope.launch {
|
||||
val name = settingsRepository.getValue(SettingsKeys.ACTIVE_INVENTORY_NAME) ?: ""
|
||||
val name = settingsRepository.getString(StringKey.ActiveInventoryName)
|
||||
_uiState.update { it.copy(activeInventoryName = name) }
|
||||
}
|
||||
}
|
||||
|
|
@ -70,8 +70,8 @@ internal class InventoryPickerViewModel @Inject constructor(
|
|||
_uiState.update { it.copy(isCreating = true, error = null) }
|
||||
syncService.createInventory(name).fold(
|
||||
onSuccess = { info ->
|
||||
settingsRepository.setValue(SettingsKeys.ACTIVE_INVENTORY_ID, info.id)
|
||||
settingsRepository.setValue(SettingsKeys.ACTIVE_INVENTORY_NAME, info.name)
|
||||
settingsRepository.setString(StringKey.ActiveInventoryId, info.id)
|
||||
settingsRepository.setString(StringKey.ActiveInventoryName, info.name)
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isCreating = false,
|
||||
|
|
@ -99,8 +99,8 @@ internal class InventoryPickerViewModel @Inject constructor(
|
|||
_uiState.update { it.copy(isSwitching = true, error = null) }
|
||||
syncService.switchInventory(inventoryId).fold(
|
||||
onSuccess = { info ->
|
||||
settingsRepository.setValue(SettingsKeys.ACTIVE_INVENTORY_ID, info.id)
|
||||
settingsRepository.setValue(SettingsKeys.ACTIVE_INVENTORY_NAME, info.name)
|
||||
settingsRepository.setString(StringKey.ActiveInventoryId, info.id)
|
||||
settingsRepository.setString(StringKey.ActiveInventoryName, info.name)
|
||||
_uiState.update {
|
||||
it.copy(isSwitching = false, activeInventoryName = info.name)
|
||||
}
|
||||
|
|
@ -121,7 +121,7 @@ internal class InventoryPickerViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun resyncFromServer() {
|
||||
settingsRepository.setValue(SettingsKeys.SYNC_LAST_TIMESTAMP, "")
|
||||
settingsRepository.setString(StringKey.SyncLastTimestamp, "")
|
||||
syncService.downloadInventory().fold(
|
||||
onSuccess = { inventoryDto ->
|
||||
importExportRepository.importFromInventoryDto(inventoryDto)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import de.krisenvorrat.app.data.sync.WebSocketClient
|
|||
import de.krisenvorrat.app.data.sync.WebSocketEvent
|
||||
import de.krisenvorrat.app.domain.model.AgeGroup
|
||||
import de.krisenvorrat.app.domain.model.AgeGroupEntry
|
||||
import de.krisenvorrat.app.domain.model.SettingsKeys
|
||||
import de.krisenvorrat.app.domain.model.SettingsKey.StringKey
|
||||
import de.krisenvorrat.app.domain.model.defaultAgeGroups
|
||||
import de.krisenvorrat.app.domain.model.parseAgeGroupsFromJson
|
||||
import de.krisenvorrat.app.domain.model.toJson
|
||||
|
|
@ -69,12 +69,12 @@ internal class SettingsViewModel @Inject constructor(
|
|||
viewModelScope.launch {
|
||||
try {
|
||||
val ageGroups = loadAgeGroupsWithMigration()
|
||||
val serverUrl = settingsRepository.getValue(KEY_SERVER_URL) ?: ""
|
||||
val authUsername = settingsRepository.getValue(KEY_AUTH_USERNAME) ?: ""
|
||||
val accessToken = settingsRepository.getValue(KEY_ACCESS_TOKEN) ?: ""
|
||||
val serverUrl = settingsRepository.getString(StringKey.ServerUrl)
|
||||
val authUsername = settingsRepository.getString(StringKey.AuthUsername)
|
||||
val accessToken = settingsRepository.getString(StringKey.AuthAccessToken)
|
||||
val isLoggedIn = authUsername.isNotBlank() && accessToken.isNotBlank()
|
||||
val openAiApiKey = settingsRepository.getValue(KEY_OPENAI_API_KEY) ?: ""
|
||||
val lastSyncTimestamp = settingsRepository.getValue(KEY_SYNC_LAST_TIMESTAMP)
|
||||
val openAiApiKey = settingsRepository.getString(StringKey.OpenAiApiKey)
|
||||
val lastSyncTimestamp = settingsRepository.getStringOrNull(StringKey.SyncLastTimestamp)
|
||||
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
|
|
@ -103,13 +103,13 @@ internal class SettingsViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun loadAgeGroupsWithMigration(): List<AgeGroupEntry> {
|
||||
val ageGroupsJsonValue = settingsRepository.getValue(KEY_AGE_GROUPS)
|
||||
val ageGroupsJsonValue = settingsRepository.getStringOrNull(StringKey.AgeGroups)
|
||||
if (ageGroupsJsonValue != null) {
|
||||
return parseAgeGroupsFromJson(ageGroupsJsonValue)
|
||||
}
|
||||
val legacyHouseholdSize = settingsRepository.getValue(KEY_HOUSEHOLD_SIZE)?.toIntOrNull()
|
||||
val legacyHouseholdSize = settingsRepository.getStringOrNull(StringKey.HouseholdSize)?.toIntOrNull()
|
||||
if (legacyHouseholdSize != null) {
|
||||
val legacyKcal = settingsRepository.getValue(KEY_DAILY_KCAL_PER_PERSON)?.toIntOrNull()
|
||||
val legacyKcal = settingsRepository.getStringOrNull(StringKey.DailyKcalPerPerson)?.toIntOrNull()
|
||||
?: AgeGroup.ERWACHSENER.defaultKcal
|
||||
return defaultAgeGroups().map { entry ->
|
||||
if (entry.ageGroup == AgeGroup.ERWACHSENER) {
|
||||
|
|
@ -170,7 +170,7 @@ internal class SettingsViewModel @Inject constructor(
|
|||
_uiState.update { it.copy(isLoggingIn = true, loginError = null) }
|
||||
syncService.login(serverUrl, username, password).fold(
|
||||
onSuccess = {
|
||||
val accessToken = settingsRepository.getValue(KEY_ACCESS_TOKEN) ?: ""
|
||||
val accessToken = settingsRepository.getString(StringKey.AuthAccessToken)
|
||||
webSocketClient.connect(serverUrl, accessToken)
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
|
|
@ -215,9 +215,9 @@ internal class SettingsViewModel @Inject constructor(
|
|||
fun saveSettings() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
settingsRepository.setValue(KEY_AGE_GROUPS, _uiState.value.ageGroups.toJson())
|
||||
settingsRepository.setValue(KEY_SERVER_URL, _uiState.value.serverUrl)
|
||||
settingsRepository.setValue(KEY_OPENAI_API_KEY, _uiState.value.openAiApiKey)
|
||||
settingsRepository.setString(StringKey.AgeGroups, _uiState.value.ageGroups.toJson())
|
||||
settingsRepository.setString(StringKey.ServerUrl, _uiState.value.serverUrl)
|
||||
settingsRepository.setString(StringKey.OpenAiApiKey, _uiState.value.openAiApiKey)
|
||||
|
||||
_uiState.update { it.copy(isSaved = true) }
|
||||
} catch (e: Exception) {
|
||||
|
|
@ -355,7 +355,7 @@ internal class SettingsViewModel @Inject constructor(
|
|||
result.fold(
|
||||
onSuccess = {
|
||||
val now = Instant.now().toEpochMilli().toString()
|
||||
settingsRepository.setValue(KEY_SYNC_LAST_TIMESTAMP, now)
|
||||
settingsRepository.setString(StringKey.SyncLastTimestamp, now)
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
syncStatus = SyncStatus.Success,
|
||||
|
|
@ -381,14 +381,14 @@ internal class SettingsViewModel @Inject constructor(
|
|||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(syncStatus = SyncStatus.Running) }
|
||||
try {
|
||||
val since = if (fullSync) null else settingsRepository.getValue(KEY_SYNC_LAST_TIMESTAMP)?.toLongOrNull()
|
||||
val since = if (fullSync) null else settingsRepository.getStringOrNull(StringKey.SyncLastTimestamp)?.toLongOrNull()
|
||||
val result = syncService.downloadInventory(since)
|
||||
result.fold(
|
||||
onSuccess = { inventoryDto ->
|
||||
importExportRepository.importFromInventoryDto(inventoryDto).fold(
|
||||
onSuccess = {
|
||||
val now = Instant.now().toEpochMilli().toString()
|
||||
settingsRepository.setValue(KEY_SYNC_LAST_TIMESTAMP, now)
|
||||
settingsRepository.setString(StringKey.SyncLastTimestamp, now)
|
||||
loadSettings()
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
|
|
@ -429,15 +429,4 @@ internal class SettingsViewModel @Inject constructor(
|
|||
return formatter.format(instant.atZone(ZoneId.systemDefault()))
|
||||
}
|
||||
|
||||
companion object {
|
||||
val KEY_HOUSEHOLD_SIZE = SettingsKeys.HOUSEHOLD_SIZE
|
||||
val KEY_DAILY_KCAL_PER_PERSON = SettingsKeys.DAILY_KCAL_PER_PERSON
|
||||
val KEY_AGE_GROUPS = SettingsKeys.AGE_GROUPS
|
||||
val KEY_SERVER_URL = SettingsKeys.SERVER_URL
|
||||
val KEY_ACCESS_TOKEN = SettingsKeys.AUTH_ACCESS_TOKEN
|
||||
val KEY_REFRESH_TOKEN = SettingsKeys.AUTH_REFRESH_TOKEN
|
||||
val KEY_AUTH_USERNAME = SettingsKeys.AUTH_USERNAME
|
||||
val KEY_SYNC_LAST_TIMESTAMP = SettingsKeys.SYNC_LAST_TIMESTAMP
|
||||
val KEY_OPENAI_API_KEY = SettingsKeys.OPENAI_API_KEY
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import de.krisenvorrat.app.data.db.entity.PendingSyncOpEntity
|
|||
import de.krisenvorrat.app.data.db.entity.SettingsEntity
|
||||
import de.krisenvorrat.app.data.sync.WebSocketClient
|
||||
import de.krisenvorrat.app.data.sync.WebSocketEvent
|
||||
import de.krisenvorrat.app.domain.model.SettingsKey
|
||||
import de.krisenvorrat.app.domain.model.SettingsKeys
|
||||
import de.krisenvorrat.app.domain.model.SyncError
|
||||
import de.krisenvorrat.app.domain.repository.SettingsRepository
|
||||
|
|
@ -124,6 +125,15 @@ private class FakeSettingsRepository : SettingsRepository {
|
|||
override suspend fun setValue(key: String, value: String) { store[key] = value }
|
||||
override fun observeValue(key: String): Flow<String?> = MutableStateFlow(store[key])
|
||||
override fun getAll(): Flow<List<SettingsEntity>> = MutableStateFlow(emptyList())
|
||||
|
||||
override suspend fun getString(key: SettingsKey.StringKey): String =
|
||||
getValue(key.key) ?: key.defaultValue
|
||||
override suspend fun getStringOrNull(key: SettingsKey.StringKey): String? =
|
||||
getValue(key.key)
|
||||
override suspend fun setString(key: SettingsKey.StringKey, value: String) =
|
||||
setValue(key.key, value)
|
||||
override fun observeString(key: SettingsKey.StringKey): Flow<String> =
|
||||
MutableStateFlow(store[key.key] ?: key.defaultValue)
|
||||
}
|
||||
|
||||
private class FakeSyncService : SyncService {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import de.krisenvorrat.app.data.db.dao.MessageDao
|
|||
import de.krisenvorrat.app.data.db.entity.MessageEntity
|
||||
import de.krisenvorrat.app.data.sync.WebSocketClient
|
||||
import de.krisenvorrat.app.data.sync.WebSocketEvent
|
||||
import de.krisenvorrat.app.domain.model.SettingsKey
|
||||
import de.krisenvorrat.app.domain.model.SettingsKeys
|
||||
import de.krisenvorrat.app.domain.repository.SettingsRepository
|
||||
import de.krisenvorrat.shared.model.MessageDto
|
||||
|
|
@ -64,6 +65,15 @@ private class FakeMessageSettingsRepository : SettingsRepository {
|
|||
override fun observeValue(key: String): Flow<String?> = MutableStateFlow(store[key])
|
||||
override fun getAll(): Flow<List<de.krisenvorrat.app.data.db.entity.SettingsEntity>> =
|
||||
MutableStateFlow(emptyList())
|
||||
|
||||
override suspend fun getString(key: SettingsKey.StringKey): String =
|
||||
getValue(key.key) ?: key.defaultValue
|
||||
override suspend fun getStringOrNull(key: SettingsKey.StringKey): String? =
|
||||
getValue(key.key)
|
||||
override suspend fun setString(key: SettingsKey.StringKey, value: String) =
|
||||
setValue(key.key, value)
|
||||
override fun observeString(key: SettingsKey.StringKey): Flow<String> =
|
||||
MutableStateFlow(store[key.key] ?: key.defaultValue)
|
||||
}
|
||||
|
||||
private class FakeMessageWsClient : WebSocketClient {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package de.krisenvorrat.app.data.repository
|
|||
import de.krisenvorrat.app.data.db.dao.SettingsDao
|
||||
import de.krisenvorrat.app.data.db.entity.SettingsEntity
|
||||
import de.krisenvorrat.app.data.security.SecureTokenStorage
|
||||
import de.krisenvorrat.app.domain.model.SettingsKey
|
||||
import de.krisenvorrat.app.domain.model.SettingsKeys
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -158,4 +159,62 @@ class SettingsRepositoryImplTest {
|
|||
assertNull(fakeDao.getValue(key))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_getString_withDefaultValue_returnsDefault() = runBlocking {
|
||||
// Given – no value set
|
||||
|
||||
// When
|
||||
val result = repository.getString(SettingsKey.StringKey.ServerUrl)
|
||||
|
||||
// Then
|
||||
assertEquals("", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_getString_withStoredValue_returnsStoredValue() = runBlocking {
|
||||
// Given
|
||||
repository.setString(SettingsKey.StringKey.ServerUrl, "https://example.com")
|
||||
|
||||
// When
|
||||
val result = repository.getString(SettingsKey.StringKey.ServerUrl)
|
||||
|
||||
// Then
|
||||
assertEquals("https://example.com", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_getStringOrNull_withNoValue_returnsNull() = runBlocking {
|
||||
// Given – no value set
|
||||
|
||||
// When
|
||||
val result = repository.getStringOrNull(SettingsKey.StringKey.AgeGroups)
|
||||
|
||||
// Then
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_setString_withSensitiveKey_routesToSecureStorage() = runBlocking {
|
||||
// Given
|
||||
val token = "secure-token-value"
|
||||
|
||||
// When
|
||||
repository.setString(SettingsKey.StringKey.AuthAccessToken, token)
|
||||
|
||||
// Then
|
||||
assertEquals(token, repository.getString(SettingsKey.StringKey.AuthAccessToken))
|
||||
assertEquals(token, fakeSecureStorage.get(SettingsKey.StringKey.AuthAccessToken.key))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_observeString_returnsDefaultWhenNotSet() = runBlocking {
|
||||
// Given – no value set
|
||||
|
||||
// When
|
||||
val result = repository.observeString(SettingsKey.StringKey.ServerUrl).first()
|
||||
|
||||
// Then
|
||||
assertEquals("", result)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package de.krisenvorrat.app.data.sync
|
||||
|
||||
import de.krisenvorrat.app.domain.model.SettingsKey.StringKey
|
||||
import de.krisenvorrat.app.domain.model.SyncError
|
||||
import de.krisenvorrat.app.domain.repository.SettingsRepository
|
||||
import de.krisenvorrat.shared.model.CategoryDto
|
||||
|
|
@ -74,9 +75,9 @@ class SyncServiceImplTest {
|
|||
}
|
||||
|
||||
private fun setupSettings(serverUrl: String? = "http://localhost:8080", accessToken: String? = "test-token") {
|
||||
coEvery { settingsRepository.getValue("server_url") } returns serverUrl
|
||||
coEvery { settingsRepository.getValue("auth_access_token") } returns accessToken
|
||||
coEvery { settingsRepository.getValue("auth_refresh_token") } returns null
|
||||
coEvery { settingsRepository.getString(StringKey.ServerUrl) } returns (serverUrl ?: "")
|
||||
coEvery { settingsRepository.getString(StringKey.AuthAccessToken) } returns (accessToken ?: "")
|
||||
coEvery { settingsRepository.getString(StringKey.AuthRefreshToken) } returns ""
|
||||
}
|
||||
|
||||
// --- downloadInventory ---
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import android.content.Context
|
|||
import android.net.Uri
|
||||
import de.krisenvorrat.app.data.remote.OpenAiVisionService
|
||||
import de.krisenvorrat.app.domain.model.ItemFormPrefill
|
||||
import de.krisenvorrat.app.domain.model.SettingsKeys
|
||||
import de.krisenvorrat.app.domain.model.SettingsKey
|
||||
import de.krisenvorrat.app.domain.repository.SettingsRepository
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
|
|
@ -59,7 +59,7 @@ class CameraViewModelTest {
|
|||
@Test
|
||||
fun test_initialState_noPhoto_stateIsEmpty() = runTest(testDispatcher) {
|
||||
// Given
|
||||
coEvery { mockSettingsRepository.getValue(any()) } returns ""
|
||||
coEvery { mockSettingsRepository.getString(any()) } returns ""
|
||||
viewModel = createViewModel()
|
||||
|
||||
// When
|
||||
|
|
@ -78,7 +78,7 @@ class CameraViewModelTest {
|
|||
@Test
|
||||
fun test_analyzePhoto_noApiKey_setsErrorMessage() = runTest(testDispatcher) {
|
||||
// Given
|
||||
coEvery { mockSettingsRepository.getValue(any()) } returns ""
|
||||
coEvery { mockSettingsRepository.getString(any()) } returns ""
|
||||
viewModel = createViewModel()
|
||||
advanceUntilIdle()
|
||||
val mockUri = mockk<Uri>()
|
||||
|
|
@ -98,7 +98,7 @@ class CameraViewModelTest {
|
|||
@Test
|
||||
fun test_toggleItemSelection_selectsItem() = runTest(testDispatcher) {
|
||||
// Given
|
||||
coEvery { mockSettingsRepository.getValue(any()) } returns ""
|
||||
coEvery { mockSettingsRepository.getString(any()) } returns ""
|
||||
viewModel = createViewModel()
|
||||
advanceUntilIdle()
|
||||
assertFalse(viewModel.uiState.value.selectedItems.contains(0))
|
||||
|
|
@ -113,7 +113,7 @@ class CameraViewModelTest {
|
|||
@Test
|
||||
fun test_toggleItemSelection_deselectsItem() = runTest(testDispatcher) {
|
||||
// Given
|
||||
coEvery { mockSettingsRepository.getValue(any()) } returns ""
|
||||
coEvery { mockSettingsRepository.getString(any()) } returns ""
|
||||
viewModel = createViewModel()
|
||||
advanceUntilIdle()
|
||||
viewModel.toggleItemSelection(1)
|
||||
|
|
@ -134,7 +134,7 @@ class CameraViewModelTest {
|
|||
ItemFormPrefill(name = "Wasser"),
|
||||
ItemFormPrefill(name = "Konserven")
|
||||
)
|
||||
coEvery { mockSettingsRepository.getValue(any()) } returns "test-api-key"
|
||||
coEvery { mockSettingsRepository.getString(any()) } returns "test-api-key"
|
||||
coEvery { mockOpenAiVisionService.analyzeImage(any(), any()) } returns items
|
||||
|
||||
val mockUri = mockk<Uri>()
|
||||
|
|
@ -158,7 +158,7 @@ class CameraViewModelTest {
|
|||
fun test_analyzePhoto_apiReturnsItems_updatesRecognizedItems() = runTest(testDispatcher) {
|
||||
// Given
|
||||
val items = listOf(ItemFormPrefill(name = "Brot"), ItemFormPrefill(name = "Wasser"))
|
||||
coEvery { mockSettingsRepository.getValue(any()) } returns "test-api-key"
|
||||
coEvery { mockSettingsRepository.getString(any()) } returns "test-api-key"
|
||||
coEvery { mockOpenAiVisionService.analyzeImage(any(), any()) } returns items
|
||||
every { mockContentResolver.openInputStream(any()) } answers { ByteArrayInputStream(ByteArray(100)) }
|
||||
val mockUri = mockk<Uri>()
|
||||
|
|
@ -180,7 +180,7 @@ class CameraViewModelTest {
|
|||
@Test
|
||||
fun test_analyzePhoto_photoNotReadable_setsError() = runTest(testDispatcher) {
|
||||
// Given – onPhotoCaptured was NOT called, capturedImageUri = null
|
||||
coEvery { mockSettingsRepository.getValue(any()) } returns "test-api-key"
|
||||
coEvery { mockSettingsRepository.getString(any()) } returns "test-api-key"
|
||||
viewModel = createViewModel()
|
||||
advanceUntilIdle()
|
||||
|
||||
|
|
@ -195,7 +195,7 @@ class CameraViewModelTest {
|
|||
@Test
|
||||
fun test_onAddFirstSelectedItem_noSelection_doesNotSetNavigation() = runTest(testDispatcher) {
|
||||
// Given – selectedItems is empty
|
||||
coEvery { mockSettingsRepository.getValue(any()) } returns ""
|
||||
coEvery { mockSettingsRepository.getString(any()) } returns ""
|
||||
viewModel = createViewModel()
|
||||
advanceUntilIdle()
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ import de.krisenvorrat.app.data.db.entity.CategoryEntity
|
|||
import de.krisenvorrat.app.data.db.entity.ItemEntity
|
||||
import de.krisenvorrat.app.data.db.entity.SettingsEntity
|
||||
import de.krisenvorrat.app.domain.model.AgeGroup
|
||||
import de.krisenvorrat.app.domain.model.SettingsKey
|
||||
import de.krisenvorrat.app.domain.model.defaultAgeGroups
|
||||
import de.krisenvorrat.app.domain.model.toJson
|
||||
import de.krisenvorrat.app.domain.repository.CategoryRepository
|
||||
import de.krisenvorrat.app.domain.repository.ItemRepository
|
||||
import de.krisenvorrat.app.domain.repository.SettingsRepository
|
||||
import de.krisenvorrat.app.ui.settings.SettingsViewModel
|
||||
import de.krisenvorrat.app.domain.usecase.CalculateCategorySummaryUseCase
|
||||
import de.krisenvorrat.app.domain.usecase.CalculateSupplyRangeUseCase
|
||||
import de.krisenvorrat.app.domain.usecase.CalculateTotalValueUseCase
|
||||
|
|
@ -18,6 +18,7 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
|
|
@ -143,7 +144,7 @@ class DashboardViewModelTest {
|
|||
else entry
|
||||
}
|
||||
fakeSettingsRepository.setValue(
|
||||
de.krisenvorrat.app.ui.settings.SettingsViewModel.KEY_AGE_GROUPS,
|
||||
SettingsKey.StringKey.AgeGroups.key,
|
||||
ageGroups.toJson()
|
||||
)
|
||||
fakeItemRepository.emit(
|
||||
|
|
@ -167,7 +168,7 @@ class DashboardViewModelTest {
|
|||
// → supplyRangeDays = 4000 / 4000 = 1.0
|
||||
val allZeroAgeGroups = defaultAgeGroups().map { it.copy(count = 0) }
|
||||
fakeSettingsRepository.setValue(
|
||||
de.krisenvorrat.app.ui.settings.SettingsViewModel.KEY_AGE_GROUPS,
|
||||
SettingsKey.StringKey.AgeGroups.key,
|
||||
allZeroAgeGroups.toJson()
|
||||
)
|
||||
fakeItemRepository.emit(
|
||||
|
|
@ -342,6 +343,20 @@ private class FakeSettingsRepository : SettingsRepository {
|
|||
observeFlows.getOrPut(key) { MutableStateFlow(store[key]) }
|
||||
|
||||
override fun getAll(): Flow<List<SettingsEntity>> = allFlow
|
||||
|
||||
override suspend fun getString(key: SettingsKey.StringKey): String =
|
||||
getValue(key.key) ?: key.defaultValue
|
||||
|
||||
override suspend fun getStringOrNull(key: SettingsKey.StringKey): String? =
|
||||
getValue(key.key)
|
||||
|
||||
override suspend fun setString(key: SettingsKey.StringKey, value: String) =
|
||||
setValue(key.key, value)
|
||||
|
||||
override fun observeString(key: SettingsKey.StringKey): Flow<String> {
|
||||
val flow = observeFlows.getOrPut(key.key) { MutableStateFlow(store[key.key]) }
|
||||
return flow.map { it ?: key.defaultValue }
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package de.krisenvorrat.app.ui.inventory
|
||||
|
||||
import de.krisenvorrat.app.data.db.entity.SettingsEntity
|
||||
import de.krisenvorrat.app.domain.model.SettingsKey
|
||||
import de.krisenvorrat.app.domain.model.SettingsKeys
|
||||
import de.krisenvorrat.app.domain.model.SyncError
|
||||
import de.krisenvorrat.app.domain.repository.ImportExportRepository
|
||||
|
|
@ -223,6 +224,18 @@ private class FakeSettingsRepository : SettingsRepository {
|
|||
|
||||
override fun getAll(): Flow<List<SettingsEntity>> =
|
||||
MutableStateFlow(store.map { SettingsEntity(key = it.key, value = it.value) })
|
||||
|
||||
override suspend fun getString(key: SettingsKey.StringKey): String =
|
||||
getValue(key.key) ?: key.defaultValue
|
||||
|
||||
override suspend fun getStringOrNull(key: SettingsKey.StringKey): String? =
|
||||
getValue(key.key)
|
||||
|
||||
override suspend fun setString(key: SettingsKey.StringKey, value: String) =
|
||||
setValue(key.key, value)
|
||||
|
||||
override fun observeString(key: SettingsKey.StringKey): Flow<String> =
|
||||
MutableStateFlow(store[key.key] ?: key.defaultValue)
|
||||
}
|
||||
|
||||
private class FakeSyncService : SyncService {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import android.net.Uri
|
|||
import de.krisenvorrat.app.data.sync.WebSocketClient
|
||||
import de.krisenvorrat.app.data.sync.WebSocketEvent
|
||||
import de.krisenvorrat.app.data.db.entity.SettingsEntity
|
||||
import de.krisenvorrat.app.domain.model.SettingsKey
|
||||
import de.krisenvorrat.app.domain.model.SyncError
|
||||
import de.krisenvorrat.app.domain.repository.ImportExportRepository
|
||||
import de.krisenvorrat.app.domain.repository.SettingsRepository
|
||||
|
|
@ -20,6 +21,7 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
|
|
@ -102,7 +104,7 @@ class SettingsViewModelTest {
|
|||
if (entry.ageGroup == AgeGroup.ERWACHSENER) entry.copy(count = 3, kcalPerDay = 2500)
|
||||
else entry
|
||||
}
|
||||
fakeSettingsRepository.store[SettingsViewModel.KEY_AGE_GROUPS] = stored.toJson()
|
||||
fakeSettingsRepository.store[SettingsKey.StringKey.AgeGroups.key] = stored.toJson()
|
||||
viewModel = createViewModel()
|
||||
|
||||
// When
|
||||
|
|
@ -117,8 +119,8 @@ class SettingsViewModelTest {
|
|||
@Test
|
||||
fun test_init_withLegacyHouseholdSize_migratesErwachsener() = runTest(testDispatcher) {
|
||||
// Given – legacy keys present, no age_groups key
|
||||
fakeSettingsRepository.store[SettingsViewModel.KEY_HOUSEHOLD_SIZE] = "3"
|
||||
fakeSettingsRepository.store[SettingsViewModel.KEY_DAILY_KCAL_PER_PERSON] = "2500"
|
||||
fakeSettingsRepository.store[SettingsKey.StringKey.HouseholdSize.key] = "3"
|
||||
fakeSettingsRepository.store[SettingsKey.StringKey.DailyKcalPerPerson.key] = "2500"
|
||||
viewModel = createViewModel()
|
||||
|
||||
// When
|
||||
|
|
@ -133,7 +135,7 @@ class SettingsViewModelTest {
|
|||
@Test
|
||||
fun test_init_withLegacyHouseholdSizeOnly_migratesWithDefaultKcal() = runTest(testDispatcher) {
|
||||
// Given – nur KEY_HOUSEHOLD_SIZE gesetzt, kein KEY_DAILY_KCAL_PER_PERSON
|
||||
fakeSettingsRepository.store[SettingsViewModel.KEY_HOUSEHOLD_SIZE] = "3"
|
||||
fakeSettingsRepository.store[SettingsKey.StringKey.HouseholdSize.key] = "3"
|
||||
viewModel = createViewModel()
|
||||
|
||||
// When
|
||||
|
|
@ -215,7 +217,7 @@ class SettingsViewModelTest {
|
|||
|
||||
// Then
|
||||
assertTrue(viewModel.uiState.value.isSaved)
|
||||
assertTrue(fakeSettingsRepository.store.containsKey(SettingsViewModel.KEY_AGE_GROUPS))
|
||||
assertTrue(fakeSettingsRepository.store.containsKey(SettingsKey.StringKey.AgeGroups.key))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -568,10 +570,10 @@ class SettingsViewModelTest {
|
|||
@Test
|
||||
fun test_init_withStoredSyncSettings_loadsSyncFields() = runTest(testDispatcher) {
|
||||
// Given
|
||||
fakeSettingsRepository.store[SettingsViewModel.KEY_SERVER_URL] = "https://example.com"
|
||||
fakeSettingsRepository.store[SettingsViewModel.KEY_AUTH_USERNAME] = "testuser"
|
||||
fakeSettingsRepository.store[SettingsViewModel.KEY_ACCESS_TOKEN] = "access-token-123"
|
||||
fakeSettingsRepository.store[SettingsViewModel.KEY_SYNC_LAST_TIMESTAMP] = "1715700000000"
|
||||
fakeSettingsRepository.store[SettingsKey.StringKey.ServerUrl.key] = "https://example.com"
|
||||
fakeSettingsRepository.store[SettingsKey.StringKey.AuthUsername.key] = "testuser"
|
||||
fakeSettingsRepository.store[SettingsKey.StringKey.AuthAccessToken.key] = "access-token-123"
|
||||
fakeSettingsRepository.store[SettingsKey.StringKey.SyncLastTimestamp.key] = "1715700000000"
|
||||
|
||||
viewModel = createViewModel()
|
||||
|
||||
|
|
@ -657,9 +659,9 @@ class SettingsViewModelTest {
|
|||
@Test
|
||||
fun test_logout_clearsLoginState() = runTest(testDispatcher) {
|
||||
// Given
|
||||
fakeSettingsRepository.store[SettingsViewModel.KEY_AUTH_USERNAME] = "testuser"
|
||||
fakeSettingsRepository.store[SettingsViewModel.KEY_ACCESS_TOKEN] = "token-123"
|
||||
fakeSettingsRepository.store[SettingsViewModel.KEY_SERVER_URL] = "https://server.com"
|
||||
fakeSettingsRepository.store[SettingsKey.StringKey.AuthUsername.key] = "testuser"
|
||||
fakeSettingsRepository.store[SettingsKey.StringKey.AuthAccessToken.key] = "token-123"
|
||||
fakeSettingsRepository.store[SettingsKey.StringKey.ServerUrl.key] = "https://server.com"
|
||||
viewModel = createViewModel()
|
||||
advanceUntilIdle()
|
||||
|
||||
|
|
@ -686,7 +688,7 @@ class SettingsViewModelTest {
|
|||
|
||||
// Then
|
||||
assertTrue(viewModel.uiState.value.isSaved)
|
||||
assertEquals("https://myserver.com", fakeSettingsRepository.store[SettingsViewModel.KEY_SERVER_URL])
|
||||
assertEquals("https://myserver.com", fakeSettingsRepository.store[SettingsKey.StringKey.ServerUrl.key])
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -703,7 +705,7 @@ class SettingsViewModelTest {
|
|||
val state = viewModel.uiState.value
|
||||
assertTrue(state.syncStatus is SyncStatus.Success)
|
||||
assertNotNull(state.lastSyncTime)
|
||||
assertNotNull(fakeSettingsRepository.store[SettingsViewModel.KEY_SYNC_LAST_TIMESTAMP])
|
||||
assertNotNull(fakeSettingsRepository.store[SettingsKey.StringKey.SyncLastTimestamp.key])
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -738,7 +740,7 @@ class SettingsViewModelTest {
|
|||
val state = viewModel.uiState.value
|
||||
assertTrue(state.syncStatus is SyncStatus.Success)
|
||||
assertNotNull(state.lastSyncTime)
|
||||
assertNotNull(fakeSettingsRepository.store[SettingsViewModel.KEY_SYNC_LAST_TIMESTAMP])
|
||||
assertNotNull(fakeSettingsRepository.store[SettingsKey.StringKey.SyncLastTimestamp.key])
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -792,6 +794,18 @@ private class FakeSettingsRepository : SettingsRepository {
|
|||
MutableStateFlow(store[key])
|
||||
|
||||
override fun getAll(): Flow<List<SettingsEntity>> = allFlow
|
||||
|
||||
override suspend fun getString(key: SettingsKey.StringKey): String =
|
||||
getValue(key.key) ?: key.defaultValue
|
||||
|
||||
override suspend fun getStringOrNull(key: SettingsKey.StringKey): String? =
|
||||
getValue(key.key)
|
||||
|
||||
override suspend fun setString(key: SettingsKey.StringKey, value: String) =
|
||||
setValue(key.key, value)
|
||||
|
||||
override fun observeString(key: SettingsKey.StringKey): Flow<String> =
|
||||
observeValue(key.key).map { it ?: key.defaultValue }
|
||||
}
|
||||
|
||||
private class FakeImportExportRepository : ImportExportRepository {
|
||||
|
|
|
|||
Loading…
Reference in a new issue