diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfigContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfigContainer.kt index 0067d966bf262c1e11e7d5343bba64dc8ba9a381..f7f73a28d6d73bad04cf992a4f94ae769965299d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfigContainer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfigContainer.kt @@ -3,8 +3,10 @@ package de.rki.coronawarnapp.appconfig import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel data class PresenceTracingConfigContainer( - override val qrCodeErrorCorrectionLevel: ErrorCorrectionLevel, - override val revokedTraceLocationVersions: List<Int>, - override val riskCalculationParameters: PresenceTracingRiskCalculationParamContainer, - override val submissionParameters: PresenceTracingSubmissionParamContainer + override val qrCodeErrorCorrectionLevel: ErrorCorrectionLevel = ErrorCorrectionLevel.H, + override val revokedTraceLocationVersions: List<Int> = listOf(), + override val riskCalculationParameters: PresenceTracingRiskCalculationParamContainer = + PresenceTracingRiskCalculationParamContainer(), + override val submissionParameters: PresenceTracingSubmissionParamContainer = + PresenceTracingSubmissionParamContainer() ) : PresenceTracingConfig diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingRiskCalculationParamContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingRiskCalculationParamContainer.kt index b50b4432397bbaf2b3a2ac9671f08cbcec2f2ace..3a4e417bf802fc7327ae299dba19d908dc1b97b5 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingRiskCalculationParamContainer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingRiskCalculationParamContainer.kt @@ -4,7 +4,7 @@ import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParamete import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass.TransmissionRiskValueMapping data class PresenceTracingRiskCalculationParamContainer( - val transmissionRiskValueMapping: List<TransmissionRiskValueMapping>, - val normalizedTimePerCheckInToRiskLevelMapping: List<NormalizedTimeToRiskLevelMapping>, - val normalizedTimePerDayToRiskLevelMapping: List<NormalizedTimeToRiskLevelMapping> + val transmissionRiskValueMapping: List<TransmissionRiskValueMapping> = emptyList(), + val normalizedTimePerCheckInToRiskLevelMapping: List<NormalizedTimeToRiskLevelMapping> = emptyList(), + val normalizedTimePerDayToRiskLevelMapping: List<NormalizedTimeToRiskLevelMapping> = emptyList() ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingSubmissionParamContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingSubmissionParamContainer.kt index 1fdae47dac094ba17e9d309f575603d7b370ebba..1b25fb5da7df33e3dcad758843256f438c4d8e00 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingSubmissionParamContainer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingSubmissionParamContainer.kt @@ -7,6 +7,6 @@ import de.rki.coronawarnapp.server.protocols.internal.v2 .PresenceTracingParametersOuterClass.PresenceTracingSubmissionParameters.AerosoleDecayFunctionLinear data class PresenceTracingSubmissionParamContainer( - val durationFilters: List<DurationFilter>, - val aerosoleDecayLinearFunctions: List<AerosoleDecayFunctionLinear> + val durationFilters: List<DurationFilter> = emptyList(), + val aerosoleDecayLinearFunctions: List<AerosoleDecayFunctionLinear> = emptyList() ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/PresenceTracingConfigMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/PresenceTracingConfigMapper.kt index a25ebee282d5839a6192d5badcf864ee4573fd43..17db7fdd3b7a1f37827093af222b76ef50584164 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/PresenceTracingConfigMapper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/PresenceTracingConfigMapper.kt @@ -24,27 +24,14 @@ class PresenceTracingConfigMapper @Inject constructor() : PresenceTracingConfig. return PresenceTracingConfigContainer( qrCodeErrorCorrectionLevel = ErrorCorrectionLevel.H, revokedTraceLocationVersions = emptyList(), - riskCalculationParameters = emptyRiskCalculationParameters(), - submissionParameters = emptySubmissionParameters() + riskCalculationParameters = PresenceTracingRiskCalculationParamContainer(), + submissionParameters = PresenceTracingSubmissionParamContainer() ) } return rawConfig.presenceTracingConfig().also { Timber.i("PresenceTracingConfig: $it") } } - private fun emptySubmissionParameters() = - PresenceTracingSubmissionParamContainer( - durationFilters = emptyList(), - aerosoleDecayLinearFunctions = emptyList() - ) - - private fun emptyRiskCalculationParameters() = - PresenceTracingRiskCalculationParamContainer( - transmissionRiskValueMapping = emptyList(), - normalizedTimePerCheckInToRiskLevelMapping = emptyList(), - normalizedTimePerDayToRiskLevelMapping = emptyList() - ) - private fun PresenceTracingSubmissionParameters.mapSubmissionParameters() = PresenceTracingSubmissionParamContainer( durationFilters = durationFiltersList, @@ -73,14 +60,14 @@ class PresenceTracingConfigMapper @Inject constructor() : PresenceTracingConfig. riskCalculationParameters.mapRiskCalculationParameters() } else { Timber.w("RiskCalculationParameters are missing") - emptyRiskCalculationParameters() + PresenceTracingRiskCalculationParamContainer() } val submissionParameters = if (hasSubmissionParameters()) { submissionParameters.mapSubmissionParameters() } else { Timber.w("SubmissionParameters are missing") - emptySubmissionParameters() + PresenceTracingSubmissionParamContainer() } PresenceTracingConfigContainer( diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt index 9ee91b904419f724f56fca741f0eba9c361f8968..225c6afeae5ea5c9307518071e7287b666654753 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt @@ -2,8 +2,6 @@ package de.rki.coronawarnapp.eventregistration import dagger.Binds import dagger.Module -import de.rki.coronawarnapp.eventregistration.checkins.CheckInsTransformer -import de.rki.coronawarnapp.eventregistration.checkins.DefaultCheckInsTransformer import de.rki.coronawarnapp.eventregistration.checkins.download.DownloadedCheckInsRepo import de.rki.coronawarnapp.eventregistration.checkins.download.FakeDownloadedCheckInsRepo import de.rki.coronawarnapp.eventregistration.storage.repo.DefaultTraceLocationRepository @@ -11,10 +9,6 @@ import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationReposito @Module abstract class EventRegistrationModule { - - @Binds - abstract fun checkInsTransformer(transformer: DefaultCheckInsTransformer): CheckInsTransformer - @Binds abstract fun traceLocationRepository(defaultTraceLocationRepo: DefaultTraceLocationRepository): TraceLocationRepository diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformer.kt index 23fe7f823ea4952d32204e733cc55bc212677dbf..3efe193a7724d1dfd09a36605a5f213ba2f906db 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformer.kt @@ -1,7 +1,102 @@ package de.rki.coronawarnapp.eventregistration.checkins +import com.google.protobuf.ByteString +import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.eventregistration.checkins.derivetime.deriveTime +import de.rki.coronawarnapp.eventregistration.checkins.split.splitByMidnightUTC import de.rki.coronawarnapp.server.protocols.internal.pt.CheckInOuterClass +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.submission.Symptoms +import de.rki.coronawarnapp.submission.task.TransmissionRiskVector +import de.rki.coronawarnapp.submission.task.TransmissionRiskVectorDeterminator +import de.rki.coronawarnapp.util.TimeAndDateExtensions.derive10MinutesInterval +import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds +import de.rki.coronawarnapp.util.TimeAndDateExtensions.secondsToInstant +import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDate +import de.rki.coronawarnapp.util.TimeStamper +import org.joda.time.Days +import org.joda.time.Instant +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton -interface CheckInsTransformer { - fun transform(checkIns: List<CheckIn>): List<CheckInOuterClass.CheckIn> +@Singleton +class CheckInsTransformer @Inject constructor( + private val timeStamper: TimeStamper, + private val transmissionDeterminator: TransmissionRiskVectorDeterminator, + private val appConfigProvider: AppConfigProvider +) { + /** + * Transforms database [CheckIn]s into [CheckInOuterClass.CheckIn]s to submit + * them to the server. + * + * It derives the time for individual check-in and split it by midnight time UTC + * and map the result into a list of [CheckInOuterClass.CheckIn]s + * + * @param checkIns [List] of local database [CheckIn] + * @param symptoms [Symptoms] symptoms to calculate transmission risk level + */ + suspend fun transform(checkIns: List<CheckIn>, symptoms: Symptoms): List<CheckInOuterClass.CheckIn> { + + val submissionParamContainer = appConfigProvider + .getAppConfig() + .presenceTracing + .submissionParameters + + val transmissionVector = transmissionDeterminator.determine(symptoms) + + return checkIns.flatMap { originalCheckIn -> + Timber.d("Transforming check-in=$originalCheckIn") + val derivedTimes = submissionParamContainer.deriveTime( + originalCheckIn.checkInStart.seconds, + originalCheckIn.checkInEnd.seconds + ) + + if (derivedTimes == null) { + Timber.d("CheckIn can't be derived") + emptyList() // Excluded from submission + } else { + Timber.d("Derived times=$derivedTimes") + val derivedCheckIn = originalCheckIn.copy( + checkInStart = derivedTimes.startTimeSeconds.secondsToInstant(), + checkInEnd = derivedTimes.endTimeSeconds.secondsToInstant() + ) + derivedCheckIn.splitByMidnightUTC().map { checkIn -> + checkIn.toOuterCheckIn(transmissionVector) + } + } + } + } + + private fun CheckIn.toOuterCheckIn( + transmissionVector: TransmissionRiskVector + ): CheckInOuterClass.CheckIn { + val signedTraceLocation = TraceLocationOuterClass.SignedTraceLocation.newBuilder() + .setLocation(traceLocationBytes.toProtoByteString()) + .setSignature(signature.toProtoByteString()) + .build() + + return CheckInOuterClass.CheckIn.newBuilder() + .setSignedLocation(signedTraceLocation) + .setStartIntervalNumber(checkInStart.derive10MinutesInterval().toInt()) + .setEndIntervalNumber(checkInEnd.derive10MinutesInterval().toInt()) + .setTransmissionRiskLevel( + determineRiskTransmission(timeStamper.nowUTC, transmissionVector) + ) + .build() + } +} + +/** + * Determine transmission risk level for [CheckIn] bases on its start time. + * @param now [Instant] + * @param transmissionVector [TransmissionRiskVector] + */ +fun CheckIn.determineRiskTransmission(now: Instant, transmissionVector: TransmissionRiskVector): Int { + val startMidnight = checkInStart.toLocalDate().toDateTimeAtStartOfDay() + val nowMidnight = now.toLocalDate().toDateTimeAtStartOfDay() + val ageInDays = Days.daysBetween(startMidnight, nowMidnight).days + return transmissionVector.raw.getOrElse(ageInDays) { 1 } // Default value } + +private fun okio.ByteString.toProtoByteString() = ByteString.copyFrom(toByteArray()) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/DefaultCheckInsTransformer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/DefaultCheckInsTransformer.kt deleted file mode 100644 index 55d37d7e57dd0a35fb902ce42e57a2bde7adcc0b..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/DefaultCheckInsTransformer.kt +++ /dev/null @@ -1,37 +0,0 @@ -package de.rki.coronawarnapp.eventregistration.checkins - -import com.google.protobuf.ByteString -import de.rki.coronawarnapp.server.protocols.internal.pt.CheckInOuterClass -import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass -import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds -import javax.inject.Inject - -class DefaultCheckInsTransformer @Inject constructor() : - CheckInsTransformer { - override fun transform(checkIns: List<CheckIn>): List<CheckInOuterClass.CheckIn> { - return checkIns.map { checkIn -> - val traceLocation = TraceLocationOuterClass.TraceLocation.newBuilder() - .setGuid(checkIn.guid) - .setVersion(checkIn.version) - .setType(TraceLocationOuterClass.TraceLocationType.forNumber(checkIn.type)) - .setDescription(checkIn.description) - .setAddress(checkIn.address) - .setStartTimestamp(checkIn.traceLocationStart?.seconds ?: 0L) - .setEndTimestamp(checkIn.traceLocationEnd?.seconds ?: 0L) - .setDefaultCheckInLengthInMinutes(checkIn.defaultCheckInLengthInMinutes ?: 0) - .build() - - val signedTraceLocation = TraceLocationOuterClass.SignedTraceLocation.newBuilder() - .setLocation(traceLocation.toByteString()) - .setSignature(ByteString.copyFrom(checkIn.signature.toByteArray())) - .build() - - CheckInOuterClass.CheckIn.newBuilder() - .setSignedLocation(signedTraceLocation) - .setStartIntervalNumber(checkIn.checkInStart.seconds.toInt()) - .setEndIntervalNumber(checkIn.checkInEnd?.seconds?.toInt() ?: 0) - // TODO .setTransmissionRiskLevel() - .build() - } - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/derivetime/TimeIntervalDeriver.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/derivetime/TimeIntervalDeriver.kt index ddde8f6ad28d8c3c80b6c9b27f5e2f8e3459ac13..9e125f866c19bdf8a515e7d201ab52b470982830 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/derivetime/TimeIntervalDeriver.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/derivetime/TimeIntervalDeriver.kt @@ -16,11 +16,14 @@ private fun alignToInterval(timestamp: Long) = * Derive CheckIn start and end times * @param startTimestampInSeconds [Long] timestamp in seconds * @param endTimestampInSeconds [Long] timestamp in seconds + * + * @return [DerivedTimes] holds derived startTime and endTime in seconds */ fun PresenceTracingSubmissionParamContainer.deriveTime( startTimestampInSeconds: Long, endTimestampInSeconds: Long -): Pair<Long, Long>? { +): DerivedTimes? { + Timber.d("Starting deriveTime ...") val durationInSeconds = max(0, endTimestampInSeconds - startTimestampInSeconds) Timber.d("durationInSeconds: $durationInSeconds") @@ -64,7 +67,10 @@ fun PresenceTracingSubmissionParamContainer.deriveTime( ) val newEndTimestamp = relevantEndIntervalTimestamp + INTERVAL_LENGTH_IN_SECONDS val newStartTimestamp = newEndTimestamp - targetDurationInSeconds - newStartTimestamp to newEndTimestamp + DerivedTimes( + startTimeSeconds = newStartTimestamp, + endTimeSeconds = newEndTimestamp + ) } else { Timber.d( "overlapWithEndInterval:%s, overlapWithStartInterval:%s", @@ -72,6 +78,17 @@ fun PresenceTracingSubmissionParamContainer.deriveTime( overlapWithStartInterval ) val newEndTimestamp = relevantStartIntervalTimestamp + targetDurationInSeconds - relevantStartIntervalTimestamp to newEndTimestamp + DerivedTimes( + startTimeSeconds = relevantStartIntervalTimestamp, + endTimeSeconds = newEndTimestamp + ) } } + +/** + * Represents output of [deriveTime] + */ +data class DerivedTimes( + val startTimeSeconds: Long, + val endTimeSeconds: Long +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/split/CheckInSplitter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/split/CheckInSplitter.kt index 89df718fe2e58c853a3e8b1bb4b5af1d7c6a4469..e423c0945fc03ecae3ef89e6889b079a32b14d0f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/split/CheckInSplitter.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/split/CheckInSplitter.kt @@ -13,6 +13,7 @@ import kotlin.math.ceil * @return [List] of [CheckIn]s */ fun CheckIn.splitByMidnightUTC(): List<CheckIn> { + Timber.d("Starting splitByMidnightUTC ...") val startTimeSeconds = checkInStart.seconds val endTimeSeconds = checkInEnd.seconds val durationSecondsUTC = endTimeSeconds - startTimeSeconds.toMidnightUTC() 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 db90d36d1983fef1244297e575b27aeec2f9221d..7265d2bb6e59b24541b4034d7e8b3029d269ef52 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 transformedCheckIns = checkInsTransformer.transform(checkIns) + 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/util/TimeAndDateExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt index cfb9c9fca4ede8f16ae9bd4bf76ac2bfffde1c9c..d11ab6f76da66eb29ff0d394d219c4b77c55e474 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt @@ -63,6 +63,17 @@ object TimeAndDateExtensions { Instant.ofEpochMilli(this) } else null + /** + * Converts a [Long] representing time in Seconds into [Instant] + */ + fun Long.secondsToInstant(): Instant = Instant.ofEpochSecond(this) + + /** + * Derive a UNIX timestamp (in seconds) and returns the corresponding 10-minute interval + */ + fun Instant.derive10MinutesInterval(): Long = + seconds / TimeUnit.MINUTES.toSeconds(10) // 10 min in seconds + /** * Converts milliseconds to human readable format hh:mm:ss * diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInTransmissionRiskLevelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInTransmissionRiskLevelTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..6128ef84e21ce0756b09078ee44fda4565c5f243 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInTransmissionRiskLevelTest.kt @@ -0,0 +1,77 @@ +package de.rki.coronawarnapp.eventregistration.checkins + +import de.rki.coronawarnapp.submission.task.TransmissionRiskVector +import io.kotest.matchers.shouldBe +import okio.ByteString +import okio.ByteString.Companion.EMPTY +import okio.ByteString.Companion.decodeBase64 +import org.joda.time.Instant +import org.junit.jupiter.api.Test + +import testhelpers.BaseTest + +class CheckInTransmissionRiskLevelTest : BaseTest() { + + private val checkIn = CheckIn( + id = 1L, + guid = "trace_location_1", + guidHash = EMPTY, + version = 1, + type = 2, + description = "restaurant_1", + address = "address_1", + traceLocationStart = null, + traceLocationEnd = null, + defaultCheckInLengthInMinutes = null, + traceLocationBytes = EMPTY, + signature = "c2lnbmF0dXJlMQ==".decodeBase64()!!, + checkInStart = Instant.parse("2021-03-04T10:20:00Z"), + checkInEnd = Instant.parse("2021-03-04T10:30:00Z"), + completed = false, + createJournalEntry = false + ) + + private val transmissionVector = TransmissionRiskVector( + intArrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + ) + + @Test + fun `1 day age`() { + checkIn.determineRiskTransmission( + Instant.parse("2021-03-05T00:00:00Z"), + transmissionVector + ) shouldBe 2 + } + + @Test + fun `8 days age`() { + checkIn.determineRiskTransmission( + Instant.parse("2021-03-12T00:00:00Z"), + transmissionVector + ) shouldBe 9 + } + + @Test + fun `age does not exist in transmission vector`() { + checkIn.determineRiskTransmission( + Instant.parse("2021-03-25T00:00:00Z"), + transmissionVector + ) shouldBe 1 + } + + @Test + fun `start and now times are the same`() { + checkIn.determineRiskTransmission( + Instant.parse("2021-03-04T10:20:00Z"), + transmissionVector + ) shouldBe 1 + } + + @Test + fun `negative age`() { + checkIn.determineRiskTransmission( + Instant.parse("2021-03-01T10:20:00Z"), + transmissionVector + ) shouldBe 1 + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformerTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..2c1332b7dc04562e7d9df118797fdc5db1484823 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformerTest.kt @@ -0,0 +1,311 @@ +package de.rki.coronawarnapp.eventregistration.checkins + +import com.google.protobuf.ByteString +import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.appconfig.ConfigData +import de.rki.coronawarnapp.appconfig.PresenceTracingConfigContainer +import de.rki.coronawarnapp.appconfig.PresenceTracingSubmissionParamContainer +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.server.protocols.internal.v2 +.PresenceTracingParametersOuterClass.PresenceTracingSubmissionParameters.DurationFilter +import de.rki.coronawarnapp.server.protocols.internal.v2 +.PresenceTracingParametersOuterClass.PresenceTracingSubmissionParameters.AerosoleDecayFunctionLinear +import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass +import de.rki.coronawarnapp.submission.Symptoms +import de.rki.coronawarnapp.submission.task.TransmissionRiskVectorDeterminator +import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds +import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDate +import de.rki.coronawarnapp.util.TimeStamper +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import kotlinx.coroutines.test.runBlockingTest +import okio.ByteString.Companion.EMPTY +import okio.ByteString.Companion.decodeBase64 +import okio.ByteString.Companion.toByteString +import org.joda.time.Instant +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import timber.log.Timber +import java.util.concurrent.TimeUnit + +class CheckInsTransformerTest : BaseTest() { + + @MockK lateinit var timeStamper: TimeStamper + @MockK lateinit var symptoms: Symptoms + @MockK lateinit var appConfigProvider: AppConfigProvider + + private lateinit var checkInTransformer: CheckInsTransformer + + // CheckIn can not be derived + private val checkIn1 = CheckIn( + id = 1L, + guid = "trace_location_1", + guidHash = EMPTY, + version = 1, + type = 1, + description = "restaurant_1", + address = "address_1", + traceLocationStart = null, + traceLocationEnd = null, + defaultCheckInLengthInMinutes = null, + traceLocationBytes = EMPTY, + signature = "c2lnbmF0dXJlMQ==".decodeBase64()!!, + checkInStart = Instant.parse("2021-03-04T10:21:00Z"), + checkInEnd = Instant.parse("2021-03-04T10:29:00Z"), + completed = false, + createJournalEntry = false + ) + + /* + CheckIn that can be derived and can't be splitted + Derived start and end times + "expStartDateStr": "2021-03-04 10:20+01:00" + "expEndDateStr": "2021-03-04 10:40+01:00" + */ + private val checkIn2 = CheckIn( + id = 2L, + guid = "trace_location_2", + guidHash = EMPTY, + version = 1, + type = 2, + description = "restaurant_2", + address = "address_2", + traceLocationStart = null, + traceLocationEnd = null, + defaultCheckInLengthInMinutes = null, + traceLocationBytes = TRACE_LOCATION_2.decodeBase64()!!, + signature = "c2lnbmF0dXJlMQ==".decodeBase64()!!, + checkInStart = Instant.parse("2021-03-04T10:20:00Z"), + checkInEnd = Instant.parse("2021-03-04T10:30:00Z"), + completed = false, + createJournalEntry = false + ) + + // CheckIn that can be derived and can be splitted + private val checkIn3 = CheckIn( + id = 3L, + guid = "trace_location_3", + guidHash = EMPTY, + version = 1, + type = 3, + description = "restaurant_3", + address = "address_3", + traceLocationStart = Instant.parse("2021-03-04T09:00:00Z"), + traceLocationEnd = Instant.parse("2021-03-06T11:00:00Z"), + defaultCheckInLengthInMinutes = 10, + traceLocationBytes = TRACE_LOCATION_3.decodeBase64()!!, + signature = "c2lnbmF0dXJlMQ==".decodeBase64()!!, + checkInStart = Instant.parse("2021-03-04T09:30:00Z"), + checkInEnd = Instant.parse("2021-03-06T09:45:00Z"), + completed = false, + createJournalEntry = false + ) + + private val presenceTracingConfig = PresenceTracingSubmissionParamContainer( + durationFilters = listOf( + DurationFilter.newBuilder() + .setDropIfMinutesInRange( + RiskCalculationParametersOuterClass.Range.newBuilder() + .setMin(0.0) + .setMax(10.0) + .setMaxExclusive(true) + .build() + ) + .build() + ), + aerosoleDecayLinearFunctions = listOf( + AerosoleDecayFunctionLinear.newBuilder() + .setMinutesRange( + RiskCalculationParametersOuterClass.Range.newBuilder() + .setMin(0.0) + .setMax(30.0) + .build() + ) + .setSlope(1.0) + .setIntercept(0.0) + .build(), + AerosoleDecayFunctionLinear.newBuilder() + .setMinutesRange( + RiskCalculationParametersOuterClass.Range.newBuilder() + .setMin(30.0) + .setMax(9999.0) + .setMinExclusive(true) + .build() + ) + .setSlope(0.0) + .setIntercept(30.0) + .build() + ) + ) + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + every { timeStamper.nowUTC } returns Instant.parse("2021-03-08T10:00:00Z") + every { symptoms.symptomIndication } returns Symptoms.Indication.POSITIVE + every { symptoms.startOfSymptoms } returns Symptoms.StartOf.Date(timeStamper.nowUTC.toLocalDate()) + coEvery { appConfigProvider.getAppConfig() } returns mockk<ConfigData>().apply { + every { presenceTracing } returns PresenceTracingConfigContainer( + submissionParameters = presenceTracingConfig + ) + } + checkInTransformer = CheckInsTransformer( + timeStamper = timeStamper, + transmissionDeterminator = TransmissionRiskVectorDeterminator(timeStamper), + appConfigProvider = appConfigProvider + ) + } + + @Test + fun `transform check-ins`() = runBlockingTest { + val outCheckIns = checkInTransformer.transform( + listOf( + checkIn1, + checkIn2, + checkIn3 + ), + symptoms + ) + + with(outCheckIns) { + size shouldBe 4 + // Check In 1 is excluded from submission due to time deriving + // Check In 2 mapping and transformation + get(0).apply { + /* + id = 2L, // Not mapped - client specific + guid = "trace_location_2", + guidHash = EMPTY, // Not mapped - client specific + version = 1, + type = 2, + description = "restaurant_2", + address = "address_2", + traceLocationStart = null, + traceLocationEnd = null, + defaultCheckInLengthInMinutes = null, + traceLocationBytes = EMPTY, + signature = "c2lnbmF0dXJlMQ==".decodeBase64()!!, + checkInStart = Instant.parse("2021-03-04T10:20:00Z"), + checkInEnd = Instant.parse("2021-03-04T10:30:00Z"), + completed = false, // Not mapped - client specific + createJournalEntry = false // Not mapped - client specific + */ + + // New derived start time + startIntervalNumber shouldBe Instant.parse("2021-03-04T10:20:00Z").seconds / TEN_MINUTES_IN_SECONDS + // New derived end time + endIntervalNumber shouldBe Instant.parse("2021-03-04T10:40:00Z").seconds / TEN_MINUTES_IN_SECONDS + signedLocation.signature shouldBe ByteString.copyFrom("signature1".toByteArray()) + parseLocation(signedLocation.location).apply { + guid shouldBe "trace_location_2" + version shouldBe 1 + type shouldBe TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER + description shouldBe "restaurant_2" + address shouldBe "address_2" + startTimestamp shouldBe 0 + endTimestamp shouldBe 0 + defaultCheckInLengthInMinutes shouldBe 0 + transmissionRiskLevel shouldBe 4 + } + } + + // Check-In 3 mappings and transformation + /* + id = 3L, // Not mapped - client specific + guid = "trace_location_3", + guidHash = EMPTY, // Not mapped - client specific + version = 1, + type = 3, + description = "restaurant_3", + address = "address_3", + traceLocationStart = Instant.parse("2021-03-04T09:00:00Z"), + traceLocationEnd = Instant.parse("2021-03-06T11:00:00Z"), + defaultCheckInLengthInMinutes = 10, + traceLocationBytes = EMPTY, + signature = "c2lnbmF0dXJlMQ==".decodeBase64()!!, + checkInStart = Instant.parse("2021-03-04T09:30:00Z"), + checkInEnd = Instant.parse("2021-03-06T09:45:00Z"), + completed = false, // Not mapped - client specific + createJournalEntry = false // Not mapped - client specific + */ + + // Splitted CheckIn 1 + get(1).apply { + // Start time from original check-in + startIntervalNumber shouldBe Instant.parse("2021-03-04T09:30:00Z").seconds / TEN_MINUTES_IN_SECONDS + // End time for splitted check-in 1 + endIntervalNumber shouldBe Instant.parse("2021-03-05T00:00:00Z").seconds / TEN_MINUTES_IN_SECONDS + signedLocation.signature shouldBe ByteString.copyFrom("signature1".toByteArray()) + parseLocation(signedLocation.location).apply { + guid shouldBe "trace_location_3" + version shouldBe 1 + type shouldBe TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_RETAIL + description shouldBe "restaurant_3" + address shouldBe "address_3" + startTimestamp shouldBe Instant.parse("2021-03-04T09:00:00Z").seconds + endTimestamp shouldBe Instant.parse("2021-03-06T11:00:00Z").seconds + defaultCheckInLengthInMinutes shouldBe 10 + transmissionRiskLevel shouldBe 4 + } + } + + // Splitted CheckIn 2 + get(2).apply { + // Start time for splitted check-in 2 + startIntervalNumber shouldBe Instant.parse("2021-03-05T00:00:00Z").seconds / TEN_MINUTES_IN_SECONDS + // End time for splitted check-in 2 + endIntervalNumber shouldBe Instant.parse("2021-03-06T00:00:00Z").seconds / TEN_MINUTES_IN_SECONDS + signedLocation.signature shouldBe ByteString.copyFrom("signature1".toByteArray()) + parseLocation(signedLocation.location).apply { + guid shouldBe "trace_location_3" + version shouldBe 1 + type shouldBe TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_RETAIL + description shouldBe "restaurant_3" + address shouldBe "address_3" + startTimestamp shouldBe Instant.parse("2021-03-04T09:00:00Z").seconds + endTimestamp shouldBe Instant.parse("2021-03-06T11:00:00Z").seconds + defaultCheckInLengthInMinutes shouldBe 10 + transmissionRiskLevel shouldBe 6 + } + } + + // Splitted CheckIn 3 + get(3).apply { + // Start time from splitted check-in 3 + startIntervalNumber shouldBe Instant.parse("2021-03-06T00:00:00Z").seconds / TEN_MINUTES_IN_SECONDS + // End time for splitted check-in 3 + endIntervalNumber shouldBe Instant.parse("2021-03-06T10:20:00Z").seconds / TEN_MINUTES_IN_SECONDS + signedLocation.signature shouldBe ByteString.copyFrom("signature1".toByteArray()) + parseLocation(signedLocation.location).apply { + guid shouldBe "trace_location_3" + version shouldBe 1 + type shouldBe TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_RETAIL + description shouldBe "restaurant_3" + address shouldBe "address_3" + startTimestamp shouldBe Instant.parse("2021-03-04T09:00:00Z").seconds + endTimestamp shouldBe Instant.parse("2021-03-06T11:00:00Z").seconds + defaultCheckInLengthInMinutes shouldBe 10 + transmissionRiskLevel shouldBe 7 + } + } + } + } + + private fun parseLocation(bytes: ByteString): TraceLocationOuterClass.TraceLocation = + TraceLocationOuterClass.TraceLocation.parseFrom(bytes) + + companion object { + private val TEN_MINUTES_IN_SECONDS = TimeUnit.MINUTES.toSeconds(10) + + // Base64 Strings of trace locations + private const val TRACE_LOCATION_2 = + "ChB0cmFjZV9sb2NhdGlvbl8yEAEYAiIMcmVzdGF1cmFudF8yKglhZGRyZXNzXzI=" + private const val TRACE_LOCATION_3 = + "ChB0cmFjZV9sb2NhdGlvbl8zEAEYAyIMcmVzdGF1cmFudF8zKglhZGRyZXNzXzMwkMOCggY4sMGNggZACg==" + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/DefaultCheckInsTransformerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/DefaultCheckInsTransformerTest.kt deleted file mode 100644 index ecd98df37d80380111b555bcdacd738d25af1054..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/DefaultCheckInsTransformerTest.kt +++ /dev/null @@ -1,102 +0,0 @@ -package de.rki.coronawarnapp.eventregistration.checkins - -import com.google.protobuf.ByteString -import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass -import io.kotest.matchers.shouldBe -import okio.ByteString.Companion.EMPTY -import okio.ByteString.Companion.decodeBase64 -import org.joda.time.Instant -import org.junit.jupiter.api.Test -import testhelpers.BaseTest - -class DefaultCheckInsTransformerTest : BaseTest() { - - private val checkInTransformer = DefaultCheckInsTransformer() - - @Test - fun `transform check-ins`() { - val checkIn1 = CheckIn( - id = 0, - guid = "3055331c-2306-43f3-9742-6d8fab54e848", - guidHash = EMPTY, - version = 1, - type = 2, - description = "description1", - address = "address1", - traceLocationStart = Instant.ofEpochMilli(2687955 * 1_000L), - traceLocationEnd = Instant.ofEpochMilli(2687991 * 1_000L), - defaultCheckInLengthInMinutes = 10, - traceLocationBytes = EMPTY, - signature = "c2lnbmF0dXJlMQ==".decodeBase64()!!, - checkInStart = Instant.ofEpochMilli(2687955 * 1_000L), - checkInEnd = Instant.ofEpochMilli(2687991 * 1_000L), - completed = false, - createJournalEntry = true - ) - - val checkIn2 = CheckIn( - id = 1, - guid = "fca84b37-61c0-4a7c-b2f8-825cadd506cf", - guidHash = EMPTY, - version = 1, - type = 1, - description = "description2", - address = "address2", - traceLocationStart = null, - traceLocationEnd = null, - defaultCheckInLengthInMinutes = 20, - traceLocationBytes = EMPTY, - signature = "c2lnbmF0dXJlMg==".decodeBase64()!!, - checkInStart = Instant.ofEpochMilli(2687955 * 1_000L), - checkInEnd = Instant.ofEpochMilli(2687956 * 1_000L), - completed = false, - createJournalEntry = false - ) - - val outCheckIns = checkInTransformer.transform( - listOf( - checkIn1, - checkIn2 - ) - ) - outCheckIns.size shouldBe 2 - - outCheckIns[0].apply { - signedLocation.apply { - TraceLocationOuterClass.TraceLocation.parseFrom(location).apply { - guid shouldBe "3055331c-2306-43f3-9742-6d8fab54e848" - version shouldBe 1 - type shouldBe TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER - description shouldBe "description1" - address shouldBe "address1" - startTimestamp shouldBe 2687955 - endTimestamp shouldBe 2687991 - defaultCheckInLengthInMinutes shouldBe 10 - } - signature shouldBe ByteString.copyFrom("signature1".toByteArray()) - } - startIntervalNumber shouldBe 2687955 - endIntervalNumber shouldBe 2687991 - // TODO transmissionRiskLevel shouldBe - } - - outCheckIns[1].apply { - signedLocation.apply { - TraceLocationOuterClass.TraceLocation.parseFrom(location).apply { - guid shouldBe "fca84b37-61c0-4a7c-b2f8-825cadd506cf" - version shouldBe 1 - type shouldBe TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER - description shouldBe "description2" - address shouldBe "address2" - startTimestamp shouldBe 0 - endTimestamp shouldBe 0 - defaultCheckInLengthInMinutes shouldBe 20 - } - signature shouldBe ByteString.copyFrom("signature2".toByteArray()) - } - startIntervalNumber shouldBe 2687955 - endIntervalNumber shouldBe 2687956 - // TODO transmissionRiskLevel shouldBe - } - } -} 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 f1514ebd9a61926d303dff9ed372b82331395e47..0cb6a4ad75326870cbe5b8c7487448a2a64689bd 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 @@ -116,7 +116,7 @@ class SubmissionTaskTest : BaseTest() { every { checkInRepository.allCheckIns } returns flowOf(emptyList()) coEvery { checkInRepository.clear() } just Runs - every { checkInsTransformer.transform(any()) } returns emptyList() + coEvery { checkInsTransformer.transform(any(), any()) } returns emptyList() } private fun createTask() = SubmissionTask( @@ -160,7 +160,7 @@ class SubmissionTaskTest : BaseTest() { tekHistoryCalculations.transformToKeyHistoryInExternalFormat(listOf(tek), userSymptoms) checkInRepository.allCheckIns - checkInsTransformer.transform(any()) + checkInsTransformer.transform(any(), any()) appConfigProvider.getAppConfig() playbook.submit( diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TimeAndDateExtensionsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TimeAndDateExtensionsTest.kt index 50960f2caaa5002c8e562a899c870b9a39317534..a02df66dd129ca32d506ebb2ae18343436000264 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TimeAndDateExtensionsTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TimeAndDateExtensionsTest.kt @@ -3,8 +3,10 @@ package de.rki.coronawarnapp.util import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.util.TimeAndDateExtensions.ageInDays import de.rki.coronawarnapp.util.TimeAndDateExtensions.calculateDays +import de.rki.coronawarnapp.util.TimeAndDateExtensions.derive10MinutesInterval import de.rki.coronawarnapp.util.TimeAndDateExtensions.getCurrentHourUTC import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds +import de.rki.coronawarnapp.util.TimeAndDateExtensions.secondsToInstant import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.mockkObject @@ -70,4 +72,31 @@ class TimeAndDateExtensionsTest : BaseTest() { Instant.ofEpochMilli(1999).seconds shouldBe 1 Instant.ofEpochMilli(2000).seconds shouldBe 2 } + + @Test + fun `seconds to instant`() { + 2687955L.secondsToInstant() shouldBe Instant.parse("1970-02-01T02:39:15.000Z") + } + + @Test + fun `0 seconds to instant`() { + 0L.secondsToInstant() shouldBe Instant.parse("1970-01-01T00:00:00.000Z") + } + + @Test + fun `-10 seconds to instant`() { + (-10).toLong().secondsToInstant() shouldBe Instant.parse("1969-12-31T23:59:50.000Z") + } + + @Test + fun `derive 10 minutes interval`() { + Instant.parse("2021-03-02T09:57:11+01:00") + .derive10MinutesInterval() shouldBe 2691125 + } + + @Test + fun `derive 10 minutes interval should be 0`() { + Instant.parse("1970-01-01T00:00:00.000Z") + .derive10MinutesInterval() shouldBe 0 + } }