feat(repo): Repository-Schicht implementieren (#20)
- 4 Repository-Interfaces in domain/repository/ (Category, Location, Item, Settings) - 4 Implementierungsklassen in data/repository/ mit Hilt @Inject - RepositoryModule mit @Binds-Bindings fuer alle Repositories - Datumslogik (getExpiringSoon) aus ItemDao in ItemRepositoryImpl verschoben - 20 Unit-Tests mit Fake-DAOs (4 pro Repository)
This commit is contained in:
parent
7380dbbdea
commit
95d8a10ed0
14 changed files with 665 additions and 3 deletions
|
|
@ -35,7 +35,4 @@ internal interface ItemDao {
|
||||||
|
|
||||||
@Query("SELECT * FROM items WHERE expiry_date IS NOT NULL AND expiry_date <= :cutoff")
|
@Query("SELECT * FROM items WHERE expiry_date IS NOT NULL AND expiry_date <= :cutoff")
|
||||||
fun getExpiringSoonByCutoff(cutoff: LocalDate): Flow<List<ItemEntity>>
|
fun getExpiringSoonByCutoff(cutoff: LocalDate): Flow<List<ItemEntity>>
|
||||||
|
|
||||||
fun getExpiringSoon(daysUntil: Int = 30): Flow<List<ItemEntity>> =
|
|
||||||
getExpiringSoonByCutoff(LocalDate.now().plusDays(daysUntil.toLong()))
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package de.krisenvorrat.app.data.repository
|
||||||
|
|
||||||
|
import de.krisenvorrat.app.data.db.dao.CategoryDao
|
||||||
|
import de.krisenvorrat.app.data.db.entity.CategoryEntity
|
||||||
|
import de.krisenvorrat.app.domain.repository.CategoryRepository
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class CategoryRepositoryImpl @Inject constructor(
|
||||||
|
private val dao: CategoryDao
|
||||||
|
) : CategoryRepository {
|
||||||
|
|
||||||
|
override fun getAll(): Flow<List<CategoryEntity>> = dao.getAll()
|
||||||
|
|
||||||
|
override suspend fun insert(category: CategoryEntity) =
|
||||||
|
withContext(Dispatchers.IO) { dao.insert(category) }
|
||||||
|
|
||||||
|
override suspend fun update(category: CategoryEntity) =
|
||||||
|
withContext(Dispatchers.IO) { dao.update(category) }
|
||||||
|
|
||||||
|
override suspend fun delete(category: CategoryEntity) =
|
||||||
|
withContext(Dispatchers.IO) { dao.delete(category) }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package de.krisenvorrat.app.data.repository
|
||||||
|
|
||||||
|
import de.krisenvorrat.app.data.db.dao.ItemDao
|
||||||
|
import de.krisenvorrat.app.data.db.entity.ItemEntity
|
||||||
|
import de.krisenvorrat.app.domain.repository.ItemRepository
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.time.LocalDate
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class ItemRepositoryImpl @Inject constructor(
|
||||||
|
private val dao: ItemDao
|
||||||
|
) : ItemRepository {
|
||||||
|
|
||||||
|
override fun getAll(): Flow<List<ItemEntity>> = dao.getAll()
|
||||||
|
|
||||||
|
override suspend fun getById(id: String): ItemEntity? =
|
||||||
|
withContext(Dispatchers.IO) { dao.getById(id) }
|
||||||
|
|
||||||
|
override suspend fun insert(item: ItemEntity) =
|
||||||
|
withContext(Dispatchers.IO) { dao.insert(item) }
|
||||||
|
|
||||||
|
override suspend fun update(item: ItemEntity) =
|
||||||
|
withContext(Dispatchers.IO) { dao.update(item) }
|
||||||
|
|
||||||
|
override suspend fun delete(item: ItemEntity) =
|
||||||
|
withContext(Dispatchers.IO) { dao.delete(item) }
|
||||||
|
|
||||||
|
override fun getByCategory(categoryId: Int): Flow<List<ItemEntity>> =
|
||||||
|
dao.getByCategory(categoryId)
|
||||||
|
|
||||||
|
override fun getByLocation(locationId: Int): Flow<List<ItemEntity>> =
|
||||||
|
dao.getByLocation(locationId)
|
||||||
|
|
||||||
|
override fun getExpiringSoon(daysUntil: Int): Flow<List<ItemEntity>> =
|
||||||
|
dao.getExpiringSoonByCutoff(LocalDate.now().plusDays(daysUntil.toLong()))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package de.krisenvorrat.app.data.repository
|
||||||
|
|
||||||
|
import de.krisenvorrat.app.data.db.dao.LocationDao
|
||||||
|
import de.krisenvorrat.app.data.db.entity.LocationEntity
|
||||||
|
import de.krisenvorrat.app.domain.repository.LocationRepository
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class LocationRepositoryImpl @Inject constructor(
|
||||||
|
private val dao: LocationDao
|
||||||
|
) : LocationRepository {
|
||||||
|
|
||||||
|
override fun getAll(): Flow<List<LocationEntity>> = dao.getAll()
|
||||||
|
|
||||||
|
override suspend fun insert(location: LocationEntity) =
|
||||||
|
withContext(Dispatchers.IO) { dao.insert(location) }
|
||||||
|
|
||||||
|
override suspend fun update(location: LocationEntity) =
|
||||||
|
withContext(Dispatchers.IO) { dao.update(location) }
|
||||||
|
|
||||||
|
override suspend fun delete(location: LocationEntity) =
|
||||||
|
withContext(Dispatchers.IO) { dao.delete(location) }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
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.domain.repository.SettingsRepository
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class SettingsRepositoryImpl @Inject constructor(
|
||||||
|
private val dao: SettingsDao
|
||||||
|
) : SettingsRepository {
|
||||||
|
|
||||||
|
override suspend fun getValue(key: String): String? =
|
||||||
|
withContext(Dispatchers.IO) { dao.getValue(key) }
|
||||||
|
|
||||||
|
override suspend fun setValue(key: String, value: String) =
|
||||||
|
withContext(Dispatchers.IO) { dao.upsert(SettingsEntity(key = key, value = value)) }
|
||||||
|
|
||||||
|
override fun getAll(): Flow<List<SettingsEntity>> = dao.getAll()
|
||||||
|
}
|
||||||
36
app/src/main/java/de/krisenvorrat/app/di/RepositoryModule.kt
Normal file
36
app/src/main/java/de/krisenvorrat/app/di/RepositoryModule.kt
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
package de.krisenvorrat.app.di
|
||||||
|
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import de.krisenvorrat.app.data.repository.CategoryRepositoryImpl
|
||||||
|
import de.krisenvorrat.app.data.repository.ItemRepositoryImpl
|
||||||
|
import de.krisenvorrat.app.data.repository.LocationRepositoryImpl
|
||||||
|
import de.krisenvorrat.app.data.repository.SettingsRepositoryImpl
|
||||||
|
import de.krisenvorrat.app.domain.repository.CategoryRepository
|
||||||
|
import de.krisenvorrat.app.domain.repository.ItemRepository
|
||||||
|
import de.krisenvorrat.app.domain.repository.LocationRepository
|
||||||
|
import de.krisenvorrat.app.domain.repository.SettingsRepository
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
internal abstract class RepositoryModule {
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
abstract fun bindCategoryRepository(impl: CategoryRepositoryImpl): CategoryRepository
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
abstract fun bindLocationRepository(impl: LocationRepositoryImpl): LocationRepository
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
abstract fun bindItemRepository(impl: ItemRepositoryImpl): ItemRepository
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
abstract fun bindSettingsRepository(impl: SettingsRepositoryImpl): SettingsRepository
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package de.krisenvorrat.app.domain.repository
|
||||||
|
|
||||||
|
import de.krisenvorrat.app.data.db.entity.CategoryEntity
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
internal interface CategoryRepository {
|
||||||
|
fun getAll(): Flow<List<CategoryEntity>>
|
||||||
|
suspend fun insert(category: CategoryEntity)
|
||||||
|
suspend fun update(category: CategoryEntity)
|
||||||
|
suspend fun delete(category: CategoryEntity)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package de.krisenvorrat.app.domain.repository
|
||||||
|
|
||||||
|
import de.krisenvorrat.app.data.db.entity.ItemEntity
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
internal interface ItemRepository {
|
||||||
|
fun getAll(): Flow<List<ItemEntity>>
|
||||||
|
suspend fun getById(id: String): ItemEntity?
|
||||||
|
suspend fun insert(item: ItemEntity)
|
||||||
|
suspend fun update(item: ItemEntity)
|
||||||
|
suspend fun delete(item: ItemEntity)
|
||||||
|
fun getByCategory(categoryId: Int): Flow<List<ItemEntity>>
|
||||||
|
fun getByLocation(locationId: Int): Flow<List<ItemEntity>>
|
||||||
|
fun getExpiringSoon(daysUntil: Int = 30): Flow<List<ItemEntity>>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package de.krisenvorrat.app.domain.repository
|
||||||
|
|
||||||
|
import de.krisenvorrat.app.data.db.entity.LocationEntity
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
internal interface LocationRepository {
|
||||||
|
fun getAll(): Flow<List<LocationEntity>>
|
||||||
|
suspend fun insert(location: LocationEntity)
|
||||||
|
suspend fun update(location: LocationEntity)
|
||||||
|
suspend fun delete(location: LocationEntity)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package de.krisenvorrat.app.domain.repository
|
||||||
|
|
||||||
|
import de.krisenvorrat.app.data.db.entity.SettingsEntity
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
internal interface SettingsRepository {
|
||||||
|
suspend fun getValue(key: String): String?
|
||||||
|
suspend fun setValue(key: String, value: String)
|
||||||
|
fun getAll(): Flow<List<SettingsEntity>>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
package de.krisenvorrat.app.data.repository
|
||||||
|
|
||||||
|
import de.krisenvorrat.app.data.db.dao.CategoryDao
|
||||||
|
import de.krisenvorrat.app.data.db.entity.CategoryEntity
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
private class FakeCategoryDao : CategoryDao {
|
||||||
|
private val items = mutableListOf<CategoryEntity>()
|
||||||
|
private val flow = MutableStateFlow<List<CategoryEntity>>(emptyList())
|
||||||
|
|
||||||
|
override suspend fun insert(category: CategoryEntity) {
|
||||||
|
items.add(category)
|
||||||
|
flow.value = items.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun update(category: CategoryEntity) {
|
||||||
|
val idx = items.indexOfFirst { it.id == category.id }
|
||||||
|
if (idx >= 0) {
|
||||||
|
items[idx] = category
|
||||||
|
flow.value = items.toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun delete(category: CategoryEntity) {
|
||||||
|
items.remove(category)
|
||||||
|
flow.value = items.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAll(): Flow<List<CategoryEntity>> = flow
|
||||||
|
|
||||||
|
override suspend fun getById(id: Int): CategoryEntity? = items.find { it.id == id }
|
||||||
|
}
|
||||||
|
|
||||||
|
class CategoryRepositoryImplTest {
|
||||||
|
|
||||||
|
private val fakeDao = FakeCategoryDao()
|
||||||
|
private val repository = CategoryRepositoryImpl(fakeDao)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_insert_withNewCategory_categoryAppearsInGetAll() = runBlocking {
|
||||||
|
// Given
|
||||||
|
val category = CategoryEntity(id = 1, name = "Lebensmittel")
|
||||||
|
|
||||||
|
// When
|
||||||
|
repository.insert(category)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
val result = repository.getAll().first()
|
||||||
|
assertTrue(result.contains(category))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_update_withExistingCategory_categoryIsUpdated() = runBlocking {
|
||||||
|
// Given
|
||||||
|
val original = CategoryEntity(id = 1, name = "Alt")
|
||||||
|
repository.insert(original)
|
||||||
|
val updated = CategoryEntity(id = 1, name = "Neu")
|
||||||
|
|
||||||
|
// When
|
||||||
|
repository.update(updated)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
val result = repository.getAll().first()
|
||||||
|
assertEquals("Neu", result.first { it.id == 1 }.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_delete_withExistingCategory_categoryRemovedFromGetAll() = runBlocking {
|
||||||
|
// Given
|
||||||
|
val category = CategoryEntity(id = 1, name = "Lebensmittel")
|
||||||
|
repository.insert(category)
|
||||||
|
|
||||||
|
// When
|
||||||
|
repository.delete(category)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
val result = repository.getAll().first()
|
||||||
|
assertFalse(result.contains(category))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_getAll_withMultipleCategories_returnsAllCategories() = runBlocking {
|
||||||
|
// Given
|
||||||
|
val cat1 = CategoryEntity(id = 1, name = "Lebensmittel")
|
||||||
|
val cat2 = CategoryEntity(id = 2, name = "Hygiene")
|
||||||
|
repository.insert(cat1)
|
||||||
|
repository.insert(cat2)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = repository.getAll().first()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertEquals(2, result.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,186 @@
|
||||||
|
package de.krisenvorrat.app.data.repository
|
||||||
|
|
||||||
|
import de.krisenvorrat.app.data.db.dao.ItemDao
|
||||||
|
import de.krisenvorrat.app.data.db.entity.ItemEntity
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
private class FakeItemDao : ItemDao {
|
||||||
|
private val items = mutableListOf<ItemEntity>()
|
||||||
|
private val flow = MutableStateFlow<List<ItemEntity>>(emptyList())
|
||||||
|
private val expiringSoonFlow = MutableStateFlow<List<ItemEntity>>(emptyList())
|
||||||
|
fun setExpiringSoonItems(items: List<ItemEntity>) { expiringSoonFlow.value = items }
|
||||||
|
|
||||||
|
private fun emit() { flow.value = items.toList() }
|
||||||
|
|
||||||
|
override suspend fun insert(item: ItemEntity) {
|
||||||
|
items.add(item)
|
||||||
|
emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun update(item: ItemEntity) {
|
||||||
|
val idx = items.indexOfFirst { it.id == item.id }
|
||||||
|
if (idx >= 0) {
|
||||||
|
items[idx] = item
|
||||||
|
emit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun delete(item: ItemEntity) {
|
||||||
|
items.remove(item)
|
||||||
|
emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAll(): Flow<List<ItemEntity>> = flow
|
||||||
|
|
||||||
|
override suspend fun getById(id: String): ItemEntity? = items.find { it.id == id }
|
||||||
|
|
||||||
|
override fun getByCategory(categoryId: Int): Flow<List<ItemEntity>> =
|
||||||
|
flow.map { list -> list.filter { it.categoryId == categoryId } }
|
||||||
|
|
||||||
|
override fun getByLocation(locationId: Int): Flow<List<ItemEntity>> =
|
||||||
|
flow.map { list -> list.filter { it.locationId == locationId } }
|
||||||
|
|
||||||
|
override fun getExpiringSoonByCutoff(cutoff: LocalDate): Flow<List<ItemEntity>> =
|
||||||
|
expiringSoonFlow
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildItem(
|
||||||
|
id: String = "id1",
|
||||||
|
categoryId: Int = 1,
|
||||||
|
locationId: Int = 1
|
||||||
|
) = ItemEntity(
|
||||||
|
id = id,
|
||||||
|
name = "Konserve",
|
||||||
|
categoryId = categoryId,
|
||||||
|
quantity = 2.0,
|
||||||
|
unit = "Stk",
|
||||||
|
unitPrice = 1.5,
|
||||||
|
kcalPer100g = null,
|
||||||
|
expiryDate = null,
|
||||||
|
locationId = locationId,
|
||||||
|
minStock = 1.0,
|
||||||
|
notes = "",
|
||||||
|
lastUpdated = 0L
|
||||||
|
)
|
||||||
|
|
||||||
|
class ItemRepositoryImplTest {
|
||||||
|
|
||||||
|
private val fakeDao = FakeItemDao()
|
||||||
|
private val repository = ItemRepositoryImpl(fakeDao)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_insert_withNewItem_itemAppearsInGetAll() = runBlocking {
|
||||||
|
// Given
|
||||||
|
val item = buildItem(id = "abc")
|
||||||
|
|
||||||
|
// When
|
||||||
|
repository.insert(item)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
val result = repository.getAll().first()
|
||||||
|
assertTrue(result.any { it.id == "abc" })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_getById_withExistingId_returnsItem() = runBlocking {
|
||||||
|
// Given
|
||||||
|
val item = buildItem(id = "abc")
|
||||||
|
repository.insert(item)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = repository.getById("abc")
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertEquals(item, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_getById_withUnknownId_returnsNull() = runBlocking {
|
||||||
|
// Given / When
|
||||||
|
val result = repository.getById("unknown")
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertNull(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_update_withExistingItem_itemIsUpdated() = runBlocking {
|
||||||
|
// Given
|
||||||
|
val item = buildItem(id = "abc")
|
||||||
|
repository.insert(item)
|
||||||
|
val updated = item.copy(name = "Aktualisiert")
|
||||||
|
|
||||||
|
// When
|
||||||
|
repository.update(updated)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
val result = repository.getById("abc")
|
||||||
|
assertEquals("Aktualisiert", result?.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_delete_withExistingItem_itemRemovedFromGetAll() = runBlocking {
|
||||||
|
// Given
|
||||||
|
val item = buildItem(id = "abc")
|
||||||
|
repository.insert(item)
|
||||||
|
|
||||||
|
// When
|
||||||
|
repository.delete(item)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
val result = repository.getAll().first()
|
||||||
|
assertFalse(result.any { it.id == "abc" })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_getByCategory_withMatchingItems_returnsFilteredItems() = runBlocking {
|
||||||
|
// Given
|
||||||
|
repository.insert(buildItem(id = "a", categoryId = 1))
|
||||||
|
repository.insert(buildItem(id = "b", categoryId = 2))
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = repository.getByCategory(1).first()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertEquals(1, result.size)
|
||||||
|
assertEquals("a", result.first().id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_getByLocation_withMatchingItems_returnsFilteredItems() = runBlocking {
|
||||||
|
// Given
|
||||||
|
repository.insert(buildItem(id = "a", locationId = 1))
|
||||||
|
repository.insert(buildItem(id = "b", locationId = 2))
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = repository.getByLocation(2).first()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertEquals(1, result.size)
|
||||||
|
assertEquals("b", result.first().id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_getExpiringSoon_withPreconfiguredFlow_returnsExpectedItems() = runBlocking {
|
||||||
|
// Given
|
||||||
|
val expiring = buildItem(id = "exp")
|
||||||
|
fakeDao.setExpiringSoonItems(listOf(expiring))
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = repository.getExpiringSoon(30).first()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertEquals(1, result.size)
|
||||||
|
assertEquals("exp", result.first().id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
package de.krisenvorrat.app.data.repository
|
||||||
|
|
||||||
|
import de.krisenvorrat.app.data.db.dao.LocationDao
|
||||||
|
import de.krisenvorrat.app.data.db.entity.LocationEntity
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
private class FakeLocationDao : LocationDao {
|
||||||
|
private val items = mutableListOf<LocationEntity>()
|
||||||
|
private val flow = MutableStateFlow<List<LocationEntity>>(emptyList())
|
||||||
|
|
||||||
|
override suspend fun insert(location: LocationEntity) {
|
||||||
|
items.add(location)
|
||||||
|
flow.value = items.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun update(location: LocationEntity) {
|
||||||
|
val idx = items.indexOfFirst { it.id == location.id }
|
||||||
|
if (idx >= 0) {
|
||||||
|
items[idx] = location
|
||||||
|
flow.value = items.toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun delete(location: LocationEntity) {
|
||||||
|
items.remove(location)
|
||||||
|
flow.value = items.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAll(): Flow<List<LocationEntity>> = flow
|
||||||
|
|
||||||
|
override suspend fun getById(id: Int): LocationEntity? = items.find { it.id == id }
|
||||||
|
}
|
||||||
|
|
||||||
|
class LocationRepositoryImplTest {
|
||||||
|
|
||||||
|
private val fakeDao = FakeLocationDao()
|
||||||
|
private val repository = LocationRepositoryImpl(fakeDao)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_insert_withNewLocation_locationAppearsInGetAll() = runBlocking {
|
||||||
|
// Given
|
||||||
|
val location = LocationEntity(id = 1, name = "Keller")
|
||||||
|
|
||||||
|
// When
|
||||||
|
repository.insert(location)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
val result = repository.getAll().first()
|
||||||
|
assertTrue(result.contains(location))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_update_withExistingLocation_locationIsUpdated() = runBlocking {
|
||||||
|
// Given
|
||||||
|
val original = LocationEntity(id = 1, name = "Keller")
|
||||||
|
repository.insert(original)
|
||||||
|
val updated = LocationEntity(id = 1, name = "Dachboden")
|
||||||
|
|
||||||
|
// When
|
||||||
|
repository.update(updated)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
val result = repository.getAll().first()
|
||||||
|
assertEquals("Dachboden", result.first { it.id == 1 }.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_delete_withExistingLocation_locationRemovedFromGetAll() = runBlocking {
|
||||||
|
// Given
|
||||||
|
val location = LocationEntity(id = 1, name = "Keller")
|
||||||
|
repository.insert(location)
|
||||||
|
|
||||||
|
// When
|
||||||
|
repository.delete(location)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
val result = repository.getAll().first()
|
||||||
|
assertFalse(result.contains(location))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_getAll_withMultipleLocations_returnsAllLocations() = runBlocking {
|
||||||
|
// Given
|
||||||
|
val loc1 = LocationEntity(id = 1, name = "Keller")
|
||||||
|
val loc2 = LocationEntity(id = 2, name = "Küche")
|
||||||
|
repository.insert(loc1)
|
||||||
|
repository.insert(loc2)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = repository.getAll().first()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertEquals(2, result.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
package de.krisenvorrat.app.data.repository
|
||||||
|
|
||||||
|
import de.krisenvorrat.app.data.db.dao.SettingsDao
|
||||||
|
import de.krisenvorrat.app.data.db.entity.SettingsEntity
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
private class FakeSettingsDao : SettingsDao {
|
||||||
|
private val store = mutableMapOf<String, String>()
|
||||||
|
private val flow = MutableStateFlow<List<SettingsEntity>>(emptyList())
|
||||||
|
|
||||||
|
override suspend fun upsert(setting: SettingsEntity) {
|
||||||
|
store[setting.key] = setting.value
|
||||||
|
flow.value = store.map { SettingsEntity(key = it.key, value = it.value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getValue(key: String): String? = store[key]
|
||||||
|
|
||||||
|
override fun getAll(): Flow<List<SettingsEntity>> = flow
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsRepositoryImplTest {
|
||||||
|
|
||||||
|
private val fakeDao = FakeSettingsDao()
|
||||||
|
private val repository = SettingsRepositoryImpl(fakeDao)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_setValue_withNewKey_valueCanBeRetrieved() = runBlocking {
|
||||||
|
// Given
|
||||||
|
val key = "theme"
|
||||||
|
val value = "dark"
|
||||||
|
|
||||||
|
// When
|
||||||
|
repository.setValue(key, value)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
val result = repository.getValue(key)
|
||||||
|
assertEquals(value, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_setValue_withExistingKey_valueIsUpdated() = runBlocking {
|
||||||
|
// Given
|
||||||
|
repository.setValue("theme", "light")
|
||||||
|
|
||||||
|
// When
|
||||||
|
repository.setValue("theme", "dark")
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertEquals("dark", repository.getValue("theme"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_getValue_withUnknownKey_returnsNull() = runBlocking {
|
||||||
|
// Given / When
|
||||||
|
val result = repository.getValue("nonexistent")
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertNull(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_getAll_afterInsertingTwoSettings_returnsBothSettings() = runBlocking {
|
||||||
|
// Given
|
||||||
|
repository.setValue("theme", "dark")
|
||||||
|
repository.setValue("language", "de")
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = repository.getAll().first()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertEquals(2, result.size)
|
||||||
|
assertTrue(result.any { it.key == "theme" && it.value == "dark" })
|
||||||
|
assertTrue(result.any { it.key == "language" && it.value == "de" })
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue