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(
+            "MIIG/gIBADANBgkqhkiG9w0BAQEFAASCBugwggbkAgEAAoIBgQCesnw9uZs6YQSYcH/zKDVueRRPuNwXhd/LYx7mKYmAtVhDRwv1Q0/3Vx8VSuAwVXd8c5zRS5rLXROasEtoR3OoMRlAfgqPS5n6FUK3Oe1UoW+K5SiHTzDEWWiC1zlLLQ2G5MbILVb+LYBfVIO963bExxzV8TjishNAQQCUbz6d8alumkUd7NQ7G5mOz1cFPFq8ONDJZ4XAwPfRQAkZqQ+E446PqKFcDX069D+1i5hMnkL1DN4r3QL+thuDZ+f6izcQaVvuIlnZXcoas0t0xwS2SIbkiIeJxo0Kfw5L9wjd3b2IkTgQJoXvXV/IXwwbUfa1uSNQtLduw81S/K8d8zZyM49GF6NrI6H4LFCRzfApqvwCm9AKOJVFbTmc7S58d08xxYmFSVyFZg7mY4lNI7y8b/1iqmdO4NzYZ6VziAwrsoBN0fe/UWwAI8o0l2m3rwh9leAZifku0nvMkHpjDxF7CPVJoX/AUpelh37knIrXzAlMtVhKmIPOJ2HgM/5HplMCAwEAAQKCAYEAhw8Bu4pduFZfEdkUm31J0+YJyjtaXE6cAr0ty9Xn5vjuz/sEC0ypHqgvlPBvUdM66FiASoMcjxx8lbaZxnqgzLBUfFWIaSF/Pp2fdM5A1Di79CpIzrcvmrs4vbmrUfZav8WuAyjLE3DoArmrkRN2tct7F/y+W/gPeCyZ8LmoQcUsXCvAzNIEYPWBP0/oEFWoJu33iqCm7T+M6LGlzQfbZE5BwrNR+ESmomjCW6AdEn/SHjlAT3Y9mUakrbXdcJXPAI+RleS90kn8AHiQuyjotlb32xhBVw6SOtfd0xkMyY67AbCo9R1f0ir54PayA38xs4yQ0O2OgNUSLTWYXV1T/mSQSQMaxNw556IEXWVQWRWIc91QwOI2TD/N+vIxLPbNtuW5lEyMCzrmBdxq7wIOGIpy62B11TW6UYU26GOkhHTXEnn7pmHGVtbCXPGoKzncxxKNhRFuGOPcd+yQkM5eYAf7dad2NySrOokMQ2eIacPwKxKFfA/QN0v9aPLj7Qa5AoHBAOynNxSKErA25ndt/xBaLwSdzPynkE5zkO3gedgqvO6/6bAGOsRkawTNGalkVTwhXEnGBUIidPqhW7ex5/ad1QPUsWT8YeeYzXoM+Gqgu6M8awpFK4cwUMCrpRJwaUBFUCNzYNDZgJoOZAOX2TKvSNH+9zFrmGrKY0KRZ0aK9T0Ksbxn8KrErvXYj05nG4A3MrsKRA8mwBtZm0/17bBtg0nYH6/Omt787LBO/sxCicwTZioJlUgndzAIwTw7BtFDzwKBwQCrq8Wx/MK+LA+HUzDmdTK04sgIebulBSTV95aSsWoN+MoNwmi9wVt3OJfpq4L7g1NVv0vNSIajk9BDIFeKvgmDcde8RV51LRCObQ9enCOQUH7e0eC3XI9Nxg3nIhQiYJuggG6QAtt07bybx3dWpYEXL4ZOPOEkTVXR5JRkx9MWw8VDTbLKTCfZJ34PPF4AdCrs3yId9FXk9pUmS68oJVRsFhnI+dSdky32Bc01G6kk0SlGOudKLzqx4fbr0itHIj0CgcAHDWCd0xOFfs1VZ8i/EwDtsUoniVLKk7UQ8ayP3Y4tyzhKj5T2v0tVJEuMebn0hcX7SNRlSSOVSHO0QK/58HAlohP7P24nea094t8QRmPxFF7YOoF2kOEHLNZJe2IXkTk3JTwQXTrw3FbsqHzHfuO7pk51gZBUNl3I4Q5j0sZGIGh1hd9tJ1lTaDW1D2uJYZu4aTDoBq6Y4g23z0tbA5hy/ebL1WtWE9F125TKP31dwII95HU3Zj2uB8TCZ7vnRo8CgcBfeUiZlFk6Kob4W+v2P3fT4cwd6pXRUOsLlIbJTqIM4zB8NoLKBZ84zuCttBVEi+Ts61bc9Fjs4GgS7QnCv63KzKWOr4W45Tcv/rdthqjAugPVKCQx1ehc+KkCwpEwDUqAGO1kajJi9VTPzj8wkRsaKfQnzvPnnJr+AIIHCpr7LiWnKK8mkvQWcUBKeOhOmEzHL9Fpl1mt3PVWNwFS8m/hLOlqPIdim1gUW2WlA50uPKUXyeqX92xNQb5xqJEpHoECgcEAw4FGJb47FivG25fD+e61GxzG/KrzQL0eVS3T2YRAiN5ZB7QyInm6vMTi0QKCScCRJjOjRyoI3VtCO7G8vUnm0UiCW4l11WqW9G4vVh5VuR0HJ+kH1CQcq1aheqF7bbZGjjK47iyZskehfa6kcEOfThE6n6G7mIE/oe5k8A6+wHoLGmBbdxwE2xuG3PorH0PgbAgva1KAgC57rTBJhHnm6ntT21vlPLev9QvrE5syo+LEDbagr5zHMC14qAwMH2fi".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