From e9e7635467001f29fbbf2828b6285c4351bf1f34 Mon Sep 17 00:00:00 2001 From: Mohamed Date: Wed, 10 Mar 2021 17:49:30 +0100 Subject: [PATCH] Event verification (EXPOSUREAPP-5423) (#2524) * Basic setup * Test verification * Update DefaultQRCodeVerifierTest.kt * Invert boolean to reflect method name * Adjustments to prepare for refactoring of "Verificationkeys" class. * Add more tests and modify encoded event data * Update ConfirmCheckInViewModelTest.kt * Consider warning case * Add test cases for warning * Fix test * Show error * Add todo * lint * Move time warnings into result * Update ConfirmCheckInViewModelTest.kt Co-authored-by: Matthias Urhahn --- .../qrcode/DefaultQRCodeVerifierTest.kt | 107 ++++++++++++++++++ .../deviceForTesters/res/values/strings.xml | 2 +- .../EventRegistrationModule.kt | 8 +- .../checkins/qrcode/DefaultQRCodeVerifier.kt | 42 +++++++ .../checkins/qrcode/EventQRCode.kt | 10 -- .../qrcode/InvalidQRCodeDataException.kt | 6 + .../qrcode/InvalidQRCodeSignatureException.kt | 6 + .../checkins/qrcode/QRCodeException.kt | 6 + .../checkins/qrcode/QRCodeVerifier.kt | 2 +- .../checkins/qrcode/QRCodeVerifyResult.kt | 15 +++ .../checkin/ConfirmCheckInFragment.kt | 11 +- .../checkin/ConfirmCheckInViewModel.kt | 36 +++--- .../checkin/ConfirmCheckInViewModelTest.kt | 24 ++-- 13 files changed, 220 insertions(+), 55 deletions(-) create mode 100644 Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultQRCodeVerifierTest.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultQRCodeVerifier.kt delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/EventQRCode.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeDataException.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeSignatureException.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeException.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeVerifyResult.kt diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultQRCodeVerifierTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultQRCodeVerifierTest.kt new file mode 100644 index 00000000000..a87914a5e56 --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultQRCodeVerifierTest.kt @@ -0,0 +1,107 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +import de.rki.coronawarnapp.environment.EnvironmentSetup +import de.rki.coronawarnapp.util.security.SignatureValidation +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Instant +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import testhelpers.BaseTestInstrumentation + +@RunWith(JUnit4::class) +class DefaultQRCodeVerifierTest : BaseTestInstrumentation() { + + @MockK lateinit var environmentSetup: EnvironmentSetup + private lateinit var qrCodeVerifier: QRCodeVerifier + + @Before + fun setUp() { + MockKAnnotations.init(this) + every { environmentSetup.appConfigVerificationKey } returns PUB_KEY + qrCodeVerifier = DefaultQRCodeVerifier(SignatureValidation(environmentSetup)) + } + + @Test + fun verifyEventSuccess() = runBlockingTest { + val time = 2687960 * 1_000L + val instant = Instant.ofEpochMilli(time) + shouldNotThrowAny { + val verifyResult = qrCodeVerifier.verify(ENCODED_EVENT) + verifyResult.apply { + singedTraceLocation.event.description shouldBe "CWA Launch Party" + verifyResult.isBeforeStartTime(instant) shouldBe false + verifyResult.isAfterEndTime(instant) shouldBe false + } + } + } + + @Test + fun verifyEventStartTimeWaning() = runBlockingTest { + val time = 2687940 * 1_000L + val instant = Instant.ofEpochMilli(time) + shouldNotThrowAny { + val verifyResult = qrCodeVerifier.verify(ENCODED_EVENT) + verifyResult.apply { + singedTraceLocation.event.description shouldBe "CWA Launch Party" + } + verifyResult.isBeforeStartTime(instant) shouldBe true + verifyResult.isAfterEndTime(instant) shouldBe false + } + } + + @Test + fun verifyEventEndTimeWarning() = runBlockingTest { + val instant = Instant.now() + shouldNotThrowAny { + val verifyResult = qrCodeVerifier.verify(ENCODED_EVENT) + verifyResult.apply { + singedTraceLocation.event.description shouldBe "CWA Launch Party" + } + verifyResult.isBeforeStartTime(instant) shouldBe false + verifyResult.isAfterEndTime(instant) shouldBe true + } + } + + @Test + fun verifyEventWithInvalidKey() = runBlockingTest { + every { environmentSetup.appConfigVerificationKey } returns INVALID_PUB_KEY + shouldThrow { + qrCodeVerifier.verify(ENCODED_EVENT) + } + } + + @Test + fun eventHasMalformedData() = runBlockingTest { + shouldThrow { + qrCodeVerifier.verify(INVALID_ENCODED_EVENT) + } + } + + companion object { + + private const val INVALID_PUB_KEY = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg" + + "3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg==" + + private const val PUB_KEY = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEafIKZOiRPuJWjKOUmKv7OTJWTyii" + + "4oCQLcGn3FgYoLQaJIvAM3Pl7anFDPPY/jxfqqrLyGc0f6hWQ9JPR3QjBw==" + + private const val INVALID_ENCODED_EVENT = + "BIPEY33SMVWSA2LQON2W2IDEN5WG64RAONUXIIDBNVSXILBAMNXRBCM4UQARRKM6UQASAHRKCC7CTDWGQ" + + "4JCO7RVZSWVIMQK4UPA.GBCAEIA7TEORBTUA25QHBOCWT26BCA5PORBS2E4FFWMJ3U" + + "U3P6SXOL7SHUBCA7UEZBDDQ2R6VRJH7WBJKVF7GZYJA6YMRN27IPEP7NKGGJSWX3XQ" + + private const val ENCODED_EVENT = + "BIYAUEDBZY6EIWF7QX6JOKSRPAGEB3H7CIIEGV2BEBGGC5LOMNUCAUDBOJ2" + + "HSGGTQ6SACIHXQ6SACKA6CJEDARQCEEAPHGEZ5JI2K2T422L5U3SMZY5DGC" + + "PUZ2RQACAYEJ3HQYMAFFBU2SQCEEAJAUCJSQJ7WDM675MCMOD3L2UL7ECJU" + + "7TYERH23B746RQTABO3CTI=" + } +} diff --git a/Corona-Warn-App/src/deviceForTesters/res/values/strings.xml b/Corona-Warn-App/src/deviceForTesters/res/values/strings.xml index 8a8f0e4c755..414ff9334ff 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/values/strings.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/values/strings.xml @@ -1,4 +1,4 @@ - "HTTPS://CORONAWARN.APP/E1/BIPEY33SMVWSA2LQON2W2IDEN5WG64RAONUXIIDBNVSXILBAMNXRBCM4UQARRKM6UQASAHRKCC7CTDWGQ4JCO7RVZSWVIMQK4UPA.GBCAEIA7TEORBTUA25QHBOCWT26BCA5PORBS2E4FFWMJ3UU3P6SXOL7SHUBCA7UEZBDDQ2R6VRJH7WBJKVF7GZYJA6YMRN27IPEP7NKGGJSWX3XQ" + "HTTPS://CORONAWARN.APP/E1/BIYAUEDBZY6EIWF7QX6JOKSRPAGEB3H7CIIEGV2BEBGGC5LOMNUCAUDBOJ2HSGGTQ6SACIHXQ6SACKA6CJEDARQCEEAPHGEZ5JI2K2T422L5U3SMZY5DGCPUZ2RQACAYEJ3HQYMAFFBU2SQCEEAJAUCJSQJ7WDM675MCMOD3L2UL7ECJU7TYERH23B746RQTABO3CTI=" \ No newline at end of file diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt index b9234ea0f4f..93780e4dd8a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt @@ -1,9 +1,13 @@ package de.rki.coronawarnapp.eventregistration +import dagger.Binds import dagger.Module +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.DefaultQRCodeVerifier +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeVerifier @Suppress("EmptyClassBlock") @Module -class EventRegistrationModule { - // TODO +abstract class EventRegistrationModule { + @Binds + abstract fun qrCodeVerifier(qrCodeVerifier: DefaultQRCodeVerifier): QRCodeVerifier } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultQRCodeVerifier.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultQRCodeVerifier.kt new file mode 100644 index 00000000000..e0f2dc582aa --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultQRCodeVerifier.kt @@ -0,0 +1,42 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +import de.rki.coronawarnapp.eventregistration.common.decodeBase32 +import de.rki.coronawarnapp.server.protocols.internal.evreg.SignedEventOuterClass +import de.rki.coronawarnapp.util.security.SignatureValidation +import timber.log.Timber +import javax.inject.Inject + +class DefaultQRCodeVerifier @Inject constructor( + private val signatureValidation: SignatureValidation +) : QRCodeVerifier { + + override suspend fun verify(encodedEvent: String): QRCodeVerifyResult { + Timber.tag(TAG).v("Verifying: %s", encodedEvent) + + val signedEvent = try { + SignedEventOuterClass.SignedEvent.parseFrom(encodedEvent.decodeBase32().toByteArray()) + } catch (e: Exception) { + throw InvalidQRCodeDataException(cause = e, message = "QR-code data could not be parsed.") + } + Timber.tag(TAG).d("Parsed to signed event: %s", signedEvent) + + val isValid = try { + signatureValidation.hasValidSignature( + signedEvent.event.toByteArray(), + sequenceOf(signedEvent.signature.toByteArray()) + ) + } catch (e: Exception) { + throw InvalidQRCodeDataException(cause = e, message = "Verification failed.") + } + + if (!isValid) { + throw InvalidQRCodeSignatureException(message = "QR-code did not match signature.") + } + + return QRCodeVerifyResult(signedEvent) + } + + companion object { + private const val TAG = "DefaultQRCodeVerifier" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/EventQRCode.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/EventQRCode.kt deleted file mode 100644 index 69edb248ef9..00000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/EventQRCode.kt +++ /dev/null @@ -1,10 +0,0 @@ -package de.rki.coronawarnapp.eventregistration.checkins.qrcode - -import org.joda.time.Instant - -data class EventQRCode( - val guid: String, - val description: String? = null, - val start: Instant? = null, - val end: Instant? = null, -) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeDataException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeDataException.kt new file mode 100644 index 00000000000..935e86b319f --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeDataException.kt @@ -0,0 +1,6 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +class InvalidQRCodeDataException constructor( + message: String? = null, + cause: Throwable? = null +) : QRCodeException(message, cause) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeSignatureException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeSignatureException.kt new file mode 100644 index 00000000000..eae77689d0a --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeSignatureException.kt @@ -0,0 +1,6 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +class InvalidQRCodeSignatureException constructor( + message: String? = null, + cause: Throwable? = null +) : QRCodeException(message, cause) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeException.kt new file mode 100644 index 00000000000..1fb4c881a81 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeException.kt @@ -0,0 +1,6 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +open class QRCodeException constructor( + message: String? = null, + cause: Throwable? = null +) : Exception(message, cause) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeVerifier.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeVerifier.kt index 910b6237eb3..8e7e2452d58 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeVerifier.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeVerifier.kt @@ -2,5 +2,5 @@ package de.rki.coronawarnapp.eventregistration.checkins.qrcode interface QRCodeVerifier { - suspend fun verify(code: EventQRCode): Boolean + suspend fun verify(encodedEvent: String): QRCodeVerifyResult } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeVerifyResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeVerifyResult.kt new file mode 100644 index 00000000000..239d4dab190 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeVerifyResult.kt @@ -0,0 +1,15 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +import de.rki.coronawarnapp.server.protocols.internal.evreg.SignedEventOuterClass +import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds +import org.joda.time.Instant + +data class QRCodeVerifyResult( + val singedTraceLocation: SignedEventOuterClass.SignedEvent +) { + fun isBeforeStartTime(now: Instant): Boolean = + singedTraceLocation.event.start != 0 && singedTraceLocation.event.start > now.seconds + + fun isAfterEndTime(now: Instant): Boolean = + singedTraceLocation.event.end != 0 && singedTraceLocation.event.end < now.seconds +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInFragment.kt index c1389bd4136..cb50a209218 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInFragment.kt @@ -39,12 +39,13 @@ class ConfirmCheckInFragment : Fragment(R.layout.fragment_confrim_check_in), Aut } // TODO bind data to actual UI - viewModel.eventData.observe2(this) { + viewModel.verifyResult.observe2(this) { + val event = it.singedTraceLocation.event with(binding) { - eventGuid.text = "GUID: %s".format(it.guid) - startTime.text = "Start time: %s".format(it.start) - endTime.text = "End time: %s".format(it.end) - description.text = "Description: %s".format(it.description) + eventGuid.text = "GUID: %s".format(event.guid) + startTime.text = "Start time: %s".format(event.start) + endTime.text = "End time: %s".format(event.end) + description.text = "Description: %s".format(event.description) } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInViewModel.kt index 011f3d4b5b2..6708ebe0084 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInViewModel.kt @@ -3,25 +3,30 @@ package de.rki.coronawarnapp.ui.eventregistration.checkin import androidx.lifecycle.MutableLiveData import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import de.rki.coronawarnapp.eventregistration.checkins.qrcode.EventQRCode -import de.rki.coronawarnapp.eventregistration.common.decodeBase32 -import de.rki.coronawarnapp.server.protocols.internal.evreg.EventOuterClass +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeVerifier +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeVerifyResult +import de.rki.coronawarnapp.exception.ExceptionCategory +import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory -import org.joda.time.Instant +import timber.log.Timber -class ConfirmCheckInViewModel @AssistedInject constructor() : CWAViewModel() { - private val eventLiveData = MutableLiveData() - val eventData = eventLiveData +class ConfirmCheckInViewModel @AssistedInject constructor( + private val qrCodeVerifier: QRCodeVerifier +) : CWAViewModel() { + private val internalVerifyResult = MutableLiveData() + val verifyResult = internalVerifyResult val navigationEvents = SingleLiveEvent() fun decodeEvent(encodedEvent: String) = launch { - // TODO Verify event(EXPOSUREAPP-5423) - // and finalise event parsing logic - val decodedEventString = encodedEvent.split(".")[0].decodeBase32() - val parseEvent = EventOuterClass.Event.parseFrom(decodedEventString.toByteArray()) - eventLiveData.postValue(parseEvent.toEventQrCode()) + // TODO this logic should moved from here. Here user should confirm event only + try { + internalVerifyResult.postValue(qrCodeVerifier.verify(encodedEvent)) + } catch (e: Exception) { + Timber.d(e) + e.report(ExceptionCategory.INTERNAL) + } } fun onClose() { @@ -32,13 +37,6 @@ class ConfirmCheckInViewModel @AssistedInject constructor() : CWAViewModel() { navigationEvents.value = ConfirmCheckInEvent.ConfirmEvent } - private fun EventOuterClass.Event.toEventQrCode() = EventQRCode( - guid = String(guid.toByteArray()), - description = description, - start = Instant.ofEpochMilli(start.toLong()), - end = Instant.ofEpochMilli(end.toLong()) - ) - @AssistedFactory interface Factory : SimpleCWAViewModelFactory } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInViewModelTest.kt index bb02e9962b5..7b9cc3973ef 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInViewModelTest.kt @@ -1,8 +1,9 @@ package de.rki.coronawarnapp.ui.eventregistration.checkin -import de.rki.coronawarnapp.eventregistration.checkins.qrcode.EventQRCode +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeVerifier import io.kotest.matchers.shouldBe -import org.joda.time.Instant +import io.mockk.MockKAnnotations +import io.mockk.impl.annotations.MockK import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -16,23 +17,12 @@ class ConfirmCheckInViewModelTest : BaseTest() { private lateinit var viewModel: ConfirmCheckInViewModel + @MockK lateinit var qrCodeVerifier: QRCodeVerifier + @BeforeEach fun setUp() { - viewModel = ConfirmCheckInViewModel() - } - - @Test - fun decodeEvent() { - val decodedEvent = - "BIPEY33SMVWSA2LQON2W2IDEN5WG64RAONUXIIDBNVSXILBAMNXRBCM4UQARRKM6UQASAHRKCC7CTDWGQ4JCO7RVZSWVIMQK4UPA" + - ".GBCAEIA7TEORBTUA25QHBOCWT26BCA5PORBS2E4FFWMJ3UU3P6SXOL7SHUBCA7UEZBDDQ2R6VRJH7WBJKVF7GZYJA6YMRN27IPEP7NKGGJSWX3XQ" - viewModel.decodeEvent(decodedEvent) - viewModel.eventData.getOrAwaitValue() shouldBe EventQRCode( - guid = "Lorem ipsum dolor sit amet, co", - description = "", - start = Instant.parse("1970-01-01T00:44:50.857Z"), - end = Instant.parse("1970-01-01T00:00:00.030Z") - ) + MockKAnnotations.init(this) + viewModel = ConfirmCheckInViewModel(qrCodeVerifier) } @Test