diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/covidcertificate/test/ui/CertificatesFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/covidcertificate/test/ui/CertificatesFragmentTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..9d1d1223d2290c7c2fd1becf3a911447d91d5843 --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/covidcertificate/test/ui/CertificatesFragmentTest.kt @@ -0,0 +1,196 @@ +package de.rki.coronawarnapp.covidcertificate.test.ui + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.test.espresso.Espresso +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.ext.junit.runners.AndroidJUnit4 +import dagger.Module +import dagger.android.ContributesAndroidInjector +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.covidcertificate.test.ui.cards.CovidTestCertificateCard +import de.rki.coronawarnapp.covidcertificate.test.ui.cards.CovidTestCertificateErrorCard +import de.rki.coronawarnapp.covidcertificate.test.ui.items.CertificatesItem +import de.rki.coronawarnapp.covidcertificate.vaccination.core.VaccinatedPerson +import de.rki.coronawarnapp.covidcertificate.vaccination.ui.cards.CreateVaccinationCard +import de.rki.coronawarnapp.covidcertificate.vaccination.ui.cards.HeaderInfoVaccinationCard +import de.rki.coronawarnapp.covidcertificate.vaccination.ui.cards.ImmuneVaccinationCard +import de.rki.coronawarnapp.covidcertificate.vaccination.ui.cards.NoCovidTestCertificatesCard +import de.rki.coronawarnapp.covidcertificate.vaccination.ui.cards.VaccinationCard +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import org.joda.time.DateTime +import org.joda.time.Duration +import org.joda.time.format.DateTimeFormat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import testhelpers.BaseUITest +import testhelpers.Screenshot +import testhelpers.launchFragment2 +import testhelpers.launchInMainActivity +import testhelpers.selectBottomNavTab +import testhelpers.takeScreenshot + +@RunWith(AndroidJUnit4::class) +class CertificatesFragmentTest : BaseUITest() { + + @MockK lateinit var viewModel: CertificatesViewModel + @MockK lateinit var vaccinatedPerson: VaccinatedPerson + + private val formatter = DateTimeFormat.forPattern("dd.MM.yyyy HH:mm") + private val testDate = DateTime.parse("12.05.2021 19:00", formatter).toInstant() + + @Before + fun setup() { + MockKAnnotations.init(this, relaxed = true) + every { vaccinatedPerson.fullName } returns "Max Mustermann" + every { vaccinatedPerson.getMostRecentVaccinationCertificate.expiresAt } returns + testDate.plus(Duration.standardDays(365)).toInstant() + + setupMockViewModel( + object : CertificatesViewModel.Factory { + override fun create(): CertificatesViewModel = viewModel + } + ) + } + + @After + fun teardown() { + clearAllViewModels() + } + + @Test + fun launch_fragment() { + launchFragment2<CertificatesFragment>() + } + + @Screenshot + @Test + fun capture_screenshot_empty() { + every { viewModel.screenItems } returns getEmptyScreenItems() + + takeScreenshotInMainActivity() + } + + private fun getEmptyScreenItems(): LiveData<List<CertificatesItem>> { + return MutableLiveData( + listOf( + HeaderInfoVaccinationCard.Item, + CreateVaccinationCard.Item {}, + NoCovidTestCertificatesCard.Item + ) + ) + } + + @Screenshot + @Test + fun capture_screenshot_vaccination_incomplete() { + every { vaccinatedPerson.getVaccinationStatus() } returns VaccinatedPerson.Status.INCOMPLETE + every { viewModel.screenItems } returns getVaccinationIncompleteScreenItems() + + takeScreenshotInMainActivity("incomplete") + } + + private fun getVaccinationIncompleteScreenItems(): LiveData<List<CertificatesItem>> { + return MutableLiveData( + listOf( + HeaderInfoVaccinationCard.Item, + VaccinationCard.Item( + vaccinatedPerson = vaccinatedPerson, + onClickAction = {} + ), + NoCovidTestCertificatesCard.Item + ) + ) + } + + @Screenshot + @Test + fun capture_screenshot_vaccination_complete() { + every { vaccinatedPerson.getVaccinationStatus() } returns VaccinatedPerson.Status.IMMUNITY + every { viewModel.screenItems } returns getVaccinationImmuneScreenItems() + + takeScreenshotInMainActivity("immune") + } + + private fun getVaccinationImmuneScreenItems(): LiveData<List<CertificatesItem>> { + return MutableLiveData( + listOf( + HeaderInfoVaccinationCard.Item, + ImmuneVaccinationCard.Item( + vaccinatedPerson = vaccinatedPerson, + onClickAction = {} + ), + NoCovidTestCertificatesCard.Item + ) + ) + } + + @Screenshot + @Test + fun capture_screenshot_green_certificate() { + every { vaccinatedPerson.getVaccinationStatus() } returns VaccinatedPerson.Status.IMMUNITY + every { viewModel.screenItems } returns getVaccinationGreenCertScreenItems() + + takeScreenshotInMainActivity("green") + } + + private fun getVaccinationGreenCertScreenItems(): LiveData<List<CertificatesItem>> { + return MutableLiveData( + listOf( + HeaderInfoVaccinationCard.Item, + ImmuneVaccinationCard.Item( + vaccinatedPerson = vaccinatedPerson, + onClickAction = {} + ), + CovidTestCertificateCard.Item( + testDate = testDate, + testPerson = "Max Mustermann" + ) { } + ) + ) + } + + @Screenshot + @Test + fun capture_screenshot_pending_certificate() { + every { vaccinatedPerson.getVaccinationStatus() } returns VaccinatedPerson.Status.IMMUNITY + every { viewModel.screenItems } returns getVaccinationPendingCertScreenItems() + + takeScreenshotInMainActivity("pending") + } + + private fun getVaccinationPendingCertScreenItems(): LiveData<List<CertificatesItem>> { + return MutableLiveData( + listOf( + HeaderInfoVaccinationCard.Item, + ImmuneVaccinationCard.Item( + vaccinatedPerson = vaccinatedPerson, + onClickAction = {} + ), + CovidTestCertificateErrorCard.Item( + testDate = testDate, + isUpdatingData = false, + onRetryAction = {}, + onDeleteAction = {} + ) + ) + ) + } + + private fun takeScreenshotInMainActivity(suffix: String = "") { + launchInMainActivity<CertificatesFragment>() + Espresso.onView(ViewMatchers.withId(R.id.fake_bottom_navigation)) + .perform(selectBottomNavTab(R.id.green_certificate_graph)) + takeScreenshot<CertificatesFragment>(suffix) + } +} + +@Module +abstract class CertificatesFragmentTestModule { + @ContributesAndroidInjector + abstract fun certificatesFragment(): CertificatesFragment +} diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/covidcertificate/test/ui/CovidCertificateDetailsFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/covidcertificate/test/ui/CovidCertificateDetailsFragmentTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..ec0076572f455d92bcc69677f6821d3a753f64eb --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/covidcertificate/test/ui/CovidCertificateDetailsFragmentTest.kt @@ -0,0 +1,143 @@ +package de.rki.coronawarnapp.covidcertificate.test.ui + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.swipeUp +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.runners.AndroidJUnit4 +import dagger.Module +import dagger.android.ContributesAndroidInjector +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.covidcertificate.common.certificate.CertificatePersonIdentifier +import de.rki.coronawarnapp.covidcertificate.common.qrcode.QrCodeString +import de.rki.coronawarnapp.covidcertificate.test.core.TestCertificate +import de.rki.coronawarnapp.covidcertificate.test.core.storage.TestCertificateIdentifier +import de.rki.coronawarnapp.covidcertificate.test.ui.details.CovidCertificateDetailsFragment +import de.rki.coronawarnapp.covidcertificate.test.ui.details.CovidCertificateDetailsFragmentArgs +import de.rki.coronawarnapp.covidcertificate.test.ui.details.CovidCertificateDetailsViewModel +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import org.joda.time.DateTime +import org.joda.time.Instant +import org.joda.time.LocalDate +import org.joda.time.format.DateTimeFormat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import testhelpers.BaseUITest +import testhelpers.Screenshot +import testhelpers.launchFragment2 +import testhelpers.launchFragmentInContainer2 +import testhelpers.takeScreenshot + +@RunWith(AndroidJUnit4::class) +class VaccinationDetailsFragmentTest : BaseUITest() { + + @MockK lateinit var vaccinationDetailsViewModel: CovidCertificateDetailsViewModel + @MockK lateinit var certificatePersonIdentifier: CertificatePersonIdentifier + + private val args = CovidCertificateDetailsFragmentArgs("testCertificateIdentifier").toBundle() + + @Before + fun setUp() { + MockKAnnotations.init(this, relaxed = true) + + every { vaccinationDetailsViewModel.qrCode } returns bitmapLiveDate() + + setupMockViewModel( + object : CovidCertificateDetailsViewModel.Factory { + override fun create(testCertificateIdentifier: TestCertificateIdentifier): + CovidCertificateDetailsViewModel = vaccinationDetailsViewModel + } + ) + } + + @Test + fun launch_fragment() { + launchFragment2<CovidCertificateDetailsFragment>(fragmentArgs = args) + } + + @Screenshot + @Test + fun capture_screenshot_incomplete() { + every { vaccinationDetailsViewModel.covidCertificate } returns vaccinationDetailsData() + launchFragmentInContainer2<CovidCertificateDetailsFragment>(fragmentArgs = args) + takeScreenshot<CovidCertificateDetailsFragment>() + onView(withId(R.id.coordinator_layout)).perform(swipeUp()) + takeScreenshot<CovidCertificateDetailsFragment>("_2") + } + + private fun bitmapLiveDate(): LiveData<Bitmap> { + val applicationContext = ApplicationProvider.getApplicationContext<Context>() + return MutableLiveData( + BitmapFactory.decodeResource(applicationContext.resources, R.drawable.test_qr_code) + ) + } + + private fun vaccinationDetailsData(): MutableLiveData<TestCertificate> { + val formatter = DateTimeFormat.forPattern("dd.MM.yyyy HH:mm") + val testDate = DateTime.parse("12.05.2021 19:00", formatter).toInstant() + return MutableLiveData( + object : TestCertificate { + override val targetName: String + get() = "Mustermann, Max" + override val testType: String + get() = "SARS-CoV-2-Test" + override val testResult: String + get() = "negative" + override val testName: String + get() = "Xep" + override val testNameAndManufactor: String + get() = "Xup" + override val sampleCollectedAt: Instant + get() = testDate + override val testResultAt: Instant + get() = testDate + override val testCenter: String + get() = "AB123" + override val issuer: String + get() = "G0593048274845483647869576478784" + override val issuedAt: Instant + get() = testDate + override val expiresAt: Instant + get() = testDate + override val qrCode: QrCodeString + get() = "" + override val firstName: String + get() = "Max" + override val lastName: String + get() = "Mustermann" + override val fullName: String + get() = "Mustermann, Max" + override val dateOfBirth: LocalDate + get() = LocalDate.parse("18.04.1943 00:00", formatter) + override val personIdentifier: CertificatePersonIdentifier + get() = certificatePersonIdentifier + override val certificateIssuer: String + get() = "G0593048274845483647869576478784" + override val certificateCountry: String + get() = "Germany" + override val certificateId: String + get() = "05930482748454836478695764787840" + } + ) + } + + @After + fun tearDown() { + clearAllViewModels() + } +} + +@Module +abstract class CovidCertificateDetailsFragmentTestModule { + @ContributesAndroidInjector + abstract fun covidCertificateDetailsFragment(): CovidCertificateDetailsFragment +} diff --git a/Corona-Warn-App/src/androidTest/java/testhelpers/FragmentTestModuleRegistrar.kt b/Corona-Warn-App/src/androidTest/java/testhelpers/FragmentTestModuleRegistrar.kt index c8d1be3d5f7627a65ec5a00f7024e585f794c9af..7291b0c9397966b18290e3c0a2f8abb1433b2ef1 100644 --- a/Corona-Warn-App/src/androidTest/java/testhelpers/FragmentTestModuleRegistrar.kt +++ b/Corona-Warn-App/src/androidTest/java/testhelpers/FragmentTestModuleRegistrar.kt @@ -3,6 +3,8 @@ package testhelpers import dagger.Module import de.rki.coronawarnapp.bugreporting.DebugLogTestModule import de.rki.coronawarnapp.bugreporting.DebugLogUploadTestModule +import de.rki.coronawarnapp.covidcertificate.test.ui.CertificatesFragmentTestModule +import de.rki.coronawarnapp.covidcertificate.test.ui.CovidCertificateDetailsFragmentTestModule import de.rki.coronawarnapp.covidcertificate.vaccination.ui.details.VaccinationDetailsFragmentTestModule import de.rki.coronawarnapp.covidcertificate.vaccination.ui.list.VaccinationListFragmentTestModule import de.rki.coronawarnapp.ui.contactdiary.ContactDiaryDayFragmentTestModule @@ -91,6 +93,8 @@ import de.rki.coronawarnapp.ui.vaccination.VaccinationConsentFragmentTestModule VaccinationConsentFragmentTestModule::class, VaccinationListFragmentTestModule::class, RequestCovidCertificateFragmentTestModule::class, + CertificatesFragmentTestModule::class, + CovidCertificateDetailsFragmentTestModule::class, ] ) class FragmentTestModuleRegistrar 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/InvalidQRCodeException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/InvalidQRCodeException.kt index 24dd420a35009d94e55b06ece7c80802cd6db8eb..30c3cf40d5d9e99b94ffb8946d5afbc4b8b8b0fc 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/InvalidQRCodeException.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/InvalidQRCodeException.kt @@ -1,5 +1,6 @@ package de.rki.coronawarnapp.coronatest.qrcode open class InvalidQRCodeException( - message: String = "An error occurred while parsing the qr code" -) : Exception(message) + message: String = "An error occurred while parsing the qr code", + cause: Throwable? = null, +) : Exception(message, cause) 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 2265512e021fba44f390d1bc7eb90bd4d79d4e7a..9175decb73488312d937e2615b641cb6c5b4d460 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/covidcertificate/common/exception/InvalidHealthCertificateException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidHealthCertificateException.kt index b082915d8c1957be2319ef9ca7894158fb9b81c5..401f396e27c7f1d2f7ccc435892e68fccb4e3db4 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidHealthCertificateException.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidHealthCertificateException.kt @@ -9,8 +9,9 @@ import de.rki.coronawarnapp.util.ui.LazyString @Suppress("MaxLineLength") open class InvalidHealthCertificateException( - val errorCode: ErrorCode -) : HasHumanReadableError, InvalidQRCodeException(errorCode.message) { + val errorCode: ErrorCode, + cause: Throwable? = null, +) : HasHumanReadableError, InvalidQRCodeException(errorCode.message, cause) { enum class ErrorCode( val message: String ) { @@ -22,6 +23,7 @@ open 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."), NO_TEST_ENTRY("Test certificate missing."), VC_PREFIX_INVALID("Prefix invalid."), VC_STORING_FAILED("Storing failed."), diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidVaccinationCertificateException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidVaccinationCertificateException.kt index 0a38b5300336e3dcfba82169715bcd38f6dab287..63fc7f6728e587b45e5efbd2966d2dbab8fca3c5 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidVaccinationCertificateException.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidVaccinationCertificateException.kt @@ -6,7 +6,10 @@ import de.rki.coronawarnapp.util.HumanReadableError import de.rki.coronawarnapp.util.ui.CachedString import de.rki.coronawarnapp.util.ui.LazyString -class InvalidVaccinationCertificateException(errorCode: ErrorCode) : InvalidHealthCertificateException(errorCode) { +class InvalidVaccinationCertificateException( + errorCode: ErrorCode, + cause: Throwable? = null, +) : InvalidHealthCertificateException(errorCode, cause) { override fun toHumanReadableError(context: Context): HumanReadableError { var errorCodeString = errorCode.toString() errorCodeString = if (errorCodeString.startsWith(PREFIX_VC)) errorCodeString else PREFIX_VC + errorCodeString @@ -15,20 +18,26 @@ class InvalidVaccinationCertificateException(errorCode: ErrorCode) : InvalidHeal ) } + val showFaqButton: Boolean + get() = errorCode in codesVcInvalid + + private val codesVcInvalid = listOf( + ErrorCode.HC_BASE45_DECODING_FAILED, + ErrorCode.HC_CBOR_DECODING_FAILED, + ErrorCode.HC_COSE_MESSAGE_INVALID, + ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED, + ErrorCode.HC_COSE_TAG_INVALID, + ErrorCode.VC_PREFIX_INVALID, + ErrorCode.HC_CWT_NO_DGC, + ErrorCode.HC_CWT_NO_EXP, + ErrorCode.HC_CWT_NO_HCERT, + ErrorCode.HC_CWT_NO_ISS, + ErrorCode.JSON_SCHEMA_INVALID + ) + override val errorMessage: LazyString get() = when (errorCode) { - ErrorCode.VC_PREFIX_INVALID, - ErrorCode.HC_BASE45_DECODING_FAILED, - ErrorCode.HC_CBOR_DECODING_FAILED, - ErrorCode.HC_COSE_MESSAGE_INVALID, - ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED, - ErrorCode.HC_COSE_TAG_INVALID, - ErrorCode.HC_CWT_NO_DGC, - ErrorCode.HC_CWT_NO_EXP, - ErrorCode.HC_CWT_NO_HCERT, - ErrorCode.HC_CWT_NO_ISS, - ErrorCode.JSON_SCHEMA_INVALID, - -> CachedString { context -> + in codesVcInvalid -> CachedString { context -> context.getString(ERROR_MESSAGE_VC_INVALID) } @@ -36,6 +45,10 @@ class InvalidVaccinationCertificateException(errorCode: ErrorCode) : InvalidHeal context.getString(ERROR_MESSAGE_VC_NOT_YET_SUPPORTED) } + ErrorCode.VC_MULTIPLE_VACCINATION_ENTRIES -> CachedString { context -> + context.getString(ERROR_MESSAGE_VC_NOT_YET_SUPPORTED) + } + ErrorCode.VC_STORING_FAILED -> CachedString { context -> context.getString(ERROR_MESSAGE_VC_SCAN_AGAIN) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/ui/CertificatesFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/ui/CertificatesFragment.kt index 96263715b079593f273d3a19bddbcd80c0d736a2..afd660e979ff7f8197838d140578d3350edb9b55 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/ui/CertificatesFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/ui/CertificatesFragment.kt @@ -89,7 +89,7 @@ class CertificatesFragment : Fragment(R.layout.fragment_certificates), AutoInjec setOnMenuItemClickListener { when (it.itemId) { R.id.menu_information -> { - doNavigate(CertificatesFragmentDirections.actionCertificatesFragmentToConsentFragment()) + doNavigate(CertificatesFragmentDirections.actionCertificatesFragmentToConsentFragment(false)) true } else -> onOptionsItemSelected(it) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/ui/details/CovidCertificateDetailsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/ui/details/CovidCertificateDetailsFragment.kt index ae32afd1905d6b97daf944cccf12e4eb8487214a..3e79944b5fb879c6f4fb86541b2ff79a77f4f837 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/ui/details/CovidCertificateDetailsFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/ui/details/CovidCertificateDetailsFragment.kt @@ -68,7 +68,7 @@ class CovidCertificateDetailsFragment : Fragment(R.layout.fragment_covid_certifi testCertificate.sampleCollectedAt.toShortDayFormat(), testCertificate.sampleCollectedAt.toShortTimeFormat(), ) - name.text = testCertificate.run { "$firstName $lastName" } + name.text = testCertificate.run { "$lastName, $firstName" } birthDate.text = testCertificate.dateOfBirth.toDayFormat() diseaseType.text = testCertificate.targetName testType.text = testCertificate.testType diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/certificate/VaccinationDccV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/certificate/VaccinationDccV1.kt index 3583458aa0c5211d4524647a6c449e81b5bd6af6..e231e3ed67601d3b246d406059bea3260c50fb4d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/certificate/VaccinationDccV1.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/certificate/VaccinationDccV1.kt @@ -2,7 +2,9 @@ package de.rki.coronawarnapp.covidcertificate.vaccination.core.certificate import com.google.gson.annotations.SerializedName import de.rki.coronawarnapp.covidcertificate.common.certificate.Dcc +import org.joda.time.DateTime import org.joda.time.LocalDate +import timber.log.Timber data class VaccinationDccV1( @SerializedName("ver") override val version: String, @@ -33,7 +35,29 @@ data class VaccinationDccV1( // Unique Certificate Identifier, e.g. "ci": "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ" @SerializedName("ci") override val uniqueCertificateIdentifier: String ) : Dcc.Payload { + // Can't use lazy because GSON will NULL it, as we have no no-args constructor + private var vaccinatedAtCache: LocalDate? = null val vaccinatedAt: LocalDate - get() = LocalDate.parse(dt) + get() = vaccinatedAtCache ?: dt.toLocalDateLeniently().also { + vaccinatedAtCache = it + } + } + + // Can't use lazy because GSON will NULL it, as we have no no-args constructor + private var dateOfBirthCache: LocalDate? = null + override val dateOfBirth: LocalDate + get() = dateOfBirthCache ?: dob.toLocalDateLeniently().also { + dateOfBirthCache = it + } +} + +private fun String.toLocalDateLeniently(): LocalDate = try { + LocalDate.parse(this) +} catch (e: Exception) { + Timber.w("Irregular date string: %s", this) + try { + DateTime.parse(this).toLocalDate() + } catch (giveUp: Exception) { + throw giveUp } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/certificate/VaccinationDccV1Parser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/certificate/VaccinationDccV1Parser.kt index 9a5a488651f200f2d9ebeb33d1ec0e7f60182d24..725d2082d5b77aa8e38c4d2a7662da7f92f5440f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/certificate/VaccinationDccV1Parser.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/certificate/VaccinationDccV1Parser.kt @@ -4,14 +4,11 @@ import com.google.gson.Gson import com.upokecenter.cbor.CBORObject import dagger.Reusable import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_CWT_NO_DGC -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_CWT_NO_HCERT -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.JSON_SCHEMA_INVALID -import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.VC_NO_VACCINATION_ENTRY +import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException import de.rki.coronawarnapp.util.serialization.BaseGson import de.rki.coronawarnapp.util.serialization.fromJson +import timber.log.Timber import javax.inject.Inject @Reusable @@ -19,47 +16,58 @@ class VaccinationDccV1Parser @Inject constructor( @BaseGson private val gson: Gson ) { - fun parse(map: CBORObject): VaccinationDccV1 = try { + fun parse(map: CBORObject, lenient: Boolean): VaccinationDccV1 = try { map[keyHCert]?.run { this[keyEuDgcV1]?.run { - toCertificate() - } ?: throw InvalidVaccinationCertificateException(HC_CWT_NO_DGC) - } ?: throw InvalidVaccinationCertificateException(HC_CWT_NO_HCERT) + this.toCertificate(lenient = lenient) + } ?: throw InvalidVaccinationCertificateException(ErrorCode.HC_CWT_NO_DGC) + } ?: throw InvalidVaccinationCertificateException(ErrorCode.HC_CWT_NO_HCERT) } catch (e: InvalidHealthCertificateException) { throw e } catch (e: Throwable) { - throw InvalidVaccinationCertificateException(HC_CBOR_DECODING_FAILED) + throw InvalidVaccinationCertificateException(ErrorCode.HC_CBOR_DECODING_FAILED, cause = e) } @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") - private fun VaccinationDccV1.validate(): VaccinationDccV1 { - if (payloads.isNullOrEmpty()) { - throw InvalidVaccinationCertificateException(VC_NO_VACCINATION_ENTRY) + private fun VaccinationDccV1.toValidated(lenient: Boolean): VaccinationDccV1 = this + .run { + if (payloads.isEmpty()) throw InvalidVaccinationCertificateException(ErrorCode.VC_NO_VACCINATION_ENTRY) + + if (payloads.size == 1) return@run this + + if (lenient) { + Timber.w("Lenient: Vaccination data contained multiple entries.") + copy(payloads = listOf(payloads.maxByOrNull { it.vaccinatedAt }!!)) + } else { + throw InvalidVaccinationCertificateException(ErrorCode.VC_MULTIPLE_VACCINATION_ENTRIES) + } } - // check for non null (Gson does not enforce it) & force date parsing - require(version.isNotBlank()) - require(nameData.familyNameStandardized.isNotBlank()) - dateOfBirth - payload.let { - it.vaccinatedAt - require(it.certificateIssuer.isNotBlank()) - require(it.certificateCountry.isNotBlank()) - require(it.marketAuthorizationHolderId.isNotBlank()) - require(it.medicalProductId.isNotBlank()) - require(it.targetId.isNotBlank()) - require(it.doseNumber > 0) - require(it.totalSeriesOfDoses > 0) + .apply { + // Apply otherwise we risk accidentally accessing the original obj in the outer scope + // Force date parsing + // check for non null (Gson does not enforce it) & force date parsing + require(version.isNotBlank()) + require(nameData.familyNameStandardized.isNotBlank()) + dateOfBirth + payload.let { + it.vaccinatedAt + require(it.certificateIssuer.isNotBlank()) + require(it.certificateCountry.isNotBlank()) + require(it.marketAuthorizationHolderId.isNotBlank()) + require(it.medicalProductId.isNotBlank()) + require(it.targetId.isNotBlank()) + require(it.doseNumber > 0) + require(it.totalSeriesOfDoses > 0) + } } - return this - } - private fun CBORObject.toCertificate() = try { + private fun CBORObject.toCertificate(lenient: Boolean): VaccinationDccV1 = try { val json = ToJSONString() - gson.fromJson<VaccinationDccV1>(json).validate() + gson.fromJson<VaccinationDccV1>(json).toValidated(lenient = lenient) } catch (e: InvalidVaccinationCertificateException) { throw e } catch (e: Throwable) { - throw InvalidVaccinationCertificateException(JSON_SCHEMA_INVALID) + throw InvalidVaccinationCertificateException(ErrorCode.JSON_SCHEMA_INVALID) } companion object { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt index 5ca2b2aff5d4e907f7af9a71470d29156220b5fa..99fd3123e6b1a3383a93a320197db335ff5c1644 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt @@ -26,14 +26,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( qrCode = rawString, @@ -55,13 +55,13 @@ class VaccinationQRCodeExtractor @Inject constructor( throw InvalidVaccinationCertificateException(HC_ZLIB_DECOMPRESSION_FAILED) } - fun RawCOSEObject.parse(): DccData<VaccinationDccV1> = try { + fun RawCOSEObject.parse(lenient: Boolean): DccData<VaccinationDccV1> = try { Timber.v("Parsing COSE for vaccination certificate.") val cbor = coseDecoder.decode(this) DccData( 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/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeValidator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeValidator.kt index 098ae60b3dee9511a2d5c57f29547982114cbad9..57c93cbf502ff71860761ac47d9b6b5354cc9902 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeValidator.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/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 InvalidVaccinationCertificateException(VC_PREFIX_INVALID) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/VaccinationContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/VaccinationContainer.kt index 61aa1c59989870f4ec1c28cb53496093cf72040b..ca7f06e31c442fba189536d5f1e0b3ea0a542730 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/VaccinationContainer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/VaccinationContainer.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.storag import androidx.annotation.Keep import com.google.gson.annotations.SerializedName +import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor.Mode import de.rki.coronawarnapp.covidcertificate.common.certificate.CertificatePersonIdentifier import de.rki.coronawarnapp.covidcertificate.common.certificate.DccData import de.rki.coronawarnapp.covidcertificate.common.certificate.DccHeader @@ -31,7 +32,7 @@ data class VaccinationContainer internal constructor( @delegate:Transient internal val certificateData: DccData<VaccinationDccV1> by lazy { - preParsedData ?: qrCodeExtractor.extract(vaccinationQrCode).data + preParsedData ?: qrCodeExtractor.extract(vaccinationQrCode, mode = Mode.CERT_VAC_LENIENT).data } val header: DccHeader diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/ui/scan/VaccinationQrCodeScanFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/ui/scan/VaccinationQrCodeScanFragment.kt index ffb9537416eda49f1d4d29d61afed701da3542a7..b7ab20f24009e3d6493e570de8dcf8f8191f7643 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/ui/scan/VaccinationQrCodeScanFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/ui/scan/VaccinationQrCodeScanFragment.kt @@ -10,8 +10,10 @@ import com.google.zxing.BarcodeFormat import com.journeyapps.barcodescanner.DefaultDecoderFactory import de.rki.coronawarnapp.R import de.rki.coronawarnapp.bugreporting.ui.toErrorDialogBuilder +import de.rki.coronawarnapp.covidcertificate.exception.InvalidVaccinationCertificateException import de.rki.coronawarnapp.databinding.FragmentScanQrCodeBinding import de.rki.coronawarnapp.util.DialogHelper +import de.rki.coronawarnapp.util.ExternalActionHelper.openUrl import de.rki.coronawarnapp.util.di.AutoInject import de.rki.coronawarnapp.util.permission.CameraPermissionHelper import de.rki.coronawarnapp.util.ui.doNavigate @@ -67,6 +69,11 @@ class VaccinationQrCodeScanFragment : binding.qrCodeScanSpinner.hide() it.toErrorDialogBuilder(requireContext()).apply { setOnDismissListener { popBackStack() } + if (it is InvalidVaccinationCertificateException && it.showFaqButton) { + setNeutralButton(R.string.error_button_vc_faq) { _, _ -> + openUrl(getString(R.string.error_button_vc_faq_link)) + } + } }.show() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInEvent.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInEvent.kt index b7c535d6e3aff991e1e08a40d8a5560e19adb1ff..ab3433e8ae64f52ff3e1f2a746131cc96811dbdb 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInEvent.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInEvent.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.ui.presencetracing.attendee.checkins import de.rki.coronawarnapp.presencetracing.checkins.CheckIn import de.rki.coronawarnapp.presencetracing.checkins.qrcode.VerifiedTraceLocation +import de.rki.coronawarnapp.util.ui.LazyString sealed class CheckInEvent { @@ -17,6 +18,8 @@ sealed class CheckInEvent { data class ConfirmSwipeItem(val checkIn: CheckIn, val position: Int) : CheckInEvent() + data class InvalidQrCode(val errorText: LazyString) : CheckInEvent() + object ShowInformation : CheckInEvent() object OpenDeviceSettings : CheckInEvent() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInsFragment.kt index 2ca5bc2612083c3f9b6fc75ae8e1c6c923f468a1..73735fe037f5e681b4ffe38b8f083f628b2ea12f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInsFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInsFragment.kt @@ -31,6 +31,7 @@ import de.rki.coronawarnapp.util.lists.decorations.TopBottomPaddingDecorator import de.rki.coronawarnapp.util.lists.diffutil.update import de.rki.coronawarnapp.util.onScroll import de.rki.coronawarnapp.util.tryHumanReadableError +import de.rki.coronawarnapp.util.ui.LazyString import de.rki.coronawarnapp.util.ui.doNavigate import de.rki.coronawarnapp.util.ui.observe2 import de.rki.coronawarnapp.util.ui.viewBinding @@ -66,14 +67,8 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag bindRecycler() bindFAB() - viewModel.checkins.observe2(this) { items -> - updateViews(items) - } - - viewModel.events.observe2(this) { - onNavigationEvent(it) - } - + viewModel.checkins.observe2(this) { items -> updateViews(items) } + viewModel.events.observe2(this) { it?.let { onNavigationEvent(it) } } viewModel.errorEvent.observe2(this) { val errorForHumans = it.tryHumanReadableError(requireContext()) Toast.makeText(requireContext(), errorForHumans.description, Toast.LENGTH_LONG).show() @@ -85,7 +80,7 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag viewModel.checkCameraSettings() } - private fun onNavigationEvent(event: CheckInEvent?) { + private fun onNavigationEvent(event: CheckInEvent) { when (event) { is CheckInEvent.ConfirmCheckIn -> { setupAxisTransition() @@ -127,9 +122,21 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag doNavigate(CheckInsFragmentDirections.actionCheckInsFragmentToCheckInOnboardingFragment(false)) } is CheckInEvent.OpenDeviceSettings -> openDeviceSettings() + is CheckInEvent.InvalidQrCode -> showInvalidQrCodeInformation(event.errorText) } } + private fun showInvalidQrCodeInformation(lazyErrorText: LazyString) { + val errorText = lazyErrorText.get(requireContext()) + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.trace_location_attendee_invalid_qr_code_dialog_title) + .setMessage(getString(R.string.trace_location_attendee_invalid_qr_code_dialog_message, errorText)) + .setPositiveButton(R.string.trace_location_attendee_invalid_qr_code_dialog_positive_button) { _, _ -> + // NO-OP + } + .show() + } + private fun updateViews(items: List<CheckInsItem>) { checkInsAdapter.update(items) binding.apply { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInsViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInsViewModel.kt index 49ff5d5c58b886f892112fdbc136c6aa3a3cb786..c8aaf8345fc7b71785f2e44711f94f95207cbbb6 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInsViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInsViewModel.kt @@ -20,6 +20,8 @@ import de.rki.coronawarnapp.util.coroutine.AppScope import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.flow.intervalFlow import de.rki.coronawarnapp.util.ui.SingleLiveEvent +import de.rki.coronawarnapp.util.ui.toLazyString +import de.rki.coronawarnapp.util.ui.toResolvingString import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory import kotlinx.coroutines.CoroutineScope @@ -140,15 +142,24 @@ class CheckInsViewModel @AssistedInject constructor( } private fun verifyUri(uri: String) = launch { - Timber.i("uri: $uri") - val qrCodePayload = qrCodeUriParser.getQrCodePayload(uri) - when (val verifyResult = traceLocationVerifier.verifyTraceLocation(qrCodePayload)) { - is TraceLocationVerifier.VerificationResult.Valid -> events.postValue( - if (cleanHistory) - CheckInEvent.ConfirmCheckInWithoutHistory(verifyResult.verifiedTraceLocation) - else - CheckInEvent.ConfirmCheckIn(verifyResult.verifiedTraceLocation) - ) + try { + Timber.i("uri: $uri") + val qrCodePayload = qrCodeUriParser.getQrCodePayload(uri) + when (val verifyResult = traceLocationVerifier.verifyTraceLocation(qrCodePayload)) { + is TraceLocationVerifier.VerificationResult.Valid -> events.postValue( + if (cleanHistory) + CheckInEvent.ConfirmCheckInWithoutHistory(verifyResult.verifiedTraceLocation) + else + CheckInEvent.ConfirmCheckIn(verifyResult.verifiedTraceLocation) + ) + is TraceLocationVerifier.VerificationResult.Invalid -> events.postValue( + CheckInEvent.InvalidQrCode(verifyResult.errorTextRes.toResolvingString()) + ) + } + } catch (e: Exception) { + Timber.d(e, "TraceLocation verification failed") + val msg = e.message ?: "QR-Code was invalid" + events.postValue(CheckInEvent.InvalidQrCode(msg.toLazyString())) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/ViewBindingExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/ViewBindingExtensions.kt index 21b663e70d2ee6a4e9859658b1ae959827b7eb0d..0ccd319854341a807c7a5ceffe10ea6f51685831 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/ViewBindingExtensions.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/ViewBindingExtensions.kt @@ -9,6 +9,7 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.viewbinding.ViewBinding +import com.google.android.material.bottomnavigation.BottomNavigationView import timber.log.Timber import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty @@ -44,12 +45,13 @@ class ViewBindingProperty<ComponentT : LifecycleOwner, BindingT : ViewBinding>( private val onDestroyObserver = object : DefaultLifecycleObserver { // Called right before Fragment.onDestroyView override fun onDestroy(owner: LifecycleOwner) { + Timber.tag(TAG).v("onDestroy(%s)", owner) localRef?.lifecycle?.removeObserver(this) ?: return localRef = null uiHandler.post { - Timber.v("Resetting viewBinding") + Timber.tag(TAG).v("Resetting viewBinding owner=%s", owner) viewBinding = null } } @@ -58,15 +60,34 @@ class ViewBindingProperty<ComponentT : LifecycleOwner, BindingT : ViewBinding>( @MainThread override fun getValue(thisRef: ComponentT, property: KProperty<*>): BindingT { if (localRef == null && viewBinding != null) { - Timber.w("Fragment.onDestroyView() was called, but the handler didn't execute our delayed reset.") + Timber.tag(TAG).w( + "Fragment.onDestroyView(%s) was called, but the handler didn't execute our delayed reset.", + thisRef + ) /** - * There is a fragment racecondition if you navigate to another fragment and quickly popBackStack(). + * There is a fragment race condition if you navigate to another fragment and quickly popBackStack(). * Our uiHandler.post { } will not have executed for some reason. * In that case we manually null the old viewBinding, to allow for clean recreation. */ viewBinding = null } + /** + * This is a fix for an edge case where the fragment is created but was never visible to the user + * [DefaultLifecycleObserver.onDestroy] was never called despite that the [Fragment.onDestroyView] was called, + * therefore the ViewBinding will not be set to `null` and will hold the old view ,while Fragment will + * inflate a new [View] when navigating back to it. As result of that the screen ends being blank. + * + * This is very specific case when navigating by deeplink to one of the [BottomNavigationView] destinations, + * that is not the "home destination" of the graph. + */ + (localRef as? Fragment)?.view?.let { + if (it != viewBinding?.root && localRef === thisRef) { + Timber.tag(TAG).w("Different view for the same fragment, resetting old viewBinding") + viewBinding = null + } + } + viewBinding?.let { // Only accessible from within the same component require(localRef === thisRef) @@ -76,9 +97,12 @@ class ViewBindingProperty<ComponentT : LifecycleOwner, BindingT : ViewBinding>( val lifecycle = lifecycleOwnerProvider(thisRef).lifecycle return bindingProvider(thisRef).also { + Timber.tag(TAG).d("bindingProvider(%s)", thisRef) viewBinding = it localRef = thisRef lifecycle.addObserver(onDestroyObserver) } } } + +private const val TAG = "ViewBindingExtension" diff --git a/Corona-Warn-App/src/main/res/layout/fragment_information.xml b/Corona-Warn-App/src/main/res/layout/fragment_information.xml index bc53ba5441279ce8870bad5d40ae449452bf192e..6d7a5f498a266e3fcf2c71262da06becd2a1b89e 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_information.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_information.xml @@ -122,7 +122,14 @@ android:paddingBottom="@dimen/spacing_tiny" tools:text="16000000" tools:visibility="visible" /> + + <!-- Workaround for scrolling issue where view is + approximately as high as available space--> + <Space + android:layout_width="match_parent" + android:layout_height="@dimen/spacing_huge" /> </LinearLayout> + </ScrollView> </androidx.constraintlayout.widget.ConstraintLayout> </layout> diff --git a/Corona-Warn-App/src/main/res/layout/vaccination_home_card.xml b/Corona-Warn-App/src/main/res/layout/vaccination_home_card.xml index 8d6853daebc5542e665479f3d2284195825f1590..dad249239d005a512851feaeb41259eba8f1782d 100644 --- a/Corona-Warn-App/src/main/res/layout/vaccination_home_card.xml +++ b/Corona-Warn-App/src/main/res/layout/vaccination_home_card.xml @@ -27,7 +27,7 @@ android:layout_marginTop="@dimen/spacing_small" android:accessibilityHeading="true" android:focusable="false" - android:text="@string/vaccination_card_registration_title_line_1" + android:text="@string/vaccination_card_status_title_line_1" android:textColor="@color/colorTextPrimary1InvertedStable" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -41,7 +41,7 @@ android:layout_marginHorizontal="@dimen/card_padding" android:accessibilityHeading="true" android:focusable="false" - android:text="@string/vaccination_card_registration_title_line_2" + android:text="@string/vaccination_card_status_title_line_2" android:textColor="@color/colorTextPrimary1InvertedStable" android:textStyle="bold" app:layout_constraintEnd_toEndOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/vaccination_home_immune_card.xml b/Corona-Warn-App/src/main/res/layout/vaccination_home_immune_card.xml index 8da269c8163145d38375e0e578e9fa33dde004b6..32bfb351b107bc3ca070de3870499eb284cb14b0 100644 --- a/Corona-Warn-App/src/main/res/layout/vaccination_home_immune_card.xml +++ b/Corona-Warn-App/src/main/res/layout/vaccination_home_immune_card.xml @@ -27,7 +27,7 @@ android:layout_marginTop="@dimen/spacing_small" android:accessibilityHeading="true" android:focusable="false" - android:text="@string/vaccination_card_registration_title_line_1" + android:text="@string/vaccination_card_status_title_line_1" android:textColor="@color/colorTextPrimary1InvertedStable" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -41,7 +41,7 @@ android:layout_marginHorizontal="@dimen/card_padding" android:accessibilityHeading="true" android:focusable="false" - android:text="@string/vaccination_card_registration_title_line_2" + android:text="@string/vaccination_card_status_title_line_2" android:textColor="@color/colorTextPrimary1InvertedStable" android:textStyle="bold" app:layout_constraintEnd_toEndOf="parent" diff --git a/Corona-Warn-App/src/main/res/values-bg/vaccination_strings.xml b/Corona-Warn-App/src/main/res/values-bg/vaccination_strings.xml index 6eeeb4c291f99bd1526977a9094253cd04784d8e..fe8bd5d56c8839e6420023b221c98576bb25c0d9 100644 --- a/Corona-Warn-App/src/main/res/values-bg/vaccination_strings.xml +++ b/Corona-Warn-App/src/main/res/values-bg/vaccination_strings.xml @@ -48,9 +48,9 @@ <!-- XBUT: Vaccination List delete button --> <string name="vaccination_list_delete_button">"Изтриване"</string> <!-- XTXT: Vaccination List deletion dialog title--> - <string name="vaccination_list_deletion_dialog_title">"Желаете ли да изтриете Ñертификата за вакÑиниране?"</string> + <string name="vaccination_list_deletion_dialog_title">"Желаете ли да изтриете вакÑÐ¸Ð½Ð°Ñ†Ð¸Ð¾Ð½Ð½Ð¸Ñ Ñертификат?"</string> <!-- XTXT: Vaccination List deletion dialog message--> - <string name="vaccination_list_deletion_dialog_message">"Ðко изтриете Ñертификата за вакÑиниране, приложението вече нÑма да може да използва вакÑинациÑта за удоÑтоверÑване на Ð²Ð°ÑˆÐ¸Ñ Ð²Ð°ÐºÑинационен ÑтатуÑ."</string> + <string name="vaccination_list_deletion_dialog_message">"Ðко изтриете Ñертификата, приложението вече нÑма да може да използва тази вакÑÐ¸Ð½Ð°Ñ†Ð¸Ñ Ð·Ð° удоÑтоверÑване на Ð²Ð°ÑˆÐ¸Ñ Ð²Ð°ÐºÑинационен ÑтатуÑ."</string> <!-- XBUT: Vaccination List deletion dialog positive button--> <string name="vaccination_list_deletion_dialog_positive_button">"Изтриване"</string> <!-- XBUT: Vaccination List deletion dialog negative button--> @@ -86,15 +86,15 @@ </plurals> <!-- XTXT: Vaccination QR code scan error message--> - <string name="error_vc_invalid">"Този QR код не е валиден Ñертификат за вакÑиниране."</string> + <string name="error_vc_invalid">"Този QR не е валиден вакÑинационен Ñертификат.\n\nЗа да разберете как можете да Ñе Ñдобиете ÑÑŠÑ Ñертификат за извършена вакÑинациÑ, вижте Ñтраницата ни Ñ Ñ‡ÐµÑто задавани въпроÑи (ЧЗВ)."</string> <!-- XTXT: Vaccination QR code scan error message--> - <string name="error_vc_not_yet_supported">"Този Ñертификат за вакÑиниране вÑе още не Ñе поддържа във верÑиÑта на вашето приложение. МолÑ, актуализирайте приложението Ñи или Ñе Ñвържете Ñ Ð³Ð¾Ñ€ÐµÑ‰Ð°Ñ‚Ð° Ð»Ð¸Ð½Ð¸Ñ Ð·Ð° техничеÑки проблеми от â€œÐ˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð·Ð° приложениетоâ€."</string> + <string name="error_vc_not_yet_supported">"Този вакÑинационен Ñертификат вÑе още не Ñе поддържа във верÑиÑта на Вашето приложение. МолÑ, актуализирайте приложението Ñи или Ñе Ñвържете Ñ Ð³Ð¾Ñ€ÐµÑ‰Ð°Ñ‚Ð° Ð»Ð¸Ð½Ð¸Ñ Ð·Ð° техничеÑки проблеми, поÑочена в â€žÐ˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð·Ð° приложението“."</string> <!-- XTXT: Vaccination QR code scan error message--> - <string name="error_vc_scan_again">"Сертификатът за вакÑиниране не може да бъде запазен на Ñмартфона ви. МолÑ, опитайте отново по-къÑно или Ñе Ñвържете Ñ Ð³Ð¾Ñ€ÐµÑ‰Ð°Ñ‚Ð° Ð»Ð¸Ð½Ð¸Ñ Ð·Ð° техничеÑки проблеми от â€œÐ˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð·Ð° приложениетоâ€."</string> + <string name="error_vc_scan_again">"ВакÑинационниÑÑ‚ Ñертификат не може да бъде запазен на Ñмартфона Ви. МолÑ, опитайте отново по-къÑно или Ñе Ñвържете Ñ Ð³Ð¾Ñ€ÐµÑ‰Ð°Ñ‚Ð° Ð»Ð¸Ð½Ð¸Ñ Ð·Ð° техничеÑки проблеми, поÑочена в â€žÐ˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð·Ð° приложението“."</string> <!-- XTXT: Vaccination QR code scan error message--> - <string name="error_vc_already_registered">"Сертификатът за вакÑиниране вече е региÑтриран в приложението ви."</string> + <string name="error_vc_already_registered">"ВакÑинационниÑÑ‚ Ñертификат вече е региÑтриран в приложението Ви."</string> <!-- XTXT: Vaccination QR code scan error message--> - <string name="error_vc_different_person">"Личната Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð² този Ñертификат за вакÑиниране не ÑъответÑтва на тази във вече региÑтрираните Ñертификати. Ð’ приложението можете да региÑтрирате Ñертификати Ñамо за едно лице."</string> + <string name="error_vc_different_person">"Личната Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð² този вакÑинационен Ñертификат не ÑъответÑтва на тази във вече региÑтрираните Ñертификати. Ð’ приложението можете да региÑтрирате Ñертификати Ñамо за едно лице."</string> <!-- XTXT: Vaccination Consent title--> <string name="vaccination_consent_title">"Вашето ÑъглаÑие"</string> @@ -112,5 +112,9 @@ <string name="vaccination_consent_onboarding_legal_information">"За повече Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾Ñ‚Ð½Ð¾Ñно обработката на данни, прочетете декларациÑта за поверителноÑÑ‚."</string> <!-- XBUT: Text for vaccination consent accept button --> <string name="vaccination_consent_accept_button">"Ðапред"</string> + <!-- XBUT: Text for invalid vaccination certificate error button, linking to FAQ--> + <string name="error_button_vc_faq">"ЧЗВ за вакÑинационни Ñертификати"</string> + <!-- XTXT: Explains user about vaccination certificate: URL, has to be "translated" into english (relevant for all languages except german) - https://www.coronawarn.app/en/faq/#vac_cert_invalid --> + <string name="error_button_vc_faq_link">"https://www.coronawarn.app/en/faq/#vac_cert_invalid"</string> </resources> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/values-de/vaccination_strings.xml b/Corona-Warn-App/src/main/res/values-de/vaccination_strings.xml index 073658eb88562ccd8179fb6d7d22387431636cd3..5dac53f66a7249075be755c34e1f42a74a3e98f1 100644 --- a/Corona-Warn-App/src/main/res/values-de/vaccination_strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/vaccination_strings.xml @@ -91,7 +91,7 @@ <string name="vaccination_card_status_vaccination_complete">"Gültig bis einschließlich %s"</string> <!-- XTXT: Vaccination QR code scan error message--> - <string name="error_vc_invalid">Dieser QR-Code ist kein gültiges Impfzertifikat.</string> + <string name="error_vc_invalid">Dieser QR-Code ist kein gültiges Impfzertifikat.\n\nWeitere Informationen zum Erhalt Ihres Impfzertifikats finden Sie in den FAQ.</string> <!-- XTXT: Vaccination QR code scan error message--> <string name="error_vc_not_yet_supported">Dieses Impfzertifikat wird in Ihrer App-Version noch nicht unterstützt. Bitte aktualisieren Sie Ihre App oder wenden Sie sich an die technische Hotline unter „App-Informationen“.</string> <!-- XTXT: Vaccination QR code scan error message--> @@ -117,5 +117,9 @@ <string name="vaccination_consent_onboarding_legal_information">"Ausführliche Informationen zur Datenverarbeitung finden Sie in der Datenschutzerklärung."</string> <!-- XBUT: Text for vaccination consent accept button --> <string name="vaccination_consent_accept_button">"Weiter"</string> + <!-- XBUT: Text for invalid vaccination certificate error button, linking to FAQ--> + <string name="error_button_vc_faq">FAQ zu Impfzertifikaten</string> + <!-- XTXT: Explains user about vaccination certificate: URL, has to be "translated" into english (relevant for all languages except german) - https://www.coronawarn.app/en/faq/#vac_cert_invalid --> + <string name="error_button_vc_faq_link">https://www.coronawarn.app/de/faq/#vac_cert_invalid</string> </resources> diff --git a/Corona-Warn-App/src/main/res/values-en/vaccination_strings.xml b/Corona-Warn-App/src/main/res/values-en/vaccination_strings.xml index 7ba8457914219ee27e49c4a26182a820c56031a0..e9f2c33cfe132f9ed57c4cc8c892449411360a5d 100644 --- a/Corona-Warn-App/src/main/res/values-en/vaccination_strings.xml +++ b/Corona-Warn-App/src/main/res/values-en/vaccination_strings.xml @@ -86,7 +86,7 @@ </plurals> <!-- XTXT: Vaccination QR code scan error message--> - <string name="error_vc_invalid">"This QR code is not a valid vaccination certificate."</string> + <string name="error_vc_invalid">"This QR code is not a valid vaccination certificate.\n\nFor more information about how to receive your vaccination certificate, please see our FAQ page."</string> <!-- XTXT: Vaccination QR code scan error message--> <string name="error_vc_not_yet_supported">"This vaccination certificate is not supported in your app version yet. Please update your app or contact the technical hotline under “App Informationâ€."</string> <!-- XTXT: Vaccination QR code scan error message--> @@ -112,5 +112,9 @@ <string name="vaccination_consent_onboarding_legal_information">"For more detailed information about data processing, please refer to the privacy notice."</string> <!-- XBUT: Text for vaccination consent accept button --> <string name="vaccination_consent_accept_button">"Continue"</string> + <!-- XBUT: Text for invalid vaccination certificate error button, linking to FAQ--> + <string name="error_button_vc_faq">"FAQ for Vaccination Certificates"</string> + <!-- XTXT: Explains user about vaccination certificate: URL, has to be "translated" into english (relevant for all languages except german) - https://www.coronawarn.app/en/faq/#vac_cert_invalid --> + <string name="error_button_vc_faq_link">"https://www.coronawarn.app/en/faq/#vac_cert_invalid"</string> </resources> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/values-pl/vaccination_strings.xml b/Corona-Warn-App/src/main/res/values-pl/vaccination_strings.xml index b0453ba0a663964ad8981315f5c3bca5759cce57..fbf8ad38370692383771ad67cd0f82eaf5a0b287 100644 --- a/Corona-Warn-App/src/main/res/values-pl/vaccination_strings.xml +++ b/Corona-Warn-App/src/main/res/values-pl/vaccination_strings.xml @@ -86,7 +86,7 @@ </plurals> <!-- XTXT: Vaccination QR code scan error message--> - <string name="error_vc_invalid">"Ten kod QR nie jest ważnym Å›wiadectwem szczepienia."</string> + <string name="error_vc_invalid">"Ten kod QR nie jest ważnym Å›wiadectwem szczepienia.\n\nWiÄ™cej informacji na temat tego, jak otrzymać swoje Å›wiadectwo szczepienia, znajduje siÄ™ na naszej stronie „CzÄ™sto zadawane pytaniaâ€."</string> <!-- XTXT: Vaccination QR code scan error message--> <string name="error_vc_not_yet_supported">"To Å›wiadectwo szczepienia nie jest jeszcze obsÅ‚ugiwane w Twojej wersji aplikacji. Zaktualizuj aplikacjÄ™ lub skontaktuj siÄ™ z infoliniÄ… technicznÄ… dostÄ™pnÄ… w sekcji „Informacje o aplikacjiâ€."</string> <!-- XTXT: Vaccination QR code scan error message--> @@ -112,5 +112,9 @@ <string name="vaccination_consent_onboarding_legal_information">"WiÄ™cej informacji na temat przetwarzania danych znajduje siÄ™ w oÅ›wiadczeniu o ochronie prywatnoÅ›ci."</string> <!-- XBUT: Text for vaccination consent accept button --> <string name="vaccination_consent_accept_button">"Kontynuuj"</string> + <!-- XBUT: Text for invalid vaccination certificate error button, linking to FAQ--> + <string name="error_button_vc_faq">"CzÄ™sto zadawane pytania na temat Å›wiadectw szczepieÅ„"</string> + <!-- XTXT: Explains user about vaccination certificate: URL, has to be "translated" into english (relevant for all languages except german) - https://www.coronawarn.app/en/faq/#vac_cert_invalid --> + <string name="error_button_vc_faq_link">"https://www.coronawarn.app/en/faq/#vac_cert_invalid"</string> </resources> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/values-ro/vaccination_strings.xml b/Corona-Warn-App/src/main/res/values-ro/vaccination_strings.xml index 94892166137f4678ec56347e3ab69beac5b9893e..276ff68ee28120c6141de936946719d1c965f212 100644 --- a/Corona-Warn-App/src/main/res/values-ro/vaccination_strings.xml +++ b/Corona-Warn-App/src/main/res/values-ro/vaccination_strings.xml @@ -86,7 +86,7 @@ </plurals> <!-- XTXT: Vaccination QR code scan error message--> - <string name="error_vc_invalid">"Acest cod QR nu este un certificat de vaccinare valabil."</string> + <string name="error_vc_invalid">"Acest cod QR nu este un certificat de vaccinare valabil.\n\nPentru mai multe informaÈ›ii despre modul în care puteÈ›i primi certificatul dvs. de vaccinare, consultaÈ›i pagina noastră de întrebări frecvente."</string> <!-- XTXT: Vaccination QR code scan error message--> <string name="error_vc_not_yet_supported">"Acest certificat de vaccinare nu este încă acceptat de versiunea aplicaÈ›iei dvs. ActualizaÈ›i-vă aplicaÈ›ia sau contactaÈ›i hotline-ul tehnic din „InformaÈ›ii aplicaÈ›ieâ€."</string> <!-- XTXT: Vaccination QR code scan error message--> @@ -112,5 +112,9 @@ <string name="vaccination_consent_onboarding_legal_information">"Pentru mai multe informaÈ›ii despre prelucrarea datelor, consultaÈ›i înÈ™tiinÈ›area de confidenÈ›ialitate."</string> <!-- XBUT: Text for vaccination consent accept button --> <string name="vaccination_consent_accept_button">"Continuare"</string> + <!-- XBUT: Text for invalid vaccination certificate error button, linking to FAQ--> + <string name="error_button_vc_faq">"ÃŽntrebări frecvente privind certificatele de vaccinare"</string> + <!-- XTXT: Explains user about vaccination certificate: URL, has to be "translated" into english (relevant for all languages except german) - https://www.coronawarn.app/en/faq/#vac_cert_invalid --> + <string name="error_button_vc_faq_link">"https://www.coronawarn.app/en/faq/#vac_cert_invalid"</string> </resources> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/values-tr/vaccination_strings.xml b/Corona-Warn-App/src/main/res/values-tr/vaccination_strings.xml index 90a2fe7d930d9215b73f187569875264b208e2ec..b0657b9bd66120e133e6c702353adb9a6e66e791 100644 --- a/Corona-Warn-App/src/main/res/values-tr/vaccination_strings.xml +++ b/Corona-Warn-App/src/main/res/values-tr/vaccination_strings.xml @@ -86,7 +86,7 @@ </plurals> <!-- XTXT: Vaccination QR code scan error message--> - <string name="error_vc_invalid">"Bu QR kod geçerli bir aşı sertifikası deÄŸildir."</string> + <string name="error_vc_invalid">"Bu QR kodu geçerli bir aşı sertifikası deÄŸildir.\n\nAşı sertifikanızı nasıl alacağınız hakkında daha fazla bilgi için lütfen SSS sayfamıza bakın."</string> <!-- XTXT: Vaccination QR code scan error message--> <string name="error_vc_not_yet_supported">"Bu aşı sertifikası henüz uygulamanızın sürümünde desteklenmiyor. Lütfen uygulamanızı güncelleyin veya “Uygulama Bilgileri†bölümünde belirtilen teknik yardım hattı ile iletiÅŸime geçin."</string> <!-- XTXT: Vaccination QR code scan error message--> @@ -112,5 +112,9 @@ <string name="vaccination_consent_onboarding_legal_information">"Veri iÅŸleme hakkında daha ayrıntılı bilgi için lütfen gizlilik bildirimine baÅŸvurun."</string> <!-- XBUT: Text for vaccination consent accept button --> <string name="vaccination_consent_accept_button">"Devam Et"</string> + <!-- XBUT: Text for invalid vaccination certificate error button, linking to FAQ--> + <string name="error_button_vc_faq">"Aşı Sertifikaları için SSS"</string> + <!-- XTXT: Explains user about vaccination certificate: URL, has to be "translated" into english (relevant for all languages except german) - https://www.coronawarn.app/en/faq/#vac_cert_invalid --> + <string name="error_button_vc_faq_link">"https://www.coronawarn.app/en/faq/#vac_cert_invalid"</string> </resources> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/values/vaccination_strings.xml b/Corona-Warn-App/src/main/res/values/vaccination_strings.xml index fd2e7fedf15353a3b458ff5bffe859868add1115..275edfe95a43f0098c32624bb8ce7dd9627c4632 100644 --- a/Corona-Warn-App/src/main/res/values/vaccination_strings.xml +++ b/Corona-Warn-App/src/main/res/values/vaccination_strings.xml @@ -92,7 +92,7 @@ <!-- XTXT: Homescreen card complete vaccination status label --> <string name="vaccination_card_status_vaccination_complete">"Gültig bis einschließlich %s"</string> <!-- XTXT: Vaccination QR code scan error message--> - <string name="error_vc_invalid">"This QR code is not a valid vaccination certificate."</string> + <string name="error_vc_invalid">"This QR code is not a valid vaccination certificate.\n\nFor more information about how to receive your vaccination certificate, please see our FAQ page."</string> <!-- XTXT: Vaccination QR code scan error message--> <string name="error_vc_not_yet_supported">"This vaccination certificate is not supported in your app version yet. Please update your app or contact the technical hotline under “App Informationâ€."</string> <!-- XTXT: Vaccination QR code scan error message--> @@ -118,5 +118,9 @@ <string name="vaccination_consent_onboarding_legal_information">"For more detailed information about data processing, please refer to the privacy notice."</string> <!-- XBUT: Text for vaccination consent accept button --> <string name="vaccination_consent_accept_button">"Continue"</string> + <!-- XBUT: Text for invalid vaccination certificate error button, linking to FAQ--> + <string name="error_button_vc_faq">"FAQ for Vaccination Certificates"</string> + <!-- XTXT: Explains user about vaccination certificate: URL, has to be "translated" into english (relevant for all languages except german) - https://www.coronawarn.app/en/faq/#vac_cert_invalid --> + <string name="error_button_vc_faq_link">"https://www.coronawarn.app/en/faq/#vac_cert_invalid"</string> -</resources> \ No newline at end of file +</resources> 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/covidcertificate/vaccination/core/VaccinationQrCodeTestData.java b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationQrCodeTestData.java index 57d6ad6ef5d0542319e8a2716773624f6ca2ec45..449bb349b7bde6a840af11ed0f3c40bf32b3981a 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationQrCodeTestData.java +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationQrCodeTestData.java @@ -10,4 +10,9 @@ public class VaccinationQrCodeTestData { static public String qrCodeWithNonsenseCountry = "HC1:NCF3Y28.P-O0PS3JPU7RBWBA2*9VTS/9VZ+PLUOVTJ$EB7W3R9B3VN/3A44E./EZ.6Y8C$.C.IK6MA$00J1TQZ9$9IU+S7HP%X9%*MW09:4WB/5SWB20V5VFBBREWO+GIIUF4+PBZR7MNX/N1JIIML/X3Z.Q67RMB6:BJYE26A5NNL:CIM-A*/UZTM+QO: ACV6212500GUC+KM-5AUYGUD1330PFBA855/SNDPCSOC3KMR9X$DB61.0AESG$:THFGP-M/VI2SG/ 22SS+V8OP3R8LDJ50HR6S94JMN-84Q0C+2/8FUV9HH6N91GB3/YCHN6ALFFZL3M116O/IBU6QKJK/3FMQ0TLK-.UQOO$%A $J%H0%*J:DE6/DOKTG*F605WRK8G7S96JG0 4IF:B9VM0CIBRF/XNBOH9 SGIFJ/2CX593I0GE7FFDEQ6+UO5D+HM/2IDBI.ET/L725IHPKB/T/Q9KRJ* NOWN$6K8VOZIHJ5R29KQWSPYKYSDZRJ+1IVBFGPMXEVY6JIYI/ CVBTJ-FY%MO%RUTF17S:1OL8PVXHRPTUOTK/VF%U%:IR G"; static public String qrCodeWithNullValues = "HC1:NCFOXN%TS3DH3ZSUZK+.V0ETD%65NL-AH.TAIOO6+I2HU7A28WAI1G$H4AT4V22F/8X*G8QHJUPZ0BR/S09T./0LWTKD33236J3TA3M*4VV2 73-E3GG396B-43O058YIB73A*G3W19UEBY5:PIDHGNTI4L6YO1%UG/YL WO*Z7ON1 *L:O8PN1QP5O PLU9A/RUX96 B0V1ZZB.T12.H.ZJ$%HN 9GTBIQ16-I5NI5K1*TB3:U-1VVS1UU15%HVLIWQHYZKOP6OH6XO9IE5IVU5P2-GA*PE1H6IO2OO9$G40GHS-O:S9UZ4+FJE 4Y3L 78OAJ/9TL4T1C9 UPVD5BT17$1MV15K1DR1FIEC2F5+1T+UC2FSH9 UP+/UXJDTW5CL52U50$EZ*N.KUW*P .UUQKC.U%KIP3FY5LG1A614I%KZYNNEVQ KB+P8$JG+SB.V Q5FN9ZK1BCTD PPQ3X:J15RM*F9TVYPVE6G1OU-5Q.-OP*17:FX+52AWI1C0:HPE2%90Q:H2SFUGVP56O1W:W7MEGZNBC3WJ0J:*JITJ%W6XK2L3S.GA/S14 FD$G"; static public String qrCodeBlankLastNameStandardized = "HC1:NCFOXN%TS3DH3ZSUZK+.V0ETD%65NL-AH:XIIOO6+ID7BO:H.I5B88MQ3ZMIN9HNO4*J8OX4W$C2VL*LA 43/IE%TE6UG+ZEAT1HQ13W1:O1YUI%F1PN1/T1%%HRP5 R14SI.J9DYHZROVZ05QNZ 20OP748$NI4L6YO1%UG/YL WO*Z7ON1 *L:O8PN1QP5O PLU9A/RUX96 B0V1ZZB.T12.H.ZJ$%HN 9GTBIQ16-I5NI5K1*TB3:U-1VVS1UU15%HVLIWQHYZKOP6OH6XO9IE5IVU5P2-GA*PE1H6IO2OO9$G40GHS-O:S9UZ4+FJE 4Y3L 78OAJ/9TL4T1C9 UP+B9MK93P5AW0:G9.G9/G9F:QQ28R3U6/V.*NT*QM.SY$N-P1S29 34S0BYBRC.UYS1U%O6QKN*Q5-QFRMLNKNM8LK4IYOP$I/XK$M8-L9HCI:ZH2E4UE1B4GC:DN5E:UNJ17$CS*J7.HRT:A /NFZ1FG7K-NXM693K+DD1ET2 D13ST$LGMUW/O7P6CJSZ9NC+Q6DJ1ZJ65QG9MS 1M$FRFTS50*G5V0"; + // Multiple: Irregular date string: 1978-01-26T00:00:00 + static public String qrCodeBulgaria = "HC1:NCFOXN*TS0BI$ZDYSHIAL*ECH 8S02109+D3NDC3LE84DIJ99HE1:G4G5%ZVGZ7:ZH6I1%4J.$2TM9*OVHABVCNN95ZTM.KM7755QLQQ5%YQ+GOSSP8/R2$Q.DPVGOP/R QH$R387-WR9KRN95U/3P+9TG90OARH97KM4HGZJK HGX2MR$CXGG0U2XW4UZ2 NVV5TN%2UP20J5/5LEBFD-48YI+T4D-4HRVUMNMD3323R13-Y6C-4A+2XEN QT QTHC31M3+E3CP456L X4CZKHKB-43.E3KD3OAJ5%IWZKRA38M7323Q05.$S3U2JKB%RBKD3ZQTVJJ$+LQ3QR$P*NIV1JHQE.7W.GLA$2ECJYGCB%GLF9$DF8PQ9Z2*HNA-5NINJ4A*PO5FKYYNJK1G%UJ441JA4JBDC9PAGYCHK4GBLEH-BB.BECH 9MD-HBO55*E12MWKP/HLIJL8JF8JF172V2I0XTWYLM$EJ%MMWB:GSTHS:DW3ZJ0MD$NRF1OS6UE4VO4PFWL:IN9$RR.535T+TCZVFGZVXJPI1EUO9VOF+XBNOSJ/E32V4 L:XMHB0WAWIUJ"; + static public String qrCodeSweden = "HC1:NCFOXN%TSMAHN-HVN8J7UQMJ4/3RZLH62V2G1PC9CMSRH+QKFNTAVD3B19*AJCBMF6.UCOMIN6R%E5BD7HG8CU6O8QGU68ORJSPAEQOIR+SPCVO.28DDQHQ1BW9XX7ZY7NTICZU1*8X/KQ96/-KKTCY73JC3KD3LWT HB3ZC64JX7JQ1LK$2965VMFD-48YI 3533LC4TZ0BR/S09T./0ZYTS P-$0R:67PPDFPVX1R270:6C$Q0R6EOMUF5LDCPF5RBQ746B46O1N646RM9AL5CBVW566LH 469/9-3AKI6%T6LEQ-P6UQK*%NH$RSC9FFFW+7H9N$W2JO2C6S3UJ92KEST.ZJ-8B ZJ83B 2TAAUZZ2LH2%EUBUJZ0KZPIR145%T0YIF0JEYI1DLNCK1627ACW-T%NSY18KT911GL.EHNTI+SB-5A-ARUQNFW$ 2:.NU6W/CU8WDTFVG:BG3JFCSAVH-4V:HP4$0/.D9OV-RM60R7Z3B8PXICK+L/S1P*O:FG"; + // vaccinatedAt: Irregular date string: 2021-05-29T15:31:00+02:00 + static public String qrCodePoland = "HC1:6BFOXN%TS3DH+M8.IAS0RTAN:2MCID:D42:O%CM9W48+5MOOP-I3Z58MJNC5FAPQHIZC4.OI1RM8ZA.A53XHMKN4NN3F85QNPZ0K8C$JCW0KK.A96UJBC.P2R9CZXIAHAPEDG8C5DL-9C.PDI9309D: C+8DV9CA$DPN0NTICZU80LZW4Z*AK.GNNVR*G0C7PHBO33/X086B QTVINMJJDG3AE3RK38FN:43JON$97*97:L32SJ.L78PJ/FJBINB/S7-SN2HOH03I31M3EG3J%4UZ2UI7Y6T4R2H4T8%K+-8*S2E6J1$48X2-36D-I/2DW9J0$9+Q6X46Q3QR$P2OIC0JBLI+USK3UBVTVIJM/I2OC8ALD-ILOVGKFWZ07Y4 CTZ/3+N0ZUIQJAZGA2:UG%UJMI:TU+MM0W5-R53W12XE2O14P3.2O55O:FA$VKN6HQK3OKPEON4QDN7T*.53%1/HVII9H2JS6VS%J*HBXUCY+TU5EBYL5%T3V79YG%Q90MURRHY5D6$NN6VAQI8OEH.5PQ2WJF"; } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationTestComponent.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationTestComponent.kt index 8d1e04bbb267f7c39592ba80aa9f8e725072164e..8bd1d78376d0bab27961029f022987fccad415eb 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationTestComponent.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationTestComponent.kt @@ -3,6 +3,7 @@ package de.rki.coronawarnapp.covidcertificate.vaccination.core import dagger.Component import dagger.Module import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationQRCodeExtractorTest +import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationQrCodeValidatorTest import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.VaccinationRepositoryTest import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.storage.VaccinationContainerTest import de.rki.coronawarnapp.covidcertificate.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/covidcertificate/vaccination/core/VaccinationTestData.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationTestData.kt index 079022777951abd1eedc80dfd9e383b08853b2f4..be9583f22614ad2f72d4a36b2d31e572b36997d8 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationTestData.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationTestData.kt @@ -186,4 +186,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/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt index f4f1ba9748525fed6f803688e938c4cf93953485..b0e8c2fbe520707ff46d6202355714fb1fcb468f 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt @@ -1,5 +1,6 @@ package de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode +import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor.Mode import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_CWT_NO_ISS @@ -30,17 +31,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.data.header) { issuer shouldBe "AT" @@ -77,60 +78,142 @@ 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 HC_CWT_NO_ISS`() { shouldThrow<InvalidVaccinationCertificateException> { - extractor.extract(VaccinationQrCodeTestData.validEncoded) + extractor.extract( + VaccinationQrCodeTestData.validEncoded, + mode = Mode.CERT_VAC_STRICT + ) }.errorCode shouldBe HC_CWT_NO_ISS } @Test fun `random string fails with HC_BASE45_DECODING_FAILED`() { shouldThrow<InvalidVaccinationCertificateException> { - 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<InvalidVaccinationCertificateException> { - 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<InvalidVaccinationCertificateException> { - 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 `null values fail with JSON_SCHEMA_INVALID`() { shouldThrow<InvalidVaccinationCertificateException> { - extractor.extract(VaccinationQrCodeTestData.qrCodeWithNullValues) + extractor.extract( + VaccinationQrCodeTestData.qrCodeWithNullValues, + mode = Mode.CERT_VAC_STRICT + ) }.errorCode shouldBe InvalidHealthCertificateException.ErrorCode.JSON_SCHEMA_INVALID } @Test fun `blank name fails with JSON_SCHEMA_INVALID`() { shouldThrow<InvalidVaccinationCertificateException> { - extractor.extract(VaccinationQrCodeTestData.qrCodeBlankLastNameStandardized) + extractor.extract( + VaccinationQrCodeTestData.qrCodeBlankLastNameStandardized, + mode = Mode.CERT_VAC_STRICT + ) }.errorCode shouldBe InvalidHealthCertificateException.ErrorCode.JSON_SCHEMA_INVALID } + + @Test + fun `Bulgarian qr code passes`() { + val qrCode = extractor.extract( + VaccinationQrCodeTestData.qrCodeBulgaria, + mode = Mode.CERT_VAC_STRICT + ) + with(qrCode.data.header) { + issuer shouldBe "BG" + issuedAt shouldBe Instant.parse("2021-06-02T14:07:56.000Z") + expiresAt shouldBe Instant.parse("2022-06-02T14:07:56.000Z") + } + + with(qrCode.data.certificate) { + with(nameData) { + familyName shouldBe "ПЕТКОВ" + familyNameStandardized shouldBe "PETKOV" + givenName shouldBe "СТÐМО ГЕОРГИЕВ" + givenNameStandardized shouldBe "STAMO<GEORGIEV" + } + dob shouldBe "1978-01-26T00:00:00" + dateOfBirth shouldBe LocalDate.parse("1978-01-26") + version shouldBe "1.0.0" + + payload.apply { + uniqueCertificateIdentifier shouldBe "urn:uvci:01:BG:UFR5PLGKU8WDSZK7#0" + certificateCountry shouldBe "BG" + doseNumber shouldBe 2 + dt shouldBe "2021-03-09T00:00:00" + certificateIssuer shouldBe "Ministry of Health" + marketAuthorizationHolderId shouldBe "ORG-100030215" + medicalProductId shouldBe "EU/1/20/1528" + totalSeriesOfDoses shouldBe 2 + targetId shouldBe "840539006" + vaccineId shouldBe "J07BX03" + vaccinatedAt shouldBe LocalDate.parse("2021-03-09") + } + } + } + + @Test + fun `Swedish qr code passes`() { + extractor.extract( + VaccinationQrCodeTestData.qrCodeSweden, + mode = Mode.CERT_VAC_STRICT + ) + } + + @Test + fun `Polish qr code passes`() { + extractor.extract( + VaccinationQrCodeTestData.qrCodePoland, + mode = Mode.CERT_VAC_STRICT + ) + } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQrCodeValidatorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQrCodeValidatorTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..1a169d4301019ab3fb5a9b2efd5b234f8fe4660f --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQrCodeValidatorTest.kt @@ -0,0 +1,34 @@ +package de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode + +import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor.Mode +import de.rki.coronawarnapp.covidcertificate.vaccination.core.DaggerVaccinationTestComponent +import de.rki.coronawarnapp.covidcertificate.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/covidcertificate/vaccination/core/repository/storage/VaccinationContainerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/VaccinationContainerTest.kt index 7c9d5d97fbaabf88fb8e2f33441b956fa62a9746..2f27974c96c717524a926d978108c9e061e1bd90 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/VaccinationContainerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/VaccinationContainerTest.kt @@ -1,11 +1,18 @@ package de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.storage +import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor import de.rki.coronawarnapp.covidcertificate.common.certificate.CertificatePersonIdentifier import de.rki.coronawarnapp.covidcertificate.vaccination.core.DaggerVaccinationTestComponent import de.rki.coronawarnapp.covidcertificate.vaccination.core.VaccinationTestData +import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationCertificateQRCode +import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationQRCodeExtractor import de.rki.coronawarnapp.covidcertificate.valueset.valuesets.DefaultValueSet import de.rki.coronawarnapp.covidcertificate.valueset.valuesets.VaccinationValueSets 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 @@ -137,4 +144,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 { data } 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.payloads.size shouldBe 1 + } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInsViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInsViewModelTest.kt index dbb826612e6d7ebc4439768e9978e2e3030643ec..7045da2f2f847cbf441d5846d646700cd37224ec 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInsViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInsViewModelTest.kt @@ -3,14 +3,17 @@ package de.rki.coronawarnapp.ui.presencetracing.attendee.checkins import androidx.lifecycle.SavedStateHandle import de.rki.coronawarnapp.presencetracing.checkins.CheckIn import de.rki.coronawarnapp.presencetracing.checkins.CheckInRepository +import de.rki.coronawarnapp.presencetracing.checkins.checkout.CheckOutHandler +import de.rki.coronawarnapp.presencetracing.checkins.qrcode.InvalidQrCodeDataException +import de.rki.coronawarnapp.presencetracing.checkins.qrcode.InvalidQrCodeUriException import de.rki.coronawarnapp.presencetracing.checkins.qrcode.QRCodeUriParser import de.rki.coronawarnapp.presencetracing.checkins.qrcode.TraceLocationVerifier -import de.rki.coronawarnapp.presencetracing.checkins.checkout.CheckOutHandler import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass import de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.items.ActiveCheckInVH import de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.items.CameraPermissionVH import de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.items.PastCheckInVH import de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.permission.CameraPermissionProvider +import io.kotest.assertions.throwables.shouldNotThrow import io.kotest.matchers.shouldBe import io.kotest.matchers.types.shouldBeInstanceOf import io.mockk.MockKAnnotations @@ -180,6 +183,32 @@ class CheckInsViewModelTest : BaseTest() { } } + @Test + fun `Handle uri InvalidQrCodeUriException`() = runBlockingTest { + every { savedState.get<String>("deeplink.last") } returns null + coEvery { qrCodeUriParser.getQrCodePayload(any()) } throws InvalidQrCodeUriException("Invalid") + val url = "https://e.coronawarn.app?v=1#place_holder" + + shouldNotThrow<InvalidQrCodeUriException> { + createInstance(deepLink = url, scope = this).apply { + events.getOrAwaitValue().shouldBeInstanceOf<CheckInEvent.InvalidQrCode>() + } + } + } + + @Test + fun `Handle uri InvalidQrCodeDataException`() = runBlockingTest { + every { savedState.get<String>("deeplink.last") } returns null + coEvery { qrCodeUriParser.getQrCodePayload(any()) } throws InvalidQrCodeDataException("Invalid") + val url = "https://e.coronawarn.app?v=1#place_holder" + + shouldNotThrow<InvalidQrCodeDataException> { + createInstance(deepLink = url, scope = this).apply { + events.getOrAwaitValue().shouldBeInstanceOf<CheckInEvent.InvalidQrCode>() + } + } + } + private fun createInstance(deepLink: String?, scope: CoroutineScope) = CheckInsViewModel( savedState = savedState,