diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateProcessor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificateProcessor.kt similarity index 78% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateProcessor.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificateProcessor.kt index fe9960c7c71e9a6add4e4d39857a47097bba1a8d..4dda48e8e6b93dd9313c5e394af85843f63a1868 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateProcessor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificateProcessor.kt @@ -1,14 +1,17 @@ -package de.rki.coronawarnapp.covidcertificate.test.core.storage +package de.rki.coronawarnapp.covidcertificate.test.core import dagger.Reusable import de.rki.coronawarnapp.appconfig.AppConfigProvider -import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidTestCertificateException import de.rki.coronawarnapp.covidcertificate.common.exception.TestCertificateServerException +import de.rki.coronawarnapp.covidcertificate.common.exception.TestCertificateServerException.ErrorCode import de.rki.coronawarnapp.covidcertificate.test.core.server.TestCertificateComponents import de.rki.coronawarnapp.covidcertificate.test.core.server.TestCertificateServer +import de.rki.coronawarnapp.covidcertificate.test.core.storage.types.PCRCertificateData +import de.rki.coronawarnapp.covidcertificate.test.core.storage.types.RACertificateData +import de.rki.coronawarnapp.covidcertificate.test.core.storage.types.RetrievedTestCertificate import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.encryption.rsa.RSACryptography import de.rki.coronawarnapp.util.encryption.rsa.RSAKeyPairGenerator @@ -34,10 +37,15 @@ class TestCertificateProcessor @Inject constructor( * the test certificate components should be available, via [obtainCertificate]. */ internal suspend fun registerPublicKey( - data: StoredTestCertificateData - ): StoredTestCertificateData { + data: RetrievedTestCertificate + ): RetrievedTestCertificate { Timber.tag(TAG).d("registerPublicKey(cert=%s)", data) + if (data is PCRCertificateData && data.labId.isNullOrBlank()) { + Timber.tag(TAG).e("PCR certificate is missing valid labId: %s", data) + throw TestCertificateServerException(ErrorCode.DCC_NOT_SUPPORTED_BY_LAB) + } + if (data.publicKeyRegisteredAt != null) { Timber.tag(TAG).d("Public key is already registered for %s", data) return data @@ -57,13 +65,13 @@ class TestCertificateProcessor @Inject constructor( val nowUTC = timeStamper.nowUTC - return when (data.type) { - CoronaTest.Type.PCR -> (data as PCRCertificateData).copy( + return when (data) { + is PCRCertificateData -> data.copy( publicKeyRegisteredAt = nowUTC, rsaPublicKey = rsaKeyPair.publicKey, rsaPrivateKey = rsaKeyPair.privateKey, ) - CoronaTest.Type.RAPID_ANTIGEN -> (data as RACertificateData).copy( + is RACertificateData -> data.copy( publicKeyRegisteredAt = nowUTC, rsaPublicKey = rsaKeyPair.publicKey, rsaPrivateKey = rsaKeyPair.privateKey, @@ -79,10 +87,15 @@ class TestCertificateProcessor @Inject constructor( * The server does not immediately return the test certificate components after registering the public key. */ internal suspend fun obtainCertificate( - data: StoredTestCertificateData - ): StoredTestCertificateData { + data: RetrievedTestCertificate + ): RetrievedTestCertificate { Timber.tag(TAG).d("requestCertificate(cert=%s)", data) + if (data is PCRCertificateData && data.labId.isNullOrBlank()) { + Timber.tag(TAG).e("PCR certificate is missing valid labId: %s", data) + throw TestCertificateServerException(ErrorCode.DCC_NOT_SUPPORTED_BY_LAB) + } + if (data.publicKeyRegisteredAt == null) { throw IllegalStateException("Public key is not registered yet.") } @@ -111,7 +124,7 @@ class TestCertificateProcessor @Inject constructor( val components = try { executeRequest() } catch (e: TestCertificateServerException) { - if (e.errorCode == TestCertificateServerException.ErrorCode.DCC_COMP_202) { + if (e.errorCode == ErrorCode.DCC_COMP_202) { delay(certConfig.waitForRetry.millis) executeRequest() } else { @@ -138,12 +151,12 @@ class TestCertificateProcessor @Inject constructor( val nowUtc = timeStamper.nowUTC - return when (data.type) { - CoronaTest.Type.PCR -> (data as PCRCertificateData).copy( + return when (data) { + is PCRCertificateData -> data.copy( testCertificateQrCode = extractedData.qrCode, certificateReceivedAt = nowUtc, ) - CoronaTest.Type.RAPID_ANTIGEN -> (data as RACertificateData).copy( + is RACertificateData -> data.copy( testCertificateQrCode = extractedData.qrCode, certificateReceivedAt = nowUtc, ) @@ -151,13 +164,13 @@ class TestCertificateProcessor @Inject constructor( } internal suspend fun updateSeenByUser( - data: StoredTestCertificateData, + data: RetrievedTestCertificate, seenByUser: Boolean, - ): StoredTestCertificateData { + ): RetrievedTestCertificate { Timber.tag(TAG).d("updateSeenByUser(data=%s, seenByUser=%b)", data, seenByUser) - return when (data.type) { - CoronaTest.Type.PCR -> (data as PCRCertificateData).copy(certificateSeenByUser = seenByUser) - CoronaTest.Type.RAPID_ANTIGEN -> (data as RACertificateData).copy(certificateSeenByUser = seenByUser) + return when (data) { + is PCRCertificateData -> data.copy(certificateSeenByUser = seenByUser) + is RACertificateData -> data.copy(certificateSeenByUser = seenByUser) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificateRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificateRepository.kt index cfa190d28b98f8aff99cdf49834ad108c9f76993..9b4b4ad40386e7a1546c15e762e3da625d589bee 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificateRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificateRepository.kt @@ -3,15 +3,18 @@ package de.rki.coronawarnapp.covidcertificate.test.core import de.rki.coronawarnapp.bugreporting.reportProblem import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor -import de.rki.coronawarnapp.covidcertificate.common.exception.TestCertificateServerException -import de.rki.coronawarnapp.covidcertificate.common.exception.TestCertificateServerException.ErrorCode.DCC_NOT_SUPPORTED_BY_LAB -import de.rki.coronawarnapp.covidcertificate.test.core.storage.PCRCertificateData -import de.rki.coronawarnapp.covidcertificate.test.core.storage.RACertificateData +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidTestCertificateException +import de.rki.coronawarnapp.covidcertificate.test.core.qrcode.TestCertificateQRCode import de.rki.coronawarnapp.covidcertificate.test.core.storage.TestCertificateContainer import de.rki.coronawarnapp.covidcertificate.test.core.storage.TestCertificateIdentifier -import de.rki.coronawarnapp.covidcertificate.test.core.storage.TestCertificateProcessor import de.rki.coronawarnapp.covidcertificate.test.core.storage.TestCertificateStorage +import de.rki.coronawarnapp.covidcertificate.test.core.storage.types.GenericTestCertificateData +import de.rki.coronawarnapp.covidcertificate.test.core.storage.types.PCRCertificateData +import de.rki.coronawarnapp.covidcertificate.test.core.storage.types.RACertificateData +import de.rki.coronawarnapp.covidcertificate.test.core.storage.types.RetrievedTestCertificate import de.rki.coronawarnapp.covidcertificate.valueset.ValueSetsRepository +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.flow.HotDataFlow @@ -39,6 +42,7 @@ class TestCertificateRepository @Inject constructor( private val storage: TestCertificateStorage, private val qrCodeExtractor: DccQrCodeExtractor, private val processor: TestCertificateProcessor, + private val timeStamper: TimeStamper, valueSetsRepository: ValueSetsRepository, ) { @@ -98,9 +102,15 @@ class TestCertificateRepository @Inject constructor( Timber.tag(TAG).d("requestCertificate(test.identifier=%s)", test.identifier) val newData = internalData.updateBlocking { - if (values.any { it.registrationToken == test.registrationToken }) { + + val matchesExisting = values + .map { it.data } + .filterIsInstance<RetrievedTestCertificate>() + .any { it.registrationToken == test.registrationToken } + + if (matchesExisting) { Timber.tag(TAG).e("Certificate entry already exists for %s", test.identifier) - throw IllegalArgumentException("A certificate was already created for this ${test.identifier}") + throw InvalidTestCertificateException(InvalidHealthCertificateException.ErrorCode.ALREADY_REGISTERED) } if (!test.isDccSupportedByPoc) { throw IllegalArgumentException("DCC is not supported by PoC for this test: ${test.identifier}") @@ -133,7 +143,42 @@ class TestCertificateRepository @Inject constructor( mutate { this[container.identifier] = container } } - return newData.values.single { it.registrationToken == test.registrationToken } + return newData.values.single { + it.data is RetrievedTestCertificate && it.data.registrationToken == test.registrationToken + } + } + + suspend fun registerTestCertificate( + qrCode: TestCertificateQRCode + ): TestCertificateContainer { + Timber.tag(TAG).v("registerTestCertificate(qrCode=%s)", qrCode) + + val updatedData = internalData.updateBlocking { + + if (values.any { it.certificateId == qrCode.uniqueCertificateIdentifier }) { + Timber.tag(TAG).e("Certificate entry already exists for %s", qrCode) + throw InvalidTestCertificateException(InvalidHealthCertificateException.ErrorCode.ALREADY_REGISTERED) + } + + val nowUtc = timeStamper.nowUTC + + val data = GenericTestCertificateData( + identifier = UUID.randomUUID().toString(), + registeredAt = nowUtc, + certificateReceivedAt = nowUtc, + testCertificateQrCode = qrCode.qrCode + ) + val container = TestCertificateContainer( + data = data, + qrCodeExtractor = qrCodeExtractor, + ) + Timber.tag(TAG).d("Adding test certificate entry: %s", container) + mutate { this[container.identifier] = container } + } + + // We just registered it, it MUST be available. + return updatedData.values + .single { it.certificateId == qrCode.uniqueCertificateIdentifier } } /** @@ -156,7 +201,6 @@ class TestCertificateRepository @Inject constructor( * * [refresh] itself will NOT throw an exception. */ - // TODO Will be addressed in 2.5? @Suppress("ComplexMethod") suspend fun refresh(identifier: TestCertificateIdentifier? = null): Set<RefreshResult> { Timber.tag(TAG).d("refresh(identifier=%s)", identifier) @@ -168,6 +212,7 @@ class TestCertificateRepository @Inject constructor( internalData.updateBlocking { val toRefresh = values .filter { it.identifier == identifier || identifier == null } // Targets of our refresh + .filter { it.data is RetrievedTestCertificate } // Can only update retrieved certificates .filter { !it.isUpdatingData && it.isCertificateRetrievalPending } // Those that need refreshing mutate { @@ -178,43 +223,15 @@ class TestCertificateRepository @Inject constructor( } } - // Not sure i really like this - internalData.updateBlocking { - Timber.tag(TAG).d("Checking for invalid lab id.") - - val refreshedCerts = values - .filter { workedOnIds.contains(it.identifier) } // Refresh targets - .filter { it.labId == null && it.data is PCRCertificateData } // Targets of this step - .map { cert -> - Timber.tag(TAG).d("%s is missing a lab id returning exception", cert) - RefreshResult( - cert, - TestCertificateServerException( - DCC_NOT_SUPPORTED_BY_LAB - ) - ) - } - - refreshedCerts.forEach { - refreshCallResults[it.certificateContainer.identifier] = it - } - - mutate { - refreshedCerts - .filter { it.error == null } - .map { it.certificateContainer } - .forEach { this[it.identifier] = it } - } - } - 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 - .filter { it.labId != null || it.data !is PCRCertificateData } - .map { cert -> + .mapNotNull { cert -> + if (cert.data !is RetrievedTestCertificate) return@mapNotNull null + if (cert.data.isPublicKeyRegistered) return@mapNotNull null + withContext(dispatcherProvider.IO) { try { val updatedData = processor.registerPublicKey(cert.data) @@ -243,9 +260,12 @@ class TestCertificateRepository @Inject constructor( val refreshedCerts = values .filter { workedOnIds.contains(it.identifier) } // Refresh targets - .filter { it.isPublicKeyRegistered && it.isCertificateRetrievalPending } // Targets of this step - .filter { it.labId != null || it.data !is PCRCertificateData } - .map { cert -> + .mapNotNull { cert -> + if (cert.data !is RetrievedTestCertificate) return@mapNotNull null + + if (!cert.data.isPublicKeyRegistered) return@mapNotNull null + if (!cert.isCertificateRetrievalPending) return@mapNotNull null + withContext(dispatcherProvider.IO) { try { val updatedData = processor.obtainCertificate(cert.data) @@ -270,12 +290,10 @@ class TestCertificateRepository @Inject constructor( } internalData.updateBlocking { - val certs = values.filter { workedOnIds.contains(it.identifier) } - mutate { - certs.forEach { - this[it.identifier] = it.copy(isUpdatingData = false) - } + values + .filter { workedOnIds.contains(it.identifier) } + .forEach { this[it.identifier] = it.copy(isUpdatingData = false) } } } @@ -314,6 +332,11 @@ class TestCertificateRepository @Inject constructor( return@updateBlocking this } + if (current.data !is RetrievedTestCertificate) { + Timber.tag(TAG).w("%s is not a retrieved certificate, so it was immediately available.", identifier) + return@updateBlocking this + } + val updated = current.copy( data = processor.updateSeenByUser(current.data, true) ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificateWrapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificateWrapper.kt index 0df7c8164ead0b32c0651a94157746965feeeab7..698531b2fbe984f50967f6272f10587e91d175d0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificateWrapper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificateWrapper.kt @@ -3,25 +3,26 @@ package de.rki.coronawarnapp.covidcertificate.test.core import de.rki.coronawarnapp.covidcertificate.test.core.storage.TestCertificateContainer import de.rki.coronawarnapp.covidcertificate.test.core.storage.TestCertificateIdentifier import de.rki.coronawarnapp.covidcertificate.valueset.valuesets.TestCertificateValueSets +import org.joda.time.Instant data class TestCertificateWrapper( private val valueSets: TestCertificateValueSets, private val container: TestCertificateContainer ) { - val identifier: TestCertificateIdentifier = container.identifier + val identifier: TestCertificateIdentifier get() = container.identifier - val isCertificateRetrievalPending = container.isCertificateRetrievalPending + val isCertificateRetrievalPending: Boolean get() = container.isCertificateRetrievalPending - val isUpdatingData = container.isUpdatingData + val isUpdatingData: Boolean get() = container.isUpdatingData - val registeredAt = container.registeredAt + val registeredAt: Instant get() = container.registeredAt - val seenByUser = container.certificateSeenByUser + val seenByUser: Boolean get() = container.certificateSeenByUser + + val registrationToken: String? get() = container.registrationToken val testCertificate: TestCertificate? by lazy { container.toTestCertificate(valueSets) } - - val registrationToken = container.registrationToken } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/StoredTestCertificateData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/StoredTestCertificateData.kt deleted file mode 100644 index 9ce2281d176f35a686386dcea9203446f856edea..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/StoredTestCertificateData.kt +++ /dev/null @@ -1,23 +0,0 @@ -package de.rki.coronawarnapp.covidcertificate.test.core.storage - -import de.rki.coronawarnapp.coronatest.type.CoronaTest -import de.rki.coronawarnapp.coronatest.type.RegistrationToken -import de.rki.coronawarnapp.util.encryption.rsa.RSAKey -import okio.ByteString -import org.joda.time.Instant - -interface StoredTestCertificateData { - val identifier: TestCertificateIdentifier - val registrationToken: RegistrationToken - val type: CoronaTest.Type - val registeredAt: Instant - val publicKeyRegisteredAt: Instant? - val rsaPublicKey: RSAKey.Public? - val rsaPrivateKey: RSAKey.Private? - val certificateReceivedAt: Instant? - val encryptedDataEncryptionkey: ByteString? - val encryptedDccCose: ByteString? - val testCertificateQrCode: String? - val labId: String? - val certificateSeenByUser: Boolean -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateContainer.kt index 3afa51c3c70cc98bc6a327a32c2dd0ac067469e4..ea176ee6de746207ded7ba152a4ee31a3dc26a98 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateContainer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateContainer.kt @@ -6,16 +6,19 @@ import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser import de.rki.coronawarnapp.covidcertificate.common.qrcode.QrCodeString import de.rki.coronawarnapp.covidcertificate.test.core.TestCertificate import de.rki.coronawarnapp.covidcertificate.test.core.qrcode.TestCertificateQRCode +import de.rki.coronawarnapp.covidcertificate.test.core.storage.types.BaseTestCertificateData +import de.rki.coronawarnapp.covidcertificate.test.core.storage.types.GenericTestCertificateData +import de.rki.coronawarnapp.covidcertificate.test.core.storage.types.RetrievedTestCertificate import de.rki.coronawarnapp.covidcertificate.valueset.valuesets.TestCertificateValueSets import org.joda.time.Instant import org.joda.time.LocalDate import java.util.Locale data class TestCertificateContainer( - internal val data: StoredTestCertificateData, - private val qrCodeExtractor: DccQrCodeExtractor, + internal val data: BaseTestCertificateData, + internal val qrCodeExtractor: DccQrCodeExtractor, val isUpdatingData: Boolean = false, -) : StoredTestCertificateData by data { +) { @delegate:Transient private val testCertificateQRCode: TestCertificateQRCode by lazy { @@ -27,8 +30,23 @@ data class TestCertificateContainer( } } - val isPublicKeyRegistered: Boolean - get() = data.publicKeyRegisteredAt != null + val registrationToken: String? + get() = when (data) { + is RetrievedTestCertificate -> data.registrationToken + is GenericTestCertificateData -> null // Has none + } + + val certificateSeenByUser: Boolean + get() = when (data) { + is RetrievedTestCertificate -> data.certificateSeenByUser + is GenericTestCertificateData -> true // Immediately available + } + + val identifier: TestCertificateIdentifier + get() = data.identifier + + val registeredAt: Instant + get() = data.registeredAt val isCertificateRetrievalPending: Boolean get() = data.certificateReceivedAt == null @@ -40,7 +58,7 @@ data class TestCertificateContainer( } fun toTestCertificate( - valueSet: TestCertificateValueSets?, + valueSet: TestCertificateValueSets? = null, userLocale: Locale = Locale.getDefault(), ): TestCertificate? { if (isCertificateRetrievalPending) return null @@ -80,15 +98,6 @@ data class TestCertificateContainer( override val testCenter: String? get() = testCertificate.testCenter - override val isUpdatingData: Boolean - get() = this@TestCertificateContainer.isUpdatingData - - override val registeredAt: Instant - get() = this@TestCertificateContainer.registeredAt - - override val isCertificateRetrievalPending: Boolean - get() = this@TestCertificateContainer.isCertificateRetrievalPending - override val certificateIssuer: String get() = header.issuer override val certificateCountry: String @@ -106,6 +115,15 @@ data class TestCertificateContainer( override val qrCode: QrCodeString get() = data.testCertificateQrCode!! + + override val isUpdatingData: Boolean + get() = this@TestCertificateContainer.isUpdatingData + + override val registeredAt: Instant + get() = data.registeredAt + + override val isCertificateRetrievalPending: Boolean + get() = this@TestCertificateContainer.isCertificateRetrievalPending } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateStorage.kt index 087486024444e89c955a9d87d4ba1b3ed5123e0c..01ddef7c27d8398d8e2b2958934b492a3882b1e2 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateStorage.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateStorage.kt @@ -1,12 +1,15 @@ package de.rki.coronawarnapp.covidcertificate.test.core.storage import android.content.Context +import android.content.SharedPreferences 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.covidcertificate.vaccination.core.repository.storage.ContainerPostProcessor +import de.rki.coronawarnapp.covidcertificate.test.core.storage.types.BaseTestCertificateData +import de.rki.coronawarnapp.covidcertificate.test.core.storage.types.GenericTestCertificateData +import de.rki.coronawarnapp.covidcertificate.test.core.storage.types.PCRCertificateData +import de.rki.coronawarnapp.covidcertificate.test.core.storage.types.RACertificateData import de.rki.coronawarnapp.util.di.AppContext import de.rki.coronawarnapp.util.serialization.BaseGson import timber.log.Timber @@ -17,7 +20,6 @@ import javax.inject.Singleton class TestCertificateStorage @Inject constructor( @AppContext val context: Context, @BaseGson val baseGson: Gson, - private val containerPostProcessor: ContainerPostProcessor, ) { private val prefs by lazy { @@ -27,73 +29,73 @@ class TestCertificateStorage @Inject constructor( 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<PCRCertificateData>>() {}.type + private val typeTokenPCR: TypeToken<Set<PCRCertificateData>> by lazy { + object : TypeToken<Set<PCRCertificateData>>() {} } - - private val typeTokenRA by lazy { - object : TypeToken<Set<RACertificateData>>() {}.type + private val typeTokenRA: TypeToken<Set<RACertificateData>> by lazy { + object : TypeToken<Set<RACertificateData>>() {} + } + private val typeTokenGeneric: TypeToken<Set<GenericTestCertificateData>> by lazy { + object : TypeToken<Set<GenericTestCertificateData>>() {} } - var testCertificates: Collection<StoredTestCertificateData> + var testCertificates: Collection<BaseTestCertificateData> get() { Timber.tag(TAG).d("load()") - val pcrCertContainers: Set<PCRCertificateData> = run { - val raw = prefs.getString(PKEY_DATA_PCR, null) ?: return@run emptySet() - gson.fromJson<Set<PCRCertificateData>>(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<RACertificateData> = run { - val raw = prefs.getString(PKEY_DATA_RA, null) ?: return@run emptySet() - gson.fromJson<Set<RACertificateData>>(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." } - } - } + val pcrCertContainers: Set<PCRCertificateData> = prefs.loadCerts(typeTokenPCR, PKEY_DATA_PCR) + val raCerts: Set<RACertificateData> = prefs.loadCerts(typeTokenRA, PKEY_DATA_RA) + val scannedCerts: Set<GenericTestCertificateData> = prefs.loadCerts(typeTokenGeneric, PKEY_DATA_SCANNED) - return (pcrCertContainers + raCerts).also { + return (pcrCertContainers + raCerts + scannedCerts).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) - } - } + + storeCerts(value.filterIsInstance<PCRCertificateData>(), typeTokenPCR, PKEY_DATA_PCR) + storeCerts(value.filterIsInstance<RACertificateData>(), typeTokenRA, PKEY_DATA_RA) + storeCerts(value.filterIsInstance<GenericTestCertificateData>(), typeTokenGeneric, PKEY_DATA_SCANNED) } } + private fun <T : BaseTestCertificateData> SharedPreferences.Editor.storeCerts( + certs: Collection<BaseTestCertificateData>, + typeToken: TypeToken<Set<T>>, + storageKey: String + ) { + val type = typeToken.type + if (certs.isNotEmpty()) { + val raw = gson.toJson(certs, type) + Timber.tag(TAG).v("Storing scanned certs ($type): %s", raw) + putString(storageKey, raw) + } else { + Timber.tag(TAG).v("No stored certificates ($type) available, clearing.") + remove(storageKey) + } + } + + private fun <T : BaseTestCertificateData> SharedPreferences.loadCerts( + typeToken: TypeToken<Set<T>>, + storageKey: String + ): Set<T> { + val type = typeToken.type + val raw = prefs.getString(storageKey, null) ?: return emptySet() + return gson.fromJson<Set<T>>(raw, type).onEach { + Timber.tag(TAG).v("Certificates ($type) loaded: %s", it) + requireNotNull(it.identifier) + } + } + 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" + private const val PKEY_DATA_SCANNED = "testcertificate.data.scanned" } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/types/BaseTestCertificateData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/types/BaseTestCertificateData.kt new file mode 100644 index 0000000000000000000000000000000000000000..9c43db6d2f8c465ca660f43b0f07f24ae0550efa --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/types/BaseTestCertificateData.kt @@ -0,0 +1,14 @@ +package de.rki.coronawarnapp.covidcertificate.test.core.storage.types + +import de.rki.coronawarnapp.covidcertificate.test.core.storage.TestCertificateIdentifier +import org.joda.time.Instant + +/** + * Common data for test certificates, idepdent of whether they were retrieved or scanned. + */ +sealed class BaseTestCertificateData { + abstract val identifier: TestCertificateIdentifier + abstract val registeredAt: Instant + abstract val certificateReceivedAt: Instant? + abstract val testCertificateQrCode: String? +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/types/GenericTestCertificateData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/types/GenericTestCertificateData.kt new file mode 100644 index 0000000000000000000000000000000000000000..c441d4a1471a2c5aa1fc607e41810a17b069df40 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/types/GenericTestCertificateData.kt @@ -0,0 +1,30 @@ +package de.rki.coronawarnapp.covidcertificate.test.core.storage.types + +import com.google.gson.annotations.SerializedName +import org.joda.time.Instant + +/** + * A generic data class used to store data for scanned qrcodes. + * May be cloned if we need to support different types of test certificates in the future. + */ +data class GenericTestCertificateData( + @SerializedName("identifier") + override val identifier: String, + + @SerializedName("registeredAt") + override val registeredAt: Instant, + + @SerializedName("certificateReceivedAt") + override val certificateReceivedAt: Instant? = null, + + @SerializedName("testCertificateQrCode") + override val testCertificateQrCode: String? = null, +) : ScannedTestCertificate() { + + // Otherwise GSON unsafes reflection to create this class, and sets the LAZY to null + @Suppress("unused") + constructor() : this( + identifier = "", + registeredAt = Instant.EPOCH + ) +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/PCRCertificateData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/types/PCRCertificateData.kt similarity index 87% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/PCRCertificateData.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/types/PCRCertificateData.kt index d53a1d54b8bd82a026349e87f191443e7564fb4f..a3e3d056ba50ce9f3da43148eb13e33ea6d729c0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/PCRCertificateData.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/types/PCRCertificateData.kt @@ -1,7 +1,6 @@ -package de.rki.coronawarnapp.covidcertificate.test.core.storage +package de.rki.coronawarnapp.covidcertificate.test.core.storage.types import com.google.gson.annotations.SerializedName -import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.coronatest.type.RegistrationToken import de.rki.coronawarnapp.util.encryption.rsa.RSAKey import okio.ByteString @@ -43,7 +42,7 @@ data class PCRCertificateData internal constructor( @SerializedName("certificateSeenByUser") override val certificateSeenByUser: Boolean = false, -) : StoredTestCertificateData { +) : RetrievedTestCertificate() { // Otherwise GSON unsafes reflection to create this class, and sets the LAZY to null @Suppress("unused") @@ -52,7 +51,4 @@ data class PCRCertificateData internal constructor( 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/covidcertificate/test/core/storage/RACertificateData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/types/RACertificateData.kt similarity index 87% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/RACertificateData.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/types/RACertificateData.kt index fe10fabb6604501b72a8f73f6482095e834db3dc..051ae776d646fef2dd58a64846b7e21ef505637b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/RACertificateData.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/types/RACertificateData.kt @@ -1,7 +1,6 @@ -package de.rki.coronawarnapp.covidcertificate.test.core.storage +package de.rki.coronawarnapp.covidcertificate.test.core.storage.types import com.google.gson.annotations.SerializedName -import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.coronatest.type.RegistrationToken import de.rki.coronawarnapp.util.encryption.rsa.RSAKey import okio.ByteString @@ -43,7 +42,7 @@ data class RACertificateData( @SerializedName("certificateSeenByUser") override val certificateSeenByUser: Boolean = false, -) : StoredTestCertificateData { +) : RetrievedTestCertificate() { // Otherwise GSON unsafes reflection to create this class, and sets the LAZY to null @Suppress("unused") @@ -52,7 +51,4 @@ data class RACertificateData( 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/covidcertificate/test/core/storage/types/RetrievedTestCertificate.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/types/RetrievedTestCertificate.kt new file mode 100644 index 0000000000000000000000000000000000000000..614e452e3b7441353ebf158e02c3cf1216f39212 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/types/RetrievedTestCertificate.kt @@ -0,0 +1,28 @@ +package de.rki.coronawarnapp.covidcertificate.test.core.storage.types + +import de.rki.coronawarnapp.coronatest.type.RegistrationToken +import de.rki.coronawarnapp.util.encryption.rsa.RSAKey +import okio.ByteString +import org.joda.time.Instant + +/** + * A test certificate that is, or will be, retrieved by the CWA. + * Either a [RACertificateData] or [PCRCertificateData] + */ +sealed class RetrievedTestCertificate : BaseTestCertificateData() { + + abstract val registrationToken: RegistrationToken + + abstract val publicKeyRegisteredAt: Instant? + abstract val rsaPublicKey: RSAKey.Public? + abstract val rsaPrivateKey: RSAKey.Private? + + abstract val encryptedDataEncryptionkey: ByteString? + abstract val encryptedDccCose: ByteString? + + abstract val labId: String? + abstract val certificateSeenByUser: Boolean + + val isPublicKeyRegistered: Boolean + get() = publicKeyRegisteredAt != null +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/types/ScannedTestCertificate.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/types/ScannedTestCertificate.kt new file mode 100644 index 0000000000000000000000000000000000000000..49a28c8cc3cd76f0495099834907f81236caa0e4 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/types/ScannedTestCertificate.kt @@ -0,0 +1,6 @@ +package de.rki.coronawarnapp.covidcertificate.test.core.storage.types + +/** + * A scanned test certificate, e.g. from a qr code. + */ +sealed class ScannedTestCertificate : BaseTestCertificateData() 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 index 79a1f43020fdb4839313f36afdc00e775afe2925..edea3c746e741a86d7be55b50b3f5aa58e33cdda 100644 --- 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 @@ -3,7 +3,8 @@ package de.rki.coronawarnapp.coronatest import dagger.Component import dagger.Module import de.rki.coronawarnapp.coronatest.type.TestCertificateContainerTest -import de.rki.coronawarnapp.covidcertificate.test.storage.TestCertificateStorageTest +import de.rki.coronawarnapp.covidcertificate.test.core.TestCertificateRepositoryTest +import de.rki.coronawarnapp.covidcertificate.test.core.storage.TestCertificateStorageTest import de.rki.coronawarnapp.util.serialization.SerializationModule import javax.inject.Singleton @@ -18,6 +19,7 @@ interface CoronaTestTestComponent { fun inject(testClass: TestCertificateStorageTest) fun inject(testClass: TestCertificateContainerTest) + fun inject(testClass: TestCertificateRepositoryTest) @Component.Factory interface Factory { 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 index 18fd7a8ed99aab9ea3405f50ac03730c32754dd5..6ebeb375d6b2d018cdcd7dbbb404db77ae6d9cac 100644 --- 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 @@ -23,7 +23,6 @@ class TestCertificateContainerTest : BaseTest() { @Test fun `ui facing test certificate creation and fallbacks`() { certificateTestData.personATest2CertContainer.apply { - isPublicKeyRegistered shouldBe true isCertificateRetrievalPending shouldBe false certificateId shouldBe "URN:UVCI:V1:DE:7WR8CE12Y8O2AN4NK320TPNKB1" data.testCertificateQrCode shouldBe certificateTestData.personATest2CertQRCodeString @@ -35,7 +34,6 @@ class TestCertificateContainerTest : BaseTest() { @Test fun `pending check and nullability`() { certificateTestData.personATest3CertNokeyContainer.apply { - isPublicKeyRegistered shouldBe false isCertificateRetrievalPending shouldBe true certificateId shouldBe null data.testCertificateQrCode shouldBe null @@ -44,7 +42,6 @@ class TestCertificateContainerTest : BaseTest() { } certificateTestData.personATest4CertPendingContainer.apply { - isPublicKeyRegistered shouldBe true isCertificateRetrievalPending shouldBe true certificateId shouldBe null data.testCertificateQrCode shouldBe null diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateRepositoryTest.kt deleted file mode 100644 index c170f486908b41df490f43cfc5071b1848f5acf2..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateRepositoryTest.kt +++ /dev/null @@ -1,79 +0,0 @@ -package de.rki.coronawarnapp.covidcertificate.test - -import de.rki.coronawarnapp.appconfig.CovidCertificateConfig -import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor -import de.rki.coronawarnapp.covidcertificate.common.qrcode.DccQrCode -import de.rki.coronawarnapp.covidcertificate.test.core.TestCertificateRepository -import de.rki.coronawarnapp.covidcertificate.test.core.storage.PCRCertificateData -import de.rki.coronawarnapp.covidcertificate.test.core.storage.StoredTestCertificateData -import de.rki.coronawarnapp.covidcertificate.test.core.storage.TestCertificateProcessor -import de.rki.coronawarnapp.covidcertificate.test.core.storage.TestCertificateStorage -import de.rki.coronawarnapp.covidcertificate.valueset.ValueSetsRepository -import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.mockk -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.emptyFlow -import org.joda.time.Duration -import org.joda.time.Instant -import org.junit.jupiter.api.BeforeEach -import testhelpers.BaseTest -import testhelpers.TestDispatcherProvider - -class TestCertificateRepositoryTest : BaseTest() { - - @MockK lateinit var storage: TestCertificateStorage - @MockK lateinit var qrCodeExtractor: DccQrCodeExtractor - @MockK lateinit var covidTestCertificateConfig: CovidCertificateConfig.TestCertificate - @MockK lateinit var valueSetsRepository: ValueSetsRepository - @MockK lateinit var testCertificateProcessor: TestCertificateProcessor - - private val testCertificateNew = PCRCertificateData( - identifier = "identifier1", - registrationToken = "regtoken1", - registeredAt = Instant.EPOCH, - ) - - private val testCertificateWithPubKey = testCertificateNew.copy( - publicKeyRegisteredAt = Instant.EPOCH, - rsaPublicKey = mockk(), - rsaPrivateKey = mockk(), - ) - - private var storageSet = mutableSetOf<StoredTestCertificateData>() - - @BeforeEach - fun setup() { - MockKAnnotations.init(this) - - 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 } - } - - coEvery { qrCodeExtractor.extract(any()) } returns mockk<DccQrCode>().apply { - every { qrCode } returns "qrCode" - every { data } returns mockk() - } - every { valueSetsRepository.latestTestCertificateValueSets } returns emptyFlow() - } - - private fun createInstance(scope: CoroutineScope) = TestCertificateRepository( - appScope = scope, - dispatcherProvider = TestDispatcherProvider(), - storage = storage, - qrCodeExtractor = qrCodeExtractor, - valueSetsRepository = valueSetsRepository, - processor = testCertificateProcessor, - ) -} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateTestData.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateTestData.kt index ae152c4d202eedd243eb3a5e4974d22bc76147da..c6e1265c938e89c6140f74301393e3c54c708243 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateTestData.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateTestData.kt @@ -1,9 +1,11 @@ package de.rki.coronawarnapp.covidcertificate.test import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor -import de.rki.coronawarnapp.covidcertificate.test.core.storage.PCRCertificateData -import de.rki.coronawarnapp.covidcertificate.test.core.storage.RACertificateData +import de.rki.coronawarnapp.covidcertificate.test.core.qrcode.TestCertificateQRCode import de.rki.coronawarnapp.covidcertificate.test.core.storage.TestCertificateContainer +import de.rki.coronawarnapp.covidcertificate.test.core.storage.types.GenericTestCertificateData +import de.rki.coronawarnapp.covidcertificate.test.core.storage.types.PCRCertificateData +import de.rki.coronawarnapp.covidcertificate.test.core.storage.types.RACertificateData import de.rki.coronawarnapp.util.encryption.rsa.RSAKey import okio.ByteString.Companion.decodeBase64 import org.joda.time.Instant @@ -17,6 +19,8 @@ class TestCertificateTestData @Inject constructor( val personATest1CertQRCodeString = "HC1:6BFQ$9FY7$\$Q00019C-5HQ57WDQTLUMS256TUMP49JLZCX/N4VB14NRINX6J7+KF\$UB.N83CMMPVNPAHJ035FT88%AJ1R*UOGR3ZWLC24+F7DO276HB.O*BEVJODVSKNOP0T4\$FZ.R/03N6QK3A05EQVBONNHSJ9WJT+B15H NBEZ3VI8-V77AFL4NY9V*JM *KOPC2H8PR5OG9:VIG$969FJRV28N9PORZJQ43EOIHIJ+83XDDRB201N.L58EB7 8GS8. 9GPNGHFV/6\$I3R3R4930TC/ZGZESM929.D59GO13E\$H7LMUGS18ABF6YD955C C6VPNMZU+3FJR1RVLBZDR2F*8A4MKWIH+/JPU1*H6E2CTHGYHPPU6U*FYFJ6RJO*I8P1T59S:V3V1SA86L8Y8A5XB*10112G8GHK77CBOK9QEI960TTNC.I3A4P8BM6DO:MI/98PC/ZP2:JW3JB.J:R2.5GX0J$1J.OOCYSS8A*DS\$AFVJR9RT-L6N%PS%CV%B8KL0%EL SXLBSA6 %M6JEN+E0Q8A:RAGW16KGUA627-Y2:EDH\$VFVT:29FWUTJSANSYCV4PSKNP\$ZV-8T27SV4CC3CJIV20TKBS22O$/MT/V*0Q0IUE0DBPT+UP03IR1CRFOP3" + val personATest1CertQRCode = qrCodeExtractor.extract(personATest1CertQRCodeString) as TestCertificateQRCode + val personATest1StoredData = 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()!! @@ -66,6 +70,12 @@ class TestCertificateTestData @Inject constructor( ) } + val personATest2CertScannedStoredData = GenericTestCertificateData( + identifier = "identifier2", + registeredAt = Instant.ofEpochMilli(12345), + testCertificateQrCode = personATest2CertQRCodeString + ) + val personATest2CertContainer = TestCertificateContainer( data = personATest2CertStoredData, qrCodeExtractor = qrCodeExtractor, diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/execution/TestCertificateProcessorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificateProcessorTest.kt similarity index 61% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/execution/TestCertificateProcessorTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificateProcessorTest.kt index 83d374bb7d6b89d32d0fe3e7b5770e2ee41bb735..dc8e11f65a00b4d4c2a3135a2ddd25937d293dc6 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/execution/TestCertificateProcessorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificateProcessorTest.kt @@ -1,18 +1,21 @@ -package de.rki.coronawarnapp.covidcertificate.test.execution +package de.rki.coronawarnapp.covidcertificate.test.core import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.appconfig.ConfigData import de.rki.coronawarnapp.appconfig.CovidCertificateConfig import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor +import de.rki.coronawarnapp.covidcertificate.common.exception.TestCertificateServerException import de.rki.coronawarnapp.covidcertificate.test.core.qrcode.TestCertificateQRCode import de.rki.coronawarnapp.covidcertificate.test.core.server.TestCertificateComponents import de.rki.coronawarnapp.covidcertificate.test.core.server.TestCertificateServer -import de.rki.coronawarnapp.covidcertificate.test.core.storage.PCRCertificateData -import de.rki.coronawarnapp.covidcertificate.test.core.storage.StoredTestCertificateData -import de.rki.coronawarnapp.covidcertificate.test.core.storage.TestCertificateProcessor +import de.rki.coronawarnapp.covidcertificate.test.core.storage.types.PCRCertificateData +import de.rki.coronawarnapp.covidcertificate.test.core.storage.types.RACertificateData import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.encryption.rsa.RSACryptography import de.rki.coronawarnapp.util.encryption.rsa.RSAKeyPairGenerator +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.Called import io.mockk.MockKAnnotations import io.mockk.Runs import io.mockk.coEvery @@ -40,13 +43,27 @@ class TestCertificateProcessorTest : BaseTest() { @MockK lateinit var appConfigData: ConfigData @MockK lateinit var covidTestCertificateConfig: CovidCertificateConfig.TestCertificate - private val testCertificateNew = PCRCertificateData( + private val pcrCertificateData = PCRCertificateData( identifier = "identifier1", registrationToken = "regtoken1", registeredAt = Instant.EPOCH, + labId = "labId" ) - private val testCertificateWithPubKey = testCertificateNew.copy( + private val pcrCertificateDataWithPubKey = pcrCertificateData.copy( + publicKeyRegisteredAt = Instant.EPOCH, + rsaPublicKey = mockk(), + rsaPrivateKey = mockk(), + ) + + private val raCertificateData = RACertificateData( + identifier = "identifier2", + registrationToken = "regtoken2", + registeredAt = Instant.EPOCH, + labId = null + ) + + private val raCertificateDataWithPubKey = raCertificateData.copy( publicKeyRegisteredAt = Instant.EPOCH, rsaPublicKey = mockk(), rsaPrivateKey = mockk(), @@ -57,8 +74,6 @@ class TestCertificateProcessorTest : BaseTest() { every { encryptedCoseTestCertificateBase64 } returns "" } - private var storageSet = mutableSetOf<StoredTestCertificateData>() - @BeforeEach fun setup() { MockKAnnotations.init(this) @@ -105,21 +120,54 @@ class TestCertificateProcessorTest : BaseTest() { @Test fun `public key registration`() = runBlockingTest2(ignoreActive = true) { val instance = createInstance() - instance.registerPublicKey(testCertificateNew) + instance.registerPublicKey(pcrCertificateData) coVerify { - certificateServer.registerPublicKeyForTest(testCertificateNew.registrationToken, any()) + certificateServer.registerPublicKeyForTest(pcrCertificateData.registrationToken, any()) + } + } + + @Test + fun `public key registration - requires valid labId only if PCR`() = runBlockingTest2(ignoreActive = true) { + val instance = createInstance() + shouldThrow<TestCertificateServerException> { + instance.registerPublicKey(pcrCertificateData.copy(labId = null)) + }.errorCode shouldBe TestCertificateServerException.ErrorCode.DCC_NOT_SUPPORTED_BY_LAB + + coVerify { certificateServer wasNot Called } + + instance.registerPublicKey(raCertificateData) + + coVerify(exactly = 1) { + certificateServer.registerPublicKeyForTest(any(), any()) } } @Test fun `obtain certificate components`() = runBlockingTest2(ignoreActive = true) { val instance = createInstance() - instance.obtainCertificate(testCertificateWithPubKey) + instance.obtainCertificate(pcrCertificateDataWithPubKey) coVerify { covidTestCertificateConfig.waitAfterPublicKeyRegistration - certificateServer.requestCertificateForTest(testCertificateNew.registrationToken) + certificateServer.requestCertificateForTest(pcrCertificateData.registrationToken) + } + } + + @Test + fun `obtain certificate components - requires valid labId only if PCR`() = runBlockingTest2(ignoreActive = true) { + val instance = createInstance() + + shouldThrow<TestCertificateServerException> { + instance.obtainCertificate(pcrCertificateDataWithPubKey.copy(labId = null)) + }.errorCode shouldBe TestCertificateServerException.ErrorCode.DCC_NOT_SUPPORTED_BY_LAB + + coVerify { certificateServer wasNot Called } + + instance.obtainCertificate(raCertificateDataWithPubKey) + + coVerify(exactly = 1) { + certificateServer.requestCertificateForTest(any()) } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificateRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificateRepositoryTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..e353f62e5b08e48ce0f7ac0aa6ceb19818466573 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificateRepositoryTest.kt @@ -0,0 +1,165 @@ +package de.rki.coronawarnapp.covidcertificate.test.core + +import de.rki.coronawarnapp.appconfig.CovidCertificateConfig +import de.rki.coronawarnapp.coronatest.DaggerCoronaTestTestComponent +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidTestCertificateException +import de.rki.coronawarnapp.covidcertificate.test.TestCertificateTestData +import de.rki.coronawarnapp.covidcertificate.test.core.storage.TestCertificateStorage +import de.rki.coronawarnapp.covidcertificate.test.core.storage.types.BaseTestCertificateData +import de.rki.coronawarnapp.covidcertificate.test.core.storage.types.GenericTestCertificateData +import de.rki.coronawarnapp.covidcertificate.test.core.storage.types.PCRCertificateData +import de.rki.coronawarnapp.covidcertificate.valueset.ValueSetsRepository +import de.rki.coronawarnapp.util.TimeStamper +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.emptyFlow +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 +import javax.inject.Inject + +class TestCertificateRepositoryTest : BaseTest() { + + @MockK lateinit var storage: TestCertificateStorage + @MockK lateinit var qrCodeExtractor: DccQrCodeExtractor + @MockK lateinit var covidTestCertificateConfig: CovidCertificateConfig.TestCertificate + @MockK lateinit var valueSetsRepository: ValueSetsRepository + @MockK lateinit var testCertificateProcessor: TestCertificateProcessor + @MockK lateinit var timeStamper: TimeStamper + + @Inject lateinit var testData: TestCertificateTestData + + private val testCertificateNew = PCRCertificateData( + identifier = "identifier1", + registrationToken = "regtoken1", + registeredAt = Instant.EPOCH, + ) + + private val testCertificateWithPubKey = testCertificateNew.copy( + publicKeyRegisteredAt = Instant.EPOCH, + rsaPublicKey = mockk(), + rsaPrivateKey = mockk(), + ) + + private var storageSet = mutableSetOf<BaseTestCertificateData>() + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + DaggerCoronaTestTestComponent.factory().create().inject(this) + + 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 } + } + + qrCodeExtractor.apply { + coEvery { extract(any(), any()) } returns testData.personATest1CertQRCode + coEvery { extract(testData.personATest1CertQRCodeString) } returns testData.personATest1CertQRCode + } + + every { valueSetsRepository.latestTestCertificateValueSets } returns emptyFlow() + + every { timeStamper.nowUTC } returns Instant.ofEpochSecond(12345678) + } + + private fun createInstance(scope: CoroutineScope) = TestCertificateRepository( + appScope = scope, + dispatcherProvider = TestDispatcherProvider(), + storage = storage, + qrCodeExtractor = qrCodeExtractor, + valueSetsRepository = valueSetsRepository, + timeStamper = timeStamper, + processor = testCertificateProcessor, + ) + + @Test + fun `register via corona test`() = runBlockingTest2(ignoreActive = true) { + val instance = createInstance(scope = this) + + instance.requestCertificate( + test = mockk<CoronaTest>().apply { + every { identifier } returns "test-identifier" + every { isDccSupportedByPoc } returns true + every { isDccConsentGiven } returns true + every { type } returns CoronaTest.Type.PCR + every { registeredAt } returns Instant.ofEpochSecond(4555) + every { registrationToken } returns "token" + every { labId } returns "best-lab" + } + ).apply { + this.qrCodeExtractor shouldBe qrCodeExtractor + + certificateId shouldBe null + data.testCertificateQrCode shouldBe null + + isCertificateRetrievalPending shouldBe true + isUpdatingData shouldBe false + } + + storageSet.single().apply { + this as PCRCertificateData + + testCertificateQrCode shouldBe null + + identifier.isNotEmpty() shouldBe true + + registeredAt shouldBe Instant.ofEpochSecond(4555) + certificateReceivedAt shouldBe null + registrationToken shouldBe "token" + } + } + + @Test + fun `register via qrcode`() = runBlockingTest2(ignoreActive = true) { + val instance = createInstance(scope = this) + + instance.registerTestCertificate( + qrCode = testData.personATest1CertQRCode + ).apply { + this.qrCodeExtractor shouldBe qrCodeExtractor + + data.testCertificateQrCode shouldBe testData.personATest1CertQRCodeString + certificateId shouldBe testData.personATest1CertQRCode.uniqueCertificateIdentifier + + isCertificateRetrievalPending shouldBe false + isUpdatingData shouldBe false + } + + storageSet.single().apply { + this as GenericTestCertificateData + testCertificateQrCode shouldBe testData.personATest1CertQRCodeString + identifier.isNotEmpty() shouldBe true + registeredAt shouldBe timeStamper.nowUTC + certificateReceivedAt shouldBe timeStamper.nowUTC + } + + shouldThrow<InvalidTestCertificateException> { + instance.registerTestCertificate( + qrCode = testData.personATest1CertQRCode + ) + }.errorCode shouldBe ErrorCode.ALREADY_REGISTERED + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/execution/TestCertificateRetrievalSchedulerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/execution/TestCertificateRetrievalSchedulerTest.kt similarity index 97% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/execution/TestCertificateRetrievalSchedulerTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/execution/TestCertificateRetrievalSchedulerTest.kt index 68a4a2c129670109c315f44f7b05f3cb00608fc9..1f64039d68f6aa23f7c670347ad139da173350e0 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/execution/TestCertificateRetrievalSchedulerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/execution/TestCertificateRetrievalSchedulerTest.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.covidcertificate.test.execution +package de.rki.coronawarnapp.covidcertificate.test.core.execution import androidx.work.OneTimeWorkRequest import androidx.work.WorkInfo @@ -7,7 +7,6 @@ import de.rki.coronawarnapp.coronatest.CoronaTestRepository import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.covidcertificate.test.core.TestCertificateRepository import de.rki.coronawarnapp.covidcertificate.test.core.TestCertificateWrapper -import de.rki.coronawarnapp.covidcertificate.test.core.execution.TestCertificateRetrievalScheduler import de.rki.coronawarnapp.util.device.ForegroundState import io.mockk.MockKAnnotations import io.mockk.Runs diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/execution/TestCertificateRetrievalWorkerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/execution/TestCertificateRetrievalWorkerTest.kt similarity index 93% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/execution/TestCertificateRetrievalWorkerTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/execution/TestCertificateRetrievalWorkerTest.kt index 951f57f800909902c26c814688f6068773ebb106..90ca54289489ae2144489478083c772435e67a2a 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/execution/TestCertificateRetrievalWorkerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/execution/TestCertificateRetrievalWorkerTest.kt @@ -1,11 +1,10 @@ -package de.rki.coronawarnapp.covidcertificate.test.execution +package de.rki.coronawarnapp.covidcertificate.test.core.execution import android.content.Context import androidx.work.ListenableWorker import androidx.work.WorkRequest import androidx.work.WorkerParameters import de.rki.coronawarnapp.covidcertificate.test.core.TestCertificateRepository -import de.rki.coronawarnapp.covidcertificate.test.core.execution.TestCertificateRetrievalWorker import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.coEvery diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/storage/PCRTestCertificateTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/PCRTestCertificateTest.kt similarity index 52% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/storage/PCRTestCertificateTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/PCRTestCertificateTest.kt index 317137016813ca25fd6d83b32efd42f416b53c29..37ce8f4016a605f31a982782f8731252aacfd0d4 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/storage/PCRTestCertificateTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/PCRTestCertificateTest.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.covidcertificate.test.storage +package de.rki.coronawarnapp.covidcertificate.test.core.storage import testhelpers.BaseTest diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/storage/RATestCertificateTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/RATestCertificateTest.kt similarity index 52% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/storage/RATestCertificateTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/RATestCertificateTest.kt index 31fa7193a03a8ff9acb0772fd9c3862220491d1e..75ab9381609c6a7866398e51e4034beb3723dd36 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/storage/RATestCertificateTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/RATestCertificateTest.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.covidcertificate.test.storage +package de.rki.coronawarnapp.covidcertificate.test.core.storage import testhelpers.BaseTest diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/storage/TestCertificateStorageTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateStorageTest.kt similarity index 92% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/storage/TestCertificateStorageTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateStorageTest.kt index 983f9549a849c04cde7d0ee2b517b9557f1a0070..93c4dbc01f5c89743ec0ac528e387fda0f0d4d5a 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/storage/TestCertificateStorageTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateStorageTest.kt @@ -1,10 +1,9 @@ -package de.rki.coronawarnapp.covidcertificate.test.storage +package de.rki.coronawarnapp.covidcertificate.test.core.storage import android.content.Context import androidx.core.content.edit import de.rki.coronawarnapp.coronatest.DaggerCoronaTestTestComponent import de.rki.coronawarnapp.covidcertificate.test.TestCertificateTestData -import de.rki.coronawarnapp.covidcertificate.test.core.storage.TestCertificateStorage import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.storage.ContainerPostProcessor import de.rki.coronawarnapp.util.serialization.SerializationModule import io.kotest.matchers.shouldBe @@ -40,8 +39,7 @@ class TestCertificateStorageTest : BaseTest() { private fun createInstance() = TestCertificateStorage( context = context, - baseGson = SerializationModule().baseGson(), - containerPostProcessor = postProcessor, + baseGson = SerializationModule().baseGson() ) @Test @@ -55,6 +53,7 @@ class TestCertificateStorageTest : BaseTest() { putString("dontdeleteme", "test") putString("testcertificate.data.ra", "test") putString("testcertificate.data.pcr", "test") + putString("testcertificate.data.scanned", "test") } createInstance().testCertificates = emptySet() @@ -65,7 +64,8 @@ class TestCertificateStorageTest : BaseTest() { fun `store two containers, one for each type`() { createInstance().testCertificates = setOf( certificateTestData.personATest1StoredData, - certificateTestData.personATest2CertStoredData + certificateTestData.personATest2CertStoredData, + certificateTestData.personATest2CertScannedStoredData, ) (mockPreferences.dataMapPeek["testcertificate.data.pcr"] as String).toComparableJsonPretty() shouldBe """ @@ -102,9 +102,20 @@ class TestCertificateStorageTest : BaseTest() { ] """.toComparableJsonPretty() + (mockPreferences.dataMapPeek["testcertificate.data.scanned"] as String).toComparableJsonPretty() shouldBe """ + [ + { + "identifier": "identifier2", + "registeredAt": 12345, + "testCertificateQrCode": "${certificateTestData.personATest2CertQRCodeString}" + } + ] + """.toComparableJsonPretty() + createInstance().testCertificates shouldBe setOf( certificateTestData.personATest1StoredData, - certificateTestData.personATest2CertStoredData + certificateTestData.personATest2CertStoredData, + certificateTestData.personATest2CertScannedStoredData, ) } }