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