Skip to content
Snippets Groups Projects
Unverified Commit 46c04965 authored by Mohamed's avatar Mohamed Committed by GitHub
Browse files

Check-In risk level transmission (EXPOSUREAPP-5770) (#2641)


* Refactoring

* lint

* Put submission steps together

* Default values

* Fix tests

* klint

* Prepare test data

* Docs and improvements

* Risk transmission tests

* Negative age

* Simplify default value logic

* CheckIn assertions

* Use specific date

* Improvements of logging and mapping

* Mark the class with Singleton - move log statement

* Assertion for splitted check-in

* Adopt CheckIn changes

* Derive 10 minutes intervals

* Map location bytes as per specs

* Base64 of test trace locations

* Create specific data class as deriveTime output

Co-authored-by: default avatarharambasicluka <64483219+harambasicluka@users.noreply.github.com>
Co-authored-by: default avatarLukas Lechner <lukas.lechner@sap.com>
parent 43272505
No related branches found
No related tags found
No related merge requests found
Showing
with 564 additions and 179 deletions
...@@ -3,8 +3,10 @@ package de.rki.coronawarnapp.appconfig ...@@ -3,8 +3,10 @@ package de.rki.coronawarnapp.appconfig
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
data class PresenceTracingConfigContainer( data class PresenceTracingConfigContainer(
override val qrCodeErrorCorrectionLevel: ErrorCorrectionLevel, override val qrCodeErrorCorrectionLevel: ErrorCorrectionLevel = ErrorCorrectionLevel.H,
override val revokedTraceLocationVersions: List<Int>, override val revokedTraceLocationVersions: List<Int> = listOf(),
override val riskCalculationParameters: PresenceTracingRiskCalculationParamContainer, override val riskCalculationParameters: PresenceTracingRiskCalculationParamContainer =
override val submissionParameters: PresenceTracingSubmissionParamContainer PresenceTracingRiskCalculationParamContainer(),
override val submissionParameters: PresenceTracingSubmissionParamContainer =
PresenceTracingSubmissionParamContainer()
) : PresenceTracingConfig ) : PresenceTracingConfig
...@@ -4,7 +4,7 @@ import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParamete ...@@ -4,7 +4,7 @@ import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParamete
import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass.TransmissionRiskValueMapping import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass.TransmissionRiskValueMapping
data class PresenceTracingRiskCalculationParamContainer( data class PresenceTracingRiskCalculationParamContainer(
val transmissionRiskValueMapping: List<TransmissionRiskValueMapping>, val transmissionRiskValueMapping: List<TransmissionRiskValueMapping> = emptyList(),
val normalizedTimePerCheckInToRiskLevelMapping: List<NormalizedTimeToRiskLevelMapping>, val normalizedTimePerCheckInToRiskLevelMapping: List<NormalizedTimeToRiskLevelMapping> = emptyList(),
val normalizedTimePerDayToRiskLevelMapping: List<NormalizedTimeToRiskLevelMapping> val normalizedTimePerDayToRiskLevelMapping: List<NormalizedTimeToRiskLevelMapping> = emptyList()
) )
...@@ -7,6 +7,6 @@ import de.rki.coronawarnapp.server.protocols.internal.v2 ...@@ -7,6 +7,6 @@ import de.rki.coronawarnapp.server.protocols.internal.v2
.PresenceTracingParametersOuterClass.PresenceTracingSubmissionParameters.AerosoleDecayFunctionLinear .PresenceTracingParametersOuterClass.PresenceTracingSubmissionParameters.AerosoleDecayFunctionLinear
data class PresenceTracingSubmissionParamContainer( data class PresenceTracingSubmissionParamContainer(
val durationFilters: List<DurationFilter>, val durationFilters: List<DurationFilter> = emptyList(),
val aerosoleDecayLinearFunctions: List<AerosoleDecayFunctionLinear> val aerosoleDecayLinearFunctions: List<AerosoleDecayFunctionLinear> = emptyList()
) )
...@@ -24,27 +24,14 @@ class PresenceTracingConfigMapper @Inject constructor() : PresenceTracingConfig. ...@@ -24,27 +24,14 @@ class PresenceTracingConfigMapper @Inject constructor() : PresenceTracingConfig.
return PresenceTracingConfigContainer( return PresenceTracingConfigContainer(
qrCodeErrorCorrectionLevel = ErrorCorrectionLevel.H, qrCodeErrorCorrectionLevel = ErrorCorrectionLevel.H,
revokedTraceLocationVersions = emptyList(), revokedTraceLocationVersions = emptyList(),
riskCalculationParameters = emptyRiskCalculationParameters(), riskCalculationParameters = PresenceTracingRiskCalculationParamContainer(),
submissionParameters = emptySubmissionParameters() submissionParameters = PresenceTracingSubmissionParamContainer()
) )
} }
return rawConfig.presenceTracingConfig().also { Timber.i("PresenceTracingConfig: $it") } 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() = private fun PresenceTracingSubmissionParameters.mapSubmissionParameters() =
PresenceTracingSubmissionParamContainer( PresenceTracingSubmissionParamContainer(
durationFilters = durationFiltersList, durationFilters = durationFiltersList,
...@@ -73,14 +60,14 @@ class PresenceTracingConfigMapper @Inject constructor() : PresenceTracingConfig. ...@@ -73,14 +60,14 @@ class PresenceTracingConfigMapper @Inject constructor() : PresenceTracingConfig.
riskCalculationParameters.mapRiskCalculationParameters() riskCalculationParameters.mapRiskCalculationParameters()
} else { } else {
Timber.w("RiskCalculationParameters are missing") Timber.w("RiskCalculationParameters are missing")
emptyRiskCalculationParameters() PresenceTracingRiskCalculationParamContainer()
} }
val submissionParameters = if (hasSubmissionParameters()) { val submissionParameters = if (hasSubmissionParameters()) {
submissionParameters.mapSubmissionParameters() submissionParameters.mapSubmissionParameters()
} else { } else {
Timber.w("SubmissionParameters are missing") Timber.w("SubmissionParameters are missing")
emptySubmissionParameters() PresenceTracingSubmissionParamContainer()
} }
PresenceTracingConfigContainer( PresenceTracingConfigContainer(
......
...@@ -2,8 +2,6 @@ package de.rki.coronawarnapp.eventregistration ...@@ -2,8 +2,6 @@ package de.rki.coronawarnapp.eventregistration
import dagger.Binds import dagger.Binds
import dagger.Module 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.DownloadedCheckInsRepo
import de.rki.coronawarnapp.eventregistration.checkins.download.FakeDownloadedCheckInsRepo import de.rki.coronawarnapp.eventregistration.checkins.download.FakeDownloadedCheckInsRepo
import de.rki.coronawarnapp.eventregistration.storage.repo.DefaultTraceLocationRepository import de.rki.coronawarnapp.eventregistration.storage.repo.DefaultTraceLocationRepository
...@@ -11,10 +9,6 @@ import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationReposito ...@@ -11,10 +9,6 @@ import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationReposito
@Module @Module
abstract class EventRegistrationModule { abstract class EventRegistrationModule {
@Binds
abstract fun checkInsTransformer(transformer: DefaultCheckInsTransformer): CheckInsTransformer
@Binds @Binds
abstract fun traceLocationRepository(defaultTraceLocationRepo: DefaultTraceLocationRepository): abstract fun traceLocationRepository(defaultTraceLocationRepo: DefaultTraceLocationRepository):
TraceLocationRepository TraceLocationRepository
......
package de.rki.coronawarnapp.eventregistration.checkins 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.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 { @Singleton
fun transform(checkIns: List<CheckIn>): List<CheckInOuterClass.CheckIn> 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())
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()
}
}
}
...@@ -16,11 +16,14 @@ private fun alignToInterval(timestamp: Long) = ...@@ -16,11 +16,14 @@ private fun alignToInterval(timestamp: Long) =
* Derive CheckIn start and end times * Derive CheckIn start and end times
* @param startTimestampInSeconds [Long] timestamp in seconds * @param startTimestampInSeconds [Long] timestamp in seconds
* @param endTimestampInSeconds [Long] timestamp in seconds * @param endTimestampInSeconds [Long] timestamp in seconds
*
* @return [DerivedTimes] holds derived startTime and endTime in seconds
*/ */
fun PresenceTracingSubmissionParamContainer.deriveTime( fun PresenceTracingSubmissionParamContainer.deriveTime(
startTimestampInSeconds: Long, startTimestampInSeconds: Long,
endTimestampInSeconds: Long endTimestampInSeconds: Long
): Pair<Long, Long>? { ): DerivedTimes? {
Timber.d("Starting deriveTime ...")
val durationInSeconds = max(0, endTimestampInSeconds - startTimestampInSeconds) val durationInSeconds = max(0, endTimestampInSeconds - startTimestampInSeconds)
Timber.d("durationInSeconds: $durationInSeconds") Timber.d("durationInSeconds: $durationInSeconds")
...@@ -64,7 +67,10 @@ fun PresenceTracingSubmissionParamContainer.deriveTime( ...@@ -64,7 +67,10 @@ fun PresenceTracingSubmissionParamContainer.deriveTime(
) )
val newEndTimestamp = relevantEndIntervalTimestamp + INTERVAL_LENGTH_IN_SECONDS val newEndTimestamp = relevantEndIntervalTimestamp + INTERVAL_LENGTH_IN_SECONDS
val newStartTimestamp = newEndTimestamp - targetDurationInSeconds val newStartTimestamp = newEndTimestamp - targetDurationInSeconds
newStartTimestamp to newEndTimestamp DerivedTimes(
startTimeSeconds = newStartTimestamp,
endTimeSeconds = newEndTimestamp
)
} else { } else {
Timber.d( Timber.d(
"overlapWithEndInterval:%s, overlapWithStartInterval:%s", "overlapWithEndInterval:%s, overlapWithStartInterval:%s",
...@@ -72,6 +78,17 @@ fun PresenceTracingSubmissionParamContainer.deriveTime( ...@@ -72,6 +78,17 @@ fun PresenceTracingSubmissionParamContainer.deriveTime(
overlapWithStartInterval overlapWithStartInterval
) )
val newEndTimestamp = relevantStartIntervalTimestamp + targetDurationInSeconds 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
)
...@@ -13,6 +13,7 @@ import kotlin.math.ceil ...@@ -13,6 +13,7 @@ import kotlin.math.ceil
* @return [List] of [CheckIn]s * @return [List] of [CheckIn]s
*/ */
fun CheckIn.splitByMidnightUTC(): List<CheckIn> { fun CheckIn.splitByMidnightUTC(): List<CheckIn> {
Timber.d("Starting splitByMidnightUTC ...")
val startTimeSeconds = checkInStart.seconds val startTimeSeconds = checkInStart.seconds
val endTimeSeconds = checkInEnd.seconds val endTimeSeconds = checkInEnd.seconds
val durationSecondsUTC = endTimeSeconds - startTimeSeconds.toMidnightUTC() val durationSecondsUTC = endTimeSeconds - startTimeSeconds.toMidnightUTC()
......
...@@ -142,7 +142,7 @@ class SubmissionTask @Inject constructor( ...@@ -142,7 +142,7 @@ class SubmissionTask @Inject constructor(
Timber.tag(TAG).d("Transformed keys with symptoms %s from %s to %s", symptoms, keys, transformedKeys) Timber.tag(TAG).d("Transformed keys with symptoms %s from %s to %s", symptoms, keys, transformedKeys)
val checkIns = checkInsRepository.allCheckIns.first() 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) Timber.tag(TAG).d("Transformed CheckIns from: %s to: %s", checkIns, transformedCheckIns)
......
...@@ -63,6 +63,17 @@ object TimeAndDateExtensions { ...@@ -63,6 +63,17 @@ object TimeAndDateExtensions {
Instant.ofEpochMilli(this) Instant.ofEpochMilli(this)
} else null } 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 * Converts milliseconds to human readable format hh:mm:ss
* *
......
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
}
}
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=="
}
}
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
}
}
}
...@@ -116,7 +116,7 @@ class SubmissionTaskTest : BaseTest() { ...@@ -116,7 +116,7 @@ class SubmissionTaskTest : BaseTest() {
every { checkInRepository.allCheckIns } returns flowOf(emptyList()) every { checkInRepository.allCheckIns } returns flowOf(emptyList())
coEvery { checkInRepository.clear() } just Runs coEvery { checkInRepository.clear() } just Runs
every { checkInsTransformer.transform(any()) } returns emptyList() coEvery { checkInsTransformer.transform(any(), any()) } returns emptyList()
} }
private fun createTask() = SubmissionTask( private fun createTask() = SubmissionTask(
...@@ -160,7 +160,7 @@ class SubmissionTaskTest : BaseTest() { ...@@ -160,7 +160,7 @@ class SubmissionTaskTest : BaseTest() {
tekHistoryCalculations.transformToKeyHistoryInExternalFormat(listOf(tek), userSymptoms) tekHistoryCalculations.transformToKeyHistoryInExternalFormat(listOf(tek), userSymptoms)
checkInRepository.allCheckIns checkInRepository.allCheckIns
checkInsTransformer.transform(any()) checkInsTransformer.transform(any(), any())
appConfigProvider.getAppConfig() appConfigProvider.getAppConfig()
playbook.submit( playbook.submit(
......
...@@ -3,8 +3,10 @@ package de.rki.coronawarnapp.util ...@@ -3,8 +3,10 @@ package de.rki.coronawarnapp.util
import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.CoronaWarnApplication
import de.rki.coronawarnapp.util.TimeAndDateExtensions.ageInDays import de.rki.coronawarnapp.util.TimeAndDateExtensions.ageInDays
import de.rki.coronawarnapp.util.TimeAndDateExtensions.calculateDays 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.getCurrentHourUTC
import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds
import de.rki.coronawarnapp.util.TimeAndDateExtensions.secondsToInstant
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import io.mockk.MockKAnnotations import io.mockk.MockKAnnotations
import io.mockk.mockkObject import io.mockk.mockkObject
...@@ -70,4 +72,31 @@ class TimeAndDateExtensionsTest : BaseTest() { ...@@ -70,4 +72,31 @@ class TimeAndDateExtensionsTest : BaseTest() {
Instant.ofEpochMilli(1999).seconds shouldBe 1 Instant.ofEpochMilli(1999).seconds shouldBe 1
Instant.ofEpochMilli(2000).seconds shouldBe 2 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
}
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment