diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt index bb81b52052a6ab2d2583276dd2bb9046d4b162f9..09ca3ef2e455fa1e4ed4e540ad7feea2773f5f08 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt @@ -60,7 +60,7 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor( private val riskLevelPerDateFlow = riskLevelStorage.ewDayRiskStates private val traceLocationCheckInRiskFlow = riskLevelStorage.traceLocationCheckInRiskStates - private val allCheckInsFlow = checkInRepository.allCheckIns + private val checkInsWithinRetentionFlow = checkInRepository.checkInsWithinRetention val listItems = combine( flowOf(dates), @@ -68,7 +68,7 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor( personEncountersFlow, riskLevelPerDateFlow, traceLocationCheckInRiskFlow, - allCheckInsFlow + checkInsWithinRetentionFlow ) { dateList, locationVisists, personEncounters, riskLevelPerDateList, traceLocationCheckInRiskList, checkInList -> mutableListOf<DiaryOverviewItem>().apply { add(OverviewSubHeaderItem) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepository.kt index 3420d4832fce1f9b0765ef075c018eba0d047391..dbac99970a4ebe12fe5a32b49d6feba633b763e8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepository.kt @@ -3,6 +3,7 @@ package de.rki.coronawarnapp.eventregistration.checkins import de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabase import de.rki.coronawarnapp.eventregistration.storage.dao.CheckInDao import de.rki.coronawarnapp.eventregistration.storage.entity.toCheckIn +import de.rki.coronawarnapp.util.TimeStamper import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -13,7 +14,8 @@ import javax.inject.Singleton @Singleton class CheckInRepository @Inject constructor( - traceLocationDatabaseFactory: TraceLocationDatabase.Factory + traceLocationDatabaseFactory: TraceLocationDatabase.Factory, + private val timeStamper: TimeStamper ) { private val traceLocationDatabase: TraceLocationDatabase by lazy { @@ -24,10 +26,27 @@ class CheckInRepository @Inject constructor( traceLocationDatabase.eventCheckInDao() } + /** + * Returns all stored check-ins + * + * Attention: this could also include check-ins that are older than + * the retention period. Therefore, you should probably use [checkInsWithinRetention] + */ val allCheckIns: Flow<List<CheckIn>> = checkInDao .allEntries() .map { list -> list.map { it.toCheckIn() } } + /** + * Returns check-ins that are within the retention period. Even though we have a worker that deletes all stale + * check-ins it's still possible to have stale check-ins in the database because the worker only runs once a day. + */ + val checkInsWithinRetention: Flow<List<CheckIn>> = allCheckIns.map { checkInList -> + val now = timeStamper.nowUTC + checkInList.filter { checkIn -> + checkIn.isWithinRetention(now) + } + } + suspend fun getCheckInById(checkInId: Long): CheckIn? { Timber.d("getCheckInById(checkInId=$checkInId)") return checkInDao.entryForId(checkInId)?.toCheckIn() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRetention.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRetention.kt new file mode 100644 index 0000000000000000000000000000000000000000..875612a2a0b00a6512964fb347a1d956b245e997 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRetention.kt @@ -0,0 +1,21 @@ +package de.rki.coronawarnapp.eventregistration.checkins + +import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds +import org.joda.time.Instant +import java.util.concurrent.TimeUnit + +private const val CHECK_IN_RETENTION_DAYS = 15 +private val CHECK_IN_RETENTION_SECONDS = TimeUnit.DAYS.toSeconds(CHECK_IN_RETENTION_DAYS.toLong()) + +/** + * returns true if the end date of the check-in isn't older than [CHECK_IN_RETENTION_DAYS], otherwise false + */ +fun CheckIn.isWithinRetention(now: Instant): Boolean { + val retentionThreshold = (now.seconds - CHECK_IN_RETENTION_SECONDS) + return checkInEnd.seconds >= retentionThreshold +} + +/** + * Returns true if a check-in is stale and therefore can be deleted, otherwise false + */ +fun CheckIn.isOutOfRetention(now: Instant) = !isWithinRetention(now) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/repo/DefaultTraceLocationRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/repo/DefaultTraceLocationRepository.kt index 46a93d873b0be3f72c3247e14814f260ba93723d..56463d80c42ac0389e1f71d9868172ff2a14d6f4 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/repo/DefaultTraceLocationRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/repo/DefaultTraceLocationRepository.kt @@ -6,6 +6,8 @@ import de.rki.coronawarnapp.eventregistration.checkins.qrcode.toTraceLocations import de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabase import de.rki.coronawarnapp.eventregistration.storage.dao.TraceLocationDao import de.rki.coronawarnapp.eventregistration.storage.entity.toTraceLocationEntity +import de.rki.coronawarnapp.eventregistration.storage.retention.isWithinRetention +import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.coroutine.AppScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -18,7 +20,8 @@ import javax.inject.Singleton @Singleton class DefaultTraceLocationRepository @Inject constructor( traceLocationDatabaseFactory: TraceLocationDatabase.Factory, - @AppScope private val appScope: CoroutineScope + @AppScope private val appScope: CoroutineScope, + private val timeStamper: TimeStamper ) : TraceLocationRepository { private val traceLocationDatabase: TraceLocationDatabase by lazy { @@ -36,9 +39,28 @@ class DefaultTraceLocationRepository @Inject constructor( return checkIn.toTraceLocation() } + /** + * Returns all stored trace locations + * + * Attention: this could also include trace locations that are older than + * the retention period. Therefore, you should probably use [traceLocationsWithinRetention] + */ override val allTraceLocations: Flow<List<TraceLocation>> get() = traceLocationDao.allEntries().map { it.toTraceLocations() } + /** + * Returns trace locations that are within the retention period. Even though we have a worker that deletes all stale + * trace locations it's still possible to have stale trace-locations in the database because the worker only runs + * once a day. + */ + override val traceLocationsWithinRetention: Flow<List<TraceLocation>> + get() = allTraceLocations.map { traceLocationList -> + val now = timeStamper.nowUTC + traceLocationList.filter { traceLocation -> + traceLocation.isWithinRetention(now) + } + } + override suspend fun addTraceLocation(traceLocation: TraceLocation): TraceLocation { Timber.d("Add trace location: %s", traceLocation) val traceLocationEntity = traceLocation.toTraceLocationEntity() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/repo/TraceLocationRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/repo/TraceLocationRepository.kt index 8baafc528adfbb6197519b26f96d5958c45a1655..df566a3cc3b8c982984871cfb3c2451fad3dc63d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/repo/TraceLocationRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/repo/TraceLocationRepository.kt @@ -5,8 +5,21 @@ import kotlinx.coroutines.flow.Flow interface TraceLocationRepository { + /** + * Returns all stored trace locations + * + * Attention: this could also include trace locations that are older than + * the retention period. Therefore, you should probably use [traceLocationsWithinRetention] + */ val allTraceLocations: Flow<List<TraceLocation>> + /** + * Returns trace locations that are within the retention period. Even though we have a worker that deletes all stale + * trace locations it's still possible to have stale trace-locations in the database because the worker only runs + * once a day. + */ + val traceLocationsWithinRetention: Flow<List<TraceLocation>> + suspend fun traceLocationForId(id: Long): TraceLocation suspend fun addTraceLocation(traceLocation: TraceLocation): TraceLocation diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/retention/CheckInCleaner.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/retention/CheckInCleaner.kt index b40efe4887783d16930c261fb140c8b68b226bb3..af5e3f80a39252d0c07ccce930c0d8f968a3e092 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/retention/CheckInCleaner.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/retention/CheckInCleaner.kt @@ -2,11 +2,10 @@ package de.rki.coronawarnapp.eventregistration.storage.retention import dagger.Reusable import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository -import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds +import de.rki.coronawarnapp.eventregistration.checkins.isOutOfRetention import de.rki.coronawarnapp.util.TimeStamper import kotlinx.coroutines.flow.first import timber.log.Timber -import java.util.concurrent.TimeUnit import javax.inject.Inject @Reusable @@ -17,18 +16,15 @@ class CheckInCleaner @Inject constructor( suspend fun cleanUp() { Timber.d("Starting to clean up stale check-ins.") - val retentionThreshold = (timeStamper.nowUTC.seconds - RETENTION_SECONDS) + + val now = timeStamper.nowUTC val checkInsToDelete = checkInRepository.allCheckIns.first() - .filter { - it.checkInEnd.seconds < retentionThreshold - } + .filter { checkIn -> checkIn.isOutOfRetention(now) } + Timber.d("Cleaning up ${checkInsToDelete.size} stale check-ins.") + checkInRepository.deleteCheckIns(checkInsToDelete) - Timber.d("Clean up of stale check-ins completed.") - } - companion object { - private const val RETENTION_DAYS = 15 - private val RETENTION_SECONDS = TimeUnit.DAYS.toSeconds(RETENTION_DAYS.toLong()) + Timber.d("Clean up of stale check-ins completed.") } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/retention/TraceLocationCleaner.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/retention/TraceLocationCleaner.kt index 6f73ddbf786282539650e6fdbf8cee3d7e74305a..69fac1678fb37a4bce0e309561003d1b68d44c36 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/retention/TraceLocationCleaner.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/retention/TraceLocationCleaner.kt @@ -2,11 +2,9 @@ package de.rki.coronawarnapp.eventregistration.storage.retention import dagger.Reusable import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository -import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds import de.rki.coronawarnapp.util.TimeStamper import kotlinx.coroutines.flow.first import timber.log.Timber -import java.util.concurrent.TimeUnit import javax.inject.Inject @Reusable @@ -17,19 +15,16 @@ class TraceLocationCleaner @Inject constructor( suspend fun cleanUp() { Timber.d("Starting to clean up stale trace locations.") - val retentionThreshold = (timeStamper.nowUTC.seconds - RETENTION_SECONDS) + + val now = timeStamper.nowUTC traceLocationRepository.allTraceLocations.first() - .filter { - it.endDate != null && it.endDate.seconds < retentionThreshold + .filter { traceLocation -> + traceLocation.isOutOfRetention(now) }.forEach { Timber.d("Cleaning up stale trace location: %s", it) traceLocationRepository.deleteTraceLocation(it) } - Timber.d("Clean up of stale trace locations completed.") - } - companion object { - private const val RETENTION_DAYS = 15 - private val RETENTION_SECONDS = TimeUnit.DAYS.toSeconds(RETENTION_DAYS.toLong()) + Timber.d("Clean up of stale trace locations completed.") } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/retention/TraceLocationRetention.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/retention/TraceLocationRetention.kt new file mode 100644 index 0000000000000000000000000000000000000000..b1c854e7f974551e34505eb05c1757e7c1cabdf8 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/retention/TraceLocationRetention.kt @@ -0,0 +1,27 @@ +package de.rki.coronawarnapp.eventregistration.storage.retention + +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation +import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds +import org.joda.time.Instant +import java.util.concurrent.TimeUnit + +private const val TRACE_LOCATION_RETENTION_DAYS = 15 +private val TRACE_LOCATION_RETENTION_SECONDS = TimeUnit.DAYS.toSeconds(TRACE_LOCATION_RETENTION_DAYS.toLong()) + +/** + * returns true if a trace location either has no end date or has an end date that isn't older than + * [TRACE_LOCATION_RETENTION_DAYS], otherwise false + */ +fun TraceLocation.isWithinRetention(now: Instant): Boolean { + val retentionThreshold = (now.seconds - TRACE_LOCATION_RETENTION_SECONDS) + return if (endDate == null) { + true + } else { + endDate.seconds >= retentionThreshold + } +} + +/** + * Returns true if a trace location is stale and therefore can be deleted, otherwise false + */ +fun TraceLocation.isOutOfRetention(now: Instant) = !isWithinRetention(now) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTask.kt index e0d9556af8f02e0ee7577eeebdcbee4a9c0f4a5b..09faec4926659c8eca97cd20252691681578ddbc 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTask.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTask.kt @@ -67,7 +67,7 @@ class PresenceTracingWarningTask @Inject constructor( presenceTracingRiskRepository.deleteStaleData() - val checkIns = checkInsRepository.allCheckIns.firstOrNull() ?: emptyList() + val checkIns = checkInsRepository.checkInsWithinRetention.firstOrNull() ?: emptyList() Timber.tag(TAG).d("There are %d check-ins to match against.", checkIns.size) if (checkIns.isEmpty()) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageSyncTool.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageSyncTool.kt index ea2d87f227017e3399c5c4b12468448f47914ec4..886b885ea966dfe32d7893d05738d3d82877539c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageSyncTool.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageSyncTool.kt @@ -41,7 +41,7 @@ class TraceWarningPackageSyncTool @Inject constructor( internal suspend fun syncPackagesForLocation(location: LocationCode): SyncResult { Timber.tag(TAG).d("syncTraceWarningPackages(location=%s)", location) - val oldestCheckIn = checkInRepository.allCheckIns.first().minByOrNull { it.checkInStart }.also { + val oldestCheckIn = checkInRepository.checkInsWithinRetention.first().minByOrNull { it.checkInStart }.also { Timber.tag(TAG).d("Our oldest check-in is %s", it) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt index b8f72f9de1d6b12a58abfe1a216583b5c7640673..4613a92bc27ef3ecf31e59fd158991964bacffac 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt @@ -142,7 +142,7 @@ class SubmissionTask @Inject constructor( ) Timber.tag(TAG).d("Transformed keys with symptoms %s from %s to %s", symptoms, keys, transformedKeys) - val checkIns = checkInsRepository.allCheckIns.first() + val checkIns = checkInsRepository.checkInsWithinRetention.first() val transformedCheckIns = checkInsTransformer.transform(checkIns, symptoms) Timber.tag(TAG).d("Transformed CheckIns from: %s to: %s", checkIns, transformedCheckIns) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModel.kt index 91ebb6441f7e51dfbe0e816f533bea0af6be9a74..ccd61616637c85b19fc82c408cecacf3a732dd36 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModel.kt @@ -56,7 +56,7 @@ class CheckInsViewModel @AssistedInject constructor( val checkins: LiveData<List<CheckInsItem>> = combine( intervalFlow(1000), - checkInsRepository.allCheckIns, + checkInsRepository.checkInsWithinRetention, cameraPermissionProvider.deniedPermanently ) { _, checkIns, denied -> mutableListOf<CheckInsItem>().apply { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsViewModel.kt index dbbb8ad2d0afa7bc77dbd49bcc04a4f20ff3b3aa..e00d80c9b5d4e9293c70382f3fda8a941c0de911 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsViewModel.kt @@ -9,7 +9,6 @@ import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository import de.rki.coronawarnapp.ui.eventregistration.organizer.list.items.TraceLocationItem import de.rki.coronawarnapp.ui.eventregistration.organizer.list.items.TraceLocationVH -import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel @@ -21,16 +20,13 @@ class TraceLocationsViewModel @AssistedInject constructor( dispatcherProvider: DispatcherProvider, checkInsRepository: CheckInRepository, private val traceLocationRepository: TraceLocationRepository, - private val timeStamper: TimeStamper ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { val events = SingleLiveEvent<TraceLocationEvent>() - val traceLocations: LiveData<List<TraceLocationItem>> = traceLocationRepository.allTraceLocations + val traceLocations: LiveData<List<TraceLocationItem>> = traceLocationRepository.traceLocationsWithinRetention .map { traceLocations -> - traceLocations - .filter { it.endDate == null || it.endDate.isAfter(timeStamper.nowUTC.toDateTime().minusDays(15)) } - .sortedBy { it.description } + traceLocations.sortedBy { it.description } } .combine(checkInsRepository.allCheckIns) { traceLocations, checkIns -> traceLocations.map { traceLocation -> diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityViewModel.kt index 4b534fb0eda9941b9cda93f24b1e6a24c7617f6f..7b8d11a2af4cee4a59ee4afdb404fc07d4302109 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityViewModel.kt @@ -42,7 +42,7 @@ class MainActivityViewModel @AssistedInject constructor( private val mutableIsTraceLocationOnboardingDone = MutableLiveData<Boolean>() val isTraceLocationOnboardingDone: LiveData<Boolean> = mutableIsTraceLocationOnboardingDone - val activeCheckIns = checkInRepository.allCheckIns + val activeCheckIns = checkInRepository.checkInsWithinRetention .map { checkins -> checkins.filter { !it.completed }.size } .asLiveData(context = dispatcherProvider.Default) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModelTest.kt index 7c0754dd1a75fa657fe35e47ee9156b1cc95ef31..aa7c8b2d9ed316af5e0d3b9f2617d531c69d26cc 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModelTest.kt @@ -69,7 +69,7 @@ open class ContactDiaryOverviewViewModelTest { every { contactDiaryRepository.personEncounters } returns flowOf(emptyList()) every { riskLevelStorage.ewDayRiskStates } returns flowOf(emptyList()) every { riskLevelStorage.traceLocationCheckInRiskStates } returns flowOf(emptyList()) - every { checkInRepository.allCheckIns } returns flowOf(emptyList()) + every { checkInRepository.checkInsWithinRetention } returns flowOf(emptyList()) mockStringsForContactDiaryExporterTests(context) every { timeStamper.nowUTC } returns Instant.ofEpochMilli(dateMillis) @@ -361,7 +361,7 @@ open class ContactDiaryOverviewViewModelTest { every { riskLevelStorage.ewDayRiskStates } returns flowOf(listOf(aggregatedRiskPerDateResultLowRisk)) every { riskLevelStorage.traceLocationCheckInRiskStates } returns flowOf(listOf(traceLocationCheckInRiskHigh)) every { contactDiaryRepository.locationVisits } returns flowOf(listOf(locationEventHighRiskVisit)) - every { checkInRepository.allCheckIns } returns flowOf(listOf(checkInHigh)) + every { checkInRepository.checkInsWithinRetention } returns flowOf(listOf(checkInHigh)) var item = createInstance().listItems.getOrAwaitValue().first { it is DayOverviewItem && it.date == date @@ -384,7 +384,7 @@ open class ContactDiaryOverviewViewModelTest { ) every { riskLevelStorage.traceLocationCheckInRiskStates } returns flowOf(listOf(traceLocationCheckInRiskLow)) every { contactDiaryRepository.locationVisits } returns flowOf(listOf(locationEventLowRiskVisit)) - every { checkInRepository.allCheckIns } returns flowOf(listOf(checkInLow)) + every { checkInRepository.checkInsWithinRetention } returns flowOf(listOf(checkInLow)) item = createInstance().listItems.getOrAwaitValue().first { it is DayOverviewItem && it.date == date @@ -414,7 +414,7 @@ open class ContactDiaryOverviewViewModelTest { fun `low risk event by attending event with low risk`() { every { contactDiaryRepository.locationVisits } returns flowOf(listOf(locationEventLowRiskVisit)) every { riskLevelStorage.traceLocationCheckInRiskStates } returns flowOf(listOf(traceLocationCheckInRiskLow)) - every { checkInRepository.allCheckIns } returns flowOf(listOf(checkInLow)) + every { checkInRepository.checkInsWithinRetention } returns flowOf(listOf(checkInLow)) val item = createInstance().listItems.getOrAwaitValue().first { it is DayOverviewItem && it.date == date @@ -436,7 +436,7 @@ open class ContactDiaryOverviewViewModelTest { fun `high risk event by attending event with high risk`() { every { contactDiaryRepository.locationVisits } returns flowOf(listOf(locationEventHighRiskVisit)) every { riskLevelStorage.traceLocationCheckInRiskStates } returns flowOf(listOf(traceLocationCheckInRiskHigh)) - every { checkInRepository.allCheckIns } returns flowOf(listOf(checkInHigh)) + every { checkInRepository.checkInsWithinRetention } returns flowOf(listOf(checkInHigh)) val item = createInstance().listItems.getOrAwaitValue().first { it is DayOverviewItem && it.date == date @@ -469,7 +469,7 @@ open class ContactDiaryOverviewViewModelTest { ) ) - every { checkInRepository.allCheckIns } returns flowOf(listOf(checkInHigh, checkInLow)) + every { checkInRepository.checkInsWithinRetention } returns flowOf(listOf(checkInHigh, checkInLow)) val item = createInstance().listItems.getOrAwaitValue().first { it is DayOverviewItem && it.date == date diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepositoryTest.kt index 0298f03aec66f49a628fb48937b3f1106e802387..162c73db7a24a5fa1d9ef88d407d356279955b1b 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepositoryTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepositoryTest.kt @@ -3,6 +3,7 @@ package de.rki.coronawarnapp.eventregistration.checkins import de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabase import de.rki.coronawarnapp.eventregistration.storage.dao.CheckInDao import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationCheckInEntity +import de.rki.coronawarnapp.util.TimeStamper import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations @@ -10,11 +11,13 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK +import io.mockk.impl.annotations.RelaxedMockK import io.mockk.mockk import io.mockk.slot import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runBlockingTest import okio.ByteString.Companion.encode import org.joda.time.Instant @@ -27,6 +30,7 @@ class CheckInRepositoryTest : BaseTest() { @MockK lateinit var factory: TraceLocationDatabase.Factory @MockK lateinit var database: TraceLocationDatabase @MockK lateinit var checkInDao: CheckInDao + @RelaxedMockK lateinit var timeStamper: TimeStamper private val allEntriesFlow = MutableStateFlow(emptyList<TraceLocationCheckInEntity>()) @BeforeEach @@ -40,7 +44,7 @@ class CheckInRepositoryTest : BaseTest() { } } - private fun createInstance(scope: CoroutineScope) = CheckInRepository(factory) + private fun createInstance(scope: CoroutineScope) = CheckInRepository(factory, timeStamper) @Test fun `new entities should have ID 0`() = runBlockingTest { @@ -177,4 +181,45 @@ class CheckInRepositoryTest : BaseTest() { ) } } + + @Test + fun `checkInsWithinRetention() should filter out stale check-ins`() = runBlockingTest { + + // Now = Jan 16th 2020, 00:00 + // CheckIns should be kept for 15 days, so every check-in with an end date before + // Jan 1st 2020, 00:00 should get deleted + every { timeStamper.nowUTC } returns Instant.parse("2020-01-16T00:00:00.000Z") + + val checkInWithinRetention = createCheckIn(Instant.parse("2020-01-01T00:00:00.000Z")) + + // should be filtered out + val staleCheckIn = createCheckIn(Instant.parse("2019-12-31T23:59:59.000Z")) + + every { checkInDao.allEntries() } returns flowOf( + listOf( + staleCheckIn.toEntity(), + checkInWithinRetention.toEntity() + ) + ) + + createInstance(scope = this).checkInsWithinRetention.first() shouldBe + listOf(checkInWithinRetention) + } + + private fun createCheckIn(checkOutDate: Instant) = CheckIn( + traceLocationId = "traceLocationId1".encode(), + version = 1, + type = 1, + description = "", + address = "", + traceLocationStart = null, + traceLocationEnd = null, + defaultCheckInLengthInMinutes = 30, + cryptographicSeed = "cryptographicSeed".encode(), + cnPublicKey = "cnPublicKey", + checkInStart = Instant.parse("1970-01-01T00:00:00.000Z"), + checkInEnd = checkOutDate, + completed = true, + createJournalEntry = true + ) } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRetentionTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRetentionTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..c95a3d181f9774bf3f772bd239d6122dce82635b --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRetentionTest.kt @@ -0,0 +1,44 @@ +package de.rki.coronawarnapp.eventregistration.checkins + +import io.kotest.matchers.shouldBe +import okio.ByteString.Companion.encode +import org.joda.time.Instant +import org.junit.jupiter.api.Test + +internal class CheckInRetentionTest { + + @Test + fun `isWithinRetention() and isOutOfRetention() should return correct result`() { + + // Now = Jan 16th 2020, 00:00 + // CheckIns should be kept for 15 days, so every check-in with an end date before + // Jan 1st 2020 is out of retention + val now = Instant.parse("2020-01-16T00:00:00.000Z") + + val checkInWithinRetention = createCheckIn(Instant.parse("2020-01-01T00:00:00.000Z")) + val checkInOutOfRetention = createCheckIn(Instant.parse("2019-12-31T23:59:59.000Z")) + + checkInWithinRetention.isWithinRetention(now) shouldBe true + checkInWithinRetention.isOutOfRetention(now) shouldBe false + + checkInOutOfRetention.isWithinRetention(now) shouldBe false + checkInOutOfRetention.isOutOfRetention(now) shouldBe true + } + + private fun createCheckIn(checkOutDate: Instant) = CheckIn( + traceLocationId = "traceLocationId1".encode(), + version = 1, + type = 1, + description = "", + address = "", + traceLocationStart = null, + traceLocationEnd = null, + defaultCheckInLengthInMinutes = 30, + cryptographicSeed = "cryptographicSeed".encode(), + cnPublicKey = "cnPublicKey", + checkInStart = Instant.parse("1970-01-01T00:00:00.000Z"), + checkInEnd = checkOutDate, + completed = true, + createJournalEntry = true + ) +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/storage/repo/DefaultTraceLocationRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/storage/repo/DefaultTraceLocationRepositoryTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..89c9e4da754a01c06001e2e7e9fe2a4976054553 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/storage/repo/DefaultTraceLocationRepositoryTest.kt @@ -0,0 +1,74 @@ +package de.rki.coronawarnapp.eventregistration.storage.repo + +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation +import de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabase +import de.rki.coronawarnapp.eventregistration.storage.dao.TraceLocationDao +import de.rki.coronawarnapp.eventregistration.storage.entity.toTraceLocationEntity +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.util.TimeStamper +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runBlockingTest +import okio.ByteString.Companion.encode +import org.joda.time.Instant +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +internal class DefaultTraceLocationRepositoryTest : BaseTest() { + + @MockK lateinit var factory: TraceLocationDatabase.Factory + @MockK lateinit var database: TraceLocationDatabase + @MockK lateinit var appScope: CoroutineScope + @MockK lateinit var timeStamper: TimeStamper + @MockK lateinit var traceLocationDao: TraceLocationDao + + @BeforeEach + fun setUp() { + MockKAnnotations.init(this) + every { factory.create() } returns database + every { database.traceLocationDao() } returns traceLocationDao + } + + private fun createInstance() = DefaultTraceLocationRepository(factory, appScope, timeStamper) + + @Test + fun `getTraceLocationsWithinRetention() should filter out stale trace locations`() = runBlockingTest { + + // Now = Jan 16th 2020, 00:00 + // TraceLocations should be kept for 15 days, so every TraceLocation with an end date before + // Jan 1st 2020, 00:00 should get deleted + every { timeStamper.nowUTC } returns Instant.parse("2020-01-16T00:00:00.000Z") + + val traceLocationWithinRetention = createTraceLocationWithEndDate(Instant.parse("2020-01-01T00:00:00.000Z")) + + // should be filtered out + val staleTraceLocation = createTraceLocationWithEndDate(Instant.parse("2019-12-31T23:59:59.000Z")) + + every { traceLocationDao.allEntries() } returns flowOf( + listOf( + staleTraceLocation.toTraceLocationEntity(), + traceLocationWithinRetention.toTraceLocationEntity() + ) + ) + + createInstance().traceLocationsWithinRetention.first() shouldBe listOf(traceLocationWithinRetention) + } + + private fun createTraceLocationWithEndDate(endDate: Instant?) = TraceLocation( + id = 1, + type = TraceLocationOuterClass.TraceLocationType.UNRECOGNIZED, + description = "", + address = "", + startDate = null, + endDate = endDate, + defaultCheckInLengthInMinutes = 30, + cryptographicSeed = "seed byte array".encode(), + cnPublicKey = "cnPublicKey" + ) +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/storage/retention/TraceLocationRetentionTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/storage/retention/TraceLocationRetentionTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..d0f58ebbc8de7827753a9d2f70cffd5423ec0b68 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/storage/retention/TraceLocationRetentionTest.kt @@ -0,0 +1,46 @@ +package de.rki.coronawarnapp.eventregistration.storage.retention + +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import io.kotest.matchers.shouldBe +import okio.ByteString.Companion.encode +import org.joda.time.Instant +import org.junit.jupiter.api.Test + +internal class TraceLocationRetentionTest { + + @Test + fun `isWithinRetention() and isOutOfRetention() should return correct result`() { + + // Now = Jan 16th 2020, 00:00 + // TraceLocations should be kept for 15 days, so every trace location with and end date before + // Jan 1st 2020, 00:00 should be out of retention + val now = Instant.parse("2020-01-16T00:00:00.000Z") + + val traceLocationWithinRetention = createTraceLocation(Instant.parse("2020-01-01T00:00:00.000Z")) + val traceLocationOutOfRetention = createTraceLocation(Instant.parse("2019-12-31T23:59:59.000Z")) + val traceLocationNoEndDate = createTraceLocation(null) + + traceLocationWithinRetention.isWithinRetention(now) shouldBe true + traceLocationWithinRetention.isOutOfRetention(now) shouldBe false + + traceLocationOutOfRetention.isWithinRetention(now) shouldBe false + traceLocationOutOfRetention.isOutOfRetention(now) shouldBe true + + // trace locations without end date are never out of retention + traceLocationNoEndDate.isWithinRetention(now) shouldBe true + traceLocationNoEndDate.isOutOfRetention(now) shouldBe false + } + + private fun createTraceLocation(endDate: Instant?) = TraceLocation( + id = 1, + type = TraceLocationOuterClass.TraceLocationType.UNRECOGNIZED, + description = "", + address = "", + startDate = null, + endDate = endDate, + defaultCheckInLengthInMinutes = 30, + cryptographicSeed = "seed byte array".encode(), + cnPublicKey = "cnPublicKey" + ) +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/MainActivityViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/MainActivityViewModelTest.kt index 349174209a5e60b842f29743c31c5be66cbee914..e456fa43ae2757a5bfa043e2ff923cc1cbb8b57f 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/MainActivityViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/MainActivityViewModelTest.kt @@ -44,7 +44,7 @@ class MainActivityViewModelTest : BaseTest() { every { environmentSetup.currentEnvironment } returns EnvironmentSetup.Type.WRU every { traceLocationSettings.onboardingStatus } returns TraceLocationSettings.OnboardingStatus.NOT_ONBOARDED every { onboardingSettings.isBackgroundCheckDone } returns true - every { checkInRepository.allCheckIns } returns MutableStateFlow(listOf()) + every { checkInRepository.checkInsWithinRetention } returns MutableStateFlow(listOf()) } private fun createInstance(): MainActivityViewModel = MainActivityViewModel( diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTaskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTaskTest.kt index e44656741f40654af7de030c9812598a591047f4..6b3aade7b48af1411204a8659818cebf487501fe 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTaskTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTaskTest.kt @@ -64,7 +64,7 @@ class PresenceTracingWarningTaskTest : BaseTest() { coEvery { markPackagesProcessed(any()) } just Runs } - coEvery { checkInsRepository.allCheckIns } returns flowOf(listOf(CHECKIN_1, CHECKIN_2)) + coEvery { checkInsRepository.checkInsWithinRetention } returns flowOf(listOf(CHECKIN_1, CHECKIN_2)) presenceTracingRiskRepository.apply { coEvery { deleteAllMatches() } just Runs @@ -89,7 +89,7 @@ class PresenceTracingWarningTaskTest : BaseTest() { coVerifySequence { syncTool.syncPackages() presenceTracingRiskRepository.deleteStaleData() - checkInsRepository.allCheckIns + checkInsRepository.checkInsWithinRetention traceWarningRepository.unprocessedWarningPackages checkInWarningMatcher.process(any(), any()) @@ -120,14 +120,14 @@ class PresenceTracingWarningTaskTest : BaseTest() { @Test fun `there are no check-ins to match against`() = runBlockingTest { - coEvery { checkInsRepository.allCheckIns } returns flowOf(emptyList()) + coEvery { checkInsRepository.checkInsWithinRetention } returns flowOf(emptyList()) createInstance().run(mockk()) shouldNotBe null coVerifySequence { syncTool.syncPackages() presenceTracingRiskRepository.deleteStaleData() - checkInsRepository.allCheckIns + checkInsRepository.checkInsWithinRetention presenceTracingRiskRepository.deleteAllMatches() presenceTracingRiskRepository.reportCalculation(successful = true) @@ -143,7 +143,7 @@ class PresenceTracingWarningTaskTest : BaseTest() { coVerifySequence { syncTool.syncPackages() presenceTracingRiskRepository.deleteStaleData() - checkInsRepository.allCheckIns + checkInsRepository.checkInsWithinRetention traceWarningRepository.unprocessedWarningPackages presenceTracingRiskRepository.reportCalculation(successful = true) @@ -160,7 +160,7 @@ class PresenceTracingWarningTaskTest : BaseTest() { coVerifySequence { syncTool.syncPackages() presenceTracingRiskRepository.deleteStaleData() - checkInsRepository.allCheckIns + checkInsRepository.checkInsWithinRetention traceWarningRepository.unprocessedWarningPackages checkInWarningMatcher.process(any(), any()) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt index 224be19421bd847fedfe9b9a13ffd8df820ed1bf..b18c793f78ebbb638b811100214a30343ca502a0 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt @@ -113,7 +113,7 @@ class SubmissionTaskTest : BaseTest() { every { timeStamper.nowUTC } returns Instant.EPOCH.plus(Duration.standardHours(1)) - every { checkInRepository.allCheckIns } returns flowOf(emptyList()) + every { checkInRepository.checkInsWithinRetention } returns flowOf(emptyList()) coEvery { checkInsTransformer.transform(any(), any()) } returns emptyList() } @@ -158,7 +158,7 @@ class SubmissionTaskTest : BaseTest() { settingSymptomsPreference.value tekHistoryCalculations.transformToKeyHistoryInExternalFormat(listOf(tek), userSymptoms) - checkInRepository.allCheckIns + checkInRepository.checkInsWithinRetention checkInsTransformer.transform(any(), any()) appConfigProvider.getAppConfig() diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModelTest.kt index bfbd074700697d48cbfc6eebf01d6c887164530c..c1adf45da1f8b75e2e530e7cd7bb86f1d120bf61 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModelTest.kt @@ -46,7 +46,7 @@ class CheckInsViewModelTest : BaseTest() { fun setup() { MockKAnnotations.init(this) every { savedState.set(any(), any<String>()) } just Runs - every { checkInsRepository.allCheckIns } returns flowOf() + every { checkInsRepository.checkInsWithinRetention } returns flowOf() every { cameraPermissionProvider.deniedPermanently } returns flowOf(false) } @@ -120,7 +120,7 @@ class CheckInsViewModelTest : BaseTest() { } val checkIns = listOf(checkIn1, checkIn2, checkIn3, checkIn4) - every { checkInsRepository.allCheckIns } returns flowOf(checkIns) + every { checkInsRepository.checkInsWithinRetention } returns flowOf(checkIns) createInstance(deepLink = null, scope = this).apply { checkins.getOrAwaitValue().apply { @@ -155,7 +155,7 @@ class CheckInsViewModelTest : BaseTest() { every { completed } returns false } - every { checkInsRepository.allCheckIns } returns flowOf(listOf(checkIn)) + every { checkInsRepository.checkInsWithinRetention } returns flowOf(listOf(checkIn)) every { cameraPermissionProvider.deniedPermanently } returns flowOf(true) createInstance(deepLink = null, scope = this).apply {