From 0d1c62b804d850ac12e69f0d05c0c1a6c4cfee3e Mon Sep 17 00:00:00 2001 From: Matthias Urhahn <matthias.urhahn@sap.com> Date: Thu, 3 Jun 2021 11:00:58 +0200 Subject: [PATCH] Test certificate repository, episode 1 (EXPOSUREAPP-7505) (#3332) * Extend corona test data structures with digital covid certificate related properties. +Some additional wiring, plumbing and tests for future PRs. * LINTs * Adjust TestRegistrationRequest to supply dcc consent and DOB on test registration. * Remove explicit assignment, defaults are sufficient. * A few additional unit tests to check defaults. * DateOfBirthKey calculation, draft 1 * Fix date parser pattern. * wip * Adjust padding calculation to take the new dobHash into account. Some refactoring to make it less complicated to adjust for future changes. * klint, ofc. * TestCertificate repo, draft1. * DGC -> dcc * TestCertificateRepository, draft 2 * TestCertificateRepository, draft 3 * TestCertificateRepository, draft 4 * Unit tests, draft 1. * Add new app config parameters and implement delay mechanism. * Unit tests, draft 2 * Unit tests, draft 3 * LINTs * Additional unit test skeletons and wiring to setup automatic certificate creation. * LINTs * I don't think we need the initial delay here? * Fix check for new unprocessed certs * Address PR comments. * Tests, fixes. Co-authored-by: Mohamed Metwalli <mohamed.metwalli@sap.com> --- .../coronawarnapp/CoronaWarnApplication.kt | 3 + .../appconfig/AppConfigModule.kt | 5 + .../appconfig/CovidCertificateConfig.kt | 16 + .../appconfig/mapping/ConfigMapping.kt | 2 + .../appconfig/mapping/ConfigParser.kt | 6 +- .../mapping/CovidCertificateConfigMapper.kt | 59 +++ .../appconfig/mapping/DefaultConfigMapping.kt | 2 + .../coronatest/CoronaTestRepository.kt | 8 + .../coronatest/TestCertificateRepository.kt | 377 ++++++++++++++++++ .../storage/TestCertificateStorage.kt | 102 +++++ .../coronatest/type/CoronaTest.kt | 2 +- .../coronatest/type/CoronaTestProcessor.kt | 2 + .../type/TestCertificateContainer.kt | 114 ++++++ .../TestCertificateRetrievalScheduler.kt | 117 ++++++ .../common/TestCertificateRetrievalWorker.kt | 51 +++ .../type/pcr/PCRCertificateContainer.kt | 55 +++ .../coronatest/type/pcr/PCRProcessor.kt | 8 + .../rapidantigen/RACertificateContainer.kt | 55 +++ .../type/rapidantigen/RAProcessor.kt | 8 + .../server/CovidCertificateServer.kt | 30 ++ .../server/TestCertificateComponents.kt | 6 + .../covidcertificate/test/TestCertificate.kt | 45 +++ .../test/TestCertificateDccV1.kt | 18 +- .../test/TestCertificateQRCodeExtractor.kt | 5 +- .../de/rki/coronawarnapp/util/DataReset.kt | 5 +- .../util/encryption/rsa/RSAKey.kt | 29 ++ .../util/serialization/SerializationModule.kt | 3 + .../coronawarnapp/util/worker/WorkerBinder.kt | 8 + ...fier.kt => CertificatePersonIdentifier.kt} | 22 +- .../vaccination/core/VaccinatedPerson.kt | 2 +- .../core/VaccinationCertificate.kt | 2 +- .../core/repository/VaccinationRepository.kt | 4 +- .../storage/ContainerPostProcessor.kt | 11 +- .../storage/VaccinatedPersonData.kt | 4 +- .../storage/VaccinationContainer.kt | 6 +- .../CoronaWarnApplicationTest.kt | 4 + .../appconfig/mapping/ConfigParserTest.kt | 4 + .../CovidCertificateConfigMapperTest.kt | 68 ++++ .../coronatest/CoronaTestTestComponent.kt | 29 ++ .../coronatest/CoronaTestTestData.kt | 167 ++++++++ .../TestCertificateRepositoryTest.kt | 138 +++++++ .../storage/TestCertificateStorageTest.kt | 114 ++++++ .../type/TestCertificateContainerTest.kt | 55 +++ .../TestCertificateRetrievalSchedulerTest.kt | 137 +++++++ .../TestCertificateRetrievalWorkerTest.kt | 74 ++++ .../coronatest/type/pcr/PCRProcessorTest.kt | 48 +++ .../type/pcr/PCRTestCertificateTest.kt | 5 + .../rapidantigen/RATestCertificateTest.kt | 5 + .../rapidantigen/RapidAntigenProcessorTest.kt | 53 +++ .../rki/coronawarnapp/util/DataResetTest.kt | 5 +- .../util/worker/WorkerBinderTest.kt | 4 + .../core/VaccinatedPersonIdentifierTest.kt | 6 +- .../storage/VaccinationContainerTest.kt | 8 +- .../storage/VaccinationStorageTest.kt | 8 + 54 files changed, 2086 insertions(+), 38 deletions(-) create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/CovidCertificateConfig.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/CovidCertificateConfigMapper.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/TestCertificateRepository.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/storage/TestCertificateStorage.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/TestCertificateContainer.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/TestCertificateRetrievalScheduler.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/TestCertificateRetrievalWorker.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCertificateContainer.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RACertificateContainer.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/server/CovidCertificateServer.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/server/TestCertificateComponents.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificate.kt rename Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/{VaccinatedPersonIdentifier.kt => CertificatePersonIdentifier.kt} (71%) create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/CovidCertificateConfigMapperTest.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/CoronaTestTestComponent.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/CoronaTestTestData.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/TestCertificateRepositoryTest.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/storage/TestCertificateStorageTest.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/TestCertificateContainerTest.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/common/TestCertificateRetrievalSchedulerTest.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/common/TestCertificateRetrievalWorkerTest.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRTestCertificateTest.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RATestCertificateTest.kt diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt index b6a782372..1a38018c9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt @@ -17,6 +17,7 @@ import de.rki.coronawarnapp.bugreporting.loghistory.LogHistoryTree import de.rki.coronawarnapp.contactdiary.retention.ContactDiaryWorkScheduler import de.rki.coronawarnapp.coronatest.CoronaTestRepository import de.rki.coronawarnapp.coronatest.notification.ShareTestResultNotificationService +import de.rki.coronawarnapp.coronatest.type.common.TestCertificateRetrievalScheduler import de.rki.coronawarnapp.coronatest.type.pcr.execution.PCRResultScheduler import de.rki.coronawarnapp.coronatest.type.pcr.notification.PCRTestResultAvailableNotificationService import de.rki.coronawarnapp.coronatest.type.rapidantigen.execution.RAResultScheduler @@ -83,6 +84,7 @@ class CoronaWarnApplication : Application(), HasAndroidInjector { @Inject lateinit var pcrTestResultAvailableNotificationService: PCRTestResultAvailableNotificationService @Inject lateinit var raTestResultAvailableNotificationService: RATTestResultAvailableNotificationService @Inject lateinit var vaccinationUpdateScheduler: VaccinationUpdateScheduler + @Inject lateinit var testCertificateRetrievalScheduler: TestCertificateRetrievalScheduler @AppScope @Inject lateinit var appScope: CoroutineScope @@ -138,6 +140,7 @@ class CoronaWarnApplication : Application(), HasAndroidInjector { Timber.v("Setting up test result available notification services.") pcrTestResultAvailableNotificationService.setup() raTestResultAvailableNotificationService.setup() + testCertificateRetrievalScheduler.setup() Timber.v("Setting up vaccination data update scheduler.") vaccinationUpdateScheduler.setup() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt index d076c0408..f1377d38f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt @@ -7,6 +7,7 @@ import de.rki.coronawarnapp.appconfig.download.AppConfigApiV2 import de.rki.coronawarnapp.appconfig.mapping.AnalyticsConfigMapper import de.rki.coronawarnapp.appconfig.mapping.CWAConfigMapper import de.rki.coronawarnapp.appconfig.mapping.CoronaTestConfigMapper +import de.rki.coronawarnapp.appconfig.mapping.CovidCertificateConfigMapper import de.rki.coronawarnapp.appconfig.mapping.ExposureDetectionConfigMapper import de.rki.coronawarnapp.appconfig.mapping.ExposureWindowRiskCalculationConfigMapper import de.rki.coronawarnapp.appconfig.mapping.KeyDownloadParametersMapper @@ -96,6 +97,10 @@ class AppConfigModule { fun coronaTestConfigMapper(mapper: CoronaTestConfigMapper): CoronaTestConfig.Mapper = mapper + @Provides + fun covidCertificateConfigMapper(mapper: CovidCertificateConfigMapper): + CovidCertificateConfig.Mapper = mapper + companion object { private val HTTP_TIMEOUT_APPCONFIG = Duration.standardSeconds(10) private const val DEFAULT_CACHE_SIZE = 2 * 1024 * 1024L // 5MB diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/CovidCertificateConfig.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/CovidCertificateConfig.kt new file mode 100644 index 000000000..620b5e4ab --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/CovidCertificateConfig.kt @@ -0,0 +1,16 @@ +package de.rki.coronawarnapp.appconfig + +import de.rki.coronawarnapp.appconfig.mapping.ConfigMapper +import org.joda.time.Duration + +interface CovidCertificateConfig { + + val testCertificate: TestCertificate + + interface TestCertificate { + val waitAfterPublicKeyRegistration: Duration + val waitForRetry: Duration + } + + interface Mapper : ConfigMapper<CovidCertificateConfig> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt index d0d379026..aaac8ae61 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt @@ -3,6 +3,7 @@ package de.rki.coronawarnapp.appconfig.mapping import de.rki.coronawarnapp.appconfig.AnalyticsConfig import de.rki.coronawarnapp.appconfig.CWAConfig import de.rki.coronawarnapp.appconfig.CoronaTestConfig +import de.rki.coronawarnapp.appconfig.CovidCertificateConfig import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig import de.rki.coronawarnapp.appconfig.KeyDownloadConfig @@ -24,4 +25,5 @@ interface ConfigMapping : val logUpload: LogUploadConfig val presenceTracing: PresenceTracingConfig val coronaTestParameters: CoronaTestConfig + val covidCertificateParameters: CovidCertificateConfig } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt index ced74a1e8..b29c6b17d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt @@ -4,6 +4,7 @@ import dagger.Reusable import de.rki.coronawarnapp.appconfig.AnalyticsConfig import de.rki.coronawarnapp.appconfig.CWAConfig import de.rki.coronawarnapp.appconfig.CoronaTestConfig +import de.rki.coronawarnapp.appconfig.CovidCertificateConfig import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig import de.rki.coronawarnapp.appconfig.KeyDownloadConfig @@ -14,6 +15,7 @@ import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid import timber.log.Timber import javax.inject.Inject +@Suppress("LongParameterList") @Reusable class ConfigParser @Inject constructor( private val cwaConfigMapper: CWAConfig.Mapper, @@ -25,6 +27,7 @@ class ConfigParser @Inject constructor( private val logUploadConfigMapper: LogUploadConfig.Mapper, private val presenceTracingConfigMapper: PresenceTracingConfig.Mapper, private val coronaTestConfigMapper: CoronaTestConfig.Mapper, + private val covidCertificateConfigMapper: CovidCertificateConfig.Mapper, ) { fun parse(configBytes: ByteArray): ConfigMapping = try { @@ -39,7 +42,8 @@ class ConfigParser @Inject constructor( analytics = analyticsConfigMapper.map(it), logUpload = logUploadConfigMapper.map(it), presenceTracing = presenceTracingConfigMapper.map(it), - coronaTestParameters = coronaTestConfigMapper.map(it) + coronaTestParameters = coronaTestConfigMapper.map(it), + covidCertificateParameters = covidCertificateConfigMapper.map(it), ) } } catch (e: Exception) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/CovidCertificateConfigMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/CovidCertificateConfigMapper.kt new file mode 100644 index 000000000..e0ca90bd9 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/CovidCertificateConfigMapper.kt @@ -0,0 +1,59 @@ +package de.rki.coronawarnapp.appconfig.mapping + +import dagger.Reusable +import de.rki.coronawarnapp.appconfig.CovidCertificateConfig +import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid +import de.rki.coronawarnapp.server.protocols.internal.v2.DgcParameters +import org.joda.time.Duration +import timber.log.Timber +import javax.inject.Inject + +@Reusable +class CovidCertificateConfigMapper @Inject constructor() : CovidCertificateConfig.Mapper { + override fun map(rawConfig: AppConfigAndroid.ApplicationConfigurationAndroid): CovidCertificateConfig { + if (!rawConfig.hasDgcParameters()) { + Timber.w("Config has no DCC parameters.") + return CovidCertificateConfigContainer() + } + + return CovidCertificateConfigContainer( + testCertificate = rawConfig.dgcParameters.mapCovidCertificateConfig() + ) + } + + private fun DgcParameters.DGCParameters.mapCovidCertificateConfig(): CovidCertificateConfig.TestCertificate { + if (!this.hasTestCertificateParameters()) { + Timber.w("DCC config has no test certificate parameters.") + return TestCertificateConfigContainer() + } + return with(testCertificateParameters) { + TestCertificateConfigContainer( + waitAfterPublicKeyRegistration = waitAfterPublicKeyRegistrationInSeconds.let { + if (it !in 0..60) { + Timber.e("Invalid value for waitAfterPublicKeyRegistration: %s", it) + TestCertificateConfigContainer().waitAfterPublicKeyRegistration + } else { + Duration.standardSeconds(it.toLong()) + } + }, + waitForRetry = waitForRetryInSeconds.let { + if (it !in 0..60) { + Timber.e("Invalid value for waitForRetryInSeconds: %s", it) + TestCertificateConfigContainer().waitForRetry + } else { + Duration.standardSeconds(it.toLong()) + } + } + ) + } + } + + data class CovidCertificateConfigContainer( + override val testCertificate: CovidCertificateConfig.TestCertificate = TestCertificateConfigContainer() + ) : CovidCertificateConfig + + data class TestCertificateConfigContainer( + override val waitAfterPublicKeyRegistration: Duration = Duration.standardSeconds(10), + override val waitForRetry: Duration = Duration.standardSeconds(10), + ) : CovidCertificateConfig.TestCertificate +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt index 220c854e0..fe1140340 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt @@ -3,6 +3,7 @@ package de.rki.coronawarnapp.appconfig.mapping import de.rki.coronawarnapp.appconfig.AnalyticsConfig import de.rki.coronawarnapp.appconfig.CWAConfig import de.rki.coronawarnapp.appconfig.CoronaTestConfig +import de.rki.coronawarnapp.appconfig.CovidCertificateConfig import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig import de.rki.coronawarnapp.appconfig.KeyDownloadConfig @@ -22,6 +23,7 @@ data class DefaultConfigMapping( override val logUpload: LogUploadConfig, override val presenceTracing: PresenceTracingConfig, override val coronaTestParameters: CoronaTestConfig, + override val covidCertificateParameters: CovidCertificateConfig, ) : ConfigMapping, CWAConfig by cwaConfig, KeyDownloadConfig by keyDownloadConfig, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt index c0815c4f3..9af108afe 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt @@ -224,6 +224,14 @@ class CoronaTestRepository @Inject constructor( } } + suspend fun markDccAsCreated(identifier: TestIdentifier, created: Boolean) { + Timber.tag(TAG).i("markDccAsCreated(identifier=%s, created=%b)", identifier, created) + + modifyTest(identifier) { processor, before -> + processor.markDccCreated(before, created) + } + } + companion object { const val TAG = "CoronaTestRepository" } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/TestCertificateRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/TestCertificateRepository.kt new file mode 100644 index 000000000..c367daa52 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/TestCertificateRepository.kt @@ -0,0 +1,377 @@ +package de.rki.coronawarnapp.coronatest + +import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.bugreporting.reportProblem +import de.rki.coronawarnapp.coronatest.storage.TestCertificateStorage +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.coronatest.type.TestCertificateContainer +import de.rki.coronawarnapp.coronatest.type.TestCertificateIdentifier +import de.rki.coronawarnapp.coronatest.type.pcr.PCRCertificateContainer +import de.rki.coronawarnapp.coronatest.type.rapidantigen.RACertificateContainer +import de.rki.coronawarnapp.covidcertificate.server.CovidCertificateServer +import de.rki.coronawarnapp.covidcertificate.server.TestCertificateComponents +import de.rki.coronawarnapp.covidcertificate.test.TestCertificateQRCodeExtractor +import de.rki.coronawarnapp.util.TimeStamper +import de.rki.coronawarnapp.util.coroutine.AppScope +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.encryption.rsa.RSACryptography +import de.rki.coronawarnapp.util.encryption.rsa.RSAKeyPairGenerator +import de.rki.coronawarnapp.util.flow.HotDataFlow +import de.rki.coronawarnapp.util.mutate +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.plus +import kotlinx.coroutines.withContext +import okio.ByteString.Companion.decodeBase64 +import org.joda.time.Duration +import timber.log.Timber +import java.util.UUID +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class TestCertificateRepository @Inject constructor( + @AppScope private val appScope: CoroutineScope, + private val dispatcherProvider: DispatcherProvider, + private val timeStamper: TimeStamper, + private val storage: TestCertificateStorage, + private val certificateServer: CovidCertificateServer, + private val rsaKeyPairGenerator: RSAKeyPairGenerator, + private val rsaCryptography: RSACryptography, + private val qrCodeExtractor: TestCertificateQRCodeExtractor, + private val appConfigProvider: AppConfigProvider, +) { + + private val internalData: HotDataFlow<Map<TestCertificateIdentifier, TestCertificateContainer>> = HotDataFlow( + loggingTag = TAG, + scope = appScope + dispatcherProvider.Default, + sharingBehavior = SharingStarted.Eagerly, + ) { + storage.testCertificates.map { it.identifier to it }.toMap().also { + Timber.tag(TAG).v("Restored TestCertificate data: %s", it) + } + } + + val certificates: Flow<Set<TestCertificateContainer>> = internalData.data.map { it.values.toSet() } + + init { + internalData.data + .onStart { Timber.tag(TAG).d("Observing TestCertificateContainer data.") } + .onEach { + Timber.tag(TAG).v("TestCertificateContainer data changed: %s", it) + storage.testCertificates = it.values.toSet() + } + .catch { + it.reportProblem(TAG, "Failed to snapshot TestCertificateContainer data to storage.") + throw it + } + .launchIn(appScope + dispatcherProvider.Default) + } + + /** + * Will create a new test certificate entry. + * Automation via [de.rki.coronawarnapp.coronatest.type.common.TestCertificateRetrievalScheduler] will kick in. + * + * Throws an exception if there already is a test certificate entry for this test + * or this is not a valid test (no consent, not supported by PoC). + */ + suspend fun requestCertificate(test: CoronaTest): TestCertificateContainer { + Timber.tag(TAG).d("createDccForTest(test.identifier=%s)", test.identifier) + + val newData = internalData.updateBlocking { + if (values.any { it.registrationToken == test.registrationToken }) { + Timber.tag(TAG).e("Certificate entry already exists for %s", test.identifier) + throw IllegalArgumentException("A certificate was already created for this ${test.identifier}") + } + if (!test.isDccSupportedByPoc) { + throw IllegalArgumentException("DCC is not supported by PoC for this test: ${test.identifier}") + } + if (!test.isDccConsentGiven) { + throw IllegalArgumentException("DCC was not given for this test: ${test.identifier}") + } + + val identifier = UUID.randomUUID().toString() + + val certificate = when (test.type) { + CoronaTest.Type.PCR -> PCRCertificateContainer( + identifier = identifier, + registeredAt = test.registeredAt, + registrationToken = test.registrationToken, + ) + CoronaTest.Type.RAPID_ANTIGEN -> RACertificateContainer( + identifier = identifier, + registeredAt = test.registeredAt, + registrationToken = test.registrationToken, + ) + } + Timber.tag(TAG).d("Adding test certificate entry: %s", certificate) + mutate { this[certificate.identifier] = certificate } + } + + return newData.values.single { it.registrationToken == test.registrationToken } + } + + /** + * If [error] is NULL, then [certificate] will be the refreshed entry. + * If [error] is not NULL, then [certificate] is the latest version before the exception occured. + * Due to refresh being a multiple process, some steps can successed, while others fail. + */ + data class RefreshResult( + val certificate: TestCertificateContainer, + val error: Exception? = null, + ) + + /** + * The refresh call checks each certificate entry for public keys and certificate state. + * It will be triggered via TestCertificateRetrievalScheduler. + * After requestCertificate, calling refresh often enough should yield a certificate eventually. + * + * This returns a set of [RefreshResult], one for each refreshed test certificate entry. + * If you specify an identifier, then the set will only contain a single element. + * + * [refresh] itself will NOT throw an exception. + */ + suspend fun refresh(identifier: TestCertificateIdentifier? = null): Set<RefreshResult> { + Timber.tag(TAG).d("refresh(identifier=%s)", identifier) + + val refreshCallResults = mutableMapOf<TestCertificateIdentifier, RefreshResult>() + + val workedOnIds = mutableSetOf<TestCertificateIdentifier>() + + internalData.updateBlocking { + val toRefresh = values + .filter { it.identifier == identifier || identifier == null } // Targets of our refresh + .filter { !it.isUpdatingData && it.isCertificateRetrievalPending } // Those that need refreshing + + mutate { + toRefresh.forEach { + workedOnIds.add(it.identifier) + this[it.identifier] = when (it.type) { + CoronaTest.Type.PCR -> (it as PCRCertificateContainer).copy(isUpdatingData = true) + CoronaTest.Type.RAPID_ANTIGEN -> (it as RACertificateContainer).copy(isUpdatingData = true) + } + } + } + } + + internalData.updateBlocking { + Timber.tag(TAG).d("Checking for unregistered public keys.") + + val refreshedCerts = values + .filter { workedOnIds.contains(it.identifier) } // Refresh targets + .filter { !it.isPublicKeyRegistered } // Targets of this step + .map { cert -> + try { + RefreshResult(registerPublicKey(cert)) + } catch (e: Exception) { + Timber.tag(TAG).e(e, "Failed to register public key for %s", cert) + RefreshResult(cert, e) + } + } + + refreshedCerts.forEach { + refreshCallResults[it.certificate.identifier] = it + } + + mutate { + refreshedCerts + .filter { it.error == null } + .map { it.certificate } + .forEach { this[it.identifier] = it } + } + } + + internalData.updateBlocking { + Timber.tag(TAG).d("Checking for pending certificates.") + + val refreshedCerts = values + .filter { workedOnIds.contains(it.identifier) } // Refresh targets + .filter { it.isPublicKeyRegistered && it.isCertificateRetrievalPending } // Targets of this step + .map { cert -> + try { + RefreshResult(obtainCertificate(cert)) + } catch (e: Exception) { + Timber.tag(TAG).e(e, "Failed to retrieve test certificate for %s", cert) + RefreshResult(cert, e) + } + } + + refreshedCerts.forEach { + refreshCallResults[it.certificate.identifier] = it + } + + mutate { + refreshedCerts + .filter { it.error == null } + .map { it.certificate } + .forEach { this[it.identifier] = it } + } + } + + internalData.updateBlocking { + val certs = values.filter { workedOnIds.contains(it.identifier) } + + mutate { + certs.forEach { + this[it.identifier] = when (it.type) { + CoronaTest.Type.PCR -> (it as PCRCertificateContainer).copy(isUpdatingData = false) + CoronaTest.Type.RAPID_ANTIGEN -> (it as RACertificateContainer).copy(isUpdatingData = false) + } + } + } + } + + return refreshCallResults.values.toSet() + } + + /** + * Register the public key with the server, a shortwhile later, + * the test certificate components should be available, via [obtainCertificate]. + */ + private suspend fun registerPublicKey( + cert: TestCertificateContainer + ): TestCertificateContainer { + return try { + Timber.tag(TAG).d("registerPublicKey(cert=%s)", cert) + + if (cert.isPublicKeyRegistered) { + Timber.tag(TAG).d("Public key is already registered for %s", cert) + return cert + } + + val rsaKeyPair = rsaKeyPairGenerator.generate() + + withContext(dispatcherProvider.IO) { + certificateServer.registerPublicKeyForTest( + testRegistrationToken = cert.registrationToken, + publicKey = rsaKeyPair.publicKey, + ) + } + Timber.tag(TAG).i("Public key successfully registered for %s", cert) + + val nowUTC = timeStamper.nowUTC + + when (cert.type) { + CoronaTest.Type.PCR -> (cert as PCRCertificateContainer).copy( + publicKeyRegisteredAt = nowUTC, + rsaPublicKey = rsaKeyPair.publicKey, + rsaPrivateKey = rsaKeyPair.privateKey, + ) + CoronaTest.Type.RAPID_ANTIGEN -> (cert as RACertificateContainer).copy( + publicKeyRegisteredAt = nowUTC, + rsaPublicKey = rsaKeyPair.publicKey, + rsaPrivateKey = rsaKeyPair.privateKey, + ) + } + } catch (e: Exception) { + Timber.tag(TAG).e("Failed to register public key for %s", cert) + throw e + } + } + + /** + * Try to obtain the actual certificate. + * PublicKey registration and certificate retrieval are two steps, because if we manage to register our public key, + * but fail to get the certificate, we are still one step further. + * + * The server does not immediately return the test certificate components after registering the public key. + */ + private suspend fun obtainCertificate( + cert: TestCertificateContainer + ): TestCertificateContainer { + return try { + Timber.tag(TAG).d("requestCertificate(cert=%s)", cert) + + if (!cert.isPublicKeyRegistered) throw IllegalStateException("Public key is not registered yet.") + + if (!cert.isCertificateRetrievalPending) { + Timber.tag(TAG).d("Dcc has already been retrieved for %s", cert) + return cert + } + + val certConfig = appConfigProvider.currentConfig.first().covidCertificateParameters.testCertificate + + val nowUTC = timeStamper.nowUTC + val certAvailableAt = cert.publicKeyRegisteredAt!!.plus(certConfig.waitAfterPublicKeyRegistration) + val certAvailableIn = Duration(nowUTC, certAvailableAt) + + val components = withContext(dispatcherProvider.IO) { + if (certAvailableIn > Duration.ZERO && certAvailableIn <= certConfig.waitAfterPublicKeyRegistration) { + Timber.tag(TAG).d("Delaying certificate retrieval by %d ms", certAvailableIn.millis) + delay(certAvailableIn.millis) + } + + val executeRequest: suspend CoroutineScope.() -> TestCertificateComponents = { + certificateServer.requestCertificateForTest(testRegistrationToken = cert.registrationToken) + } + + try { + executeRequest() + } catch (e: Exception) { + // TODO catch a specific error that reflects error code DGC_COMP_202 + delay(certConfig.waitForRetry.millis) + executeRequest() + } + } + Timber.tag(TAG).i("Test certificate components successfully request for %s: %s", cert, components) + + val encryptionkey = rsaCryptography.decrypt( + toDecrypt = components.dataEncryptionKeyBase64.decodeBase64()!!, + privateKey = cert.rsaPrivateKey!! + ) + + val extractedData = qrCodeExtractor.extract( + decryptionKey = encryptionkey.toByteArray(), + encryptedCoseComponents = components.encryptedCoseTestCertificateBase64.decodeBase64()!!.toByteArray() + ) + + val nowUtc = timeStamper.nowUTC + + when (cert.type) { + CoronaTest.Type.PCR -> (cert as PCRCertificateContainer).copy( + testCertificateQrCode = extractedData.qrCode, + certificateReceivedAt = nowUtc, + ) + CoronaTest.Type.RAPID_ANTIGEN -> (cert as RACertificateContainer).copy( + testCertificateQrCode = extractedData.qrCode, + certificateReceivedAt = nowUtc, + ) + }.also { + it.qrCodeExtractor = qrCodeExtractor + it.preParsedData = extractedData.testCertificateData + } + } catch (e: Exception) { + Timber.tag(TAG).e("Failed to retrieve certificate components for %s", cert) + throw e + } + } + + /** + * [deleteCertificate] does not throw an exception, if the deletion target already does not exist. + */ + suspend fun deleteCertificate(identifier: TestCertificateIdentifier) { + Timber.tag(TAG).d("deleteTestCertificate(identifier=%s)", identifier) + internalData.updateBlocking { + mutate { + remove(identifier) + } + } + } + + suspend fun clear() { + Timber.tag(TAG).i("clear()") + internalData.updateBlocking { emptyMap() } + } + + companion object { + private val TAG = TestCertificateRepository::class.simpleName!! + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/storage/TestCertificateStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/storage/TestCertificateStorage.kt new file mode 100644 index 000000000..c01d849d1 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/storage/TestCertificateStorage.kt @@ -0,0 +1,102 @@ +package de.rki.coronawarnapp.coronatest.storage + +import android.content.Context +import androidx.core.content.edit +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.coronatest.type.TestCertificateContainer +import de.rki.coronawarnapp.coronatest.type.pcr.PCRCertificateContainer +import de.rki.coronawarnapp.coronatest.type.rapidantigen.RACertificateContainer +import de.rki.coronawarnapp.util.di.AppContext +import de.rki.coronawarnapp.util.serialization.BaseGson +import de.rki.coronawarnapp.vaccination.core.repository.storage.ContainerPostProcessor +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class TestCertificateStorage @Inject constructor( + @AppContext val context: Context, + @BaseGson val baseGson: Gson, + private val containerPostProcessor: ContainerPostProcessor, +) { + + private val prefs by lazy { + context.getSharedPreferences("coronatest_certificate_localdata", Context.MODE_PRIVATE) + } + + private val gson by lazy { + baseGson.newBuilder().apply { + registerTypeAdapter(CoronaTestResult::class.java, CoronaTestResult.GsonAdapter()) + registerTypeAdapterFactory(containerPostProcessor) + }.create() + } + + private val typeTokenPCR by lazy { + object : TypeToken<Set<PCRCertificateContainer>>() {}.type + } + + private val typeTokenRA by lazy { + object : TypeToken<Set<RACertificateContainer>>() {}.type + } + + var testCertificates: Collection<TestCertificateContainer> + get() { + Timber.tag(TAG).d("load()") + + val pcrCerts: Set<PCRCertificateContainer> = run { + val raw = prefs.getString(PKEY_DATA_PCR, null) ?: return@run emptySet() + gson.fromJson<Set<PCRCertificateContainer>>(raw, typeTokenPCR).onEach { + Timber.tag(TAG).v("PCR loaded: %s", it) + requireNotNull(it.identifier) + requireNotNull(it.type) { "PCR type should not be null, GSON footgun." } + } + } + + val raCerts: Set<RACertificateContainer> = run { + val raw = prefs.getString(PKEY_DATA_RA, null) ?: return@run emptySet() + gson.fromJson<Set<RACertificateContainer>>(raw, typeTokenRA).onEach { + Timber.tag(TAG).v("RA loaded: %s", it) + requireNotNull(it.identifier) + requireNotNull(it.type) { "RA type should not be null, GSON footgun." } + } + } + + return (pcrCerts + raCerts).also { + Timber.tag(TAG).v("Loaded %d certificates.", it.size) + } + } + set(value) { + Timber.tag(TAG).d("save(testCertificates=%s)", value) + prefs.edit { + value.filter { it.type == CoronaTest.Type.PCR }.run { + if (isNotEmpty()) { + val raw = gson.toJson(this, typeTokenPCR) + Timber.tag(TAG).v("PCR storing: %s", raw) + putString(PKEY_DATA_PCR, raw) + } else { + Timber.tag(TAG).v("No PCR certificates available, clearing.") + remove(PKEY_DATA_PCR) + } + } + value.filter { it.type == CoronaTest.Type.RAPID_ANTIGEN }.run { + if (isNotEmpty()) { + val raw = gson.toJson(this, typeTokenRA) + Timber.tag(TAG).v("RA storing: %s", raw) + putString(PKEY_DATA_RA, raw) + } else { + Timber.tag(TAG).v("No RA certificates available, clearing.") + remove(PKEY_DATA_RA) + } + } + } + } + + companion object { + private const val TAG = "TestCertificateStorage" + private const val PKEY_DATA_RA = "testcertificate.data.ra" + private const val PKEY_DATA_PCR = "testcertificate.data.pcr" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTest.kt index f516a6e5e..824a468b7 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTest.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTest.kt @@ -43,7 +43,7 @@ interface CoronaTest { // Is the digital green certificate supported by the point of care that issued the test val isDccSupportedByPoc: Boolean - // Has the user given consent to us obtaining the DGC + // Has the user given consent to us obtaining the DCC val isDccConsentGiven: Boolean // Has the corresponding entry been created in the test certificate storage diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTestProcessor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTestProcessor.kt index 68b69445c..32351e107 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTestProcessor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTestProcessor.kt @@ -24,4 +24,6 @@ interface CoronaTestProcessor { suspend fun updateSubmissionConsent(test: CoronaTest, consented: Boolean): CoronaTest suspend fun updateResultNotification(test: CoronaTest, sent: Boolean): CoronaTest + + suspend fun markDccCreated(test: CoronaTest, created: Boolean): CoronaTest } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/TestCertificateContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/TestCertificateContainer.kt new file mode 100644 index 000000000..40f3c02ce --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/TestCertificateContainer.kt @@ -0,0 +1,114 @@ +package de.rki.coronawarnapp.coronatest.type + +import de.rki.coronawarnapp.covidcertificate.test.TestCertificate +import de.rki.coronawarnapp.covidcertificate.test.TestCertificateData +import de.rki.coronawarnapp.covidcertificate.test.TestCertificateQRCodeExtractor +import de.rki.coronawarnapp.util.encryption.rsa.RSAKey +import de.rki.coronawarnapp.vaccination.core.CertificatePersonIdentifier +import de.rki.coronawarnapp.vaccination.core.personIdentifier +import de.rki.coronawarnapp.vaccination.core.qrcode.QrCodeString +import de.rki.coronawarnapp.vaccination.core.server.valueset.VaccinationValueSet +import de.rki.coronawarnapp.vaccination.core.server.valueset.getDisplayText +import okio.ByteString +import org.joda.time.Instant +import org.joda.time.LocalDate +import java.util.Locale + +abstract class TestCertificateContainer { + abstract val identifier: TestCertificateIdentifier + abstract val registrationToken: RegistrationToken + abstract val type: CoronaTest.Type + abstract val registeredAt: Instant + abstract val publicKeyRegisteredAt: Instant? + abstract val rsaPublicKey: RSAKey.Public? + abstract val rsaPrivateKey: RSAKey.Private? + abstract val certificateReceivedAt: Instant? + abstract val encryptedDataEncryptionkey: ByteString? + abstract val encryptedDccCose: ByteString? + abstract val testCertificateQrCode: String? + + abstract val isUpdatingData: Boolean + + // Either set by [ContainerPostProcessor] or during first update + @Transient internal lateinit var qrCodeExtractor: TestCertificateQRCodeExtractor + + @Transient internal var preParsedData: TestCertificateData? = null + + @delegate:Transient + private val certificateData: TestCertificateData by lazy { + preParsedData ?: testCertificateQrCode!!.let { qrCodeExtractor.extract(it).testCertificateData } + } + + val isPublicKeyRegistered: Boolean + get() = publicKeyRegisteredAt != null + + val isCertificateRetrievalPending: Boolean + get() = certificateReceivedAt == null + + val certificateId: String? + get() { + if (isCertificateRetrievalPending) return null + return certificateData.certificate.testCertificateData.single().uniqueCertificateIdentifier + } + + fun toTestCertificate( + valueSet: VaccinationValueSet?, + userLocale: Locale = Locale.getDefault(), + ): TestCertificate? { + if (isCertificateRetrievalPending) return null + + val header = certificateData.header + val certificate = certificateData.certificate + val testCertificate = certificate.testCertificateData.single() + + return object : TestCertificate { + override val personIdentifier: CertificatePersonIdentifier + get() = certificate.personIdentifier + + override val firstName: String? + get() = certificate.nameData.givenName + override val lastName: String + get() = certificate.nameData.familyName ?: certificate.nameData.familyNameStandardized + + override val dateOfBirth: LocalDate + get() = certificate.dateOfBirth + + override val targetName: String + get() = valueSet?.getDisplayText(testCertificate.targetId) ?: testCertificate.targetId + override val testType: String + get() = valueSet?.getDisplayText(testCertificate.testType) ?: testCertificate.testType + override val testResult: String + get() = valueSet?.getDisplayText(testCertificate.testResult) ?: testCertificate.testResult + override val testName: String? + get() = testCertificate.testName?.let { valueSet?.getDisplayText(it) ?: it } + override val testNameAndManufactor: String? + get() = testCertificate.testNameAndManufactor?.let { valueSet?.getDisplayText(it) ?: it } + override val sampleCollectedAt: Instant + get() = testCertificate.sampleCollectedAt + override val testResultAt: Instant + get() = testCertificate.testResultAt + override val testCenter: String + get() = testCertificate.testCenter + + override val certificateIssuer: String + get() = header.issuer + override val certificateCountry: String + get() = Locale(userLocale.language, testCertificate.countryOfTest.uppercase()) + .getDisplayCountry(userLocale) + override val certificateId: String + get() = testCertificate.uniqueCertificateIdentifier + + override val issuer: String + get() = header.issuer + override val issuedAt: Instant + get() = header.issuedAt + override val expiresAt: Instant + get() = header.expiresAt + + override val qrCode: QrCodeString + get() = testCertificateQrCode!! + } + } +} + +typealias TestCertificateIdentifier = String diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/TestCertificateRetrievalScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/TestCertificateRetrievalScheduler.kt new file mode 100644 index 000000000..f0391294d --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/TestCertificateRetrievalScheduler.kt @@ -0,0 +1,117 @@ +package de.rki.coronawarnapp.coronatest.type.common + +import androidx.work.BackoffPolicy +import androidx.work.Constraints +import androidx.work.ExistingWorkPolicy +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.coronatest.TestCertificateRepository +import de.rki.coronawarnapp.util.coroutine.AppScope +import de.rki.coronawarnapp.util.device.ForegroundState +import de.rki.coronawarnapp.util.flow.combine +import de.rki.coronawarnapp.worker.BackgroundConstants +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import timber.log.Timber +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class TestCertificateRetrievalScheduler @Inject constructor( + @AppScope private val appScope: CoroutineScope, + private val workManager: WorkManager, + private val certificateRepo: TestCertificateRepository, + private val testRepo: CoronaTestRepository, + private val foregroundState: ForegroundState, +) : ResultScheduler( + workManager = workManager +) { + private val processedNewCerts = mutableSetOf<String>() + + private val creationTrigger = testRepo.coronaTests + .map { tests -> + tests + .filter { it.isDccSupportedByPoc } // Only those that support it + .filter { it.isNegative } // Certs only to proof negative state + .filter { it.isDccConsentGiven && !it.isDccDataSetCreated } // Consent and doesn't exist already? + } + .distinctUntilChanged() + + private val refreshTrigger = combine( + certificateRepo.certificates, + foregroundState.isInForeground, + ) { certificates, isForeground -> + + val hasNewCert = certificates.any { + val isNew = !processedNewCerts.contains(it.identifier) + if (isNew) processedNewCerts.add(it.identifier) + isNew + } + + val hasWorkToDo = certificates.any { it.isCertificateRetrievalPending && !it.isUpdatingData } + Timber.tag(TAG).v("shouldPollDcc? hasNewCert=$hasNewCert, hasWorkTodo=$hasWorkToDo, foreground=$isForeground") + (isForeground || hasNewCert) && hasWorkToDo + } + + fun setup() { + Timber.tag(TAG).i("setup() - TestCertificateRetrievalScheduler") + + // Create a certificate entry for each viable test that has none + creationTrigger + .onEach { testsWithoutCert -> + Timber.tag(TAG).d("State change: testsWithoutCert=$testsWithoutCert") + testsWithoutCert.forEach { test -> + val cert = certificateRepo.requestCertificate(test) + Timber.tag(TAG).v("Certificate was created: %s", cert) + testRepo.markDccAsCreated(test.identifier, created = true) + } + } + .catch { Timber.tag(TAG).e(it, "Creation trigger failed.") } + .launchIn(appScope) + + // For each change to the set of existing certificates, check if we need to refresh/load data + refreshTrigger + .onEach { checkCerts -> + Timber.tag(TAG).d("State change: checkCerts=$checkCerts") + if (checkCerts) scheduleWorker() + } + .catch { Timber.tag(TAG).e(it, "Refresh trigger failed.") } + .launchIn(appScope) + } + + internal suspend fun scheduleWorker() { + Timber.tag(TAG).i("scheduleWorker()") + + if (isScheduled(WORKER_ID)) { + Timber.tag(TAG).d("Worker already queued, skipping requeue.") + } + + Timber.tag(TAG).d("enqueueUniqueWork PCR_TESTRESULT_WORKER_UNIQUEUNAME") + workManager.enqueueUniqueWork( + WORKER_ID, + ExistingWorkPolicy.KEEP, + buildWorkerRequest() + ) + } + + private fun buildWorkerRequest() = + OneTimeWorkRequestBuilder<TestCertificateRetrievalWorker>() + .setConstraints( + Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build() + ) + .setBackoffCriteria(BackoffPolicy.LINEAR, BackgroundConstants.KIND_DELAY, TimeUnit.MINUTES) + .build() + + companion object { + private const val WORKER_ID = "TestCertificateRetrievalWorker" + + private val TAG = TestCertificateRetrievalScheduler::class.simpleName!! + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/TestCertificateRetrievalWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/TestCertificateRetrievalWorker.kt new file mode 100644 index 000000000..262204eb0 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/TestCertificateRetrievalWorker.kt @@ -0,0 +1,51 @@ +package de.rki.coronawarnapp.coronatest.type.common + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.coronatest.TestCertificateRepository +import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory +import de.rki.coronawarnapp.worker.BackgroundConstants +import timber.log.Timber + +class TestCertificateRetrievalWorker @AssistedInject constructor( + @Assisted val context: Context, + @Assisted workerParams: WorkerParameters, + private val testCertificateRepository: TestCertificateRepository, +) : CoroutineWorker(context, workerParams) { + + override suspend fun doWork(): Result { + Timber.tag(TAG).d("$id: doWork() started. Run attempt: $runAttemptCount") + + if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) { + Timber.tag(TAG).d("$id doWork() failed after $runAttemptCount attempts. Aborting...") + return Result.failure() + } + + return try { + Timber.tag(TAG).v("Refreshing test certificates.") + val results = testCertificateRepository.refresh() + + if (results.any { it.error != null }) { + Timber.tag(TAG).w("Some test certificates failed refresh, will retry.") + Result.retry() + } else { + Timber.tag(TAG).d("No errors during test certificate refresh :).") + Result.success() + } + } catch (e: Exception) { + Timber.tag(TAG).e(e, "Test result retrieval worker failed.") + Result.retry() + } + } + + @AssistedFactory + interface Factory : InjectedWorkerFactory<TestCertificateRetrievalWorker> + + companion object { + private val TAG = TestCertificateRetrievalWorker::class.java.simpleName + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCertificateContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCertificateContainer.kt new file mode 100644 index 000000000..efa8149d1 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCertificateContainer.kt @@ -0,0 +1,55 @@ +package de.rki.coronawarnapp.coronatest.type.pcr + +import com.google.gson.annotations.SerializedName +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.coronatest.type.RegistrationToken +import de.rki.coronawarnapp.coronatest.type.TestCertificateContainer +import de.rki.coronawarnapp.util.encryption.rsa.RSAKey +import okio.ByteString +import org.joda.time.Instant + +data class PCRCertificateContainer internal constructor( + @SerializedName("identifier") + override val identifier: String, + + @SerializedName("registrationToken") + override val registrationToken: RegistrationToken, + + @SerializedName("registeredAt") + override val registeredAt: Instant, + + @SerializedName("publicKeyRegisteredAt") + override val publicKeyRegisteredAt: Instant? = null, + + @SerializedName("rsaPublicKey") + override val rsaPublicKey: RSAKey.Public? = null, + + @SerializedName("rsaPrivateKey") + override val rsaPrivateKey: RSAKey.Private? = null, + + @SerializedName("certificateReceivedAt") + override val certificateReceivedAt: Instant? = null, + + @SerializedName("encryptedDataEncryptionkey") + override val encryptedDataEncryptionkey: ByteString? = null, + + @SerializedName("encryptedDccCose") + override val encryptedDccCose: ByteString? = null, + + @SerializedName("testCertificateQrCode") + override val testCertificateQrCode: String? = null, + + @Transient override val isUpdatingData: Boolean = false, +) : TestCertificateContainer() { + + // Otherwise GSON unsafes reflection to create this class, and sets the LAZY to null + @Suppress("unused") + constructor() : this( + identifier = "", + registrationToken = "", + registeredAt = Instant.EPOCH + ) + + override val type: CoronaTest.Type + get() = CoronaTest.Type.PCR +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessor.kt index a4e0346e2..b46f1f7ad 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessor.kt @@ -129,6 +129,7 @@ class PCRProcessor @Inject constructor( registrationToken = response.registrationToken, testResult = testResult, testResultReceivedAt = determineReceivedDate(null, testResult), + isDccConsentGiven = request.isDccConsentGiven, ) } @@ -245,6 +246,13 @@ class PCRProcessor @Inject constructor( return test.copy(isResultAvailableNotificationSent = sent) } + override suspend fun markDccCreated(test: CoronaTest, created: Boolean): CoronaTest { + Timber.tag(TAG).v("markDccCreated(test=%s, created=%b)", test, created) + test as PCRCoronaTest + + return test.copy(isDccDataSetCreated = created) + } + companion object { private val FINAL_STATES = setOf(PCR_POSITIVE, PCR_NEGATIVE, PCR_REDEEMED) internal const val TAG = "PCRProcessor" diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RACertificateContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RACertificateContainer.kt new file mode 100644 index 000000000..87fbfaabe --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RACertificateContainer.kt @@ -0,0 +1,55 @@ +package de.rki.coronawarnapp.coronatest.type.rapidantigen + +import com.google.gson.annotations.SerializedName +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.coronatest.type.RegistrationToken +import de.rki.coronawarnapp.coronatest.type.TestCertificateContainer +import de.rki.coronawarnapp.util.encryption.rsa.RSAKey +import okio.ByteString +import org.joda.time.Instant + +data class RACertificateContainer( + @SerializedName("identifier") + override val identifier: String, + + @SerializedName("registrationToken") + override val registrationToken: RegistrationToken, + + @SerializedName("registeredAt") + override val registeredAt: Instant, + + @SerializedName("publicKeyRegisteredAt") + override val publicKeyRegisteredAt: Instant? = null, + + @SerializedName("rsaPublicKey") + override val rsaPublicKey: RSAKey.Public? = null, + + @SerializedName("rsaPrivateKey") + override val rsaPrivateKey: RSAKey.Private? = null, + + @SerializedName("certificateReceivedAt") + override val certificateReceivedAt: Instant? = null, + + @SerializedName("encryptedDataEncryptionkey") + override val encryptedDataEncryptionkey: ByteString? = null, + + @SerializedName("encryptedDccCose") + override val encryptedDccCose: ByteString? = null, + + @SerializedName("testCertificateQrCode") + override val testCertificateQrCode: String? = null, + + @Transient override val isUpdatingData: Boolean = false, +) : TestCertificateContainer() { + + // Otherwise GSON unsafes reflection to create this class, and sets the LAZY to null + @Suppress("unused") + constructor() : this( + identifier = "", + registrationToken = "", + registeredAt = Instant.EPOCH + ) + + override val type: CoronaTest.Type + get() = CoronaTest.Type.RAPID_ANTIGEN +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RAProcessor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RAProcessor.kt index c2b3911c6..9f5364756 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RAProcessor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RAProcessor.kt @@ -102,6 +102,7 @@ class RAProcessor @Inject constructor( dateOfBirth = request.dateOfBirth, sampleCollectedAt = sampleCollectedAt, isDccSupportedByPoc = request.isDccSupportedByPoc, + isDccConsentGiven = request.isDccConsentGiven, ) } @@ -224,6 +225,13 @@ class RAProcessor @Inject constructor( return test.copy(isResultAvailableNotificationSent = sent) } + override suspend fun markDccCreated(test: CoronaTest, created: Boolean): CoronaTest { + Timber.tag(TAG).v("markDccCreated(test=%s, created=%b)", test, created) + test as RACoronaTest + + return test.copy(isDccDataSetCreated = created) + } + companion object { private val FINAL_STATES = setOf(RAT_POSITIVE, RAT_NEGATIVE, RAT_REDEEMED) internal const val TAG = "RapidAntigenProcessor" diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/server/CovidCertificateServer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/server/CovidCertificateServer.kt new file mode 100644 index 000000000..912482395 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/server/CovidCertificateServer.kt @@ -0,0 +1,30 @@ +package de.rki.coronawarnapp.covidcertificate.server + +import dagger.Reusable +import de.rki.coronawarnapp.coronatest.type.RegistrationToken +import de.rki.coronawarnapp.util.encryption.rsa.RSAKey +import timber.log.Timber +import javax.inject.Inject + +@Reusable +class CovidCertificateServer @Inject constructor() { + + suspend fun registerPublicKeyForTest( + testRegistrationToken: RegistrationToken, + publicKey: RSAKey.Public, + ) { + Timber.tag(TAG).v("registerPublicKeyForTest(token=%s, key=%s)", testRegistrationToken, publicKey) + throw NotImplementedError() + } + + suspend fun requestCertificateForTest( + testRegistrationToken: RegistrationToken, + ): TestCertificateComponents { + Timber.tag(TAG).v("requestCertificateForTest(token=%s)", testRegistrationToken) + throw NotImplementedError() + } + + companion object { + private const val TAG = "CovidCertificateServer" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/server/TestCertificateComponents.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/server/TestCertificateComponents.kt new file mode 100644 index 000000000..d27332408 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/server/TestCertificateComponents.kt @@ -0,0 +1,6 @@ +package de.rki.coronawarnapp.covidcertificate.server + +data class TestCertificateComponents( + val dataEncryptionKeyBase64: String, + val encryptedCoseTestCertificateBase64: String, +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificate.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificate.kt new file mode 100644 index 000000000..4d246e1dc --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificate.kt @@ -0,0 +1,45 @@ +package de.rki.coronawarnapp.covidcertificate.test + +import de.rki.coronawarnapp.vaccination.core.CertificatePersonIdentifier +import de.rki.coronawarnapp.vaccination.core.qrcode.QrCodeString +import org.joda.time.Instant +import org.joda.time.LocalDate + +interface TestCertificate { + val firstName: String? + val lastName: String + + val dateOfBirth: LocalDate + + /** + * Disease or agent targeted (required) + */ + val targetName: String + val testType: String + val testResult: String + + /** + * NAA Test Name (only for PCR tests, but not required) + */ + val testName: String? + + /** + * RAT Test name and manufacturer (only for RAT tests, but not required) + */ + val testNameAndManufactor: String? + val sampleCollectedAt: Instant + val testResultAt: Instant + val testCenter: String + + val certificateIssuer: String + val certificateCountry: String + val certificateId: String + + val personIdentifier: CertificatePersonIdentifier + + val issuer: String + val issuedAt: Instant + val expiresAt: Instant + + val qrCode: QrCodeString +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateDccV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateDccV1.kt index d66086dfe..35182a1ce 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateDccV1.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateDccV1.kt @@ -20,12 +20,14 @@ data class TestCertificateDccV1( data class TestCertificateData( // Disease or agent targeted, e.g. "tg": "840539006" @SerializedName("tg") val targetId: String, - // Vaccine or prophylaxis, e.g. "vp": "1119349007" - @SerializedName("vp") val vaccineId: String, - // Vaccine medicinal product,e.g. "mp": "EU/1/20/1528", - @SerializedName("mp") val medicalProductId: String, - // Marketing Authorization Holder, e.g. "ma": "ORG-100030215", - @SerializedName("ma") val marketAuthorizationHolderId: String, + // Type of Test (required) + @SerializedName("tt") val testType: String, + // Test Result (required) + @SerializedName("tr") val testResult: String, + // NAA Test Name (only for PCR tests, but not required) + @SerializedName("nm") val testName: String?, + // RAT Test name and manufacturer (only for RAT tests, but not required) + @SerializedName("ma") val testNameAndManufactor: String?, // Date/Time of Sample Collection (required) // "sc": "2021-04-13T14:20:00+00:00", @SerializedName("sc") val sampleCollectedAt: Instant, @@ -35,8 +37,8 @@ data class TestCertificateDccV1( // Testing Center (required) // "tc": "GGD Fryslân, L-Heliconweg", @SerializedName("tc") val testCenter: String, - // Country of Vaccination, e.g. "co": "NL" - @SerializedName("co") val countryOfVaccination: String, + // Country of Test (required) + @SerializedName("co") val countryOfTest: String, // Certificate Issuer, e.g. "is": "Ministry of Public Health, Welfare and Sport", @SerializedName("is") val certificateIssuer: String, // Unique Certificate Identifier, e.g. "ci": "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ" diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateQRCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateQRCodeExtractor.kt index e5c34fc64..6737944ce 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateQRCodeExtractor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateQRCodeExtractor.kt @@ -1,7 +1,6 @@ package de.rki.coronawarnapp.covidcertificate.test import dagger.Reusable -import okio.ByteString import javax.inject.Inject @Reusable @@ -12,8 +11,8 @@ class TestCertificateQRCodeExtractor @Inject constructor() { */ fun extract( decryptionKey: ByteArray, - encryptedCoseComponents: ByteString, - ): TestCertificateData { + encryptedCoseComponents: ByteArray, + ): TestCertificateQRCode { throw NotImplementedError() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt index df7f430bb..a4b739830 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt @@ -6,6 +6,7 @@ import de.rki.coronawarnapp.bugreporting.BugReportingSettings import de.rki.coronawarnapp.contactdiary.storage.ContactDiaryPreferences import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.coronatest.TestCertificateRepository import de.rki.coronawarnapp.coronatest.antigen.profile.RATProfileSettings import de.rki.coronawarnapp.datadonation.analytics.Analytics import de.rki.coronawarnapp.datadonation.analytics.storage.AnalyticsSettings @@ -25,9 +26,9 @@ import de.rki.coronawarnapp.storage.TracingSettings import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.ui.presencetracing.TraceLocationPreferences -import de.rki.coronawarnapp.vaccination.core.repository.ValueSetsRepository import de.rki.coronawarnapp.vaccination.core.VaccinationPreferences import de.rki.coronawarnapp.vaccination.core.repository.VaccinationRepository +import de.rki.coronawarnapp.vaccination.core.repository.ValueSetsRepository import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import timber.log.Timber @@ -67,6 +68,7 @@ class DataReset @Inject constructor( private val valueSetsRepository: ValueSetsRepository, private val vaccinationPreferences: VaccinationPreferences, private val vaccinationRepository: VaccinationRepository, + private val testCertificateRepository: TestCertificateRepository, ) { private val mutex = Mutex() @@ -109,6 +111,7 @@ class DataReset @Inject constructor( traceLocationRepository.deleteAllTraceLocations() checkInRepository.clear() coronaTestRepository.clear() + testCertificateRepository.clear() ratProfileSettings.deleteProfile() valueSetsRepository.clear() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encryption/rsa/RSAKey.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encryption/rsa/RSAKey.kt index 1018360a1..9e417278c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encryption/rsa/RSAKey.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encryption/rsa/RSAKey.kt @@ -1,8 +1,13 @@ package de.rki.coronawarnapp.util.encryption.rsa +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter import de.rki.coronawarnapp.util.trimToLength import okio.ByteString +import okio.ByteString.Companion.decodeBase64 import okio.ByteString.Companion.toByteString +import org.json.JSONObject import java.security.Key import java.security.KeyFactory import java.security.PrivateKey @@ -29,6 +34,18 @@ interface RSAKey { get() = KEY_FACTORY.generatePrivate(PKCS8EncodedKeySpec(rawKey.toByteArray())) override fun toString(): String = base64.trimToLength(16) + + class GsonAdapter : TypeAdapter<Private>() { + override fun write(out: JsonWriter, value: Private?) { + if (value == null) out.nullValue() + else out.value(value.rawKey.base64()) + } + + override fun read(reader: JsonReader): Private? = when (reader.peek()) { + JSONObject.NULL -> reader.nextNull().let { null } + else -> Private(reader.nextString().decodeBase64()!!) + } + } } data class Public(override val rawKey: ByteString) : RSAKey { @@ -39,5 +56,17 @@ interface RSAKey { get() = KEY_FACTORY.generatePublic(X509EncodedKeySpec(rawKey.toByteArray())) override fun toString(): String = base64.trimToLength(16) + + class GsonAdapter : TypeAdapter<Public>() { + override fun write(out: JsonWriter, value: Public?) { + if (value == null) out.nullValue() + else out.value(value.rawKey.base64()) + } + + override fun read(reader: JsonReader): Public? = when (reader.peek()) { + JSONObject.NULL -> reader.nextNull().let { null } + else -> Public(reader.nextString().decodeBase64()!!) + } + } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/SerializationModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/SerializationModule.kt index 1a35bb036..f7823f6e1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/SerializationModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/SerializationModule.kt @@ -5,6 +5,7 @@ import com.google.gson.GsonBuilder import dagger.Module import dagger.Provides import dagger.Reusable +import de.rki.coronawarnapp.util.encryption.rsa.RSAKey import de.rki.coronawarnapp.util.serialization.adapter.ByteArrayAdapter import de.rki.coronawarnapp.util.serialization.adapter.ByteStringBase64Adapter import de.rki.coronawarnapp.util.serialization.adapter.DurationAdapter @@ -27,5 +28,7 @@ class SerializationModule { .registerTypeAdapter(Duration::class.java, DurationAdapter()) .registerTypeAdapter(ByteArray::class.java, ByteArrayAdapter()) .registerTypeAdapter(ByteString::class.java, ByteStringBase64Adapter()) + .registerTypeAdapter(RSAKey.Public::class.java, RSAKey.Public.GsonAdapter()) + .registerTypeAdapter(RSAKey.Private::class.java, RSAKey.Private.GsonAdapter()) .create() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt index b877317c4..5c751de71 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt @@ -5,6 +5,7 @@ import dagger.Binds import dagger.Module import dagger.multibindings.IntoMap import de.rki.coronawarnapp.contactdiary.retention.ContactDiaryRetentionWorker +import de.rki.coronawarnapp.coronatest.type.common.TestCertificateRetrievalWorker import de.rki.coronawarnapp.coronatest.type.pcr.execution.PCRResultRetrievalWorker import de.rki.coronawarnapp.coronatest.type.rapidantigen.execution.RAResultRetrievalWorker import de.rki.coronawarnapp.datadonation.analytics.worker.DataDonationAnalyticsPeriodicWorker @@ -127,4 +128,11 @@ abstract class WorkerBinder { abstract fun vaccinationUpdateWorker( factory: VaccinationUpdateWorker.Factory ): InjectedWorkerFactory<out ListenableWorker> + + @Binds + @IntoMap + @WorkerKey(TestCertificateRetrievalWorker::class) + abstract fun testCertificateRetrievalWorker( + factory: TestCertificateRetrievalWorker.Factory + ): InjectedWorkerFactory<out ListenableWorker> } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPersonIdentifier.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/CertificatePersonIdentifier.kt similarity index 71% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPersonIdentifier.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/CertificatePersonIdentifier.kt index aea20ab9b..4350879cc 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPersonIdentifier.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/CertificatePersonIdentifier.kt @@ -1,5 +1,7 @@ package de.rki.coronawarnapp.vaccination.core +import de.rki.coronawarnapp.covidcertificate.test.TestCertificateDccV1 +import de.rki.coronawarnapp.covidcertificate.test.TestCertificateQRCode import de.rki.coronawarnapp.util.HashExtensions.toSHA256 import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode @@ -8,7 +10,7 @@ import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateQRCode import org.joda.time.LocalDate import timber.log.Timber -data class VaccinatedPersonIdentifier( +data class CertificatePersonIdentifier( val dateOfBirth: LocalDate, val lastNameStandardized: String, val firstNameStandardized: String? @@ -32,7 +34,7 @@ data class VaccinatedPersonIdentifier( code.toSHA256() } - fun requireMatch(other: VaccinatedPersonIdentifier) { + fun requireMatch(other: CertificatePersonIdentifier) { if (lastNameStandardized != other.lastNameStandardized) { Timber.d("Family name does not match, got ${other.lastNameStandardized}, expected $lastNameStandardized") throw InvalidHealthCertificateException(ErrorCode.VC_NAME_MISMATCH) @@ -48,12 +50,22 @@ data class VaccinatedPersonIdentifier( } } -val VaccinationDGCV1.personIdentifier: VaccinatedPersonIdentifier - get() = VaccinatedPersonIdentifier( +val VaccinationDGCV1.personIdentifier: CertificatePersonIdentifier + get() = CertificatePersonIdentifier( dateOfBirth = dateOfBirth, lastNameStandardized = nameData.familyNameStandardized, firstNameStandardized = nameData.givenNameStandardized ) -val VaccinationCertificateQRCode.personIdentifier: VaccinatedPersonIdentifier +val VaccinationCertificateQRCode.personIdentifier: CertificatePersonIdentifier get() = parsedData.certificate.personIdentifier + +val TestCertificateDccV1.personIdentifier: CertificatePersonIdentifier + get() = CertificatePersonIdentifier( + dateOfBirth = dateOfBirth, + lastNameStandardized = nameData.familyNameStandardized, + firstNameStandardized = nameData.givenNameStandardized + ) + +val TestCertificateQRCode.personIdentifier: CertificatePersonIdentifier + get() = testCertificateData.certificate.personIdentifier diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPerson.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPerson.kt index 597a623db..a4df788f1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPerson.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPerson.kt @@ -13,7 +13,7 @@ data class VaccinatedPerson( val isUpdatingData: Boolean = false, val lastError: Throwable? = null, ) { - val identifier: VaccinatedPersonIdentifier + val identifier: CertificatePersonIdentifier get() = data.identifier val vaccinationCertificates: Set<VaccinationCertificate> by lazy { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinationCertificate.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinationCertificate.kt index 92e96ea0b..191514b46 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinationCertificate.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/VaccinationCertificate.kt @@ -22,7 +22,7 @@ interface VaccinationCertificate { val certificateCountry: String val certificateId: String - val personIdentifier: VaccinatedPersonIdentifier + val personIdentifier: CertificatePersonIdentifier val issuer: String val issuedAt: Instant diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepository.kt index ab3b35f4d..c134c34fc 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepository.kt @@ -6,8 +6,8 @@ import de.rki.coronawarnapp.util.coroutine.AppScope import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.flow.HotDataFlow import de.rki.coronawarnapp.util.flow.combine +import de.rki.coronawarnapp.vaccination.core.CertificatePersonIdentifier import de.rki.coronawarnapp.vaccination.core.VaccinatedPerson -import de.rki.coronawarnapp.vaccination.core.VaccinatedPersonIdentifier import de.rki.coronawarnapp.vaccination.core.VaccinationCertificate import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode @@ -132,7 +132,7 @@ class VaccinationRepository @Inject constructor( * Passing null as identifier will refresh all available data, if within constraints. * Throws VaccinatedPersonNotFoundException is you try to refresh a person that is unknown. */ - suspend fun refresh(personIdentifier: VaccinatedPersonIdentifier? = null) { + suspend fun refresh(personIdentifier: CertificatePersonIdentifier? = null) { Timber.tag(TAG).d("refresh(personIdentifier=%s)", personIdentifier) // NOOP diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/ContainerPostProcessor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/ContainerPostProcessor.kt index c4f2debb0..a8f210876 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/ContainerPostProcessor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/ContainerPostProcessor.kt @@ -7,6 +7,8 @@ import com.google.gson.reflect.TypeToken import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonWriter import dagger.Reusable +import de.rki.coronawarnapp.coronatest.type.TestCertificateContainer +import de.rki.coronawarnapp.covidcertificate.test.TestCertificateQRCodeExtractor import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationQRCodeExtractor import timber.log.Timber import java.io.IOException @@ -14,7 +16,8 @@ import javax.inject.Inject @Reusable class ContainerPostProcessor @Inject constructor( - private val qrCodeExtractor: VaccinationQRCodeExtractor, + private val vaccinationQrCodeExtractor: VaccinationQRCodeExtractor, + private val testCertificateQRCodeExtractor: TestCertificateQRCodeExtractor, ) : TypeAdapterFactory { override fun <T> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T> { val delegate = gson.getDelegateAdapter(this, type) @@ -30,7 +33,11 @@ class ContainerPostProcessor @Inject constructor( when (obj) { is VaccinationContainer -> { Timber.v("Injecting VaccinationContainer %s", obj.hashCode()) - obj.qrCodeExtractor = qrCodeExtractor + obj.qrCodeExtractor = vaccinationQrCodeExtractor + } + is TestCertificateContainer -> { + Timber.v("Injecting TestCertificateContainer %s", obj.hashCode()) + obj.qrCodeExtractor = testCertificateQRCodeExtractor } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinatedPersonData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinatedPersonData.kt index 06978d490..ab6502589 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinatedPersonData.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinatedPersonData.kt @@ -1,11 +1,11 @@ package de.rki.coronawarnapp.vaccination.core.repository.storage import com.google.gson.annotations.SerializedName -import de.rki.coronawarnapp.vaccination.core.VaccinatedPersonIdentifier +import de.rki.coronawarnapp.vaccination.core.CertificatePersonIdentifier data class VaccinatedPersonData( @SerializedName("vaccinationData") val vaccinations: Set<VaccinationContainer> = emptySet() ) { - val identifier: VaccinatedPersonIdentifier + val identifier: CertificatePersonIdentifier get() = vaccinations.first().personIdentifier } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt index 9694f5f9d..06ed285db 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt @@ -2,7 +2,7 @@ package de.rki.coronawarnapp.vaccination.core.repository.storage import androidx.annotation.Keep import com.google.gson.annotations.SerializedName -import de.rki.coronawarnapp.vaccination.core.VaccinatedPersonIdentifier +import de.rki.coronawarnapp.vaccination.core.CertificatePersonIdentifier import de.rki.coronawarnapp.vaccination.core.VaccinationCertificate import de.rki.coronawarnapp.vaccination.core.certificate.CoseCertificateHeader import de.rki.coronawarnapp.vaccination.core.certificate.VaccinationDGCV1 @@ -48,14 +48,14 @@ data class VaccinationContainer internal constructor( val certificateId: String get() = vaccination.uniqueCertificateIdentifier - val personIdentifier: VaccinatedPersonIdentifier + val personIdentifier: CertificatePersonIdentifier get() = certificate.personIdentifier fun toVaccinationCertificate( valueSet: VaccinationValueSet?, userLocale: Locale = Locale.getDefault(), ) = object : VaccinationCertificate { - override val personIdentifier: VaccinatedPersonIdentifier + override val personIdentifier: CertificatePersonIdentifier get() = certificate.personIdentifier override val firstName: String? diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/CoronaWarnApplicationTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/CoronaWarnApplicationTest.kt index c32f6f68c..b42bb0832 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/CoronaWarnApplicationTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/CoronaWarnApplicationTest.kt @@ -7,6 +7,7 @@ import de.rki.coronawarnapp.appconfig.devicetime.DeviceTimeHandler import de.rki.coronawarnapp.contactdiary.retention.ContactDiaryWorkScheduler import de.rki.coronawarnapp.coronatest.CoronaTestRepository import de.rki.coronawarnapp.coronatest.notification.ShareTestResultNotificationService +import de.rki.coronawarnapp.coronatest.type.common.TestCertificateRetrievalScheduler import de.rki.coronawarnapp.coronatest.type.pcr.execution.PCRResultScheduler import de.rki.coronawarnapp.coronatest.type.pcr.notification.PCRTestResultAvailableNotificationService import de.rki.coronawarnapp.coronatest.type.rapidantigen.execution.RAResultScheduler @@ -70,6 +71,7 @@ class CoronaWarnApplicationTest : BaseTest() { @MockK lateinit var presenceTracingRiskWorkScheduler: PresenceTracingRiskWorkScheduler @MockK lateinit var pcrTestResultScheduler: PCRResultScheduler @MockK lateinit var raTestResultScheduler: RAResultScheduler + @MockK lateinit var testCertificateRetrievalScheduler: TestCertificateRetrievalScheduler @MockK lateinit var pcrTestResultAvailableNotificationService: PCRTestResultAvailableNotificationService @@ -126,6 +128,7 @@ class CoronaWarnApplicationTest : BaseTest() { app.pcrTestResultAvailableNotificationService = pcrTestResultAvailableNotificationService app.raTestResultAvailableNotificationService = raTestResultAvailableNotificationService app.vaccinationUpdateScheduler = vaccinationUpdateScheduler + app.testCertificateRetrievalScheduler = testCertificateRetrievalScheduler app.appScope = TestCoroutineScope() app.rollingLogHistory = object : Timber.Tree() { override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { @@ -157,6 +160,7 @@ class CoronaWarnApplicationTest : BaseTest() { pcrTestResultAvailableNotificationService.setup() raTestResultAvailableNotificationService.setup() + testCertificateRetrievalScheduler.setup() vaccinationUpdateScheduler.setup() diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt index d33e8d774..bbfdba52c 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt @@ -4,6 +4,7 @@ import com.google.protobuf.InvalidProtocolBufferException import de.rki.coronawarnapp.appconfig.AnalyticsConfig import de.rki.coronawarnapp.appconfig.CWAConfig import de.rki.coronawarnapp.appconfig.CoronaTestConfig +import de.rki.coronawarnapp.appconfig.CovidCertificateConfig import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig import de.rki.coronawarnapp.appconfig.KeyDownloadConfig @@ -34,6 +35,7 @@ class ConfigParserTest : BaseTest() { @MockK lateinit var logUploadConfigMapper: LogUploadConfig.Mapper @MockK lateinit var presenceTracingConfigMapper: PresenceTracingConfig.Mapper @MockK lateinit var coronaTestConfigMapper: CoronaTestConfig.Mapper + @MockK lateinit var covidCertificateConfigMapper: CovidCertificateConfig.Mapper private val appConfig171 = File("src/test/resources/appconfig_1_7_1.bin") private val appConfig180 = File("src/test/resources/appconfig_1_8_0.bin") @@ -51,6 +53,7 @@ class ConfigParserTest : BaseTest() { every { logUploadConfigMapper.map(any()) } returns mockk() every { presenceTracingConfigMapper.map(any()) } returns mockk() every { coronaTestConfigMapper.map(any()) } returns mockk() + every { covidCertificateConfigMapper.map(any()) } returns mockk() appConfig171.exists() shouldBe true appConfig180.exists() shouldBe true @@ -66,6 +69,7 @@ class ConfigParserTest : BaseTest() { logUploadConfigMapper = logUploadConfigMapper, presenceTracingConfigMapper = presenceTracingConfigMapper, coronaTestConfigMapper = coronaTestConfigMapper, + covidCertificateConfigMapper = covidCertificateConfigMapper, ) @Test diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/CovidCertificateConfigMapperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/CovidCertificateConfigMapperTest.kt new file mode 100644 index 000000000..1e9af041d --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/CovidCertificateConfigMapperTest.kt @@ -0,0 +1,68 @@ +package de.rki.coronawarnapp.appconfig.mapping + +import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid +import de.rki.coronawarnapp.server.protocols.internal.v2.DgcParameters +import io.kotest.matchers.shouldBe +import org.joda.time.Duration +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class CovidCertificateConfigMapperTest : BaseTest() { + + private fun createInstance() = CovidCertificateConfigMapper() + + @Test + fun `values are mapped`() { + val config = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder() + .setDgcParameters( + DgcParameters.DGCParameters.newBuilder() + .setTestCertificateParameters( + DgcParameters.DGCTestCertificateParameters.newBuilder() + .setWaitForRetryInSeconds(60) + .setWaitAfterPublicKeyRegistrationInSeconds(60) + ) + ) + .build() + createInstance().map(config).apply { + testCertificate.waitAfterPublicKeyRegistration shouldBe Duration.standardSeconds(60) + testCertificate.waitForRetry shouldBe Duration.standardSeconds(60) + } + } + + @Test + fun `defaults are returned if all dcc parameters are missing`() { + createInstance().map(AppConfigAndroid.ApplicationConfigurationAndroid.getDefaultInstance()).apply { + testCertificate.waitAfterPublicKeyRegistration shouldBe Duration.standardSeconds(10) + testCertificate.waitForRetry shouldBe Duration.standardSeconds(10) + } + } + + @Test + fun `defaults are returned if just test certificate parameters are missing`() { + val config = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder() + .setDgcParameters(DgcParameters.DGCParameters.getDefaultInstance()) + .build() + createInstance().map(config).apply { + testCertificate.waitAfterPublicKeyRegistration shouldBe Duration.standardSeconds(10) + testCertificate.waitForRetry shouldBe Duration.standardSeconds(10) + } + } + + @Test + fun `values are checked for sanity`() { + val config = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder() + .setDgcParameters( + DgcParameters.DGCParameters.newBuilder() + .setTestCertificateParameters( + DgcParameters.DGCTestCertificateParameters.newBuilder() + .setWaitForRetryInSeconds(61) + .setWaitAfterPublicKeyRegistrationInSeconds(61) + ) + ) + .build() + createInstance().map(config).apply { + testCertificate.waitAfterPublicKeyRegistration shouldBe Duration.standardSeconds(10) + testCertificate.waitForRetry shouldBe Duration.standardSeconds(10) + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/CoronaTestTestComponent.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/CoronaTestTestComponent.kt new file mode 100644 index 000000000..98d5cd505 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/CoronaTestTestComponent.kt @@ -0,0 +1,29 @@ +package de.rki.coronawarnapp.coronatest + +import dagger.Component +import dagger.Module +import de.rki.coronawarnapp.coronatest.storage.TestCertificateStorageTest +import de.rki.coronawarnapp.coronatest.type.TestCertificateContainerTest +import de.rki.coronawarnapp.util.serialization.SerializationModule +import javax.inject.Singleton + +@Singleton +@Component( + modules = [ + CoronaTestMockProvider::class, + SerializationModule::class + ] +) +interface CoronaTestTestComponent { + + fun inject(testClass: TestCertificateStorageTest) + fun inject(testClass: TestCertificateContainerTest) + + @Component.Factory + interface Factory { + fun create(): CoronaTestTestComponent + } +} + +@Module +class CoronaTestMockProvider diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/CoronaTestTestData.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/CoronaTestTestData.kt new file mode 100644 index 000000000..9b4ce4c49 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/CoronaTestTestData.kt @@ -0,0 +1,167 @@ +package de.rki.coronawarnapp.coronatest + +import de.rki.coronawarnapp.coronatest.type.pcr.PCRCertificateContainer +import de.rki.coronawarnapp.coronatest.type.rapidantigen.RACertificateContainer +import de.rki.coronawarnapp.covidcertificate.test.TestCertificateData +import de.rki.coronawarnapp.covidcertificate.test.TestCertificateDccV1 +import de.rki.coronawarnapp.covidcertificate.test.TestCertificateQRCodeExtractor +import de.rki.coronawarnapp.util.encryption.rsa.RSAKey +import de.rki.coronawarnapp.util.encryption.rsa.RSAKeyPairGenerator +import de.rki.coronawarnapp.vaccination.core.certificate.HealthCertificateHeader +import okio.ByteString.Companion.decodeBase64 +import org.joda.time.Instant +import javax.inject.Inject + +@Suppress("MaxLineLength") +class CoronaTestTestData @Inject constructor( + private val qrCodeExtractor: TestCertificateQRCodeExtractor, + private val rsaKeyPairGenerator: RSAKeyPairGenerator, +) { + + val personATest1CertQRCodeString = "personATest1CertQRCodeString" + + val personATest1CertHeader = HealthCertificateHeader( + issuer = "DE", + issuedAt = Instant.parse("2021-05-10T09:25:00.000Z"), + expiresAt = Instant.parse("2022-05-19T09:25:00.000Z"), + ) + + val personATest1Cert = TestCertificateDccV1( + version = "1.0.0", + nameData = TestCertificateDccV1.NameData( + givenName = "Andreas", + givenNameStandardized = "ANDREAS", + familyName = "Astrá Eins", + familyNameStandardized = "ASTRA<EINS", + ), + dob = "1966-11-11", + testCertificateData = listOf( + TestCertificateDccV1.TestCertificateData( + targetId = "840539006", + countryOfTest = "DE", + sampleCollectedAt = Instant.EPOCH, + testResultAt = Instant.EPOCH, + testCenter = "TODO", + testName = "TODO", + testNameAndManufactor = "TODO", + testResult = "TODO", + testType = "TODO", + certificateIssuer = "Bundesministerium für Gesundheit - Test01", + uniqueCertificateIdentifier = "01DE/00001/1119305005/7T1UG87G61Y7NRXIBQJDTYQ9#S", + ) + ) + ) + + val personATest1CertData = TestCertificateData( + header = personATest1CertHeader, + certificate = personATest1Cert, + ) + + val personATest1CertContainer = run { + val publicKey = RSAKey.Public( + "MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA2+WCCvy0SNqZMy/V1FYYMkBTGp/5BQt/NxUW1nIkj84u6duqNNQh4GjugoDc8epyl/yi3D61Jt7qArwk+eTcnW4/jEOexT5pCabRKrFm6IMndSefYrP3CeaD86ZU47uhnRuCG3TcPhIqUN2E37EbOsI9Z59JXc5tmmB71CxTF0bjE0PNLgbTU2snnsO6+oz/JLo7D2nw6E9yxSJ8JBjM5j+FC4sYLuO2nYi/BzAGZL/wsKrajg2hjA3f8r1cgst8HdzAJjMUG90pb3UG2K2KVRScbvF8pvRrzLCvJ/gqAGDXX/M00jr407vU8V4O2A9YdSavaC02iRFTNail65cbOW96p3ptjeejofj8l5PO5eBYWERla8NrlD9EcW93+aSmswn4w9iSSq+j38GMyhYulLcOlhKTeWumc5goDjcHyri48Ki70ddGzrxFxggaC/FqlCG85A6/43fVaWH/Wi2uPDPzaRGNQzXRy4LCuE/dvUzp8TlkpcT0QFy/Q4Ke0u1dAgMBAAE\u003d".decodeBase64()!! + ) + val privateKey = RSAKey.Private( + "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".decodeBase64()!! + ) + PCRCertificateContainer( + identifier = "identifier", + registrationToken = "registrationToken", + registeredAt = Instant.ofEpochMilli(12345), + publicKeyRegisteredAt = Instant.ofEpochMilli(6789), + rsaPublicKey = publicKey, + rsaPrivateKey = privateKey, + certificateReceivedAt = Instant.ofEpochMilli(123456789), + encryptedDataEncryptionkey = "ZW5jcnlwdGVkRGF0YUVuY3J5cHRpb25rZXk=".decodeBase64()!!, + testCertificateQrCode = personATest1CertQRCodeString, + ).apply { + preParsedData = personATest1CertData + } + } + + val personATest2CertQRCodeString = "personATest2CertQRCodeString" + + val personATest2CertHeader = HealthCertificateHeader( + issuer = "DE", + issuedAt = Instant.parse("2021-05-11T09:25:00.000Z"), + expiresAt = Instant.parse("2022-05-11T09:25:00.000Z"), + ) + + val personATest2Cert = TestCertificateDccV1( + version = "1.0.0", + nameData = TestCertificateDccV1.NameData( + givenName = "Andreas", + givenNameStandardized = "ANDREAS", + familyName = "Astrá Eins", + familyNameStandardized = "ASTRA<EINS", + ), + dob = "1966-11-11", + testCertificateData = listOf( + TestCertificateDccV1.TestCertificateData( + targetId = "840539006", + countryOfTest = "DE", + sampleCollectedAt = Instant.EPOCH, + testResultAt = Instant.EPOCH, + testCenter = "TODO", + testName = "TODO", + testNameAndManufactor = "TODO", + testResult = "TODO", + testType = "TODO", + certificateIssuer = "Bundesministerium für Gesundheit - Test01", + uniqueCertificateIdentifier = "01DE/00001/1119305005/TODO", + ) + ) + ) + + val personATest2CertData = TestCertificateData( + header = personATest2CertHeader, + certificate = personATest2Cert, + ) + + val personATest2CertContainer = run { + val publicKey = RSAKey.Public( + "MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAnrJ8PbmbOmEEmHB/8yg1bnkUT7jcF4Xfy2Me5imJgLVYQ0cL9UNP91cfFUrgMFV3fHOc0Uuay10TmrBLaEdzqDEZQH4Kj0uZ+hVCtzntVKFviuUoh08wxFlogtc5Sy0NhuTGyC1W/i2AX1SDvet2xMcc1fE44rITQEEAlG8+nfGpbppFHezUOxuZjs9XBTxavDjQyWeFwMD30UAJGakPhOOOj6ihXA19OvQ/tYuYTJ5C9QzeK90C/rYbg2fn+os3EGlb7iJZ2V3KGrNLdMcEtkiG5IiHicaNCn8OS/cI3d29iJE4ECaF711fyF8MG1H2tbkjULS3bsPNUvyvHfM2cjOPRhejayOh+CxQkc3wKar8ApvQCjiVRW05nO0ufHdPMcWJhUlchWYO5mOJTSO8vG/9YqpnTuDc2Gelc4gMK7KATdH3v1FsACPKNJdpt68IfZXgGYn5LtJ7zJB6Yw8Rewj1SaF/wFKXpYd+5JyK18wJTLVYSpiDzidh4DP+R6ZTAgMBAAE\u003d".decodeBase64()!! + ) + val privateKey = RSAKey.Private( + "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".decodeBase64()!! + ) + RACertificateContainer( + identifier = "identifier2", + registrationToken = "registrationToken2", + registeredAt = Instant.ofEpochMilli(12345), + publicKeyRegisteredAt = Instant.ofEpochMilli(6789), + rsaPublicKey = publicKey, + rsaPrivateKey = privateKey, + certificateReceivedAt = Instant.ofEpochMilli(123456789), + encryptedDataEncryptionkey = "ZW5jcnlwdGVkRGF0YUVuY3J5cHRpb25rZXk=".decodeBase64()!!, + testCertificateQrCode = personATest2CertQRCodeString + ).apply { + preParsedData = personATest2CertData + } + } + + val personATest3CertContainerNokey = run { + RACertificateContainer( + identifier = "identifier2", + registrationToken = "registrationToken2", + registeredAt = Instant.ofEpochMilli(12345), + ) + } + + val personATest4CertContainerPending = run { + val publicKey = RSAKey.Public( + "MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAnrJ8PbmbOmEEmHB/8yg1bnkUT7jcF4Xfy2Me5imJgLVYQ0cL9UNP91cfFUrgMFV3fHOc0Uuay10TmrBLaEdzqDEZQH4Kj0uZ+hVCtzntVKFviuUoh08wxFlogtc5Sy0NhuTGyC1W/i2AX1SDvet2xMcc1fE44rITQEEAlG8+nfGpbppFHezUOxuZjs9XBTxavDjQyWeFwMD30UAJGakPhOOOj6ihXA19OvQ/tYuYTJ5C9QzeK90C/rYbg2fn+os3EGlb7iJZ2V3KGrNLdMcEtkiG5IiHicaNCn8OS/cI3d29iJE4ECaF711fyF8MG1H2tbkjULS3bsPNUvyvHfM2cjOPRhejayOh+CxQkc3wKar8ApvQCjiVRW05nO0ufHdPMcWJhUlchWYO5mOJTSO8vG/9YqpnTuDc2Gelc4gMK7KATdH3v1FsACPKNJdpt68IfZXgGYn5LtJ7zJB6Yw8Rewj1SaF/wFKXpYd+5JyK18wJTLVYSpiDzidh4DP+R6ZTAgMBAAE\u003d".decodeBase64()!! + ) + val privateKey = RSAKey.Private( + "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".decodeBase64()!! + ) + RACertificateContainer( + identifier = "identifier2", + registrationToken = "registrationToken2", + registeredAt = Instant.ofEpochMilli(12345), + publicKeyRegisteredAt = Instant.ofEpochMilli(6789), + rsaPublicKey = publicKey, + rsaPrivateKey = privateKey, + ) + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/TestCertificateRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/TestCertificateRepositoryTest.kt new file mode 100644 index 000000000..6c5344c30 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/TestCertificateRepositoryTest.kt @@ -0,0 +1,138 @@ +package de.rki.coronawarnapp.coronatest + +import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.appconfig.ConfigData +import de.rki.coronawarnapp.appconfig.CovidCertificateConfig +import de.rki.coronawarnapp.coronatest.storage.TestCertificateStorage +import de.rki.coronawarnapp.coronatest.type.TestCertificateContainer +import de.rki.coronawarnapp.coronatest.type.pcr.PCRCertificateContainer +import de.rki.coronawarnapp.covidcertificate.server.CovidCertificateServer +import de.rki.coronawarnapp.covidcertificate.server.TestCertificateComponents +import de.rki.coronawarnapp.covidcertificate.test.TestCertificateQRCode +import de.rki.coronawarnapp.covidcertificate.test.TestCertificateQRCodeExtractor +import de.rki.coronawarnapp.util.TimeStamper +import de.rki.coronawarnapp.util.encryption.rsa.RSACryptography +import de.rki.coronawarnapp.util.encryption.rsa.RSAKeyPairGenerator +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerify +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 okio.ByteString +import org.joda.time.Duration +import org.joda.time.Instant +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import testhelpers.TestDispatcherProvider +import testhelpers.coroutines.runBlockingTest2 + +class TestCertificateRepositoryTest : BaseTest() { + + @MockK lateinit var timeStamper: TimeStamper + @MockK lateinit var storage: TestCertificateStorage + @MockK lateinit var certificateServer: CovidCertificateServer + @MockK lateinit var rsaCryptography: RSACryptography + @MockK lateinit var qrCodeExtractor: TestCertificateQRCodeExtractor + @MockK lateinit var appConfigProvider: AppConfigProvider + @MockK lateinit var appConfigData: ConfigData + @MockK lateinit var covidTestCertificateConfig: CovidCertificateConfig.TestCertificate + + private val testCertificateNew = PCRCertificateContainer( + identifier = "identifier1", + registrationToken = "regtoken1", + registeredAt = Instant.EPOCH, + ) + + private val testCertificateWithPubKey = testCertificateNew.copy( + publicKeyRegisteredAt = Instant.EPOCH, + rsaPublicKey = mockk(), + rsaPrivateKey = mockk(), + ) + + private val testCerticateComponents = mockk<TestCertificateComponents>().apply { + every { dataEncryptionKeyBase64 } returns "dek" + every { encryptedCoseTestCertificateBase64 } returns "" + } + + private var storageSet = mutableSetOf<TestCertificateContainer>() + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + every { timeStamper.nowUTC } returns Instant.EPOCH + + every { appConfigProvider.currentConfig } returns flowOf(appConfigData) + every { appConfigData.covidCertificateParameters } returns mockk<CovidCertificateConfig>().apply { + every { testCertificate } returns covidTestCertificateConfig + } + + covidTestCertificateConfig.apply { + every { waitForRetry } returns Duration.standardSeconds(10) + every { waitAfterPublicKeyRegistration } returns Duration.standardSeconds(10) + } + + storage.apply { + every { storage.testCertificates = any() } answers { + storageSet.clear() + storageSet.addAll(arg(0)) + } + every { storage.testCertificates } answers { storageSet } + } + + certificateServer.apply { + coEvery { registerPublicKeyForTest(any(), any()) } just Runs + coEvery { requestCertificateForTest(any()) } returns testCerticateComponents + } + + every { rsaCryptography.decrypt(any(), any()) } returns ByteString.Companion.EMPTY + + coEvery { qrCodeExtractor.extract(any(), any()) } returns mockk<TestCertificateQRCode>().apply { + every { qrCode } returns "qrCode" + every { testCertificateData } returns mockk() + } + } + + private fun createInstance(scope: CoroutineScope) = TestCertificateRepository( + appScope = scope, + dispatcherProvider = TestDispatcherProvider(), + timeStamper = timeStamper, + storage = storage, + certificateServer = certificateServer, + rsaKeyPairGenerator = RSAKeyPairGenerator(), + rsaCryptography = rsaCryptography, + qrCodeExtractor = qrCodeExtractor, + appConfigProvider = appConfigProvider, + ) + + @Test + fun `refresh tries public key registration`() = runBlockingTest2(ignoreActive = true) { + storage.testCertificates = setOf(testCertificateNew) + + val instance = createInstance(scope = this) + instance.refresh() + + coVerify { + certificateServer.registerPublicKeyForTest(testCertificateNew.registrationToken, any()) + } + } + + @Test + fun `refresh skips public key registration already registered`() = runBlockingTest2(ignoreActive = true) { + storage.testCertificates = setOf(testCertificateWithPubKey) + + val instance = createInstance(scope = this) + instance.refresh() + + coVerify { + covidTestCertificateConfig.waitAfterPublicKeyRegistration + certificateServer.requestCertificateForTest(testCertificateNew.registrationToken) + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/storage/TestCertificateStorageTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/storage/TestCertificateStorageTest.kt new file mode 100644 index 000000000..f26fd0bca --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/storage/TestCertificateStorageTest.kt @@ -0,0 +1,114 @@ +package de.rki.coronawarnapp.coronatest.storage + +import android.content.Context +import androidx.core.content.edit +import de.rki.coronawarnapp.coronatest.CoronaTestTestData +import de.rki.coronawarnapp.coronatest.DaggerCoronaTestTestComponent +import de.rki.coronawarnapp.util.serialization.SerializationModule +import de.rki.coronawarnapp.vaccination.core.repository.storage.ContainerPostProcessor +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import testhelpers.preferences.MockSharedPreferences +import javax.inject.Inject + +@Suppress("MaxLineLength") +class TestCertificateStorageTest : BaseTest() { + @MockK lateinit var context: Context + private lateinit var mockPreferences: MockSharedPreferences + @Inject lateinit var testData: CoronaTestTestData + @Inject lateinit var postProcessor: ContainerPostProcessor + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + DaggerCoronaTestTestComponent.factory().create().inject(this) + + mockPreferences = MockSharedPreferences() + + every { + context.getSharedPreferences("coronatest_certificate_localdata", Context.MODE_PRIVATE) + } returns mockPreferences + } + + private fun createInstance() = TestCertificateStorage( + context = context, + baseGson = SerializationModule().baseGson(), + containerPostProcessor = postProcessor, + ) + + @Test + fun `init is sideeffect free`() { + createInstance() + } + + @Test + fun `storing empty set deletes data`() { + mockPreferences.edit { + putString("dontdeleteme", "test") + putString("testcertificate.data.ra", "test") + putString("testcertificate.data.pcr", "test") + } + createInstance().testCertificates = emptySet() + + mockPreferences.dataMapPeek.keys.single() shouldBe "dontdeleteme" + } + + @Test + fun `store two containers, one for each type`() { + createInstance().testCertificates = setOf( + testData.personATest1CertContainer, + testData.personATest2CertContainer + ) + +// (mockPreferences.dataMapPeek["testcertificate.data.pcr"] as String).toComparableJsonPretty() shouldBe """ +// [ +// { +// "identifier": "identifier", +// "registrationToken": "registrationToken", +// "registeredAt": 12345, +// "publicKeyRegisteredAt": 6789, +// "rsaPublicKey": "MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA2+WCCvy0SNqZMy/V1FYYMkBTGp/5BQt/NxUW1nIkj84u6duqNNQh4GjugoDc8epyl/yi3D61Jt7qArwk+eTcnW4/jEOexT5pCabRKrFm6IMndSefYrP3CeaD86ZU47uhnRuCG3TcPhIqUN2E37EbOsI9Z59JXc5tmmB71CxTF0bjE0PNLgbTU2snnsO6+oz/JLo7D2nw6E9yxSJ8JBjM5j+FC4sYLuO2nYi/BzAGZL/wsKrajg2hjA3f8r1cgst8HdzAJjMUG90pb3UG2K2KVRScbvF8pvRrzLCvJ/gqAGDXX/M00jr407vU8V4O2A9YdSavaC02iRFTNail65cbOW96p3ptjeejofj8l5PO5eBYWERla8NrlD9EcW93+aSmswn4w9iSSq+j38GMyhYulLcOlhKTeWumc5goDjcHyri48Ki70ddGzrxFxggaC/FqlCG85A6/43fVaWH/Wi2uPDPzaRGNQzXRy4LCuE/dvUzp8TlkpcT0QFy/Q4Ke0u1dAgMBAAE\u003d", +// "rsaPrivateKey": "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", +// "certificateReceivedAt": 123456789, +// "encryptedDataEncryptionkey": "ZW5jcnlwdGVkRGF0YUVuY3J5cHRpb25rZXk\u003d", +// "testCertificateQrCode": "personATest1CertQRCodeString" +// } +// ] +// """.toComparableJsonPretty() +// +// (mockPreferences.dataMapPeek["testcertificate.data.ra"] as String).toComparableJsonPretty() shouldBe """ +// [ +// { +// "identifier": "identifier2", +// "registrationToken": "registrationToken2", +// "registeredAt": 12345, +// "publicKeyRegisteredAt": 6789, +// "rsaPublicKey": "MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAnrJ8PbmbOmEEmHB/8yg1bnkUT7jcF4Xfy2Me5imJgLVYQ0cL9UNP91cfFUrgMFV3fHOc0Uuay10TmrBLaEdzqDEZQH4Kj0uZ+hVCtzntVKFviuUoh08wxFlogtc5Sy0NhuTGyC1W/i2AX1SDvet2xMcc1fE44rITQEEAlG8+nfGpbppFHezUOxuZjs9XBTxavDjQyWeFwMD30UAJGakPhOOOj6ihXA19OvQ/tYuYTJ5C9QzeK90C/rYbg2fn+os3EGlb7iJZ2V3KGrNLdMcEtkiG5IiHicaNCn8OS/cI3d29iJE4ECaF711fyF8MG1H2tbkjULS3bsPNUvyvHfM2cjOPRhejayOh+CxQkc3wKar8ApvQCjiVRW05nO0ufHdPMcWJhUlchWYO5mOJTSO8vG/9YqpnTuDc2Gelc4gMK7KATdH3v1FsACPKNJdpt68IfZXgGYn5LtJ7zJB6Yw8Rewj1SaF/wFKXpYd+5JyK18wJTLVYSpiDzidh4DP+R6ZTAgMBAAE\u003d", +// "rsaPrivateKey": "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", +// "certificateReceivedAt": 123456789, +// "encryptedDataEncryptionkey": "ZW5jcnlwdGVkRGF0YUVuY3J5cHRpb25rZXk\u003d", +// "testCertificateQrCode": "personATest2CertQRCodeString" +// } +// ] +// """.toComparableJsonPretty() + + createInstance().testCertificates shouldBe setOf( + testData.personATest1CertContainer, + testData.personATest2CertContainer + ) + } + + @Test + fun `post processor injects data extractors`() { + createInstance().testCertificates = setOf(testData.personATest1CertContainer) + + createInstance().testCertificates.single().qrCodeExtractor shouldNotBe null + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/TestCertificateContainerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/TestCertificateContainerTest.kt new file mode 100644 index 000000000..6459f54ed --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/TestCertificateContainerTest.kt @@ -0,0 +1,55 @@ +package de.rki.coronawarnapp.coronatest.type + +import de.rki.coronawarnapp.coronatest.CoronaTestTestData +import de.rki.coronawarnapp.coronatest.DaggerCoronaTestTestComponent +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.mockk.mockk +import org.joda.time.Instant +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import javax.inject.Inject + +class TestCertificateContainerTest : BaseTest() { + + @Inject lateinit var testData: CoronaTestTestData + + @BeforeEach + fun setup() { + DaggerCoronaTestTestComponent.factory().create().inject(this) + } + + @Test + fun `ui facing test certificate creation and fallbacks`() { + testData.personATest2CertContainer.apply { + isPublicKeyRegistered shouldBe true + isCertificateRetrievalPending shouldBe false + certificateId shouldBe "01DE/00001/1119305005/TODO" + testCertificateQrCode shouldBe "personATest2CertQRCodeString" + certificateReceivedAt shouldBe Instant.parse("1970-01-02T10:17:36.789Z") + toTestCertificate(null) shouldNotBe null + } + } + + @Test + fun `pending check and nullability`() { + testData.personATest3CertContainerNokey.apply { + isPublicKeyRegistered shouldBe false + isCertificateRetrievalPending shouldBe true + certificateId shouldBe null + testCertificateQrCode shouldBe null + certificateReceivedAt shouldBe null + toTestCertificate(mockk()) shouldBe null + } + + testData.personATest4CertContainerPending.apply { + isPublicKeyRegistered shouldBe true + isCertificateRetrievalPending shouldBe true + certificateId shouldBe null + testCertificateQrCode shouldBe null + certificateReceivedAt shouldBe null + toTestCertificate(mockk()) shouldBe null + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/common/TestCertificateRetrievalSchedulerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/common/TestCertificateRetrievalSchedulerTest.kt new file mode 100644 index 000000000..bf93a2efd --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/common/TestCertificateRetrievalSchedulerTest.kt @@ -0,0 +1,137 @@ +package de.rki.coronawarnapp.coronatest.type.common + +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkInfo +import androidx.work.WorkManager +import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.coronatest.TestCertificateRepository +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.coronatest.type.TestCertificateContainer +import de.rki.coronawarnapp.util.device.ForegroundState +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerify +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.MutableStateFlow +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import testhelpers.coroutines.runBlockingTest2 +import testhelpers.gms.MockListenableFuture + +class TestCertificateRetrievalSchedulerTest : BaseTest() { + + @MockK lateinit var workManager: WorkManager + @MockK lateinit var certificateRepo: TestCertificateRepository + @MockK lateinit var testRepo: CoronaTestRepository + @MockK lateinit var foregroundState: ForegroundState + @MockK lateinit var workInfo: WorkInfo + + private val mockTest = mockk<CoronaTest>().apply { + every { identifier } returns "identifier1" + every { isDccConsentGiven } returns true + every { isDccDataSetCreated } returns false + every { isDccSupportedByPoc } returns true + every { isNegative } returns true + } + + private val mockCertificate = mockk<TestCertificateContainer>().apply { + every { identifier } returns "UUID" + every { isCertificateRetrievalPending } returns true + every { isUpdatingData } returns false + } + + private val testsFlow = MutableStateFlow(setOf(mockTest)) + private val certificatesFlow = MutableStateFlow(setOf(mockCertificate)) + private val foregroundFlow = MutableStateFlow(false) + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + workManager.apply { + every { enqueueUniqueWork(any(), any(), any<OneTimeWorkRequest>()) } returns mockk() + every { getWorkInfosForUniqueWork(any()) } returns MockListenableFuture.forResult(listOf(workInfo)) + } + + every { workInfo.state } returns WorkInfo.State.SUCCEEDED + + testRepo.apply { + every { testRepo.coronaTests } returns testsFlow + coEvery { markDccAsCreated(any(), any()) } just Runs + } + certificateRepo.apply { + every { certificates } returns certificatesFlow + coEvery { requestCertificate(any()) } returns mockk() + } + + every { foregroundState.isInForeground } returns foregroundFlow + } + + private fun createInstance(scope: CoroutineScope) = TestCertificateRetrievalScheduler( + appScope = scope, + workManager = workManager, + certificateRepo = certificateRepo, + foregroundState = foregroundState, + testRepo = testRepo, + ) + + @Test + fun `new negative corona tests create a dcc if supported and consented`() = runBlockingTest2(ignoreActive = true) { + createInstance(scope = this).setup() + coVerify { certificateRepo.requestCertificate(mockTest) } + } + + @Test + fun `certificates only for negative results`() = runBlockingTest2(ignoreActive = true) { + every { mockTest.isNegative } returns false + createInstance(scope = this).setup() + advanceUntilIdle() + coVerify(exactly = 0) { certificateRepo.requestCertificate(any()) } + } + + @Test + fun `no duplicate certificates for flaky test results`() = runBlockingTest2(ignoreActive = true) { + every { mockTest.isDccDataSetCreated } returns true + createInstance(scope = this).setup() + advanceUntilIdle() + coVerify(exactly = 0) { certificateRepo.requestCertificate(any()) } + } + + @Test + fun `refresh on foreground`() = runBlockingTest2(ignoreActive = true) { + testsFlow.value = emptySet() + + createInstance(scope = this).setup() + advanceUntilIdle() + coVerify(exactly = 1) { workManager.enqueueUniqueWork(any(), any(), any<OneTimeWorkRequest>()) } + + foregroundFlow.value = true + advanceUntilIdle() + coVerify(exactly = 2) { workManager.enqueueUniqueWork(any(), any(), any<OneTimeWorkRequest>()) } + } + + @Test + fun `refresh on new certificate entry`() = runBlockingTest2(ignoreActive = true) { + testsFlow.value = emptySet() + createInstance(scope = this).setup() + + advanceUntilIdle() + coVerify(exactly = 1) { workManager.enqueueUniqueWork(any(), any(), any<OneTimeWorkRequest>()) } + + val mockCertificate2 = mockk<TestCertificateContainer>().apply { + every { identifier } returns "UUID2" + every { isCertificateRetrievalPending } returns true + every { isUpdatingData } returns false + } + + certificatesFlow.value = setOf(mockCertificate, mockCertificate2) + advanceUntilIdle() + coVerify(exactly = 2) { workManager.enqueueUniqueWork(any(), any(), any<OneTimeWorkRequest>()) } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/common/TestCertificateRetrievalWorkerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/common/TestCertificateRetrievalWorkerTest.kt new file mode 100644 index 000000000..4d189398b --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/common/TestCertificateRetrievalWorkerTest.kt @@ -0,0 +1,74 @@ +package de.rki.coronawarnapp.coronatest.type.common + +import android.content.Context +import androidx.work.ListenableWorker +import androidx.work.WorkRequest +import androidx.work.WorkerParameters +import de.rki.coronawarnapp.coronatest.TestCertificateRepository +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.impl.annotations.RelaxedMockK +import kotlinx.coroutines.test.runBlockingTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import java.net.UnknownHostException + +class TestCertificateRetrievalWorkerTest : BaseTest() { + @MockK lateinit var context: Context + @MockK lateinit var request: WorkRequest + @MockK lateinit var testCertificateRepository: TestCertificateRepository + + @RelaxedMockK lateinit var workerParams: WorkerParameters + + @BeforeEach + fun setUp() { + MockKAnnotations.init(this) + + coEvery { testCertificateRepository.refresh() } returns emptySet() + } + + private fun createWorker( + runAttempts: Int = 0 + ) = TestCertificateRetrievalWorker( + context = context, + workerParams = workerParams.also { + every { it.runAttemptCount } returns runAttempts + }, + testCertificateRepository = testCertificateRepository, + ) + + @Test + fun `certificate refresh`() = runBlockingTest { + val result = createWorker().doWork() + + coVerify(exactly = 1) { testCertificateRepository.refresh() } + + result shouldBe ListenableWorker.Result.success() + } + + @Test + fun `retry on error`() = runBlockingTest { + coEvery { testCertificateRepository.refresh() } throws UnknownHostException() + + val result = createWorker().doWork() + + coVerify(exactly = 1) { testCertificateRepository.refresh() } + + result shouldBe ListenableWorker.Result.retry() + } + + @Test + fun `failure after 2 retries`() = runBlockingTest { + + val result = createWorker(runAttempts = 3).doWork() + + coVerify(exactly = 0) { testCertificateRepository.refresh() } + + result shouldBe ListenableWorker.Result.failure() + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessorTest.kt index 82bfa9382..5a59b2404 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessorTest.kt @@ -35,6 +35,7 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runBlockingTest import org.joda.time.Duration import org.joda.time.Instant +import org.joda.time.LocalDate import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -275,4 +276,51 @@ class PCRProcessorTest : BaseTest() { isAdvancedConsentGiven = false ) } + + @Test + fun `request parameters for dcc are mapped`() = runBlockingTest { + val registrationData = RegistrationData( + registrationToken = "regtoken", + testResultResponse = CoronaTestResultResponse( + coronaTestResult = PCR_OR_RAT_PENDING, + sampleCollectedAt = null, + ) + ) + coEvery { submissionService.registerTest(any()) } answers { registrationData } + + createInstance().create( + CoronaTestQRCode.PCR( + qrCodeGUID = "guid", + isDccConsentGiven = true, + dateOfBirth = LocalDate.parse("2021-06-02"), + ) + ).apply { + isDccConsentGiven shouldBe true + isDccDataSetCreated shouldBe false + isDccSupportedByPoc shouldBe true + } + + createInstance().create( + CoronaTestQRCode.PCR( + qrCodeGUID = "guid", + dateOfBirth = LocalDate.parse("2021-06-02"), + ) + ).apply { + isDccConsentGiven shouldBe false + isDccDataSetCreated shouldBe false + isDccSupportedByPoc shouldBe true + } + } + + @Test + fun `marking dcc as created`() = runBlockingTest { + val instance = createInstance() + + instance.markDccCreated(defaultTest, true) shouldBe defaultTest.copy( + isDccDataSetCreated = true + ) + instance.markDccCreated(defaultTest, false) shouldBe defaultTest.copy( + isDccDataSetCreated = false + ) + } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRTestCertificateTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRTestCertificateTest.kt new file mode 100644 index 000000000..b07ad0c9e --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRTestCertificateTest.kt @@ -0,0 +1,5 @@ +package de.rki.coronawarnapp.coronatest.type.pcr + +import testhelpers.BaseTest + +class PCRTestCertificateTest : BaseTest() diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RATestCertificateTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RATestCertificateTest.kt new file mode 100644 index 000000000..cc9762273 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RATestCertificateTest.kt @@ -0,0 +1,5 @@ +package de.rki.coronawarnapp.coronatest.type.rapidantigen + +import testhelpers.BaseTest + +class RATestCertificateTest : BaseTest() diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessorTest.kt index 8f5ba33f3..8d3affbb2 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessorTest.kt @@ -32,6 +32,7 @@ import io.mockk.just import kotlinx.coroutines.test.runBlockingTest import org.joda.time.Duration import org.joda.time.Instant +import org.joda.time.LocalDate import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import testhelpers.BaseTest @@ -280,4 +281,56 @@ class RapidAntigenProcessorTest : BaseTest() { isAdvancedConsentGiven = false ) } + + @Test + fun `request parameters for dcc are mapped`() = runBlockingTest { + val registrationData = RegistrationData( + registrationToken = "regtoken", + testResultResponse = CoronaTestResultResponse( + coronaTestResult = PCR_OR_RAT_PENDING, + sampleCollectedAt = null, + ) + ) + coEvery { submissionService.registerTest(any()) } answers { registrationData } + + createInstance().create( + CoronaTestQRCode.RapidAntigen( + hash = "hash", + createdAt = Instant.EPOCH, + isDccConsentGiven = false, + dateOfBirth = LocalDate.parse("2021-06-02"), + isDccSupportedByPoc = false + ) + ).apply { + isDccConsentGiven shouldBe false + isDccDataSetCreated shouldBe false + isDccSupportedByPoc shouldBe false + } + + createInstance().create( + CoronaTestQRCode.RapidAntigen( + hash = "hash", + createdAt = Instant.EPOCH, + isDccConsentGiven = true, + dateOfBirth = LocalDate.parse("2021-06-02"), + isDccSupportedByPoc = true + ) + ).apply { + isDccConsentGiven shouldBe true + isDccDataSetCreated shouldBe false + isDccSupportedByPoc shouldBe true + } + } + + @Test + fun `marking dcc as created`() = runBlockingTest { + val instance = createInstance() + + instance.markDccCreated(defaultTest, true) shouldBe defaultTest.copy( + isDccDataSetCreated = true + ) + instance.markDccCreated(defaultTest, false) shouldBe defaultTest.copy( + isDccDataSetCreated = false + ) + } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/DataResetTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/DataResetTest.kt index 6f8799d44..d0630d26f 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/DataResetTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/DataResetTest.kt @@ -5,6 +5,7 @@ import de.rki.coronawarnapp.bugreporting.BugReportingSettings import de.rki.coronawarnapp.contactdiary.storage.ContactDiaryPreferences import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.coronatest.TestCertificateRepository import de.rki.coronawarnapp.coronatest.antigen.profile.RATProfileSettings import de.rki.coronawarnapp.datadonation.analytics.Analytics import de.rki.coronawarnapp.datadonation.analytics.storage.AnalyticsSettings @@ -64,6 +65,7 @@ internal class DataResetTest : BaseTest() { @MockK lateinit var vaccinationRepository: VaccinationRepository @MockK lateinit var vaccinationPreferences: VaccinationPreferences @MockK lateinit var valueSetsRepository: ValueSetsRepository + @MockK lateinit var testCertificateRepository: TestCertificateRepository @BeforeEach fun setUp() { @@ -97,7 +99,8 @@ internal class DataResetTest : BaseTest() { ratProfileSettings = ratProfileSettings, vaccinationPreferences = vaccinationPreferences, vaccinationRepository = vaccinationRepository, - valueSetsRepository = valueSetsRepository + valueSetsRepository = valueSetsRepository, + testCertificateRepository = testCertificateRepository, ) @Test diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt index d5ec4edc8..dd06cbdc0 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt @@ -7,6 +7,7 @@ import dagger.Component import dagger.Module import dagger.Provides import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.coronatest.TestCertificateRepository import de.rki.coronawarnapp.coronatest.type.pcr.execution.PCRResultScheduler import de.rki.coronawarnapp.coronatest.type.pcr.notification.PCRTestResultAvailableNotificationService import de.rki.coronawarnapp.coronatest.type.rapidantigen.execution.RAResultScheduler @@ -172,4 +173,7 @@ class MockProvider { @Provides fun vaccinationRepository(): VaccinationRepository = mockk() + + @Provides + fun testCertificateRepository(): TestCertificateRepository = mockk() } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPersonIdentifierTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPersonIdentifierTest.kt index 5822f0e69..571eff96c 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPersonIdentifierTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPersonIdentifierTest.kt @@ -10,13 +10,13 @@ import org.junit.jupiter.api.Test import testhelpers.BaseTest class VaccinatedPersonIdentifierTest : BaseTest() { - private val testPersonMaxData = VaccinatedPersonIdentifier( + private val testPersonMaxData = CertificatePersonIdentifier( dateOfBirth = LocalDate.parse("1966-11-11"), firstNameStandardized = "ANDREAS", lastNameStandardized = "ASTRA<EINS" ) - private val testPersonMin = VaccinatedPersonIdentifier( + private val testPersonMin = CertificatePersonIdentifier( dateOfBirth = LocalDate.parse("1900-01-01"), lastNameStandardized = "#", firstNameStandardized = null @@ -37,7 +37,7 @@ class VaccinatedPersonIdentifierTest : BaseTest() { @Test fun `person equality`() { val person1 = testPersonMaxData - val person2 = VaccinatedPersonIdentifier( + val person2 = CertificatePersonIdentifier( dateOfBirth = LocalDate.parse("1966-11-11"), firstNameStandardized = "ANDREAS", lastNameStandardized = "ASTRA<EINS" diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainerTest.kt index fad39f683..5fe372fc7 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainerTest.kt @@ -1,7 +1,7 @@ package de.rki.coronawarnapp.vaccination.core.repository.storage +import de.rki.coronawarnapp.vaccination.core.CertificatePersonIdentifier import de.rki.coronawarnapp.vaccination.core.DaggerVaccinationTestComponent -import de.rki.coronawarnapp.vaccination.core.VaccinatedPersonIdentifier import de.rki.coronawarnapp.vaccination.core.VaccinationTestData import de.rki.coronawarnapp.vaccination.core.server.valueset.VaccinationValueSet import io.kotest.matchers.shouldBe @@ -26,7 +26,7 @@ class VaccinationContainerTest : BaseTest() { @Test fun `person identifier calculation`() { - testData.personAVac1Container.personIdentifier shouldBe VaccinatedPersonIdentifier( + testData.personAVac1Container.personIdentifier shouldBe CertificatePersonIdentifier( dateOfBirth = LocalDate.parse("1966-11-11"), firstNameStandardized = "ANDREAS", lastNameStandardized = "ASTRA<EINS" @@ -73,7 +73,7 @@ class VaccinationContainerTest : BaseTest() { certificateIssuer shouldBe "Bundesministerium für Gesundheit - Test01" certificateCountry shouldBe "Deutschland" certificateId shouldBe "01DE/00001/1119305005/7T1UG87G61Y7NRXIBQJDTYQ9#S" - personIdentifier shouldBe VaccinatedPersonIdentifier( + personIdentifier shouldBe CertificatePersonIdentifier( dateOfBirth = LocalDate.parse("1966-11-11"), firstNameStandardized = "ANDREAS", lastNameStandardized = "ASTRA<EINS" @@ -121,7 +121,7 @@ class VaccinationContainerTest : BaseTest() { certificateIssuer shouldBe "Bundesministerium für Gesundheit - Test01" certificateCountry shouldBe "Deutschland" certificateId shouldBe "01DE/00001/1119305005/7T1UG87G61Y7NRXIBQJDTYQ9#S" - personIdentifier shouldBe VaccinatedPersonIdentifier( + personIdentifier shouldBe CertificatePersonIdentifier( dateOfBirth = LocalDate.parse("1966-11-11"), firstNameStandardized = "ANDREAS", lastNameStandardized = "ASTRA<EINS" diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationStorageTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationStorageTest.kt index 9af9da65b..0649370ff 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationStorageTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationStorageTest.kt @@ -6,6 +6,7 @@ import de.rki.coronawarnapp.util.serialization.SerializationModule import de.rki.coronawarnapp.vaccination.core.DaggerVaccinationTestComponent import de.rki.coronawarnapp.vaccination.core.VaccinationTestData import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.MockK @@ -88,4 +89,11 @@ class VaccinationStorageTest : BaseTest() { ) } } + + @Test + fun `post processor injects data extractors`() { + createInstance().personContainers = setOf(testData.personAData2Vac) + + createInstance().personContainers.single().vaccinations.first().qrCodeExtractor shouldNotBe null + } } -- GitLab