From 1dbb2bfdcff9f1e1e6fece4eeb827a5f557e7b0b Mon Sep 17 00:00:00 2001
From: Matthias Urhahn <matthias.urhahn@sap.com>
Date: Tue, 8 Jun 2021 15:43:46 +0200
Subject: [PATCH] TestCertificate Repository polishing & improvements
 (EXPSUREAPP-7505) (#3373)

* fixed components path

* Improve error handling and fix crash due to uncaught throwable.

* fixed dcc server di

* + api test

* Add wrapper class around TestCertificateContainer to include valuesets on repository level already.

* Provide data extractor on container creation to prevent accidental early access of `lateinit var`

* Improve comments.

* Add missing click listener and refactor deep nesting.

* LINTs

Co-authored-by: chris-cwa <chris.cwa.sap@gmail.com>
Co-authored-by: harambasicluka <64483219+harambasicluka@users.noreply.github.com>
Co-authored-by: Mohamed Metwalli <mohamed.metwalli@sap.com>
---
 .../coronatest/TestCertificateRepository.kt   | 237 +++++++++---------
 .../storage/TestCertificateStorage.kt         |   6 +-
 .../coronatest/type/TestCertificateWrapper.kt |  24 ++
 .../{ => common}/TestCertificateContainer.kt  |   7 +-
 .../type/pcr/PCRCertificateContainer.kt       |   2 +-
 .../rapidantigen/RACertificateContainer.kt    |   2 +-
 .../TestCertificateServerException.kt         |   2 +-
 .../server/CovidCertificateServer.kt          |   3 +-
 .../ui/certificates/CertificatesViewModel.kt  | 137 +++++-----
 .../cards/CovidTestCertificateCard.kt         |  12 +-
 .../CovidCertificateDetailsViewModel.kt       |   4 +-
 .../storage/ContainerPostProcessor.kt         |   2 +-
 .../TestCertificateRepositoryTest.kt          |   7 +-
 .../TestCertificateRetrievalSchedulerTest.kt  |   6 +-
 14 files changed, 238 insertions(+), 213 deletions(-)
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/TestCertificateWrapper.kt
 rename Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/{ => common}/TestCertificateContainer.kt (92%)

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
index 8656d45a6..6bd544343 100644
--- 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
@@ -4,8 +4,9 @@ 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.TestCertificateWrapper
+import de.rki.coronawarnapp.coronatest.type.common.TestCertificateContainer
+import de.rki.coronawarnapp.coronatest.type.common.TestCertificateIdentifier
 import de.rki.coronawarnapp.coronatest.type.pcr.PCRCertificateContainer
 import de.rki.coronawarnapp.coronatest.type.rapidantigen.RACertificateContainer
 import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.RSA_DECRYPTION_FAILED
@@ -22,7 +23,9 @@ 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.flow.combine
 import de.rki.coronawarnapp.util.mutate
+import de.rki.coronawarnapp.vaccination.core.repository.ValueSetsRepository
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
@@ -30,7 +33,6 @@ 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
@@ -43,6 +45,7 @@ import javax.inject.Inject
 import javax.inject.Singleton
 
 @Singleton
+@Suppress("LongParameterList")
 class TestCertificateRepository @Inject constructor(
     @AppScope private val appScope: CoroutineScope,
     private val dispatcherProvider: DispatcherProvider,
@@ -53,6 +56,7 @@ class TestCertificateRepository @Inject constructor(
     private val rsaCryptography: RSACryptography,
     private val qrCodeExtractor: TestCertificateQRCodeExtractor,
     private val appConfigProvider: AppConfigProvider,
+    private val valueSetsRepository: ValueSetsRepository,
 ) {
 
     private val internalData: HotDataFlow<Map<TestCertificateIdentifier, TestCertificateContainer>> = HotDataFlow(
@@ -65,7 +69,17 @@ class TestCertificateRepository @Inject constructor(
         }
     }
 
-    val certificates: Flow<Set<TestCertificateContainer>> = internalData.data.map { it.values.toSet() }
+    val certificates: Flow<Set<TestCertificateWrapper>> = combine(
+        internalData.data,
+        valueSetsRepository.latestTestCertificateValueSets
+    ) { certMap, valueSets ->
+        certMap.values.map { container ->
+            TestCertificateWrapper(
+                valueSets = valueSets,
+                container = container,
+            )
+        }.toSet()
+    }
 
     init {
         internalData.data
@@ -89,7 +103,7 @@ class TestCertificateRepository @Inject constructor(
      * 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)
+        Timber.tag(TAG).d("requestCertificate(test.identifier=%s)", test.identifier)
 
         val newData = internalData.updateBlocking {
             if (values.any { it.registrationToken == test.registrationToken }) {
@@ -116,6 +130,8 @@ class TestCertificateRepository @Inject constructor(
                     registeredAt = test.registeredAt,
                     registrationToken = test.registrationToken,
                 )
+            }.also {
+                it.qrCodeExtractor = qrCodeExtractor
             }
             Timber.tag(TAG).d("Adding test certificate entry: %s", certificate)
             mutate { this[certificate.identifier] = certificate }
@@ -125,12 +141,12 @@ class TestCertificateRepository @Inject constructor(
     }
 
     /**
-     * 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.
+     * If [error] is NULL, then [certificateContainer] will be the refreshed entry.
+     * If [error] is not NULL, then [certificateContainer] 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 certificateContainer: TestCertificateContainer,
         val error: Exception? = null,
     )
 
@@ -174,22 +190,24 @@ class TestCertificateRepository @Inject constructor(
                 .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)
+                    withContext(dispatcherProvider.IO) {
+                        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
+                refreshCallResults[it.certificateContainer.identifier] = it
             }
 
             mutate {
                 refreshedCerts
                     .filter { it.error == null }
-                    .map { it.certificate }
+                    .map { it.certificateContainer }
                     .forEach { this[it.identifier] = it }
             }
         }
@@ -201,22 +219,24 @@ class TestCertificateRepository @Inject constructor(
                 .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)
+                    withContext(dispatcherProvider.IO) {
+                        try {
+                            RefreshResult(obtainCertificate(cert))
+                        } catch (e: Exception) {
+                            Timber.tag(TAG).e(e, "Failed to retrieve certificate components for %s", cert)
+                            RefreshResult(cert, e)
+                        }
                     }
                 }
 
             refreshedCerts.forEach {
-                refreshCallResults[it.certificate.identifier] = it
+                refreshCallResults[it.certificateContainer.identifier] = it
             }
 
             mutate {
                 refreshedCerts
                     .filter { it.error == null }
-                    .map { it.certificate }
+                    .map { it.certificateContainer }
                     .forEach { this[it.identifier] = it }
             }
         }
@@ -244,45 +264,38 @@ class TestCertificateRepository @Inject constructor(
     private suspend fun registerPublicKey(
         cert: TestCertificateContainer
     ): TestCertificateContainer {
-        return try {
-            Timber.tag(TAG).d("registerPublicKey(cert=%s)", cert)
+        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
-            }
+        if (cert.isPublicKeyRegistered) {
+            Timber.tag(TAG).d("Public key is already registered for %s", cert)
+            return cert
+        }
 
-            val rsaKeyPair = try {
-                rsaKeyPairGenerator.generate()
-            } catch (e: Throwable) {
-                throw InvalidTestCertificateException(RSA_KP_GENERATION_FAILED)
-            }
+        val rsaKeyPair = try {
+            rsaKeyPairGenerator.generate()
+        } catch (e: Throwable) {
+            throw InvalidTestCertificateException(RSA_KP_GENERATION_FAILED)
+        }
 
-            withContext(dispatcherProvider.IO) {
-                certificateServer.registerPublicKeyForTest(
-                    testRegistrationToken = cert.registrationToken,
-                    publicKey = rsaKeyPair.publicKey,
-                )
-            }
-            Timber.tag(TAG).i("Public key successfully registered for %s", cert)
+        certificateServer.registerPublicKeyForTest(
+            testRegistrationToken = cert.registrationToken,
+            publicKey = rsaKeyPair.publicKey,
+        )
+        Timber.tag(TAG).i("Public key successfully registered for %s", cert)
 
-            val nowUTC = timeStamper.nowUTC
+        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
+        return 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,
+            )
         }
     }
 
@@ -296,78 +309,70 @@ class TestCertificateRepository @Inject constructor(
     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.")
+        Timber.tag(TAG).d("requestCertificate(cert=%s)", cert)
 
-            if (!cert.isCertificateRetrievalPending) {
-                Timber.tag(TAG).d("Dcc has already been retrieved for %s", cert)
-                return cert
-            }
+        if (!cert.isPublicKeyRegistered) throw IllegalStateException("Public key is not registered yet.")
 
-            val certConfig = appConfigProvider.currentConfig.first().covidCertificateParameters.testCertificate
+        if (!cert.isCertificateRetrievalPending) {
+            Timber.tag(TAG).d("Dcc has already been retrieved for %s", cert)
+            return cert
+        }
 
-            val nowUTC = timeStamper.nowUTC
-            val certAvailableAt = cert.publicKeyRegisteredAt!!.plus(certConfig.waitAfterPublicKeyRegistration)
-            val certAvailableIn = Duration(nowUTC, certAvailableAt)
+        val certConfig = appConfigProvider.currentConfig.first().covidCertificateParameters.testCertificate
 
-            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 nowUTC = timeStamper.nowUTC
+        val certAvailableAt = cert.publicKeyRegisteredAt!!.plus(certConfig.waitAfterPublicKeyRegistration)
+        val certAvailableIn = Duration(nowUTC, certAvailableAt)
 
-                val executeRequest: suspend CoroutineScope.() -> TestCertificateComponents = {
-                    certificateServer.requestCertificateForTest(testRegistrationToken = cert.registrationToken)
-                }
+        if (certAvailableIn > Duration.ZERO && certAvailableIn <= certConfig.waitAfterPublicKeyRegistration) {
+            Timber.tag(TAG).d("Delaying certificate retrieval by %d ms", certAvailableIn.millis)
+            delay(certAvailableIn.millis)
+        }
 
-                try {
-                    executeRequest()
-                } catch (e: TestCertificateServerException) {
-                    if (e.errorCode == DCC_COMP_202) {
-                        delay(certConfig.waitForRetry.millis)
-                        executeRequest()
-                    } else {
-                        throw e
-                    }
-                }
-            }
-            Timber.tag(TAG).i("Test certificate components successfully request for %s: %s", cert, components)
+        val executeRequest: suspend () -> TestCertificateComponents = {
+            certificateServer.requestCertificateForTest(testRegistrationToken = cert.registrationToken)
+        }
 
-            val encryptionKey = try {
-                rsaCryptography.decrypt(
-                    toDecrypt = components.dataEncryptionKeyBase64.decodeBase64()!!,
-                    privateKey = cert.rsaPrivateKey!!
-                )
-            } catch (e: Throwable) {
-                Timber.tag(TAG).e(e, "RSA_DECRYPTION_FAILED")
-                throw InvalidTestCertificateException(RSA_DECRYPTION_FAILED)
+        val components = try {
+            executeRequest()
+        } catch (e: TestCertificateServerException) {
+            if (e.errorCode == DCC_COMP_202) {
+                delay(certConfig.waitForRetry.millis)
+                executeRequest()
+            } else {
+                throw e
             }
+        }
+        Timber.tag(TAG).i("Test certificate components successfully request for %s: %s", cert, components)
 
-            val extractedData = qrCodeExtractor.extract(
-                decryptionKey = encryptionKey.toByteArray(),
-                rawCoseObjectEncrypted = components.encryptedCoseTestCertificateBase64.decodeBase64()!!.toByteArray()
+        val encryptionKey = try {
+            rsaCryptography.decrypt(
+                toDecrypt = components.dataEncryptionKeyBase64.decodeBase64()!!,
+                privateKey = cert.rsaPrivateKey!!
             )
+        } catch (e: Throwable) {
+            Timber.tag(TAG).e(e, "RSA_DECRYPTION_FAILED")
+            throw InvalidTestCertificateException(RSA_DECRYPTION_FAILED)
+        }
 
-            val nowUtc = timeStamper.nowUTC
+        val extractedData = qrCodeExtractor.extract(
+            decryptionKey = encryptionKey.toByteArray(),
+            rawCoseObjectEncrypted = components.encryptedCoseTestCertificateBase64.decodeBase64()!!.toByteArray()
+        )
 
-            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
+        val nowUtc = timeStamper.nowUTC
+
+        return 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.preParsedData = extractedData.testCertificateData
         }
     }
 
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
index c01d849d1..e6933d950 100644
--- 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
@@ -6,7 +6,7 @@ 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.common.TestCertificateContainer
 import de.rki.coronawarnapp.coronatest.type.pcr.PCRCertificateContainer
 import de.rki.coronawarnapp.coronatest.type.rapidantigen.RACertificateContainer
 import de.rki.coronawarnapp.util.di.AppContext
@@ -46,7 +46,7 @@ class TestCertificateStorage @Inject constructor(
         get() {
             Timber.tag(TAG).d("load()")
 
-            val pcrCerts: Set<PCRCertificateContainer> = run {
+            val pcrCertContainers: 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)
@@ -64,7 +64,7 @@ class TestCertificateStorage @Inject constructor(
                 }
             }
 
-            return (pcrCerts + raCerts).also {
+            return (pcrCertContainers + raCerts).also {
                 Timber.tag(TAG).v("Loaded %d certificates.", it.size)
             }
         }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/TestCertificateWrapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/TestCertificateWrapper.kt
new file mode 100644
index 000000000..b9ec3f160
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/TestCertificateWrapper.kt
@@ -0,0 +1,24 @@
+package de.rki.coronawarnapp.coronatest.type
+
+import de.rki.coronawarnapp.coronatest.type.common.TestCertificateContainer
+import de.rki.coronawarnapp.coronatest.type.common.TestCertificateIdentifier
+import de.rki.coronawarnapp.covidcertificate.test.TestCertificate
+import de.rki.coronawarnapp.vaccination.core.server.valueset.valuesets.TestCertificateValueSets
+
+data class TestCertificateWrapper(
+    private val valueSets: TestCertificateValueSets,
+    private val container: TestCertificateContainer
+) {
+
+    val identifier: TestCertificateIdentifier = container.identifier
+
+    val isCertificateRetrievalPending = container.isCertificateRetrievalPending
+
+    val isUpdatingData = container.isUpdatingData
+
+    val registeredAt = container.registeredAt
+
+    val testCertificate: TestCertificate? by lazy {
+        container.toTestCertificate(valueSets)
+    }
+}
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/common/TestCertificateContainer.kt
similarity index 92%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/TestCertificateContainer.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/TestCertificateContainer.kt
index beb061ce0..f49ffb3d0 100644
--- 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/common/TestCertificateContainer.kt
@@ -1,5 +1,7 @@
-package de.rki.coronawarnapp.coronatest.type
+package de.rki.coronawarnapp.coronatest.type.common
 
+import de.rki.coronawarnapp.coronatest.type.CoronaTest
+import de.rki.coronawarnapp.coronatest.type.RegistrationToken
 import de.rki.coronawarnapp.covidcertificate.test.TestCertificate
 import de.rki.coronawarnapp.covidcertificate.test.TestCertificateData
 import de.rki.coronawarnapp.covidcertificate.test.TestCertificateQRCodeExtractor
@@ -28,9 +30,10 @@ abstract class TestCertificateContainer {
 
     abstract val isUpdatingData: Boolean
 
-    // Either set by [ContainerPostProcessor] or during first update
+    // Either set by [ContainerPostProcessor] (if from storage) or during first creation (when new)
     @Transient internal lateinit var qrCodeExtractor: TestCertificateQRCodeExtractor
 
+    // When we create this container initially, we don't need to pare the data again, we already have it.
     @Transient internal var preParsedData: TestCertificateData? = null
 
     @delegate:Transient
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
index efa8149d1..bee0ea96c 100644
--- 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
@@ -3,7 +3,7 @@ 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.coronatest.type.common.TestCertificateContainer
 import de.rki.coronawarnapp.util.encryption.rsa.RSAKey
 import okio.ByteString
 import org.joda.time.Instant
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
index 87fbfaabe..ccea19091 100644
--- 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
@@ -3,7 +3,7 @@ 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.coronatest.type.common.TestCertificateContainer
 import de.rki.coronawarnapp.util.encryption.rsa.RSAKey
 import okio.ByteString
 import org.joda.time.Instant
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/TestCertificateServerException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/TestCertificateServerException.kt
index 49238be36..6cbe138c4 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/TestCertificateServerException.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/TestCertificateServerException.kt
@@ -9,7 +9,7 @@ import de.rki.coronawarnapp.util.ui.LazyString
 
 class TestCertificateServerException(
     val errorCode: ErrorCode
-) : HasHumanReadableError, Throwable(errorCode.message) {
+) : HasHumanReadableError, Exception(errorCode.message) {
 
     override fun toHumanReadableError(context: Context): HumanReadableError {
         return HumanReadableError(
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
index 7d8996322..cd3b0b40b 100644
--- 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
@@ -66,7 +66,8 @@ class CovidCertificateServer @Inject constructor(
                 409 -> throw TestCertificateServerException(PKR_409)
                 500 -> throw TestCertificateServerException(PKR_500)
             }
-        } catch (e: Throwable) {
+        } catch (e: Exception) {
+            Timber.tag(TAG).w(e, "registerPublicKeyForTest failed")
             throw TestCertificateServerException(PKR_FAILED)
         }
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/greencertificate/ui/certificates/CertificatesViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/greencertificate/ui/certificates/CertificatesViewModel.kt
index da600d0cf..c6a1d592f 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/greencertificate/ui/certificates/CertificatesViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/greencertificate/ui/certificates/CertificatesViewModel.kt
@@ -5,8 +5,10 @@ import androidx.lifecycle.asLiveData
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import de.rki.coronawarnapp.coronatest.TestCertificateRepository
-import de.rki.coronawarnapp.coronatest.type.TestCertificateContainer
-import de.rki.coronawarnapp.coronatest.type.TestCertificateIdentifier
+import de.rki.coronawarnapp.coronatest.type.TestCertificateWrapper
+import de.rki.coronawarnapp.coronatest.type.common.TestCertificateIdentifier
+import de.rki.coronawarnapp.greencertificate.ui.certificates.cards.CovidTestCertificateCard
+import de.rki.coronawarnapp.greencertificate.ui.certificates.cards.CovidTestCertificateErrorCard
 import de.rki.coronawarnapp.greencertificate.ui.certificates.items.CertificatesItem
 import de.rki.coronawarnapp.util.ui.SingleLiveEvent
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
@@ -14,14 +16,12 @@ import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
 import de.rki.coronawarnapp.vaccination.core.VaccinatedPerson
 import de.rki.coronawarnapp.vaccination.core.VaccinationSettings
 import de.rki.coronawarnapp.vaccination.core.repository.VaccinationRepository
-import de.rki.coronawarnapp.vaccination.ui.cards.NoCovidTestCertificatesCard
 import de.rki.coronawarnapp.vaccination.ui.cards.CreateVaccinationCard
 import de.rki.coronawarnapp.vaccination.ui.cards.HeaderInfoVaccinationCard
 import de.rki.coronawarnapp.vaccination.ui.cards.ImmuneVaccinationCard
+import de.rki.coronawarnapp.vaccination.ui.cards.NoCovidTestCertificatesCard
 import de.rki.coronawarnapp.vaccination.ui.cards.VaccinationCard
 import kotlinx.coroutines.flow.combine
-import de.rki.coronawarnapp.greencertificate.ui.certificates.cards.CovidTestCertificateErrorCard
-import de.rki.coronawarnapp.greencertificate.ui.certificates.cards.CovidTestCertificateCard
 
 class CertificatesViewModel @AssistedInject constructor(
     vaccinationRepository: VaccinationRepository,
@@ -42,85 +42,70 @@ class CertificatesViewModel @AssistedInject constructor(
             .combine(testCertificateRepository.certificates) { vaccinatedPersons, certificates ->
                 mutableListOf<CertificatesItem>().apply {
                     add(HeaderInfoVaccinationCard.Item)
-                    addVaccinationCards(vaccinatedPersons)
-                    addTestCertificateCards(certificates)
-                }
-            }.asLiveData()
-
-    private fun MutableList<CertificatesItem>.addVaccinationCards(vaccinatedPersons: Set<VaccinatedPerson>) {
-        vaccinatedPersons.forEach { vaccinatedPerson ->
-            val card = when (vaccinatedPerson.getVaccinationStatus()) {
-                VaccinatedPerson.Status.COMPLETE,
-                VaccinatedPerson.Status.INCOMPLETE -> VaccinationCard.Item(
-                    vaccinatedPerson = vaccinatedPerson,
-                    onClickAction = {
-                        events.postValue(
-                            CertificatesFragmentEvents.GoToVaccinationList(
-                                vaccinatedPerson.identifier.codeSHA256
-                            )
-                        )
-                    }
-                )
-                VaccinatedPerson.Status.IMMUNITY -> ImmuneVaccinationCard.Item(
-                    vaccinatedPerson = vaccinatedPerson,
-                    onClickAction = {
-                        events.postValue(
-                            CertificatesFragmentEvents.GoToVaccinationList(
-                                vaccinatedPerson.identifier.codeSHA256
+                    if (vaccinatedPersons.isEmpty()) {
+                        add(
+                            CreateVaccinationCard.Item(
+                                onClickAction = {
+                                    CertificatesFragmentEvents.OpenVaccinationRegistrationGraph(
+                                        vaccinationSettings.registrationAcknowledged
+                                    ).run { events.postValue(this) }
+                                }
                             )
                         )
+                    } else {
+                        addAll(vaccinatedPersons.toCertificateItems())
                     }
-                )
-            }
-            add(card)
-        }
-        if (vaccinatedPersons.isEmpty()) {
-            add(
-                CreateVaccinationCard.Item(
-                    onClickAction = {
-                        events.postValue(
-                            CertificatesFragmentEvents.OpenVaccinationRegistrationGraph(
-                                vaccinationSettings.registrationAcknowledged
-                            )
-                        )
+
+                    if (certificates.isEmpty()) {
+                        add(NoCovidTestCertificatesCard.Item)
+                    } else {
+                        addAll(certificates.toCertificateItems())
                     }
-                )
+                }
+            }.asLiveData()
+
+    private fun Set<VaccinatedPerson>.toCertificateItems(): List<CertificatesItem> = map { vaccinatedPerson ->
+        when (vaccinatedPerson.getVaccinationStatus()) {
+            VaccinatedPerson.Status.COMPLETE,
+            VaccinatedPerson.Status.INCOMPLETE -> VaccinationCard.Item(
+                vaccinatedPerson = vaccinatedPerson,
+                onClickAction = {
+                    CertificatesFragmentEvents.GoToVaccinationList(
+                        vaccinatedPerson.identifier.codeSHA256
+                    ).run { events.postValue(this) }
+                }
+            )
+            VaccinatedPerson.Status.IMMUNITY -> ImmuneVaccinationCard.Item(
+                vaccinatedPerson = vaccinatedPerson,
+                onClickAction = {
+                    CertificatesFragmentEvents.GoToVaccinationList(
+                        vaccinatedPerson.identifier.codeSHA256
+                    ).run { events.postValue(this) }
+                }
             )
         }
     }
 
-    private fun MutableList<CertificatesItem>.addTestCertificateCards(certificates: Set<TestCertificateContainer>) {
-        certificates.forEach { certificate ->
-            if (certificate.isCertificateRetrievalPending) {
-                add(
-                    CovidTestCertificateErrorCard.Item(
-                        testDate = certificate.registeredAt,
-                        onClickAction = {
-                            refreshTestCertificate(certificate.identifier)
-                        }
-                    )
-                )
-            } else {
-                add(
-                    CovidTestCertificateCard.Item(
-                        testDate = certificate.registeredAt,
-                        testPerson =
-                        certificate.toTestCertificate(null)?.firstName + " " +
-                            certificate.toTestCertificate(null)?.lastName,
-                        onClickAction = {
-                            events.postValue(
-                                CertificatesFragmentEvents.GoToCovidCertificateDetailScreen(
-                                    certificate.identifier
-                                )
-                            )
-                        }
-                    )
-                )
-            }
-        }
-
-        if (certificates.isEmpty()) {
-            add(NoCovidTestCertificatesCard.Item)
+    private fun Collection<TestCertificateWrapper>.toCertificateItems(): List<CertificatesItem> = map { certificate ->
+        if (certificate.isCertificateRetrievalPending) {
+            CovidTestCertificateErrorCard.Item(
+                testDate = certificate.registeredAt,
+                onClickAction = {
+                    refreshTestCertificate(certificate.identifier)
+                }
+            )
+        } else {
+            CovidTestCertificateCard.Item(
+                testDate = certificate.registeredAt,
+                testPerson =
+                certificate.testCertificate?.firstName + " " +
+                    certificate.testCertificate?.lastName,
+                onClickAction = {
+                    CertificatesFragmentEvents.GoToCovidCertificateDetailScreen(
+                        certificate.identifier
+                    ).run { events.postValue(this) }
+                }
+            )
         }
     }
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/greencertificate/ui/certificates/cards/CovidTestCertificateCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/greencertificate/ui/certificates/cards/CovidTestCertificateCard.kt
index 52154f49f..57d1fd370 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/greencertificate/ui/certificates/cards/CovidTestCertificateCard.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/greencertificate/ui/certificates/cards/CovidTestCertificateCard.kt
@@ -22,15 +22,17 @@ class CovidTestCertificateCard(parent: ViewGroup) :
     override val onBindData: CovidTestSuccessCardBinding.(
         item: Item,
         payloads: List<Any>
-    ) -> Unit = { item, _ ->
-
+    ) -> Unit = { item, payloads ->
+        val curItem = payloads.filterIsInstance<Item>().singleOrNull() ?: item
         testTime.text = context.getString(
             R.string.test_certificate_time,
-            item.testDate.toShortDayFormat(),
-            item.testDate.toShortTimeFormat(),
+            curItem.testDate.toShortDayFormat(),
+            curItem.testDate.toShortTimeFormat(),
         )
 
-        personName.text = item.testPerson
+        personName.text = curItem.testPerson
+
+        itemView.setOnClickListener { curItem.onClickAction(curItem) }
     }
 
     data class Item(
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/greencertificate/ui/certificates/details/CovidCertificateDetailsViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/greencertificate/ui/certificates/details/CovidCertificateDetailsViewModel.kt
index f882c1c6a..7c3bb3b99 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/greencertificate/ui/certificates/details/CovidCertificateDetailsViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/greencertificate/ui/certificates/details/CovidCertificateDetailsViewModel.kt
@@ -8,7 +8,7 @@ import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import de.rki.coronawarnapp.coronatest.TestCertificateRepository
-import de.rki.coronawarnapp.coronatest.type.TestCertificateIdentifier
+import de.rki.coronawarnapp.coronatest.type.common.TestCertificateIdentifier
 import de.rki.coronawarnapp.covidcertificate.test.TestCertificate
 import de.rki.coronawarnapp.presencetracing.checkins.qrcode.QrCodeGenerator
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
@@ -31,7 +31,7 @@ class CovidCertificateDetailsViewModel @AssistedInject constructor(
     val events = SingleLiveEvent<CovidCertificateDetailsNavigation>()
     val errors = SingleLiveEvent<Throwable>()
     val covidCertificate = testCertificateRepository.certificates.map { certificates ->
-        certificates.find { it.identifier == testCertificateIdentifier }?.toTestCertificate(null)
+        certificates.find { it.identifier == testCertificateIdentifier }?.testCertificate
             .also { generateQrCode(it) }
     }.asLiveData(dispatcherProvider.Default)
 
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 a8f210876..627861630 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,7 +7,7 @@ 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.coronatest.type.common.TestCertificateContainer
 import de.rki.coronawarnapp.covidcertificate.test.TestCertificateQRCodeExtractor
 import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationQRCodeExtractor
 import timber.log.Timber
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
index 6c5344c30..2ba03c246 100644
--- 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
@@ -4,7 +4,7 @@ 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.common.TestCertificateContainer
 import de.rki.coronawarnapp.coronatest.type.pcr.PCRCertificateContainer
 import de.rki.coronawarnapp.covidcertificate.server.CovidCertificateServer
 import de.rki.coronawarnapp.covidcertificate.server.TestCertificateComponents
@@ -13,6 +13,7 @@ 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 de.rki.coronawarnapp.vaccination.core.repository.ValueSetsRepository
 import io.mockk.MockKAnnotations
 import io.mockk.Runs
 import io.mockk.coEvery
@@ -22,6 +23,7 @@ import io.mockk.impl.annotations.MockK
 import io.mockk.just
 import io.mockk.mockk
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.flowOf
 import okio.ByteString
 import org.joda.time.Duration
@@ -42,6 +44,7 @@ class TestCertificateRepositoryTest : BaseTest() {
     @MockK lateinit var appConfigProvider: AppConfigProvider
     @MockK lateinit var appConfigData: ConfigData
     @MockK lateinit var covidTestCertificateConfig: CovidCertificateConfig.TestCertificate
+    @MockK lateinit var valueSetsRepository: ValueSetsRepository
 
     private val testCertificateNew = PCRCertificateContainer(
         identifier = "identifier1",
@@ -97,6 +100,7 @@ class TestCertificateRepositoryTest : BaseTest() {
             every { qrCode } returns "qrCode"
             every { testCertificateData } returns mockk()
         }
+        every { valueSetsRepository.latestTestCertificateValueSets } returns emptyFlow()
     }
 
     private fun createInstance(scope: CoroutineScope) = TestCertificateRepository(
@@ -109,6 +113,7 @@ class TestCertificateRepositoryTest : BaseTest() {
         rsaCryptography = rsaCryptography,
         qrCodeExtractor = qrCodeExtractor,
         appConfigProvider = appConfigProvider,
+        valueSetsRepository = valueSetsRepository,
     )
 
     @Test
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
index bf93a2efd..fb8cf540c 100644
--- 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
@@ -6,7 +6,7 @@ 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.coronatest.type.TestCertificateWrapper
 import de.rki.coronawarnapp.util.device.ForegroundState
 import io.mockk.MockKAnnotations
 import io.mockk.Runs
@@ -40,7 +40,7 @@ class TestCertificateRetrievalSchedulerTest : BaseTest() {
         every { isNegative } returns true
     }
 
-    private val mockCertificate = mockk<TestCertificateContainer>().apply {
+    private val mockCertificate = mockk<TestCertificateWrapper>().apply {
         every { identifier } returns "UUID"
         every { isCertificateRetrievalPending } returns true
         every { isUpdatingData } returns false
@@ -124,7 +124,7 @@ class TestCertificateRetrievalSchedulerTest : BaseTest() {
         advanceUntilIdle()
         coVerify(exactly = 1) { workManager.enqueueUniqueWork(any(), any(), any<OneTimeWorkRequest>()) }
 
-        val mockCertificate2 = mockk<TestCertificateContainer>().apply {
+        val mockCertificate2 = mockk<TestCertificateWrapper>().apply {
             every { identifier } returns "UUID2"
             every { isCertificateRetrievalPending } returns true
             every { isUpdatingData } returns false
-- 
GitLab