From d90f5a4554f83105fb9cf2a5ce605f0893400ef2 Mon Sep 17 00:00:00 2001 From: Matthias Urhahn <matthias.urhahn@sap.com> Date: Fri, 30 Apr 2021 12:33:21 +0200 Subject: [PATCH] Fix positive test abort condition for ENS key provision (EXPOSUREAPP-6879) (#3014) * Fix positive test abort condition for ENS submission. We want to abort if there is a positive test, and not just when we are allowed to submit (which is transient). * Also abort PresenceTracing risk calculation on positive tests. Co-authored-by: Mohamed <mohamed.metwalli@sap.com> --- .../download/DownloadDiagnosisKeysTask.kt | 26 +++++----- .../execution/PresenceTracingWarningTask.kt | 25 ++++++---- .../download/DownloadDiagnosisKeysTaskTest.kt | 4 +- .../PresenceTracingWarningTaskTest.kt | 47 ++++++++++++++++++- 4 files changed, 77 insertions(+), 25 deletions(-) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt index 9c415cdc5..ba1448d41 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt @@ -35,14 +35,14 @@ class DownloadDiagnosisKeysTask @Inject constructor( private val timeStamper: TimeStamper, private val settings: DownloadDiagnosisKeysSettings, private val coronaTestRepository: CoronaTestRepository, -) : Task<DownloadDiagnosisKeysTask.Progress, Task.Result> { +) : Task<DownloadDiagnosisKeysTask.Progress, DownloadDiagnosisKeysTask.Result> { private val internalProgress = ConflatedBroadcastChannel<Progress>() override val progress: Flow<Progress> = internalProgress.asFlow() private var isCanceled = false - override suspend fun run(arguments: Task.Arguments): Task.Result { + override suspend fun run(arguments: Task.Arguments): Result { val rollbackItems = mutableListOf<RollbackItem>() try { Timber.d("Running with arguments=%s", arguments) @@ -55,7 +55,7 @@ class DownloadDiagnosisKeysTask @Inject constructor( */ if (!enfClient.isTracingEnabled.first()) { Timber.tag(TAG).w("EN is not enabled, skipping RetrieveDiagnosisKeys") - return object : Task.Result {} + return Result() } throwIfCancelled() @@ -76,14 +76,14 @@ class DownloadDiagnosisKeysTask @Inject constructor( if (!exposureConfig.isDeviceTimeCorrect) { Timber.tag(TAG).w("Aborting, Device time is incorrect, offset=%s", exposureConfig.localOffset) - return object : Task.Result {} + return Result() } val now = timeStamper.nowUTC if (exposureConfig.maxExposureDetectionsPerUTCDay == 0) { Timber.tag(TAG).w("Exposure detections are disabled! maxExposureDetectionsPerUTCDay=0") - return object : Task.Result {} + return Result() } val trackedExposureDetections = enfClient.latestTrackedExposureDetection().first() @@ -93,12 +93,12 @@ class DownloadDiagnosisKeysTask @Inject constructor( if (!isUpdateToEnfV2 && wasLastDetectionPerformedRecently(now, exposureConfig, trackedExposureDetections)) { // At most one detection every 6h Timber.tag(TAG).i("task aborted, because detection was performed recently") - return object : Task.Result {} + return Result() } if (!isUpdateToEnfV2 && hasRecentDetectionAndNoNewFiles(now, keySyncResult, trackedExposureDetections)) { Timber.tag(TAG).i("task aborted, last check was within 24h, and there are no new files") - return object : Task.Result {} + return Result() } val availableKeyFiles = keySyncResult.availableKeys.map { it.path } @@ -114,10 +114,10 @@ class DownloadDiagnosisKeysTask @Inject constructor( // remember version code of this execution for next time settings.updateLastVersionCodeToCurrent() - val isAllowedToSubmitKeys = coronaTestRepository.coronaTests.first().any { it.isSubmissionAllowed } - if (isAllowedToSubmitKeys) { - Timber.tag(TAG).i("task aborted, positive test result") - return object : Task.Result {} + val isPositive = coronaTestRepository.coronaTests.first().any { it.isPositive } + if (isPositive) { + Timber.tag(TAG).i("EW risk calculation aborted, positive test result available.") + return Result() } Timber.tag(TAG).d("Attempting submission to ENF") @@ -129,7 +129,7 @@ class DownloadDiagnosisKeysTask @Inject constructor( internalProgress.send(Progress.ApiSubmissionFinished) - return object : Task.Result {} + return Result() } catch (error: Exception) { Timber.tag(TAG).e(error) @@ -215,6 +215,8 @@ class DownloadDiagnosisKeysTask @Inject constructor( isCanceled = true } + class Result : Task.Result + sealed class Progress : Task.Progress { object ApiSubmissionStarted : Progress() object ApiSubmissionFinished : Progress() 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 b5c6b195d..b8b64db0f 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 @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.presencetracing.risk.execution import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.bugreporting.reportProblem +import de.rki.coronawarnapp.coronatest.CoronaTestRepository import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.presencetracing.checkins.CheckInRepository @@ -17,9 +18,9 @@ import de.rki.coronawarnapp.util.TimeStamper import kotlinx.coroutines.channels.ConflatedBroadcastChannel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull import org.joda.time.Duration -import org.joda.time.Instant import timber.log.Timber import javax.inject.Inject import javax.inject.Provider @@ -31,7 +32,8 @@ class PresenceTracingWarningTask @Inject constructor( private val presenceTracingRiskRepository: PresenceTracingRiskRepository, private val traceWarningRepository: TraceWarningRepository, private val checkInsRepository: CheckInRepository, - private val presenceTracingRiskMapper: PresenceTracingRiskMapper + private val presenceTracingRiskMapper: PresenceTracingRiskMapper, + private val coronaTestRepository: CoronaTestRepository, ) : Task<PresenceTracingWarningTaskProgress, PresenceTracingWarningTask.Result> { private val internalProgress = ConflatedBroadcastChannel<PresenceTracingWarningTaskProgress>() @@ -59,7 +61,6 @@ class PresenceTracingWarningTask @Inject constructor( } private suspend fun doWork(): Result { - val nowUTC = timeStamper.nowUTC checkCancel() Timber.tag(TAG).d("Resetting config to make sure latest changes are considered.") @@ -75,7 +76,7 @@ class PresenceTracingWarningTask @Inject constructor( } else { Timber.tag(TAG).w("WarningPackage sync failed: %s", syncResult) presenceTracingRiskRepository.reportCalculation(successful = false) - return Result(calculatedAt = nowUTC) + return Result() } presenceTracingRiskRepository.deleteStaleData() @@ -89,7 +90,13 @@ class PresenceTracingWarningTask @Inject constructor( presenceTracingRiskRepository.reportCalculation(successful = true) - return Result(calculatedAt = nowUTC) + return Result() + } + + val isPositive = coronaTestRepository.coronaTests.first().any { it.isPositive } + if (isPositive) { + Timber.tag(TAG).i("PT risk calculation aborted, positive test result available.") + return Result() } val unprocessedPackages = traceWarningRepository.unprocessedWarningPackages.firstOrNull() ?: emptyList() @@ -100,7 +107,7 @@ class PresenceTracingWarningTask @Inject constructor( presenceTracingRiskRepository.reportCalculation(successful = true) - return Result(calculatedAt = nowUTC) + return Result() } Timber.tag(TAG).d("Running check-in matcher.") @@ -131,7 +138,7 @@ class PresenceTracingWarningTask @Inject constructor( matcherResult.processedPackages.map { it.warningPackage.packageId } ) - return Result(calculatedAt = nowUTC) + return Result() } private fun checkCancel() { @@ -143,9 +150,7 @@ class PresenceTracingWarningTask @Inject constructor( isCanceled = true } - data class Result( - val calculatedAt: Instant - ) : Task.Result + class Result : Task.Result data class Config( override val executionTimeout: Duration = Duration.standardMinutes(9), diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTaskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTaskTest.kt index 3ad5c90bc..55891e4f3 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTaskTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTaskTest.kt @@ -53,7 +53,7 @@ class DownloadDiagnosisKeysTaskTest : BaseTest() { private val coronaTests: MutableStateFlow<Set<CoronaTest>> = MutableStateFlow( setOf( - mockk<CoronaTest>().apply { every { isSubmissionAllowed } returns false } + mockk<CoronaTest>().apply { every { isPositive } returns false } ) ) @@ -241,7 +241,7 @@ class DownloadDiagnosisKeysTaskTest : BaseTest() { @Test fun `we do not submit keys if user got positive test results`() = runBlockingTest { coronaTests.value = setOf( - mockk<CoronaTest>().apply { every { isSubmissionAllowed } returns true } + mockk<CoronaTest>().apply { every { isPositive } returns true } ) createInstance().run(DownloadDiagnosisKeysTask.Arguments()) 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 57b4cfb40..79a5518b6 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 @@ -1,5 +1,7 @@ package de.rki.coronawarnapp.presencetracing.risk.execution +import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.presencetracing.checkins.CheckInRepository import de.rki.coronawarnapp.presencetracing.risk.calculation.CheckInWarningMatcher import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingRiskMapper @@ -24,6 +26,7 @@ import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.just import io.mockk.mockk +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runBlockingTest import org.joda.time.Duration @@ -42,11 +45,20 @@ class PresenceTracingWarningTaskTest : BaseTest() { @MockK lateinit var traceWarningRepository: TraceWarningRepository @MockK lateinit var checkInsRepository: CheckInRepository @MockK lateinit var presenceTracingRiskMapper: PresenceTracingRiskMapper + @MockK lateinit var coronaTestRepository: CoronaTestRepository + + private val coronaTests: MutableStateFlow<Set<CoronaTest>> = MutableStateFlow( + setOf( + mockk<CoronaTest>().apply { every { isPositive } returns false } + ) + ) @BeforeEach fun setup() { MockKAnnotations.init(this) + every { coronaTestRepository.coronaTests } returns coronaTests + every { timeStamper.nowUTC } returns Instant.ofEpochMilli(9000) coEvery { syncTool.syncPackages() } returns TraceWarningPackageSyncTool.SyncResult(successful = true) coEvery { checkInWarningMatcher.process(any(), any()) } answers { @@ -84,7 +96,8 @@ class PresenceTracingWarningTaskTest : BaseTest() { presenceTracingRiskRepository = presenceTracingRiskRepository, traceWarningRepository = traceWarningRepository, checkInsRepository = checkInsRepository, - presenceTracingRiskMapper = presenceTracingRiskMapper + presenceTracingRiskMapper = presenceTracingRiskMapper, + coronaTestRepository = coronaTestRepository, ) @Test @@ -95,6 +108,9 @@ class PresenceTracingWarningTaskTest : BaseTest() { syncTool.syncPackages() presenceTracingRiskRepository.deleteStaleData() checkInsRepository.checkInsWithinRetention + + coronaTestRepository.coronaTests + traceWarningRepository.unprocessedWarningPackages checkInWarningMatcher.process(any(), any()) @@ -229,6 +245,35 @@ class PresenceTracingWarningTaskTest : BaseTest() { PresenceTracingWarningTask.Config().executionTimeout shouldBeLessThan maxDuration } + @Test + fun `we do not submit keys if user got positive test results`() = runBlockingTest { + coronaTests.value = setOf( + mockk<CoronaTest>().apply { every { isPositive } returns true } + ) + + createInstance().run(mockk()) shouldNotBe null + + coVerifySequence { + syncTool.syncPackages() + presenceTracingRiskRepository.deleteStaleData() + checkInsRepository.checkInsWithinRetention + + coronaTestRepository.coronaTests + } + + coVerify(exactly = 0) { + traceWarningRepository.unprocessedWarningPackages + + checkInWarningMatcher.process(any(), any()) + + presenceTracingRiskRepository.reportCalculation( + successful = any(), + overlaps = any() + ) + traceWarningRepository.markPackagesProcessed(any()) + } + } + companion object { val CHECKIN_1 = createCheckIn( id = 2L, -- GitLab