diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDonor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDonor.kt index 34f8be0bb2a6df2029de72f0de6a2888206da592..4cf07f10e528732e5e7c71c6fc973c3cfb792983 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDonor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDonor.kt @@ -3,13 +3,10 @@ package de.rki.coronawarnapp.datadonation.analytics.modules.registeredtest import de.rki.coronawarnapp.datadonation.analytics.common.calculateDaysSinceMostRecentDateAtRiskLevelAtTestRegistration import de.rki.coronawarnapp.datadonation.analytics.modules.DonorModule import de.rki.coronawarnapp.datadonation.analytics.storage.TestResultDonorSettings -import de.rki.coronawarnapp.risk.RiskLevelSettings -import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.formatter.TestResult -import kotlinx.coroutines.flow.first import org.joda.time.Duration import org.joda.time.Instant import timber.log.Timber @@ -19,8 +16,6 @@ import javax.inject.Singleton @Singleton class TestResultDonor @Inject constructor( private val testResultDonorSettings: TestResultDonorSettings, - private val riskLevelSettings: RiskLevelSettings, - private val riskLevelStorage: RiskLevelStorage, private val timeStamper: TimeStamper, private val submissionSettings: SubmissionSettings ) : DonorModule { @@ -28,42 +23,66 @@ class TestResultDonor @Inject constructor( override suspend fun beginDonation(request: DonorModule.Request): DonorModule.Contribution { val scannedAfterConsent = testResultDonorSettings.testScannedAfterConsent.value if (!scannedAfterConsent) { - Timber.d("Skipping TestResultMetadata donation (testScannedAfterConsent=%s)", scannedAfterConsent) + Timber.d("Skipping TestResultMetadata donation (scannedAfterConsent=%s)", scannedAfterConsent) return TestResultMetadataNoContribution } val timestampAtRegistration = submissionSettings.initialTestResultReceivedAt - if (timestampAtRegistration == null) { - Timber.d("Skipping TestResultMetadata donation timestampAtRegistration isn't found") + Timber.d("Skipping TestResultMetadata donation (timestampAtRegistration is missing)") return TestResultMetadataNoContribution } - val configHours = request - .currentConfig - .analytics - .hoursSinceTestRegistrationToSubmitTestResultMetadata - - val hoursSinceTestRegistrationTime = Duration(timestampAtRegistration, timeStamper.nowUTC).standardHours.toInt() - val isDiffHoursMoreThanConfigHoursForPendingTest = hoursSinceTestRegistrationTime >= configHours + val testResultAtRegistration = testResultDonorSettings.testResultAtRegistration.value + if (testResultAtRegistration == null) { + Timber.d("Skipping TestResultMetadata donation (testResultAtRegistration is missing)") + return TestResultMetadataNoContribution + } - val testResultAtRegistration = - testResultDonorSettings.testResultAtRegistration.value ?: return TestResultMetadataNoContribution + val lastChangeCheckedRiskLevelTimestamp = testResultDonorSettings.mostRecentDateWithHighOrLowRiskLevel.value + if (lastChangeCheckedRiskLevelTimestamp == null) { + Timber.d("Skipping TestResultMetadata donation (lastChangeCheckedRiskLevelTimestamp is missing)") + return TestResultMetadataNoContribution + } val daysSinceMostRecentDateAtRiskLevelAtTestRegistration = calculateDaysSinceMostRecentDateAtRiskLevelAtTestRegistration( - riskLevelSettings.lastChangeCheckedRiskLevelTimestamp, + lastChangeCheckedRiskLevelTimestamp, timestampAtRegistration ) + Timber.i( + "daysSinceMostRecentDateAtRiskLevelAtTestRegistration: %s", + daysSinceMostRecentDateAtRiskLevelAtTestRegistration + ) + val riskLevelAtRegistration = testResultDonorSettings.riskLevelAtTestRegistration.value + val highRiskResultCalculatedAt = testResultDonorSettings.riskLevelTurnedRedTime.value val hoursSinceHighRiskWarningAtTestRegistration = if (riskLevelAtRegistration == PpaData.PPARiskLevel.RISK_LEVEL_LOW) { DEFAULT_HOURS_SINCE_HIGH_RISK_WARNING } else { - calculatedHoursSinceHighRiskWarning(timestampAtRegistration) + if (highRiskResultCalculatedAt == null) { + Timber.d("Skipping TestResultMetadata donation (highRiskResultCalculatedAt is missing)") + return TestResultMetadataNoContribution + } + + Timber.i( + "highRiskResultCalculatedAt: %s, timestampAtRegistration: %s", + highRiskResultCalculatedAt, + timestampAtRegistration + ) + calculatedHoursSinceHighRiskWarning(highRiskResultCalculatedAt, timestampAtRegistration) } + Timber.i( + "hoursSinceHighRiskWarningAtTestRegistration: %s", + hoursSinceHighRiskWarningAtTestRegistration + ) + + val configHours = request.currentConfig.analytics.hoursSinceTestRegistrationToSubmitTestResultMetadata + val hoursSinceTestRegistrationTime = Duration(timestampAtRegistration, timeStamper.nowUTC).standardHours.toInt() + val isDiffHoursMoreThanConfigHoursForPendingTest = hoursSinceTestRegistrationTime >= configHours return when { /** @@ -134,8 +153,13 @@ class TestResultDonor @Inject constructor( ): DonorModule.Contribution { val finalTestResultReceivedAt = testResultDonorSettings.finalTestResultReceivedAt.value val hoursSinceTestRegistrationTime = if (finalTestResultReceivedAt != null) { - Duration(registrationTime, finalTestResultReceivedAt).standardHours.toInt() + Timber.i("finalTestResultReceivedAt: %s", finalTestResultReceivedAt) + Timber.i("registrationTime: %s", registrationTime) + Duration(registrationTime, finalTestResultReceivedAt).standardHours.toInt().also { + Timber.i("Calculated hoursSinceTestRegistrationTime: %s", it) + } } else { + Timber.i("Default hoursSinceTestRegistrationTime") DEFAULT_HOURS_SINCE_TEST_REGISTRATION_TIME } @@ -153,14 +177,10 @@ class TestResultDonor @Inject constructor( return TestResultMetadataContribution(testResultMetaData, ::cleanUp) } - private suspend fun calculatedHoursSinceHighRiskWarning(registrationTime: Instant): Int { - val highRiskResultCalculatedAt = riskLevelStorage - .latestAndLastSuccessful - .first() - .filter { it.isIncreasedRisk } - .minByOrNull { it.calculatedAt } - ?.calculatedAt ?: return DEFAULT_HOURS_SINCE_HIGH_RISK_WARNING - + private fun calculatedHoursSinceHighRiskWarning( + highRiskResultCalculatedAt: Instant, + registrationTime: Instant + ): Int { return Duration( highRiskResultCalculatedAt, registrationTime diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/storage/TestResultDonorSettings.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/storage/TestResultDonorSettings.kt index 11b76710c64c003c3c22715ce996e2afaee6d2f3..ccc15cb891da84070ffc49410ffcb03cc6acafff 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/storage/TestResultDonorSettings.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/storage/TestResultDonorSettings.kt @@ -65,6 +65,34 @@ class TestResultDonorSettings @Inject constructor( } ) + val mostRecentDateWithHighOrLowRiskLevel = prefs.createFlowPreference( + key = PREFS_KEY_MOST_RECENT_WITH_HIGH_OR_LOW_RISK_LEVEL, + reader = { key -> + getLong(key, 0L).let { + if (it != 0L) { + Instant.ofEpochMilli(it) + } else null + } + }, + writer = { key, value -> + putLong(key, value?.millis ?: 0L) + } + ) + + val riskLevelTurnedRedTime = prefs.createFlowPreference( + key = PREFS_KEY_RISK_LEVEL_TURNED_RED_TIME, + reader = { key -> + getLong(key, 0L).let { + if (it != 0L) { + Instant.ofEpochMilli(it) + } else null + } + }, + writer = { key, value -> + putLong(key, value?.millis ?: 0L) + } + ) + fun saveTestResultDonorDataAtRegistration(testResult: TestResult, lastRiskResult: RiskLevelResult) { testScannedAfterConsent.update { true } testResultAtRegistration.update { testResult } @@ -82,5 +110,8 @@ class TestResultDonorSettings @Inject constructor( private const val PREFS_KEY_TEST_RESULT_AT_REGISTRATION = "testResultDonor.testResultAtRegistration" private const val PREFS_KEY_RISK_LEVEL_AT_REGISTRATION = "testResultDonor.riskLevelAtRegistration" private const val PREFS_KEY_FINAL_TEST_RESULT_RECEIVED_AT = "testResultDonor.finalTestResultReceivedAt" + private const val PREFS_KEY_RISK_LEVEL_TURNED_RED_TIME = "testResultDonor.riskLevelTurnedRedTime" + private const val PREFS_KEY_MOST_RECENT_WITH_HIGH_OR_LOW_RISK_LEVEL = + "testResultDonor.mostRecentWithHighOrLowRiskLevel" } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt index 8a6fe31799602ee50ff2aabe8522054b497c6d00..fbee2b6d4a04294dee741b7565c18047c0cb5631 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.annotation.VisibleForTesting import androidx.core.app.NotificationManagerCompat import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.datadonation.analytics.storage.TestResultDonorSettings import de.rki.coronawarnapp.datadonation.survey.Surveys import de.rki.coronawarnapp.notification.NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID import de.rki.coronawarnapp.notification.NotificationHelper @@ -34,7 +35,8 @@ class RiskLevelChangeDetector @Inject constructor( private val notificationHelper: NotificationHelper, private val surveys: Surveys, private val submissionSettings: SubmissionSettings, - private val tracingSettings: TracingSettings + private val tracingSettings: TracingSettings, + private val testResultDonorSettings: TestResultDonorSettings ) { fun launch() { @@ -65,9 +67,54 @@ class RiskLevelChangeDetector @Inject constructor( val oldRiskState = oldResult.riskState val newRiskState = newResult.riskState - Timber.d("Last state was $oldRiskState and current state is $newRiskState") + // Check sending a notification when risk level changes + checkSendingNotification(oldRiskState, newRiskState) + + // Save Survey related data based on the risk state + saveSurveyRiskState(oldRiskState, newRiskState, newResult) + + // Save TestDonor risk level timestamps + saveTestDonorRiskLevelAnalytics(newResult) + } + + private fun saveTestDonorRiskLevelAnalytics( + newRiskState: RiskLevelResult + ) { + // Save riskLevelTurnedRedTime if not already set before for high risk detection + Timber.i("riskLevelTurnedRedTime=%s", testResultDonorSettings.riskLevelTurnedRedTime.value) + if (testResultDonorSettings.riskLevelTurnedRedTime.value == null) { + if (newRiskState.isIncreasedRisk) { + testResultDonorSettings.riskLevelTurnedRedTime.update { + newRiskState.calculatedAt + } + Timber.i( + "riskLevelTurnedRedTime: newRiskState=%s, riskLevelTurnedRedTime=%s", + newRiskState.riskState, + newRiskState.calculatedAt + ) + } + } + + // Save most recent date of high or low risks + if (newRiskState.riskState in listOf(RiskState.INCREASED_RISK, RiskState.LOW_RISK)) { + Timber.i( + "mostRecentDateWithHighOrLowRiskLevel: newRiskState=%s, lastRiskEncounterAt=%s", + newRiskState.riskState, + newRiskState.lastRiskEncounterAt + ) + + testResultDonorSettings.mostRecentDateWithHighOrLowRiskLevel.update { + newRiskState.lastRiskEncounterAt + } + } + } + + private suspend fun checkSendingNotification( + oldRiskState: RiskState, + newRiskState: RiskState + ) { if (hasHighLowLevelChanged(oldRiskState, newRiskState) && !submissionSettings.isSubmissionSuccessful) { Timber.d("Notification Permission = ${notificationManagerCompat.areNotificationsEnabled()}") @@ -82,7 +129,13 @@ class RiskLevelChangeDetector @Inject constructor( Timber.d("Risk level changed and notification sent. Current Risk level is $newRiskState") } + } + private fun saveSurveyRiskState( + oldRiskState: RiskState, + newRiskState: RiskState, + newResult: RiskLevelResult + ) { if (oldRiskState == RiskState.INCREASED_RISK && newRiskState == RiskState.LOW_RISK) { tracingSettings.isUserToBeNotifiedOfLoweredRiskLevel.update { true } Timber.d("Risk level changed LocalData is updated. Current Risk level is $newRiskState") diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDonorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDonorTest.kt index f128a602166778abbbcae049ec4a8e24a0fefd15..ac97cc321836a06795df3bb79985366634134468 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDonorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDonorTest.kt @@ -4,8 +4,6 @@ import de.rki.coronawarnapp.appconfig.AnalyticsConfig import de.rki.coronawarnapp.appconfig.ConfigData import de.rki.coronawarnapp.datadonation.analytics.modules.DonorModule import de.rki.coronawarnapp.datadonation.analytics.storage.TestResultDonorSettings -import de.rki.coronawarnapp.risk.RiskLevelSettings -import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.util.TimeStamper @@ -31,8 +29,6 @@ import testhelpers.preferences.mockFlowPreference class TestResultDonorTest : BaseTest() { @MockK lateinit var testResultDonorSettings: TestResultDonorSettings - @MockK lateinit var riskLevelSettings: RiskLevelSettings - @MockK lateinit var riskLevelStorage: RiskLevelStorage @MockK lateinit var timeStamper: TimeStamper @MockK lateinit var submissionSettings: SubmissionSettings @@ -43,16 +39,16 @@ class TestResultDonorTest : BaseTest() { @BeforeEach fun setUp() { MockKAnnotations.init(this, true) + with(testResultDonorSettings) { + every { mostRecentDateWithHighOrLowRiskLevel } returns mockFlowPreference(baseTime) + every { riskLevelTurnedRedTime } returns mockFlowPreference(baseTime) + every { riskLevelAtTestRegistration } returns mockFlowPreference(PpaData.PPARiskLevel.RISK_LEVEL_LOW) + } every { timeStamper.nowUTC } returns baseTime - every { riskLevelSettings.lastChangeCheckedRiskLevelTimestamp } returns baseTime - every { testResultDonorSettings.riskLevelAtTestRegistration } returns - mockFlowPreference(PpaData.PPARiskLevel.RISK_LEVEL_LOW) every { submissionSettings.initialTestResultReceivedAt } returns baseTime testResultDonor = TestResultDonor( testResultDonorSettings, - riskLevelSettings, - riskLevelStorage, timeStamper, submissionSettings ) @@ -108,7 +104,9 @@ class TestResultDonorTest : BaseTest() { val timeDayBefore = baseTime.minus(Duration.standardDays(1)) every { submissionSettings.initialTestResultReceivedAt } returns timeDayBefore - every { riskLevelSettings.lastChangeCheckedRiskLevelTimestamp } returns timeDayBefore + every { testResultDonorSettings.mostRecentDateWithHighOrLowRiskLevel } returns mockFlowPreference( + timeDayBefore + ) val donation = testResultDonor.beginDonation(TestRequest) donation.shouldBeInstanceOf<TestResultDonor.TestResultMetadataContribution>() @@ -141,6 +139,86 @@ class TestResultDonorTest : BaseTest() { } } + @Test + fun `No donation when test is POSITIVE and HighRisk but riskLevelTurnedRedTime is missing`() = + runBlockingTest { + with(testResultDonorSettings) { + every { testScannedAfterConsent } returns mockFlowPreference(true) + every { testResultAtRegistration } returns mockFlowPreference(TestResult.POSITIVE) + every { finalTestResultReceivedAt } returns mockFlowPreference(baseTime) + every { riskLevelTurnedRedTime } returns mockFlowPreference(null) + every { riskLevelAtTestRegistration } returns mockFlowPreference(PpaData.PPARiskLevel.RISK_LEVEL_HIGH) + } + testResultDonor.beginDonation(TestRequest) shouldBe TestResultDonor.TestResultMetadataNoContribution + } + + @Test + fun `No donation when test is NEGATIVE and HighRisk but riskLevelTurnedRedTime is missing`() = + runBlockingTest { + with(testResultDonorSettings) { + every { testScannedAfterConsent } returns mockFlowPreference(true) + every { testResultAtRegistration } returns mockFlowPreference(TestResult.NEGATIVE) + every { finalTestResultReceivedAt } returns mockFlowPreference(baseTime) + every { riskLevelTurnedRedTime } returns mockFlowPreference(null) + every { riskLevelAtTestRegistration } returns mockFlowPreference(PpaData.PPARiskLevel.RISK_LEVEL_HIGH) + } + testResultDonor.beginDonation(TestRequest) shouldBe TestResultDonor.TestResultMetadataNoContribution + } + + @Test + fun `No donation when test is POSITIVE and HighRisk but mostRecentDateWithHighOrLowRiskLevel is missing`() = + runBlockingTest { + with(testResultDonorSettings) { + every { testScannedAfterConsent } returns mockFlowPreference(true) + every { testResultAtRegistration } returns mockFlowPreference(TestResult.POSITIVE) + every { finalTestResultReceivedAt } returns mockFlowPreference(baseTime) + every { riskLevelTurnedRedTime } returns mockFlowPreference(baseTime) + every { mostRecentDateWithHighOrLowRiskLevel } returns mockFlowPreference(null) + every { riskLevelAtTestRegistration } returns mockFlowPreference(PpaData.PPARiskLevel.RISK_LEVEL_HIGH) + } + testResultDonor.beginDonation(TestRequest) shouldBe TestResultDonor.TestResultMetadataNoContribution + } + + @Test + fun `No donation when test is NEGATIVE and HighRisk but mostRecentDateWithHighOrLowRiskLevel is missing`() = + runBlockingTest { + with(testResultDonorSettings) { + every { testScannedAfterConsent } returns mockFlowPreference(true) + every { testResultAtRegistration } returns mockFlowPreference(TestResult.NEGATIVE) + every { finalTestResultReceivedAt } returns mockFlowPreference(baseTime) + every { riskLevelTurnedRedTime } returns mockFlowPreference(baseTime) + every { mostRecentDateWithHighOrLowRiskLevel } returns mockFlowPreference(null) + every { riskLevelAtTestRegistration } returns mockFlowPreference(PpaData.PPARiskLevel.RISK_LEVEL_HIGH) + } + testResultDonor.beginDonation(TestRequest) shouldBe TestResultDonor.TestResultMetadataNoContribution + } + + @Test + fun `No donation when test is POSITIVE and LowRisk but mostRecentDateWithHighOrLowRiskLevel is missing`() = + runBlockingTest { + with(testResultDonorSettings) { + every { testScannedAfterConsent } returns mockFlowPreference(true) + every { testResultAtRegistration } returns mockFlowPreference(TestResult.POSITIVE) + every { finalTestResultReceivedAt } returns mockFlowPreference(baseTime) + every { riskLevelTurnedRedTime } returns mockFlowPreference(null) + every { mostRecentDateWithHighOrLowRiskLevel } returns mockFlowPreference(null) + } + testResultDonor.beginDonation(TestRequest) shouldBe TestResultDonor.TestResultMetadataNoContribution + } + + @Test + fun `No donation when test is NEGATIVE and LowRisk but mostRecentDateWithHighOrLowRiskLevel is missing`() = + runBlockingTest { + with(testResultDonorSettings) { + every { testScannedAfterConsent } returns mockFlowPreference(true) + every { testResultAtRegistration } returns mockFlowPreference(TestResult.NEGATIVE) + every { finalTestResultReceivedAt } returns mockFlowPreference(baseTime) + every { riskLevelTurnedRedTime } returns mockFlowPreference(null) + every { mostRecentDateWithHighOrLowRiskLevel } returns mockFlowPreference(null) + } + testResultDonor.beginDonation(TestRequest) shouldBe TestResultDonor.TestResultMetadataNoContribution + } + @Test fun `Donation is collected when test result is NEGATIVE`() { runBlockingTest { @@ -160,6 +238,61 @@ class TestResultDonorTest : BaseTest() { } } + @Test + fun `Scenario 1 LowRisk`() = runBlockingTest { + with(testResultDonorSettings) { + every { testScannedAfterConsent } returns mockFlowPreference(true) + every { testResultAtRegistration } returns mockFlowPreference(TestResult.NEGATIVE) + every { finalTestResultReceivedAt } returns mockFlowPreference( + Instant.parse("2021-03-20T20:00:00Z") + ) + every { riskLevelTurnedRedTime } returns mockFlowPreference(null) // No High risk + every { mostRecentDateWithHighOrLowRiskLevel } returns + mockFlowPreference(Instant.parse("2021-03-18T00:00:00Z")) + every { riskLevelAtTestRegistration } returns mockFlowPreference(PpaData.PPARiskLevel.RISK_LEVEL_LOW) + } + every { timeStamper.nowUTC } returns Instant.parse("2021-03-20T00:00:00Z") + every { submissionSettings.initialTestResultReceivedAt } returns Instant.parse("2021-03-20T00:00:00Z") + + val donation = testResultDonor.beginDonation(TestRequest) + donation.shouldBeInstanceOf<TestResultDonor.TestResultMetadataContribution>() + with(donation.testResultMetadata) { + testResult shouldBe PpaData.PPATestResult.TEST_RESULT_NEGATIVE + hoursSinceTestRegistration shouldBe 20 // hours + riskLevelAtTestRegistration shouldBe PpaData.PPARiskLevel.RISK_LEVEL_LOW + hoursSinceHighRiskWarningAtTestRegistration shouldBe -1 // expected for low risk + daysSinceMostRecentDateAtRiskLevelAtTestRegistration shouldBe 2 // days + } + } + + @Test + fun `Scenario 2 HighRisk`() = runBlockingTest { + with(testResultDonorSettings) { + every { testScannedAfterConsent } returns mockFlowPreference(true) + every { testResultAtRegistration } returns mockFlowPreference(TestResult.POSITIVE) + every { finalTestResultReceivedAt } returns mockFlowPreference( + Instant.parse("2021-03-20T20:00:00Z") + ) + every { riskLevelTurnedRedTime } returns mockFlowPreference(Instant.parse("2021-03-01T00:00:00Z")) + every { mostRecentDateWithHighOrLowRiskLevel } returns + mockFlowPreference(Instant.parse("2021-03-18T00:00:00Z")) + every { riskLevelAtTestRegistration } returns mockFlowPreference(PpaData.PPARiskLevel.RISK_LEVEL_HIGH) + } + + every { timeStamper.nowUTC } returns Instant.parse("2021-03-20T00:00:00Z") + every { submissionSettings.initialTestResultReceivedAt } returns Instant.parse("2021-03-20T00:00:00Z") + + val donation = testResultDonor.beginDonation(TestRequest) + donation.shouldBeInstanceOf<TestResultDonor.TestResultMetadataContribution>() + with(donation.testResultMetadata) { + testResult shouldBe PpaData.PPATestResult.TEST_RESULT_POSITIVE + hoursSinceTestRegistration shouldBe 20 // hours + riskLevelAtTestRegistration shouldBe PpaData.PPARiskLevel.RISK_LEVEL_HIGH + hoursSinceHighRiskWarningAtTestRegistration shouldBe 456 // 19 days in hours + daysSinceMostRecentDateAtRiskLevelAtTestRegistration shouldBe 2 // days + } + } + @Test fun deleteData() = runBlockingTest { every { testResultDonorSettings.clear() } just Runs diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt index 13fea4bb9f7b0a2bb24c3848256cfe2431e2ba14..29f1c11c468e47c8e721dd77bb079f5eea69ff7e 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt @@ -3,6 +3,7 @@ package de.rki.coronawarnapp.risk import android.content.Context import androidx.core.app.NotificationManagerCompat import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import de.rki.coronawarnapp.datadonation.analytics.storage.TestResultDonorSettings import de.rki.coronawarnapp.datadonation.survey.Surveys import de.rki.coronawarnapp.notification.NotificationHelper import de.rki.coronawarnapp.risk.RiskState.CALCULATION_FAILED @@ -23,6 +24,7 @@ import io.mockk.coVerifySequence import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.just +import io.mockk.mockk import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runBlockingTest @@ -43,6 +45,7 @@ class RiskLevelChangeDetectorTest : BaseTest() { @MockK lateinit var surveys: Surveys @MockK lateinit var submissionSettings: SubmissionSettings @MockK lateinit var tracingSettings: TracingSettings + @MockK lateinit var testResultDonorSettings: TestResultDonorSettings @BeforeEach fun setup() { @@ -52,18 +55,27 @@ class RiskLevelChangeDetectorTest : BaseTest() { every { submissionSettings.isSubmissionSuccessful } returns false every { foregroundState.isInForeground } returns flowOf(true) every { notificationManagerCompat.areNotificationsEnabled() } returns true + every { riskLevelSettings.lastChangeCheckedRiskLevelTimestamp = any() } just Runs every { riskLevelSettings.lastChangeCheckedRiskLevelTimestamp } returns null + + every { riskLevelSettings.lastChangeToHighRiskLevelTimestamp = any() } just Runs + every { riskLevelSettings.lastChangeToHighRiskLevelTimestamp } returns null + coEvery { surveys.resetSurvey(Surveys.Type.HIGH_RISK_ENCOUNTER) } just Runs + + every { testResultDonorSettings.riskLevelTurnedRedTime } returns mockFlowPreference(null) + every { testResultDonorSettings.mostRecentDateWithHighOrLowRiskLevel } returns mockFlowPreference(null) } private fun createRiskLevel( riskState: RiskState, - calculatedAt: Instant = Instant.EPOCH + calculatedAt: Instant = Instant.EPOCH, + aggregatedRiskResult: AggregatedRiskResult? = null ): RiskLevelResult = object : RiskLevelResult { override val riskState: RiskState = riskState override val calculatedAt: Instant = calculatedAt - override val aggregatedRiskResult: AggregatedRiskResult? = null + override val aggregatedRiskResult: AggregatedRiskResult? = aggregatedRiskResult override val failureReason: RiskLevelResult.FailureReason? = null override val exposureWindows: List<ExposureWindow>? = null override val matchedKeyCount: Int = 0 @@ -80,7 +92,8 @@ class RiskLevelChangeDetectorTest : BaseTest() { notificationHelper = notificationHelper, surveys = surveys, submissionSettings = submissionSettings, - tracingSettings = tracingSettings + tracingSettings = tracingSettings, + testResultDonorSettings = testResultDonorSettings ) @Test @@ -191,6 +204,89 @@ class RiskLevelChangeDetectorTest : BaseTest() { } } + @Test + fun `riskLevelTurnedRedTime is only set once`() { + testResultDonorSettings.riskLevelTurnedRedTime.update { Instant.EPOCH.plus(1) } + + every { riskLevelStorage.latestRiskLevelResults } returns flowOf( + listOf( + createRiskLevel( + INCREASED_RISK, + calculatedAt = Instant.EPOCH.plus(2), + aggregatedRiskResult = mockk<AggregatedRiskResult>().apply { + every { isIncreasedRisk() } returns true + } + ), + createRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) + ) + ) + + runBlockingTest { + val instance = createInstance(scope = this) + instance.launch() + advanceUntilIdle() + } + + testResultDonorSettings.riskLevelTurnedRedTime.value shouldBe Instant.EPOCH.plus(1) + + testResultDonorSettings.riskLevelTurnedRedTime.update { null } + + runBlockingTest { + val instance = createInstance(scope = this) + instance.launch() + advanceUntilIdle() + } + + testResultDonorSettings.riskLevelTurnedRedTime.value shouldBe Instant.EPOCH.plus(2) + } + + @Test + fun `mostRecentDateWithHighOrLowRiskLevel is updated every time`() { + every { riskLevelStorage.latestRiskLevelResults } returns flowOf( + listOf( + createRiskLevel( + INCREASED_RISK, + calculatedAt = Instant.EPOCH.plus(1), + aggregatedRiskResult = mockk<AggregatedRiskResult>().apply { + every { mostRecentDateWithHighRisk } returns Instant.EPOCH.plus(10) + every { isIncreasedRisk() } returns true + } + ), + createRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) + ) + ) + + runBlockingTest { + val instance = createInstance(scope = this) + instance.launch() + advanceUntilIdle() + } + + testResultDonorSettings.mostRecentDateWithHighOrLowRiskLevel.value shouldBe Instant.EPOCH.plus(10) + + every { riskLevelStorage.latestRiskLevelResults } returns flowOf( + listOf( + createRiskLevel( + INCREASED_RISK, + calculatedAt = Instant.EPOCH.plus(1), + aggregatedRiskResult = mockk<AggregatedRiskResult>().apply { + every { mostRecentDateWithLowRisk } returns Instant.EPOCH.plus(20) + every { isIncreasedRisk() } returns false + } + ), + createRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) + ) + ) + + runBlockingTest { + val instance = createInstance(scope = this) + instance.launch() + advanceUntilIdle() + } + + testResultDonorSettings.mostRecentDateWithHighOrLowRiskLevel.value shouldBe Instant.EPOCH.plus(20) + } + @Test fun `evaluate risk level change detection function`() { RiskLevelChangeDetector.hasHighLowLevelChanged(CALCULATION_FAILED, CALCULATION_FAILED) shouldBe false