diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCodeValidator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCodeValidator.kt index 7b31a977326e8cf8f72a7b76938766d79e1c1d3f..4f12913e4e45402456e2a37edb17a920429ae085 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCodeValidator.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCodeValidator.kt @@ -13,7 +13,7 @@ class CoronaTestQrCodeValidator @Inject constructor( fun validate(rawString: String): CoronaTestQRCode { return findExtractor(rawString) - ?.extract(rawString) + ?.extract(rawString, mode = QrCodeExtractor.Mode.TEST_STRICT) ?.also { Timber.i("Extracted data from QR code is %s", it) } ?: throw InvalidQRCodeException() } @@ -25,5 +25,11 @@ class CoronaTestQrCodeValidator @Inject constructor( interface QrCodeExtractor<T> { fun canHandle(rawString: String): Boolean - fun extract(rawString: String): T + fun extract(rawString: String, mode: Mode): T + + enum class Mode { + TEST_STRICT, + CERT_VAC_STRICT, + CERT_VAC_LENIENT + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractor.kt index 49633e3decd06846753a897897b235a16409cbdd..c94eb873ff48e73c1fca6cd54a7b7994fe46373f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractor.kt @@ -8,7 +8,7 @@ class PcrQrCodeExtractor @Inject constructor() : QrCodeExtractor<CoronaTestQRCod override fun canHandle(rawString: String): Boolean = rawString.startsWith(prefix, ignoreCase = true) - override fun extract(rawString: String): CoronaTestQRCode.PCR { + override fun extract(rawString: String, mode: QrCodeExtractor.Mode): CoronaTestQRCode.PCR { val guid = extractGUID(rawString) PcrQrCodeCensor.lastGUID = guid return CoronaTestQRCode.PCR(guid) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractor.kt index c5881f97364a97bd6c5f8c795b3b1df7c1c72df0..96dd01a7122a7c1a7c43f1497a96e774aff4ef2b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractor.kt @@ -19,7 +19,7 @@ class RapidAntigenQrCodeExtractor @Inject constructor() : QrCodeExtractor<Corona return rawString.startsWith(PREFIX1, ignoreCase = true) || rawString.startsWith(PREFIX2, ignoreCase = true) } - override fun extract(rawString: String): CoronaTestQRCode.RapidAntigen { + override fun extract(rawString: String, mode: QrCodeExtractor.Mode): CoronaTestQRCode.RapidAntigen { Timber.v("extract(rawString=%s)", rawString) val payload = CleanPayload(extractData(rawString)) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/InvalidHealthCertificateException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/InvalidHealthCertificateException.kt index 39061747c4f618f90d15b5d58f4b3a6bf1d3f785..14110a6f27dea9b9fabf4becdca12ade72163147 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/InvalidHealthCertificateException.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/InvalidHealthCertificateException.kt @@ -19,6 +19,7 @@ import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificat import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_HCERT import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_ISS import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_JSON_SCHEMA_INVALID +import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_MULTIPLE_VACCINATION_ENTRIES import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_NAME_MISMATCH import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_NO_VACCINATION_ENTRY import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_PREFIX_INVALID @@ -36,6 +37,7 @@ class InvalidHealthCertificateException( HC_COSE_MESSAGE_INVALID("COSE message invalid."), HC_CBOR_DECODING_FAILED("CBOR decoding failed."), VC_NO_VACCINATION_ENTRY("Vaccination certificate missing."), + VC_MULTIPLE_VACCINATION_ENTRIES("Multiple vaccination certificates."), VC_PREFIX_INVALID("Prefix invalid."), VC_STORING_FAILED("Storing failed."), VC_JSON_SCHEMA_INVALID("Json schema invalid."), @@ -73,6 +75,9 @@ class InvalidHealthCertificateException( VC_NO_VACCINATION_ENTRY -> CachedString { context -> context.getString(ERROR_MESSAGE_VC_NOT_YET_SUPPORTED) } + VC_MULTIPLE_VACCINATION_ENTRIES -> CachedString { context -> + context.getString(ERROR_MESSAGE_VC_NOT_YET_SUPPORTED) + } VC_STORING_FAILED -> CachedString { context -> context.getString(ERROR_MESSAGE_VC_SCAN_AGAIN) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/VaccinationDGCV1Parser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/VaccinationDGCV1Parser.kt index e25defbc9cab81c1be8c37c0f6aba41716b14f75..be296d648c23287016daedb4ba60b168e5e36756 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/VaccinationDGCV1Parser.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/VaccinationDGCV1Parser.kt @@ -9,7 +9,9 @@ import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificat import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_DGC import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_HCERT import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_JSON_SCHEMA_INVALID +import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_MULTIPLE_VACCINATION_ENTRIES import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_NO_VACCINATION_ENTRY +import timber.log.Timber import javax.inject.Inject @Reusable @@ -17,35 +19,44 @@ class VaccinationDGCV1Parser @Inject constructor( @BaseGson private val gson: Gson ) { - fun parse(map: CBORObject): VaccinationDGCV1 = try { + fun parse(map: CBORObject, lenient: Boolean): VaccinationDGCV1 = try { val certificate: VaccinationDGCV1 = map[keyHCert]?.run { this[keyEuDgcV1]?.run { toCertificate() } ?: throw InvalidHealthCertificateException(VC_HC_CWT_NO_DGC) } ?: throw InvalidHealthCertificateException(VC_HC_CWT_NO_HCERT) - certificate.validate() + certificate.toValidated(lenient) } catch (e: InvalidHealthCertificateException) { throw e } catch (e: Throwable) { throw InvalidHealthCertificateException(HC_CBOR_DECODING_FAILED) } - private fun VaccinationDGCV1.validate(): VaccinationDGCV1 { - if (vaccinationDatas.isEmpty()) { - throw InvalidHealthCertificateException(VC_NO_VACCINATION_ENTRY) + private fun VaccinationDGCV1.toValidated(lenient: Boolean): VaccinationDGCV1 = this + .run { + if (vaccinationDatas.isEmpty()) throw InvalidHealthCertificateException(VC_NO_VACCINATION_ENTRY) + + if (vaccinationDatas.size == 1) return@run this + + if (lenient) { + Timber.w("Lenient: Vaccination data contained multiple entries.") + copy(vaccinationDatas = listOf(vaccinationDatas.maxByOrNull { it.vaccinatedAt }!!)) + } else { + throw InvalidHealthCertificateException(VC_MULTIPLE_VACCINATION_ENTRIES) + } } - // Force date parsing - dateOfBirth - vaccinationDatas.forEach { - it.vaccinatedAt + .also { + // Force date parsing + dateOfBirth + vaccinationDatas.forEach { + it.vaccinatedAt + } } - return this - } - private fun CBORObject.toCertificate() = try { + private fun CBORObject.toCertificate(): VaccinationDGCV1 = try { val json = ToJSONString() - gson.fromJson<VaccinationDGCV1>(json) + gson.fromJson(json) } catch (e: Throwable) { throw InvalidHealthCertificateException(VC_JSON_SCHEMA_INVALID) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt index ae3065df399be47ee02e1d6fe5650ad3ea51b2a0..d1d9496bf223ce36540593ec572234cf84343a56 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt @@ -22,14 +22,14 @@ class VaccinationQRCodeExtractor @Inject constructor( override fun canHandle(rawString: String): Boolean = rawString.startsWith(PREFIX) - override fun extract(rawString: String): VaccinationCertificateQRCode { + override fun extract(rawString: String, mode: QrCodeExtractor.Mode): VaccinationCertificateQRCode { CertificateQrCodeCensor.addQRCodeStringToCensor(rawString) val parsedData = rawString .removePrefix(PREFIX) .decodeBase45() .decompress() - .parse() + .parse(lenient = mode == QrCodeExtractor.Mode.CERT_VAC_LENIENT) return VaccinationCertificateQRCode( parsedData = parsedData, @@ -51,13 +51,13 @@ class VaccinationQRCodeExtractor @Inject constructor( throw InvalidHealthCertificateException(HC_ZLIB_DECOMPRESSION_FAILED) } - fun RawCOSEObject.parse(): VaccinationCertificateData { + fun RawCOSEObject.parse(lenient: Boolean): VaccinationCertificateData { Timber.v("Parsing COSE for vaccination certificate.") val cbor = coseDecoder.decode(this) return VaccinationCertificateData( header = headerParser.parse(cbor), - certificate = bodyParser.parse(cbor) + certificate = bodyParser.parse(cbor, lenient = lenient) ).also { CertificateQrCodeCensor.addCertificateToCensor(it) }.also { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeValidator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeValidator.kt index 3e7f603962d7306ea8bde1da607008559c94c7c1..aefdff6e55018d336945ec001a55372081040f66 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeValidator.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeValidator.kt @@ -17,7 +17,7 @@ class VaccinationQRCodeValidator @Inject constructor( // If there is more than one "extractor" in the future, check censoring again. // CertificateQrCodeCensor.addQRCodeStringToCensor(rawString) return findExtractor(rawString) - ?.extract(rawString) + ?.extract(rawString, mode = QrCodeExtractor.Mode.CERT_VAC_STRICT) ?.also { Timber.i("Extracted data from QR code is %s", it) } ?: throw InvalidHealthCertificateException(VC_PREFIX_INVALID) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt index 9694f5f9d6875b21d3075f90a8dfd52068574962..7914dd2788155a949e4c73860b1aaaaa82d7d685 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.vaccination.core.repository.storage import androidx.annotation.Keep import com.google.gson.annotations.SerializedName +import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor.Mode import de.rki.coronawarnapp.vaccination.core.VaccinatedPersonIdentifier import de.rki.coronawarnapp.vaccination.core.VaccinationCertificate import de.rki.coronawarnapp.vaccination.core.certificate.CoseCertificateHeader @@ -33,7 +34,7 @@ data class VaccinationContainer internal constructor( @delegate:Transient internal val certificateData: VaccinationCertificateData by lazy { - preParsedData ?: qrCodeExtractor.extract(vaccinationQrCode).parsedData + preParsedData ?: qrCodeExtractor.extract(vaccinationQrCode, mode = Mode.CERT_VAC_LENIENT).parsedData } val header: CoseCertificateHeader diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQrCodeValidatorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQrCodeValidatorTest.kt index f9665d0af78f7a9f90b8e8157cd5d915f0e689af..e5cdb3f4c89a6d10f13910bbc6610c7cbb7ebf9f 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQrCodeValidatorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQrCodeValidatorTest.kt @@ -1,16 +1,21 @@ package de.rki.coronawarnapp.coronatest.qrcode +import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor.Mode import de.rki.coronawarnapp.coronatest.type.CoronaTest import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe +import io.mockk.spyk +import io.mockk.verify import org.junit.jupiter.api.Test import testhelpers.BaseTest class CoronaTestQrCodeValidatorTest : BaseTest() { + private val raExtractor = spyk(RapidAntigenQrCodeExtractor()) + private val pcrExtractor = spyk(PcrQrCodeExtractor()) @Test fun `valid codes are extracted by corresponding extractor`() { - val instance = CoronaTestQrCodeValidator(RapidAntigenQrCodeExtractor(), PcrQrCodeExtractor()) + val instance = CoronaTestQrCodeValidator(raExtractor, pcrExtractor) instance.validate(pcrQrCode1).type shouldBe CoronaTest.Type.PCR instance.validate(pcrQrCode2).type shouldBe CoronaTest.Type.PCR instance.validate(pcrQrCode3).type shouldBe CoronaTest.Type.PCR @@ -22,7 +27,7 @@ class CoronaTestQrCodeValidatorTest : BaseTest() { @Test fun `invalid prefix throws exception`() { val invalidCode = "HTTPS://somethingelse/?123456-12345678-1234-4DA7-B166-B86D85475064" - val instance = CoronaTestQrCodeValidator(RapidAntigenQrCodeExtractor(), PcrQrCodeExtractor()) + val instance = CoronaTestQrCodeValidator(raExtractor, pcrExtractor) shouldThrow<InvalidQRCodeException> { instance.validate(invalidCode) } @@ -31,9 +36,18 @@ class CoronaTestQrCodeValidatorTest : BaseTest() { @Test fun `invalid json throws exception`() { val invalidCode = "https://s.coronawarn.app/?v=1#eyJ0aW1lc3RhbXAiOjE2" - val instance = CoronaTestQrCodeValidator(RapidAntigenQrCodeExtractor(), PcrQrCodeExtractor()) + val instance = CoronaTestQrCodeValidator(raExtractor, pcrExtractor) shouldThrow<InvalidQRCodeException> { instance.validate(invalidCode) } } + + @Test + fun `validator uses strict extraction mode`() { + val instance = CoronaTestQrCodeValidator(raExtractor, pcrExtractor) + instance.validate(pcrQrCode1).type shouldBe CoronaTest.Type.PCR + verify { pcrExtractor.extract(pcrQrCode1, Mode.TEST_STRICT) } + instance.validate(raQrCode1).type shouldBe CoronaTest.Type.RAPID_ANTIGEN + verify { raExtractor.extract(raQrCode1, Mode.TEST_STRICT) } + } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractorTest.kt index e4537ff5a6113f3d0c46a6691851bdf94a3a9a69..86e1812719a3d4b1289a23f9568f14f8adc72833 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractorTest.kt @@ -1,5 +1,6 @@ package de.rki.coronawarnapp.coronatest.qrcode +import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor.Mode.TEST_STRICT import io.kotest.matchers.shouldBe import org.junit.Test import testhelpers.BaseTest @@ -16,7 +17,7 @@ class PcrQrCodeExtractorTest : BaseTest() { val extractor = PcrQrCodeExtractor() try { if (extractor.canHandle("$prefixString$guid")) { - extractor.extract("$prefixString$guid") + extractor.extract("$prefixString$guid", mode = TEST_STRICT) conditionToMatch shouldBe true } else { conditionToMatch shouldBe false @@ -77,16 +78,43 @@ class PcrQrCodeExtractorTest : BaseTest() { @Test fun extractGUID() { - PcrQrCodeExtractor().extract("$localhostUpperCase$guidUpperCase").qrCodeGUID shouldBe guidUpperCase - PcrQrCodeExtractor().extract("$localhostUpperCase$guidLowerCase").qrCodeGUID shouldBe guidLowerCase - PcrQrCodeExtractor().extract("$localhostUpperCase$guidMixedCase").qrCodeGUID shouldBe guidMixedCase + PcrQrCodeExtractor().extract( + "$localhostUpperCase$guidUpperCase", + mode = TEST_STRICT + ).qrCodeGUID shouldBe guidUpperCase + PcrQrCodeExtractor().extract( + "$localhostUpperCase$guidLowerCase", + mode = TEST_STRICT + ).qrCodeGUID shouldBe guidLowerCase + PcrQrCodeExtractor().extract( + "$localhostUpperCase$guidMixedCase", + mode = TEST_STRICT + ).qrCodeGUID shouldBe guidMixedCase - PcrQrCodeExtractor().extract("$localhostLowerCase$guidUpperCase").qrCodeGUID shouldBe guidUpperCase - PcrQrCodeExtractor().extract("$localhostLowerCase$guidLowerCase").qrCodeGUID shouldBe guidLowerCase - PcrQrCodeExtractor().extract("$localhostLowerCase$guidMixedCase").qrCodeGUID shouldBe guidMixedCase + PcrQrCodeExtractor().extract( + "$localhostLowerCase$guidUpperCase", + mode = TEST_STRICT + ).qrCodeGUID shouldBe guidUpperCase + PcrQrCodeExtractor().extract( + "$localhostLowerCase$guidLowerCase", + mode = TEST_STRICT + ).qrCodeGUID shouldBe guidLowerCase + PcrQrCodeExtractor().extract( + "$localhostLowerCase$guidMixedCase", + mode = TEST_STRICT + ).qrCodeGUID shouldBe guidMixedCase - PcrQrCodeExtractor().extract("$localhostMixedCase$guidUpperCase").qrCodeGUID shouldBe guidUpperCase - PcrQrCodeExtractor().extract("$localhostMixedCase$guidLowerCase").qrCodeGUID shouldBe guidLowerCase - PcrQrCodeExtractor().extract("$localhostMixedCase$guidMixedCase").qrCodeGUID shouldBe guidMixedCase + PcrQrCodeExtractor().extract( + "$localhostMixedCase$guidUpperCase", + mode = TEST_STRICT + ).qrCodeGUID shouldBe guidUpperCase + PcrQrCodeExtractor().extract( + "$localhostMixedCase$guidLowerCase", + mode = TEST_STRICT + ).qrCodeGUID shouldBe guidLowerCase + PcrQrCodeExtractor().extract( + "$localhostMixedCase$guidMixedCase", + mode = TEST_STRICT + ).qrCodeGUID shouldBe guidMixedCase } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractorTest.kt index e78658b69175283df92b1e3ebb58182c62749a61..4a746b6edd43282f85b60c2a88039eb7194f42e4 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractorTest.kt @@ -1,5 +1,6 @@ package de.rki.coronawarnapp.coronatest.qrcode +import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor.Mode.TEST_STRICT import de.rki.coronawarnapp.coronatest.type.CoronaTest import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe @@ -29,13 +30,13 @@ class RapidAntigenQrCodeExtractorTest : BaseTest() { @Test fun `extracting valid codes does not throw exception`() { listOf(raQrCode1, raQrCode2, raQrCode3, raQrCode4, raQrCode5, raQrCode6, raQrCode7, raQrCode8).forEach { - instance.extract(it) + instance.extract(it, mode = TEST_STRICT) } } @Test fun `personal data is extracted`() { - val data = instance.extract(raQrCode3) + val data = instance.extract(raQrCode3, mode = TEST_STRICT) data.type shouldBe CoronaTest.Type.RAPID_ANTIGEN data.hash shouldBe "7dce08db0d4abd5ac1d2498b571afb221ca947c75c847d05466b4cfe9d95dc66" data.createdAt shouldBe Instant.ofEpochMilli(1619618352000) @@ -46,7 +47,7 @@ class RapidAntigenQrCodeExtractorTest : BaseTest() { @Test fun `empty strings are treated as null or notset`() { - val data = instance.extract(raQrCodeEmptyStrings) + val data = instance.extract(raQrCodeEmptyStrings, mode = TEST_STRICT) data.type shouldBe CoronaTest.Type.RAPID_ANTIGEN data.hash shouldBe "d6e4d0181d8109bf05b346a0d2e0ef0cc472eed70d9df8c4b9ae5c7a009f3e34" data.createdAt shouldBe Instant.ofEpochMilli(1619012952000) @@ -57,14 +58,14 @@ class RapidAntigenQrCodeExtractorTest : BaseTest() { @Test fun `personal data is only valid if complete or completely missing`() { - shouldThrow<InvalidQRCodeException> { instance.extract(raQrIncompletePersonalData) } + shouldThrow<InvalidQRCodeException> { instance.extract(raQrIncompletePersonalData, mode = TEST_STRICT) } } @Test fun `invalid json throws exception`() { val invalidCode = "https://s.coronawarn.app/?v=1#eyJ0aW1lc3RhbXAiOjE2" shouldThrow<InvalidQRCodeException> { - RapidAntigenQrCodeExtractor().extract(invalidCode) + RapidAntigenQrCodeExtractor().extract(invalidCode, mode = TEST_STRICT) } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinationTestComponent.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinationTestComponent.kt index 181970bd621adbbc1eb156126f6bd70dcbd01dd7..7f80b1fbbe61087271d832cd31c1776329589f71 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinationTestComponent.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinationTestComponent.kt @@ -4,6 +4,7 @@ import dagger.Component import dagger.Module import de.rki.coronawarnapp.util.serialization.SerializationModule import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationQRCodeExtractorTest +import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationQrCodeValidatorTest import de.rki.coronawarnapp.vaccination.core.repository.VaccinationRepositoryTest import de.rki.coronawarnapp.vaccination.core.repository.storage.VaccinationContainerTest import de.rki.coronawarnapp.vaccination.core.repository.storage.VaccinationStorageTest @@ -23,6 +24,7 @@ interface VaccinationTestComponent { fun inject(testClass: VaccinationQRCodeExtractorTest) fun inject(testClass: VaccinatedPersonTest) fun inject(testClass: VaccinationRepositoryTest) + fun inject(testClass: VaccinationQrCodeValidatorTest) @Component.Factory interface Factory { diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinationTestData.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinationTestData.kt index 0c5117d63094019d089ac68493a1edd962fda0cf..9ff6c95e55f6186c6067a56e6018911787da5e71 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinationTestData.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinationTestData.kt @@ -184,4 +184,14 @@ class VaccinationTestData @Inject constructor( ).apply { qrCodeExtractor = this@VaccinationTestData.qrCodeExtractor } + + val personYVacTwoEntriesQrCode = + "HC1:6BFOXN%TSMAHN-HVN8J7UQMJ4/36 L-AHQ+R1WG%MP8*ICG5QKM0658WAULO8NASA3/-2E%5G%5TW5A 6YO6XL6Q3QR\$P*NI92KV6TKOJ06JYZJV1JJ7UGOJUTIJ7J:ZJ83BL8TFVTV9T.ZJC0J*PIZ.TJ STPT*IJ5OI9YI:8DJ:D%PDDIKIWCHAB.YMAHLW 70SO:GOLIROGO3T59YLY1S7HOPC5NDOEC5/64ND7BT5PE4D/5:/6N9R%EPXCROGO+GOVIR-PQ395R4IUHLW\$G-B5ET42HPPEPHCR6W97DON95N14Q6SP+PJD1W9L \$N3-Q.VBAO8MN9*QHAO96Y2/*13A5-8E6V59I9BZK6:IZW4I:A6J3ARN QT1BGL4OMJKR.K\$A1EB14UVC2O+5T3.CE1M33KS2JKA8Y*99CCLLOR/CH0GRP8 GLY 1LA7551DC2U.NVOTJOII:8DKEK%N92T9YQ$0MK%P6\$G9K7QQUY9KI.EK*8XRS-DPA5W64SMVR1NF6D0 2S0.7R:ASENTI094PIDS:T32DRE8N" + + val personYVacTwoEntriesContainer = VaccinationContainer( + scannedAt = Instant.ofEpochMilli(1620062834471), + vaccinationQrCode = personYVacTwoEntriesQrCode, + ).apply { + qrCodeExtractor = this@VaccinationTestData.qrCodeExtractor + } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt index 1b41ddfa08b51bae20ea21a7186b1c8d32ca7564..0c7670d9aef4c687f1d56a9cf1190ae3cfd6f668 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt @@ -1,5 +1,6 @@ package de.rki.coronawarnapp.vaccination.core.qrcode +import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor.Mode import de.rki.coronawarnapp.vaccination.core.DaggerVaccinationTestComponent import de.rki.coronawarnapp.vaccination.core.VaccinationQrCodeTestData import de.rki.coronawarnapp.vaccination.core.VaccinationTestData @@ -29,17 +30,17 @@ class VaccinationQRCodeExtractorTest : BaseTest() { @Test fun `happy path extraction`() { - extractor.extract(VaccinationQrCodeTestData.validVaccinationQrCode) + extractor.extract(VaccinationQrCodeTestData.validVaccinationQrCode, mode = Mode.CERT_VAC_STRICT) } @Test fun `happy path extraction 2`() { - extractor.extract(VaccinationQrCodeTestData.validVaccinationQrCode2) + extractor.extract(VaccinationQrCodeTestData.validVaccinationQrCode2, mode = Mode.CERT_VAC_STRICT) } @Test fun `happy path extraction with data`() { - val qrCode = extractor.extract(VaccinationQrCodeTestData.validVaccinationQrCode3) + val qrCode = extractor.extract(VaccinationQrCodeTestData.validVaccinationQrCode3, mode = Mode.CERT_VAC_STRICT) with(qrCode.parsedData.header) { issuer shouldBe "AT" @@ -76,52 +77,76 @@ class VaccinationQRCodeExtractorTest : BaseTest() { @Test fun `happy path extraction 4`() { - extractor.extract(VaccinationQrCodeTestData.validVaccinationQrCode4) + extractor.extract( + VaccinationQrCodeTestData.validVaccinationQrCode4, + mode = Mode.CERT_VAC_STRICT + ) } @Test fun `valid encoding but not a health certificate fails with VC_HC_CWT_NO_ISS`() { shouldThrow<InvalidHealthCertificateException> { - extractor.extract(VaccinationQrCodeTestData.validEncoded) + extractor.extract( + VaccinationQrCodeTestData.validEncoded, + mode = Mode.CERT_VAC_STRICT + ) }.errorCode shouldBe VC_HC_CWT_NO_ISS } @Test fun `random string fails with HC_BASE45_DECODING_FAILED`() { shouldThrow<InvalidHealthCertificateException> { - extractor.extract("nothing here to see") + extractor.extract( + "nothing here to see", + mode = Mode.CERT_VAC_STRICT + ) }.errorCode shouldBe HC_BASE45_DECODING_FAILED } @Test fun `uncompressed base45 string fails with HC_ZLIB_DECOMPRESSION_FAILED`() { shouldThrow<InvalidHealthCertificateException> { - extractor.extract("6BFOABCDEFGHIJKLMNOPQRSTUVWXYZ %*+-./:") + extractor.extract( + "6BFOABCDEFGHIJKLMNOPQRSTUVWXYZ %*+-./:", + mode = Mode.CERT_VAC_STRICT + ) }.errorCode shouldBe HC_ZLIB_DECOMPRESSION_FAILED } @Test fun `vaccination certificate missing fails with VC_NO_VACCINATION_ENTRY`() { shouldThrow<InvalidHealthCertificateException> { - extractor.extract(VaccinationQrCodeTestData.certificateMissing) + extractor.extract( + VaccinationQrCodeTestData.certificateMissing, + mode = Mode.CERT_VAC_STRICT + ) }.errorCode shouldBe VC_NO_VACCINATION_ENTRY } @Test fun `test data person A check`() { - val extracted = extractor.extract(vaccinationTestData.personAVac1QRCodeString) + val extracted = extractor.extract( + vaccinationTestData.personAVac1QRCodeString, + mode = Mode.CERT_VAC_STRICT + ) extracted shouldBe vaccinationTestData.personAVac1QRCode } @Test fun `test data person B check`() { - val extracted = extractor.extract(vaccinationTestData.personBVac1QRCodeString) + val extracted = extractor.extract( + vaccinationTestData.personBVac1QRCodeString, + mode = Mode.CERT_VAC_STRICT + ) extracted shouldBe vaccinationTestData.personBVac1QRCode } @Test fun `Bulgarian qr code passes`() { - val qrCode = extractor.extract(VaccinationQrCodeTestData.qrCodeBulgaria) + val qrCode = extractor.extract( + VaccinationQrCodeTestData.qrCodeBulgaria, + mode = Mode.CERT_VAC_STRICT + ) with(qrCode.parsedData.header) { issuer shouldBe "BG" issuedAt shouldBe Instant.parse("2021-06-02T14:07:56.000Z") @@ -157,6 +182,9 @@ class VaccinationQRCodeExtractorTest : BaseTest() { @Test fun `Swedish qr code passes`() { - extractor.extract(VaccinationQrCodeTestData.qrCodeSweden) + extractor.extract( + VaccinationQrCodeTestData.qrCodeSweden, + mode = Mode.CERT_VAC_STRICT + ) } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQrCodeValidatorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQrCodeValidatorTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..9c08a3e0e4a1350d0ecce4a851118675ef1f187b --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQrCodeValidatorTest.kt @@ -0,0 +1,34 @@ +package de.rki.coronawarnapp.vaccination.core.qrcode + +import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor.Mode +import de.rki.coronawarnapp.vaccination.core.DaggerVaccinationTestComponent +import de.rki.coronawarnapp.vaccination.core.VaccinationTestData +import io.kotest.matchers.shouldBe +import io.mockk.spyk +import io.mockk.verify +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import javax.inject.Inject + +class VaccinationQrCodeValidatorTest : BaseTest() { + @Inject lateinit var testData: VaccinationTestData + @Inject lateinit var vacExtractor: VaccinationQRCodeExtractor + private lateinit var vacExtractorSpy: VaccinationQRCodeExtractor + + @BeforeEach + fun setup() { + DaggerVaccinationTestComponent.factory().create().inject(this) + + vacExtractorSpy = spyk(vacExtractor) + } + + @Test + fun `validator uses strict extraction mode`() { + val instance = VaccinationQRCodeValidator(vacExtractorSpy) + instance.validate(testData.personAVac1QRCodeString).apply { + uniqueCertificateIdentifier shouldBe testData.personAVac1Container.certificateId + } + verify { vacExtractorSpy.extract(testData.personAVac1QRCodeString, Mode.CERT_VAC_STRICT) } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainerTest.kt index fad39f683f0e946dda8f94afb044c82a403a554d..a2817d54405fe05efeeeb2e6272d38603961f6ae 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainerTest.kt @@ -1,12 +1,17 @@ package de.rki.coronawarnapp.vaccination.core.repository.storage +import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor import de.rki.coronawarnapp.vaccination.core.DaggerVaccinationTestComponent import de.rki.coronawarnapp.vaccination.core.VaccinatedPersonIdentifier import de.rki.coronawarnapp.vaccination.core.VaccinationTestData +import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateQRCode +import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationQRCodeExtractor import de.rki.coronawarnapp.vaccination.core.server.valueset.VaccinationValueSet import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe import io.mockk.every import io.mockk.mockk +import io.mockk.verify import org.joda.time.Instant import org.joda.time.LocalDate import org.junit.jupiter.api.BeforeEach @@ -138,4 +143,27 @@ class VaccinationContainerTest : BaseTest() { certificateCountry shouldBe "YY" } } + + @Test + fun `default parsing mode for containers is lenient`() { + val container = VaccinationContainer( + vaccinationQrCode = testData.personYVacTwoEntriesQrCode, + scannedAt = Instant.EPOCH + ) + val extractor = mockk<VaccinationQRCodeExtractor>().apply { + every { extract(any(), any()) } returns mockk<VaccinationCertificateQRCode>().apply { + every { parsedData } returns mockk() + } + } + container.qrCodeExtractor = extractor + + container.certificateData shouldNotBe null + + verify { extractor.extract(testData.personYVacTwoEntriesQrCode, QrCodeExtractor.Mode.CERT_VAC_LENIENT) } + } + + @Test + fun `gracefully handle semi invalid data - multiple entries`() { + testData.personYVacTwoEntriesContainer.certificate.vaccinationDatas.size shouldBe 1 + } }