From 98da5decced1854a10f265bbe281b2f9cb693e92 Mon Sep 17 00:00:00 2001
From: Matthias Urhahn <matthias.urhahn@sap.com>
Date: Wed, 26 May 2021 16:47:01 +0200
Subject: [PATCH] Adjust reference time for deadman notification
 (EXPOSUREAPP-7386) (#3269)

* Use last successful diagnosis key download as time reference for the deadman notification.

* Use last successful diagnosis key download as time reference for the deadman notification.

* Reuse mocking function.

Co-authored-by: harambasicluka <64483219+harambasicluka@users.noreply.github.com>
Co-authored-by: Alex Paulescu <alex.paulescu@gmail.com>
---
 .../DeadmanNotificationTimeCalculation.kt     | 18 ++--
 .../download/DayPackageSyncTool.kt            |  3 +-
 .../download/HourPackageSyncTool.kt           |  3 +-
 .../diagnosiskeys/storage/CachedKeyInfo.kt    | 11 +--
 .../rki/coronawarnapp/risk/RiskLevelTask.kt   |  5 +-
 .../DeadmanNotificationTimeCalculationTest.kt | 89 +++++++++++++------
 .../download/CommonSyncToolTest.kt            | 63 ++++++++-----
 .../download/DayPackageSyncToolTest.kt        | 16 +---
 .../download/HourPackageSyncToolTest.kt       | 22 +----
 .../coronawarnapp/risk/RiskLevelTaskTest.kt   | 39 +++-----
 10 files changed, 145 insertions(+), 124 deletions(-)

diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deadman/DeadmanNotificationTimeCalculation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deadman/DeadmanNotificationTimeCalculation.kt
index a78205544..de21b1095 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deadman/DeadmanNotificationTimeCalculation.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deadman/DeadmanNotificationTimeCalculation.kt
@@ -1,7 +1,8 @@
 package de.rki.coronawarnapp.deadman
 
 import dagger.Reusable
-import de.rki.coronawarnapp.nearby.ENFClient
+import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository
+import de.rki.coronawarnapp.diagnosiskeys.storage.pkgDateTime
 import de.rki.coronawarnapp.util.TimeStamper
 import kotlinx.coroutines.flow.first
 import org.joda.time.DateTimeConstants
@@ -12,8 +13,8 @@ import javax.inject.Inject
 
 @Reusable
 class DeadmanNotificationTimeCalculation @Inject constructor(
-    val timeStamper: TimeStamper,
-    val enfClient: ENFClient
+    private val timeStamper: TimeStamper,
+    private val keyCacheRepository: KeyCacheRepository,
 ) {
 
     /**
@@ -29,10 +30,15 @@ class DeadmanNotificationTimeCalculation @Inject constructor(
      * If last success date time is null (eg: on application first start) - return [DEADMAN_NOTIFICATION_DELAY]
      */
     suspend fun getDelay(): Long {
-        val lastSuccess = enfClient.lastSuccessfulTrackedExposureDetection().first()?.finishedAt
-        Timber.d("enfClient.lastSuccessfulTrackedExposureDetection: $lastSuccess")
+        val lastSuccess = keyCacheRepository.allCachedKeys()
+            .first()
+            .filter { it.info.isDownloadComplete }
+            .maxByOrNull { it.info.pkgDateTime }
+            ?.info
+
+        Timber.d("Last successful diagnosis key package download: $lastSuccess")
         return if (lastSuccess != null) {
-            getHoursDiff(lastSuccess).toLong()
+            getHoursDiff(lastSuccess.pkgDateTime.toInstant()).toLong()
         } else {
             (DEADMAN_NOTIFICATION_DELAY * DateTimeConstants.MINUTES_PER_HOUR).toLong()
         }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DayPackageSyncTool.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DayPackageSyncTool.kt
index 7598c00a6..6744d5a6a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DayPackageSyncTool.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DayPackageSyncTool.kt
@@ -9,6 +9,7 @@ import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode
 import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKey
 import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKeyInfo.Type
 import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository
+import de.rki.coronawarnapp.diagnosiskeys.storage.pkgDateTime
 import de.rki.coronawarnapp.exception.http.CwaUnknownHostException
 import de.rki.coronawarnapp.storage.DeviceStorage
 import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc
@@ -81,7 +82,7 @@ class DayPackageSyncTool @Inject constructor(
     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
     internal fun expectNewDayPackages(cachedDays: List<CachedKey>): Boolean {
         val yesterday = timeStamper.nowUTC.toLocalDateUtc().minusDays(1)
-        val newestDay = cachedDays.map { it.info.toDateTime() }.maxOrNull()?.toLocalDate()
+        val newestDay = cachedDays.map { it.info.pkgDateTime }.maxOrNull()?.toLocalDate()
 
         return yesterday != newestDay
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncTool.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncTool.kt
index caa3143bc..6a2c01262 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncTool.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncTool.kt
@@ -9,6 +9,7 @@ import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode
 import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKey
 import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKeyInfo.Type
 import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository
+import de.rki.coronawarnapp.diagnosiskeys.storage.pkgDateTime
 import de.rki.coronawarnapp.exception.http.CwaServerError
 import de.rki.coronawarnapp.exception.http.CwaUnknownHostException
 import de.rki.coronawarnapp.exception.http.NetworkConnectTimeoutException
@@ -126,7 +127,7 @@ class HourPackageSyncTool @Inject constructor(
     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
     internal fun expectNewHourPackages(cachedHours: List<CachedKey>, now: Instant): Boolean {
         val today = now.toDateTime(DateTimeZone.UTC)
-        val newestHour = cachedHours.map { it.info.toDateTime() }.maxOrNull()
+        val newestHour = cachedHours.map { it.info.pkgDateTime }.maxOrNull()
 
         return today.minusHours(1).hourOfDay != newestHour?.hourOfDay || today.toLocalDate() != newestHour.toLocalDate()
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/storage/CachedKeyInfo.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/storage/CachedKeyInfo.kt
index ada80369a..b7d7f3954 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/storage/CachedKeyInfo.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/storage/CachedKeyInfo.kt
@@ -50,11 +50,6 @@ data class CachedKeyInfo(
         isDownloadComplete = true
     )
 
-    fun toDateTime(): DateTime = when (type) {
-        Type.LOCATION_DAY -> day.toDateTimeAtStartOfDay(DateTimeZone.UTC)
-        Type.LOCATION_HOUR -> day.toDateTime(hour, DateTimeZone.UTC)
-    }
-
     companion object {
         fun calcluateId(
             location: LocationCode,
@@ -89,3 +84,9 @@ data class CachedKeyInfo(
         @ColumnInfo(name = "completed") val isDownloadComplete: Boolean
     )
 }
+
+val CachedKeyInfo.pkgDateTime: DateTime
+    get() = when (type) {
+        CachedKeyInfo.Type.LOCATION_DAY -> day.toDateTimeAtStartOfDay(DateTimeZone.UTC)
+        CachedKeyInfo.Type.LOCATION_HOUR -> day.toDateTime(hour, DateTimeZone.UTC)
+    }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt
index cab719c6c..ea42e6f17 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt
@@ -7,6 +7,7 @@ import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig
 import de.rki.coronawarnapp.coronatest.CoronaTestRepository
 import de.rki.coronawarnapp.datadonation.analytics.modules.exposurewindows.AnalyticsExposureWindowCollector
 import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository
+import de.rki.coronawarnapp.diagnosiskeys.storage.pkgDateTime
 import de.rki.coronawarnapp.exception.ExceptionCategory
 import de.rki.coronawarnapp.exception.reporting.report
 import de.rki.coronawarnapp.nearby.ENFClient
@@ -124,14 +125,14 @@ class RiskLevelTask @Inject constructor(
         Timber.tag(TAG).d("Evaluating areKeyPkgsOutDated(nowUTC=%s)", nowUTC)
 
         val latestDownload = keyCacheRepository.getAllCachedKeys().maxByOrNull {
-            it.info.toDateTime()
+            it.info.pkgDateTime
         }
         if (latestDownload == null) {
             Timber.w("areKeyPkgsOutDated(): No downloads available, why is the RiskLevelTask running? Aborting!")
             return true
         }
 
-        val downloadAge = Duration(latestDownload.info.toDateTime(), nowUTC).also {
+        val downloadAge = Duration(latestDownload.info.pkgDateTime, nowUTC).also {
             Timber.d("areKeyPkgsOutDated(): Age is %dh for latest key package: %s", it.standardHours, latestDownload)
         }
 
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/deadman/DeadmanNotificationTimeCalculationTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/deadman/DeadmanNotificationTimeCalculationTest.kt
index f83e96389..ef310d304 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/deadman/DeadmanNotificationTimeCalculationTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/deadman/DeadmanNotificationTimeCalculationTest.kt
@@ -1,16 +1,21 @@
 package de.rki.coronawarnapp.deadman
 
-import de.rki.coronawarnapp.nearby.ENFClient
-import de.rki.coronawarnapp.nearby.modules.detectiontracker.TrackedExposureDetection
+import de.rki.coronawarnapp.diagnosiskeys.download.createMockCachedKeyInfo
+import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKey
+import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository
 import de.rki.coronawarnapp.util.TimeStamper
 import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
+import io.mockk.coEvery
+import io.mockk.coVerify
 import io.mockk.every
 import io.mockk.impl.annotations.MockK
-import io.mockk.verify
-import kotlinx.coroutines.flow.flowOf
+import io.mockk.mockk
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.test.runBlockingTest
 import org.joda.time.Instant
+import org.joda.time.LocalDate
+import org.joda.time.LocalTime
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
 import testhelpers.BaseTest
@@ -18,79 +23,109 @@ import testhelpers.BaseTest
 class DeadmanNotificationTimeCalculationTest : BaseTest() {
 
     @MockK lateinit var timeStamper: TimeStamper
-    @MockK lateinit var enfClient: ENFClient
-    @MockK lateinit var mockExposureDetection: TrackedExposureDetection
+    @MockK lateinit var keyCacheRepository: KeyCacheRepository
+
+    private val allCachedKeysFlow = MutableStateFlow(emptyList<CachedKey>())
 
     @BeforeEach
     fun setup() {
         MockKAnnotations.init(this)
-        every { timeStamper.nowUTC } returns Instant.parse("2020-08-01T23:00:00.000Z")
-        every { enfClient.lastSuccessfulTrackedExposureDetection() } returns flowOf(mockExposureDetection)
+        every { timeStamper.nowUTC } returns Instant.parse("2020-08-01T23:00:00")
+        coEvery { keyCacheRepository.allCachedKeys() } returns allCachedKeysFlow
     }
 
     private fun createTimeCalculator() = DeadmanNotificationTimeCalculation(
         timeStamper = timeStamper,
-        enfClient = enfClient
+        keyCacheRepository = keyCacheRepository
     )
 
+    private fun mockCachedKey(
+        keyDay: LocalDate,
+        keyHour: LocalTime? = null,
+        isComplete: Boolean = true,
+    ): CachedKey = mockk<CachedKey>().apply {
+        every { info } returns createMockCachedKeyInfo(
+            dayIdentifier = keyDay,
+            hourIdentifier = keyHour,
+            isComplete = isComplete,
+        )
+    }
+
     @Test
     fun `12 hours difference`() {
-        every { timeStamper.nowUTC } returns Instant.parse("2020-08-28T14:00:00.000Z")
+        every { timeStamper.nowUTC } returns Instant.parse("2020-08-28T14:00:00")
 
-        createTimeCalculator().getHoursDiff(Instant.parse("2020-08-27T14:00:00.000Z")) shouldBe 720
+        createTimeCalculator().getHoursDiff(Instant.parse("2020-08-27T14:00:00")) shouldBe 720
     }
 
     @Test
     fun `negative time difference`() {
-        every { timeStamper.nowUTC } returns Instant.parse("2020-08-30T14:00:00.000Z")
+        every { timeStamper.nowUTC } returns Instant.parse("2020-08-30T14:00:00")
 
-        createTimeCalculator().getHoursDiff(Instant.parse("2020-08-27T14:00:00.000Z")) shouldBe -2160
+        createTimeCalculator().getHoursDiff(Instant.parse("2020-08-27T14:00:00")) shouldBe -2160
     }
 
     @Test
     fun `success in future case`() {
-        every { timeStamper.nowUTC } returns Instant.parse("2020-08-27T14:00:00.000Z")
+        every { timeStamper.nowUTC } returns Instant.parse("2020-08-27T14:00:00")
 
-        createTimeCalculator().getHoursDiff(Instant.parse("2020-08-27T15:00:00.000Z")) shouldBe 2220
+        createTimeCalculator().getHoursDiff(Instant.parse("2020-08-27T15:00:00")) shouldBe 2220
     }
 
     @Test
     fun `12 hours delay`() = runBlockingTest {
         every { timeStamper.nowUTC } returns Instant.parse("2020-08-28T14:00:00.000Z")
-        every { mockExposureDetection.finishedAt } returns Instant.parse("2020-08-27T14:00:00.000Z")
+        allCachedKeysFlow.value = listOf(
+            mockCachedKey(keyDay = LocalDate.parse("2020-08-27"), keyHour = LocalTime.parse("14:00:00"))
+        )
 
         createTimeCalculator().getDelay() shouldBe 720
 
-        verify(exactly = 1) { enfClient.lastSuccessfulTrackedExposureDetection() }
+        coVerify(exactly = 1) { keyCacheRepository.allCachedKeys() }
+    }
+
+    @Test
+    fun `12 hours delay - only completed results count`() = runBlockingTest {
+        every { timeStamper.nowUTC } returns Instant.parse("2020-08-28T14:00:00.000Z")
+        allCachedKeysFlow.value = listOf(
+            mockCachedKey(keyDay = LocalDate.parse("2020-08-27"), keyHour = LocalTime.parse("14:00:00")),
+            mockCachedKey(
+                keyDay = LocalDate.parse("2020-08-27"),
+                keyHour = LocalTime.parse("16:00:00"),
+                isComplete = false
+            )
+        )
+
+        createTimeCalculator().getDelay() shouldBe 720
+
+        coVerify(exactly = 1) { keyCacheRepository.allCachedKeys() }
     }
 
     @Test
     fun `negative delay`() = runBlockingTest {
         every { timeStamper.nowUTC } returns Instant.parse("2020-08-30T14:00:00.000Z")
-        every { mockExposureDetection.finishedAt } returns Instant.parse("2020-08-27T14:00:00.000Z")
+        allCachedKeysFlow.value = listOf(
+            mockCachedKey(keyDay = LocalDate.parse("2020-08-27"), keyHour = LocalTime.parse("14:00:00")),
+        )
 
         createTimeCalculator().getDelay() shouldBe -2160
-
-        verify(exactly = 1) { enfClient.lastSuccessfulTrackedExposureDetection() }
     }
 
     @Test
     fun `success in future delay`() = runBlockingTest {
         every { timeStamper.nowUTC } returns Instant.parse("2020-08-27T14:00:00.000Z")
-        every { mockExposureDetection.finishedAt } returns Instant.parse("2020-08-27T15:00:00.000Z")
+        allCachedKeysFlow.value = listOf(
+            mockCachedKey(keyDay = LocalDate.parse("2020-08-27"), keyHour = LocalTime.parse("15:00:00")),
+        )
 
         createTimeCalculator().getDelay() shouldBe 2220
-
-        verify(exactly = 1) { enfClient.lastSuccessfulTrackedExposureDetection() }
     }
 
     @Test
     fun `initial delay - no successful calculations yet`() = runBlockingTest {
-        every { timeStamper.nowUTC } returns Instant.parse("2020-08-27T14:00:00.000Z")
-        every { enfClient.lastSuccessfulTrackedExposureDetection() } returns flowOf(null)
+        every { timeStamper.nowUTC } returns Instant.parse("2020-08-27T14:00:00")
+        allCachedKeysFlow.value = emptyList()
 
         createTimeCalculator().getDelay() shouldBe 2160
-
-        verify(exactly = 1) { enfClient.lastSuccessfulTrackedExposureDetection() }
     }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/CommonSyncToolTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/CommonSyncToolTest.kt
index 29b079f8e..ff29aad05 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/CommonSyncToolTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/CommonSyncToolTest.kt
@@ -38,9 +38,6 @@ abstract class CommonSyncToolTest : BaseIOTest() {
 
     private val testDir = File(IO_TEST_BASEDIR, this::class.simpleName!!)
 
-    internal val String.loc get() = LocationCode(this)
-    internal val String.day get() = LocalDate.parse(this)
-    internal val String.hour get() = LocalTime.parse(this)
     val keyRepoData = mutableMapOf<String, CachedKey>()
 
     @BeforeEach
@@ -130,28 +127,12 @@ abstract class CommonSyncToolTest : BaseIOTest() {
         hourIdentifier: LocalTime?,
         isComplete: Boolean = true
     ): CachedKey {
-        var keyInfo = CachedKeyInfo(
-            type = when (hourIdentifier) {
-                null -> CachedKeyInfo.Type.LOCATION_DAY
-                else -> CachedKeyInfo.Type.LOCATION_HOUR
-            },
+        val keyInfo = createMockCachedKeyInfo(
             location = location,
-            day = dayIdentifier,
-            hour = hourIdentifier,
-            createdAt = when (hourIdentifier) {
-                null -> dayIdentifier.toLocalDateTime(LocalTime.MIDNIGHT).toDateTime(DateTimeZone.UTC).toInstant()
-                else -> dayIdentifier.toLocalDateTime(hourIdentifier).toDateTime(DateTimeZone.UTC).toInstant()
-            }
+            dayIdentifier = dayIdentifier,
+            hourIdentifier = hourIdentifier,
+            isComplete = isComplete
         )
-        if (isComplete) {
-            keyInfo = keyInfo.copy(
-                etag = when (hourIdentifier) {
-                    null -> "$location-$dayIdentifier"
-                    else -> "$location-$dayIdentifier-$hourIdentifier"
-                },
-                isDownloadComplete = true
-            )
-        }
         Timber.i("mockKeyCacheCreateEntry(...): %s", keyInfo)
         val file = File(testDir, keyInfo.id)
         file.createNewFile()
@@ -160,3 +141,39 @@ abstract class CommonSyncToolTest : BaseIOTest() {
         }
     }
 }
+
+internal val String.loc get() = LocationCode(this)
+internal val String.day get() = LocalDate.parse(this)
+internal val String.hour get() = LocalTime.parse(this)
+
+fun createMockCachedKeyInfo(
+    dayIdentifier: LocalDate,
+    hourIdentifier: LocalTime?,
+    isComplete: Boolean,
+    location: LocationCode = "EUR".loc,
+): CachedKeyInfo {
+    var keyInfo = CachedKeyInfo(
+        type = when (hourIdentifier) {
+            null -> CachedKeyInfo.Type.LOCATION_DAY
+            else -> CachedKeyInfo.Type.LOCATION_HOUR
+        },
+        location = location,
+        day = dayIdentifier,
+        hour = hourIdentifier,
+        createdAt = when (hourIdentifier) {
+            null -> dayIdentifier.toLocalDateTime(LocalTime.MIDNIGHT).toDateTime(DateTimeZone.UTC).toInstant()
+            else -> dayIdentifier.toLocalDateTime(hourIdentifier).toDateTime(DateTimeZone.UTC).toInstant()
+        }
+    )
+    if (isComplete) {
+        keyInfo = keyInfo.copy(
+            etag = when (hourIdentifier) {
+                null -> "$location-$dayIdentifier"
+                else -> "$location-$dayIdentifier-$hourIdentifier"
+            },
+            isDownloadComplete = true
+        )
+    }
+    Timber.i("createMockCachedKeyInfo(...): %s", keyInfo)
+    return keyInfo
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/DayPackageSyncToolTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/DayPackageSyncToolTest.kt
index 17c498967..2b9a8f4ce 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/DayPackageSyncToolTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/DayPackageSyncToolTest.kt
@@ -1,17 +1,13 @@
 package de.rki.coronawarnapp.diagnosiskeys.download
 
 import de.rki.coronawarnapp.appconfig.mapping.RevokedKeyPackage
-import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKey
-import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKeyInfo
 import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKeyInfo.Type
 import io.kotest.matchers.shouldBe
 import io.mockk.coEvery
 import io.mockk.coVerify
 import io.mockk.coVerifySequence
 import io.mockk.every
-import io.mockk.mockk
 import kotlinx.coroutines.test.runBlockingTest
-import org.joda.time.DateTimeZone
 import org.joda.time.Instant
 import org.junit.jupiter.api.AfterEach
 import org.junit.jupiter.api.BeforeEach
@@ -98,16 +94,8 @@ class DayPackageSyncToolTest : CommonSyncToolTest() {
 
     @Test
     fun `EXPECT_NEW_DAY_PACKAGES evaluation`() = runBlockingTest {
-        val cachedKey1 = mockk<CachedKey>().apply {
-            every { info } returns mockk<CachedKeyInfo>().apply {
-                every { toDateTime() } returns Instant.parse("2020-10-30T01:02:03.000Z").toDateTime(DateTimeZone.UTC)
-            }
-        }
-        val cachedKey2 = mockk<CachedKey>().apply {
-            every { info } returns mockk<CachedKeyInfo>().apply {
-                every { toDateTime() } returns Instant.parse("2020-10-31T01:02:03.000Z").toDateTime(DateTimeZone.UTC)
-            }
-        }
+        val cachedKey1 = mockCachedDay("EUR".loc, "2020-10-30".day)
+        val cachedKey2 = mockCachedDay("EUR".loc, "2020-10-31".day)
 
         val instance = createInstance()
 
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncToolTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncToolTest.kt
index 0a58b9578..1f5590a38 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncToolTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncToolTest.kt
@@ -1,8 +1,6 @@
 package de.rki.coronawarnapp.diagnosiskeys.download
 
 import de.rki.coronawarnapp.appconfig.mapping.RevokedKeyPackage
-import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKey
-import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKeyInfo
 import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKeyInfo.Type
 import de.rki.coronawarnapp.exception.http.NetworkConnectTimeoutException
 import io.kotest.matchers.shouldBe
@@ -10,9 +8,7 @@ import io.mockk.coEvery
 import io.mockk.coVerify
 import io.mockk.coVerifySequence
 import io.mockk.every
-import io.mockk.mockk
 import kotlinx.coroutines.test.runBlockingTest
-import org.joda.time.DateTimeZone
 import org.joda.time.Instant
 import org.junit.jupiter.api.AfterEach
 import org.junit.jupiter.api.BeforeEach
@@ -184,16 +180,8 @@ class HourPackageSyncToolTest : CommonSyncToolTest() {
 
     @Test
     fun `EXPECT_NEW_HOUR_PACKAGES evaluation`() = runBlockingTest {
-        val cachedKey1 = mockk<CachedKey>().apply {
-            every { info } returns mockk<CachedKeyInfo>().apply {
-                every { toDateTime() } returns Instant.parse("2020-01-01T00:00:03.000Z").toDateTime(DateTimeZone.UTC)
-            }
-        }
-        val cachedKey2 = mockk<CachedKey>().apply {
-            every { info } returns mockk<CachedKeyInfo>().apply {
-                every { toDateTime() } returns Instant.parse("2020-01-01T01:00:03.000Z").toDateTime(DateTimeZone.UTC)
-            }
-        }
+        val cachedKey1 = mockCachedHour("EUR".loc, "2020-01-01".day, "00:00".hour)
+        val cachedKey2 = mockCachedHour("EUR".loc, "2020-01-01".day, "01:00".hour)
 
         val instance = createInstance()
 
@@ -207,11 +195,7 @@ class HourPackageSyncToolTest : CommonSyncToolTest() {
 
     @Test
     fun `EXPECT_NEW_HOUR_PACKAGES does not get confused by same hour on next day`() = runBlockingTest {
-        val cachedKey1 = mockk<CachedKey>().apply {
-            every { info } returns mockk<CachedKeyInfo>().apply {
-                every { toDateTime() } returns Instant.parse("2020-01-01T00:00:03.000Z").toDateTime(DateTimeZone.UTC)
-            }
-        }
+        val cachedKey1 = mockCachedHour("EUR".loc, "2020-01-01".day, "00:00".hour)
 
         val instance = createInstance()
 
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskTest.kt
index 7f3f3be61..728fc06a9 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskTest.kt
@@ -5,8 +5,8 @@ import de.rki.coronawarnapp.appconfig.ConfigData
 import de.rki.coronawarnapp.coronatest.CoronaTestRepository
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
 import de.rki.coronawarnapp.datadonation.analytics.modules.exposurewindows.AnalyticsExposureWindowCollector
+import de.rki.coronawarnapp.diagnosiskeys.download.createMockCachedKeyInfo
 import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKey
-import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKeyInfo
 import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository
 import de.rki.coronawarnapp.nearby.ENFClient
 import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult
@@ -64,11 +64,7 @@ class RiskLevelTaskTest : BaseTest() {
     private val testAggregatedResult = mockk<EwAggregatedRiskResult>().apply {
         every { isIncreasedRisk() } returns true
     }
-    private val testCachedKey = mockk<CachedKey>().apply {
-        every { info } returns mockk<CachedKeyInfo>().apply {
-            every { toDateTime() } returns testTimeNow.toDateTime().minusDays(1)
-        }
-    }
+    private val testCachedKey = mockCachedKey(testTimeNow.toDateTime().minusDays(1))
 
     @BeforeEach
     fun setup() {
@@ -119,6 +115,13 @@ class RiskLevelTaskTest : BaseTest() {
         analyticsExposureWindowCollector = analyticsExposureWindowCollector,
     )
 
+    private fun mockCachedKey(
+        dateTime: DateTime,
+        isComplete: Boolean = true,
+    ): CachedKey = mockk<CachedKey>().apply {
+        every { info } returns createMockCachedKeyInfo(dateTime.toLocalDate(), dateTime.toLocalTime(), isComplete)
+    }
+
     @Test
     fun `last used config ID is set after calculation`() = runBlockingTest {
         every { configData.isDeviceTimeCorrect } returns true
@@ -172,11 +175,7 @@ class RiskLevelTaskTest : BaseTest() {
 
     @Test
     fun `risk calculation is skipped if results are outdated while in background mode`() = runBlockingTest {
-        val cachedKey = mockk<CachedKey>().apply {
-            every { info } returns mockk<CachedKeyInfo>().apply {
-                every { toDateTime() } returns DateTime.parse("2020-12-28").minusDays(3)
-            }
-        }
+        val cachedKey = mockCachedKey(DateTime.parse("2020-12-28").minusDays(3))
         val now = Instant.parse("2020-12-28")
 
         coEvery { keyCacheRepository.getAllCachedKeys() } returns listOf(cachedKey)
@@ -191,11 +190,7 @@ class RiskLevelTaskTest : BaseTest() {
 
     @Test
     fun `risk calculation is skipped if results are outdated while no background mode`() = runBlockingTest {
-        val cachedKey = mockk<CachedKey>().apply {
-            every { info } returns mockk<CachedKeyInfo>().apply {
-                every { toDateTime() } returns DateTime.parse("2020-12-28").minusDays(3)
-            }
-        }
+        val cachedKey = mockCachedKey(DateTime.parse("2020-12-28").minusDays(3))
         val now = Instant.parse("2020-12-28")
 
         coEvery { keyCacheRepository.getAllCachedKeys() } returns listOf(cachedKey)
@@ -210,11 +205,7 @@ class RiskLevelTaskTest : BaseTest() {
 
     @Test
     fun `risk calculation is skipped if positive test is registered and viewed`() = runBlockingTest {
-        val cachedKey = mockk<CachedKey>().apply {
-            every { info } returns mockk<CachedKeyInfo>().apply {
-                every { toDateTime() } returns DateTime.parse("2020-12-28").minusDays(1)
-            }
-        }
+        val cachedKey = mockCachedKey(DateTime.parse("2020-12-28").minusDays(1))
         val now = Instant.parse("2020-12-28")
 
         coEvery { keyCacheRepository.getAllCachedKeys() } returns listOf(cachedKey)
@@ -236,11 +227,7 @@ class RiskLevelTaskTest : BaseTest() {
 
     @Test
     fun `risk calculation is not skipped if positive test is registered and not viewed`() = runBlockingTest {
-        val cachedKey = mockk<CachedKey>().apply {
-            every { info } returns mockk<CachedKeyInfo>().apply {
-                every { toDateTime() } returns DateTime.parse("2020-12-28").minusDays(1)
-            }
-        }
+        val cachedKey = mockCachedKey(DateTime.parse("2020-12-28").minusDays(1))
         val now = Instant.parse("2020-12-28")
         val aggregatedRiskResult = mockk<EwAggregatedRiskResult>().apply {
             every { isIncreasedRisk() } returns true
-- 
GitLab