From 3e51ced0bf4a46128e7e276e3637f2f21170dbc2 Mon Sep 17 00:00:00 2001
From: Matthias Urhahn <matthias.urhahn@sap.com>
Date: Fri, 11 Dec 2020 16:36:45 +0100
Subject: [PATCH] Fix incorrect TRL data being applied to submitted TEKs
 (EXPOSUREAPP-4260) (#1877)

* Explicitly log invalid symptom data.

* Unless the symptom flow is successfully completed, we assume Symptoms.NO_INFORMATION.
The information is based from one fragment to the next.
The stored symptom data is only updated if the last fragment successfully completes.

* Unit test for symptom indication.

* Update Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/calendar/SubmissionSymptomCalendarViewModel.kt

Co-authored-by: chris-cwa <69595386+chris-cwa@users.noreply.github.com>

* Update Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/TransmissionRiskVectorDeterminator.kt

Co-authored-by: chris-cwa <69595386+chris-cwa@users.noreply.github.com>

* fixed super-fatal code formatting

Co-authored-by: chris-cwa <69595386+chris-cwa@users.noreply.github.com>
Co-authored-by: Ralf Gehrer <ralfgehrer@users.noreply.github.com>
Co-authored-by: chris-cwa <chris.cwa.sap@gmail.com>
---
 .../TransmissionRiskVectorDeterminator.kt     |  13 +-
 .../SubmissionSymptomCalendarFragment.kt      |  17 +--
 .../SubmissionSymptomCalendarViewModel.kt     |  43 ++++--
 .../SubmissionSymptomIntroductionFragment.kt  |  14 +-
 .../SubmissionSymptomIntroductionViewModel.kt |  33 +++--
 .../viewmodel/SubmissionNavigationEvents.kt   |  12 --
 .../src/main/res/navigation/nav_graph.xml     |   3 +
 .../SubmissionSymptomCalendarViewModelTest.kt | 114 ++++++++++++++++
 ...missionSymptomIntroductionViewModelTest.kt | 124 ++++++++++++++++++
 9 files changed, 308 insertions(+), 65 deletions(-)
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/symptoms/calendar/SubmissionSymptomCalendarViewModelTest.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/symptoms/introduction/SubmissionSymptomIntroductionViewModelTest.kt

diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/TransmissionRiskVectorDeterminator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/TransmissionRiskVectorDeterminator.kt
index 20d4eb3aa..f3b45ad55 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/TransmissionRiskVectorDeterminator.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/TransmissionRiskVectorDeterminator.kt
@@ -1,6 +1,7 @@
 package de.rki.coronawarnapp.submission
 
 import dagger.Reusable
+import de.rki.coronawarnapp.bugreporting.reportProblem
 import de.rki.coronawarnapp.submission.Symptoms.Indication
 import de.rki.coronawarnapp.submission.Symptoms.StartOf
 import de.rki.coronawarnapp.util.TimeAndDateExtensions.ageInDays
@@ -47,9 +48,17 @@ class TransmissionRiskVectorDeterminator @Inject constructor(
                 is StartOf.MoreThanTwoWeeks -> intArrayOf(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 4, 5)
                 is StartOf.NoInformation -> intArrayOf(5, 6, 8, 8, 8, 7, 5, 3, 2, 1, 1, 1, 1, 1, 1)
                 is StartOf.OneToTwoWeeksAgo -> intArrayOf(1, 1, 1, 1, 2, 3, 4, 5, 6, 6, 7, 7, 6, 6, 4)
-                else -> intArrayOf(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1)
+                else -> {
+                    IllegalStateException("Positive indication without startDate is not allowed: $symptoms")
+                        .reportProblem(
+                            tag = "TransmissionRiskVectorDeterminator",
+                            info = "Symptoms has an invalid state."
+                        )
+                    intArrayOf(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1)
+                }
             }
             Indication.NEGATIVE -> intArrayOf(4, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1)
             Indication.NO_INFORMATION -> intArrayOf(5, 6, 7, 7, 7, 6, 4, 3, 2, 1, 1, 1, 1, 1, 1)
-        })
+        }
+    )
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/calendar/SubmissionSymptomCalendarFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/calendar/SubmissionSymptomCalendarFragment.kt
index 16fdcdafa..ad3e5120a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/calendar/SubmissionSymptomCalendarFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/calendar/SubmissionSymptomCalendarFragment.kt
@@ -4,13 +4,12 @@ import android.content.res.ColorStateList
 import android.os.Bundle
 import android.view.View
 import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.navArgs
 import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.databinding.FragmentSubmissionSymptomCalendarBinding
 import de.rki.coronawarnapp.submission.Symptoms
 import de.rki.coronawarnapp.ui.submission.SubmissionBlockingDialog
 import de.rki.coronawarnapp.ui.submission.SubmissionCancelDialog
-import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents.NavigateToMainActivity
-import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents.NavigateToResultPositiveOtherWarning
 import de.rki.coronawarnapp.util.di.AutoInject
 import de.rki.coronawarnapp.util.formatter.formatCalendarBackgroundButtonStyleByState
 import de.rki.coronawarnapp.util.formatter.formatCalendarButtonStyleByState
@@ -24,12 +23,14 @@ import javax.inject.Inject
 class SubmissionSymptomCalendarFragment : Fragment(R.layout.fragment_submission_symptom_calendar),
     AutoInject {
 
+    private val navArgs by navArgs<SubmissionSymptomCalendarFragmentArgs>()
+
     @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory
     private val viewModel: SubmissionSymptomCalendarViewModel by cwaViewModelsAssisted(
         factoryProducer = { viewModelFactory },
         constructorCall = { factory, _ ->
             factory as SubmissionSymptomCalendarViewModel.Factory
-            factory.create()
+            factory.create(navArgs.symptomIndication)
         }
     )
 
@@ -54,15 +55,7 @@ class SubmissionSymptomCalendarFragment : Fragment(R.layout.fragment_submission_
         }
 
         viewModel.routeToScreen.observe2(this) {
-            when (it) {
-                is NavigateToResultPositiveOtherWarning -> doNavigate(
-                    SubmissionSymptomCalendarFragmentDirections
-                        .actionSubmissionSymptomCalendarFragmentToSubmissionResultPositiveOtherWarningFragment()
-                )
-                is NavigateToMainActivity -> doNavigate(
-                    SubmissionSymptomCalendarFragmentDirections.actionSubmissionSymptomCalendarFragmentToMainFragment()
-                )
-            }
+            doNavigate(it)
         }
 
         viewModel.symptomStart.observe2(this) {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/calendar/SubmissionSymptomCalendarViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/calendar/SubmissionSymptomCalendarViewModel.kt
index 2e7385ca2..4c99371b8 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/calendar/SubmissionSymptomCalendarViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/calendar/SubmissionSymptomCalendarViewModel.kt
@@ -1,28 +1,30 @@
 package de.rki.coronawarnapp.ui.submission.symptoms.calendar
 
 import androidx.lifecycle.asLiveData
+import androidx.navigation.NavDirections
+import com.squareup.inject.assisted.Assisted
 import com.squareup.inject.assisted.AssistedInject
+import de.rki.coronawarnapp.bugreporting.reportProblem
 import de.rki.coronawarnapp.storage.SubmissionRepository
 import de.rki.coronawarnapp.submission.Symptoms
-import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
 import de.rki.coronawarnapp.util.ui.SingleLiveEvent
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory
-import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.MutableStateFlow
 import org.joda.time.LocalDate
 import timber.log.Timber
 
 class SubmissionSymptomCalendarViewModel @AssistedInject constructor(
+    @Assisted val symptomIndication: Symptoms.Indication,
     dispatcherProvider: DispatcherProvider,
     private val submissionRepository: SubmissionRepository
 ) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
 
-    val symptomStart = submissionRepository.currentSymptoms.flow
-        .map { it?.startOfSymptoms }
-        .asLiveData(context = dispatcherProvider.Default)
+    private val symptomStartInternal = MutableStateFlow<Symptoms.StartOf?>(null)
+    val symptomStart = symptomStartInternal.asLiveData(context = dispatcherProvider.Default)
 
-    val routeToScreen: SingleLiveEvent<SubmissionNavigationEvents> = SingleLiveEvent()
+    val routeToScreen = SingleLiveEvent<NavDirections>()
     val showCancelDialog = SingleLiveEvent<Unit>()
     val showUploadDialog = submissionRepository.isSubmissionRunning
         .asLiveData(context = dispatcherProvider.Default)
@@ -48,9 +50,7 @@ class SubmissionSymptomCalendarViewModel @AssistedInject constructor(
     }
 
     private fun updateSymptomStart(startOf: Symptoms.StartOf?) {
-        submissionRepository.currentSymptoms.update {
-            (it ?: Symptoms.NO_INFO_GIVEN).copy(startOfSymptoms = startOf)
-        }
+        symptomStartInternal.value = startOf
     }
 
     fun onCalendarPreviousClicked() {
@@ -58,7 +58,18 @@ class SubmissionSymptomCalendarViewModel @AssistedInject constructor(
     }
 
     fun onDone() {
-        Timber.d("onDone() clicked on calender screen.")
+        if (symptomStartInternal.value == null) {
+            IllegalStateException("Can't finish symptom indication without symptomStart value.")
+                .reportProblem(tag = TAG, "UI should not allow symptom submission without start date.")
+            return
+        }
+        Timber.tag(TAG).d("onDone() clicked on calender screen.")
+        submissionRepository.currentSymptoms.update {
+            Symptoms(
+                symptomIndication = symptomIndication,
+                startOfSymptoms = symptomStartInternal.value
+            ).also { Timber.tag(TAG).v("Symptoms updated to %s", it) }
+        }
         performSubmission()
     }
 
@@ -72,9 +83,11 @@ class SubmissionSymptomCalendarViewModel @AssistedInject constructor(
             try {
                 submissionRepository.startSubmission()
             } catch (e: Exception) {
-                Timber.e(e, "performSubmission() failed.")
+                Timber.tag(TAG).e(e, "performSubmission() failed.")
             } finally {
-                routeToScreen.postValue(SubmissionNavigationEvents.NavigateToMainActivity)
+                routeToScreen.postValue(
+                    SubmissionSymptomCalendarFragmentDirections.actionSubmissionSymptomCalendarFragmentToMainFragment()
+                )
             }
         }
     }
@@ -82,6 +95,10 @@ class SubmissionSymptomCalendarViewModel @AssistedInject constructor(
     @AssistedInject.Factory
     interface Factory : CWAViewModelFactory<SubmissionSymptomCalendarViewModel> {
 
-        fun create(): SubmissionSymptomCalendarViewModel
+        fun create(symptomIndication: Symptoms.Indication): SubmissionSymptomCalendarViewModel
+    }
+
+    companion object {
+        private const val TAG = "SymptomsCalenderVM"
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/introduction/SubmissionSymptomIntroductionFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/introduction/SubmissionSymptomIntroductionFragment.kt
index 077c1167c..98c2da675 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/introduction/SubmissionSymptomIntroductionFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/introduction/SubmissionSymptomIntroductionFragment.kt
@@ -9,7 +9,6 @@ import de.rki.coronawarnapp.databinding.FragmentSubmissionSymptomIntroBinding
 import de.rki.coronawarnapp.submission.Symptoms
 import de.rki.coronawarnapp.ui.submission.SubmissionBlockingDialog
 import de.rki.coronawarnapp.ui.submission.SubmissionCancelDialog
-import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents
 import de.rki.coronawarnapp.util.di.AutoInject
 import de.rki.coronawarnapp.util.formatter.formatBackgroundButtonStyleByState
 import de.rki.coronawarnapp.util.formatter.formatButtonStyleByState
@@ -37,17 +36,8 @@ class SubmissionSymptomIntroductionFragment : Fragment(R.layout.fragment_submiss
         super.onViewCreated(view, savedInstanceState)
         uploadDialog = SubmissionBlockingDialog(requireContext())
 
-        viewModel.routeToScreen.observe2(this) {
-            when (it) {
-                is SubmissionNavigationEvents.NavigateToSymptomCalendar -> doNavigate(
-                    SubmissionSymptomIntroductionFragmentDirections
-                        .actionSubmissionSymptomIntroductionFragmentToSubmissionSymptomCalendarFragment()
-                )
-                is SubmissionNavigationEvents.NavigateToMainActivity -> doNavigate(
-                    SubmissionSymptomIntroductionFragmentDirections
-                        .actionSubmissionSymptomIntroductionFragmentToMainFragment()
-                )
-            }
+        viewModel.navigation.observe2(this) {
+            doNavigate(it)
         }
 
         viewModel.showCancelDialog.observe2(this) {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/introduction/SubmissionSymptomIntroductionViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/introduction/SubmissionSymptomIntroductionViewModel.kt
index 0ca4d571f..f7c697b4f 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/introduction/SubmissionSymptomIntroductionViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/introduction/SubmissionSymptomIntroductionViewModel.kt
@@ -1,17 +1,15 @@
 package de.rki.coronawarnapp.ui.submission.symptoms.introduction
 
 import androidx.lifecycle.asLiveData
+import androidx.navigation.NavDirections
 import com.squareup.inject.assisted.AssistedInject
 import de.rki.coronawarnapp.storage.SubmissionRepository
 import de.rki.coronawarnapp.submission.Symptoms
-import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents
-import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents.NavigateToMainActivity
-import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents.NavigateToSymptomCalendar
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
 import de.rki.coronawarnapp.util.ui.SingleLiveEvent
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
-import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.MutableStateFlow
 import timber.log.Timber
 
 class SubmissionSymptomIntroductionViewModel @AssistedInject constructor(
@@ -19,11 +17,10 @@ class SubmissionSymptomIntroductionViewModel @AssistedInject constructor(
     private val submissionRepository: SubmissionRepository
 ) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
 
-    val symptomIndication = submissionRepository.currentSymptoms.flow
-        .map { it?.symptomIndication }
-        .asLiveData(context = dispatcherProvider.Default)
+    private val symptomIndicationInternal = MutableStateFlow<Symptoms.Indication?>(null)
+    val symptomIndication = symptomIndicationInternal.asLiveData(context = dispatcherProvider.Default)
 
-    val routeToScreen: SingleLiveEvent<SubmissionNavigationEvents> = SingleLiveEvent()
+    val navigation = SingleLiveEvent<NavDirections>()
 
     val showCancelDialog = SingleLiveEvent<Unit>()
     val showUploadDialog = submissionRepository.isSubmissionRunning
@@ -31,8 +28,15 @@ class SubmissionSymptomIntroductionViewModel @AssistedInject constructor(
 
     fun onNextClicked() {
         launch {
-            when (submissionRepository.currentSymptoms.value?.symptomIndication) {
-                Symptoms.Indication.POSITIVE -> routeToScreen.postValue(NavigateToSymptomCalendar)
+            when (symptomIndicationInternal.value) {
+                Symptoms.Indication.POSITIVE -> {
+                    navigation.postValue(
+                        SubmissionSymptomIntroductionFragmentDirections
+                            .actionSubmissionSymptomIntroductionFragmentToSubmissionSymptomCalendarFragment(
+                                symptomIndication = Symptoms.Indication.POSITIVE
+                            )
+                    )
+                }
                 Symptoms.Indication.NEGATIVE -> doSubmit()
                 Symptoms.Indication.NO_INFORMATION -> showCancelDialog.postValue(Unit)
             }
@@ -57,9 +61,7 @@ class SubmissionSymptomIntroductionViewModel @AssistedInject constructor(
 
     private fun updateSymptomIndication(indication: Symptoms.Indication) {
         Timber.d("updateSymptomIndication(indication=$indication)")
-        submissionRepository.currentSymptoms.update {
-            (it ?: Symptoms.NO_INFO_GIVEN).copy(symptomIndication = indication)
-        }
+        symptomIndicationInternal.value = indication
     }
 
     fun onCancelConfirmed() {
@@ -74,7 +76,10 @@ class SubmissionSymptomIntroductionViewModel @AssistedInject constructor(
             } catch (e: Exception) {
                 Timber.e(e, "doSubmit() failed.")
             } finally {
-                routeToScreen.postValue(NavigateToMainActivity)
+                navigation.postValue(
+                    SubmissionSymptomIntroductionFragmentDirections
+                        .actionSubmissionSymptomIntroductionFragmentToMainFragment()
+                )
             }
         }
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionNavigationEvents.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionNavigationEvents.kt
index 3fa560ce1..7bb0e89b2 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionNavigationEvents.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionNavigationEvents.kt
@@ -3,23 +3,11 @@ package de.rki.coronawarnapp.ui.submission.viewmodel
 sealed class SubmissionNavigationEvents {
     object NavigateToContact : SubmissionNavigationEvents()
     object NavigateToDispatcher : SubmissionNavigationEvents()
-    object NavigateToSubmissionDone : SubmissionNavigationEvents()
-    object NavigateToSubmissionIntro : SubmissionNavigationEvents()
     object NavigateToQRCodeScan : SubmissionNavigationEvents()
     object NavigateToDataPrivacy : SubmissionNavigationEvents()
 
-    object NavigateToResultPositiveOtherWarning : SubmissionNavigationEvents()
-
-    object NavigateToResultPositiveOtherWarningNoConsent : SubmissionNavigationEvents()
-
-    object NavigateToSymptomSubmission : SubmissionNavigationEvents()
-    object NavigateToSymptomCalendar : SubmissionNavigationEvents()
-
     object NavigateToSymptomIntroduction : SubmissionNavigationEvents()
     object NavigateToTAN : SubmissionNavigationEvents()
-    object NavigateToTestResult : SubmissionNavigationEvents()
     object NavigateToConsent : SubmissionNavigationEvents()
-    object NavigateToYourConsent : SubmissionNavigationEvents()
     object NavigateToMainActivity : SubmissionNavigationEvents()
-    object ShowCancelDialog : SubmissionNavigationEvents()
 }
diff --git a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml
index 47e0e681b..77bad0e0c 100644
--- a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml
+++ b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml
@@ -374,6 +374,9 @@
         android:id="@+id/submissionSymptomCalendarFragment"
         android:name="de.rki.coronawarnapp.ui.submission.symptoms.calendar.SubmissionSymptomCalendarFragment"
         android:label="SubmissionSymptomCalendarFragment">
+        <argument
+            android:name="symptomIndication"
+            app:argType="de.rki.coronawarnapp.submission.Symptoms$Indication" />
         <action
             android:id="@+id/action_submissionCalendarFragment_to_submissionSymptomIntroductionFragment"
             app:destination="@id/submissionSymptomIntroductionFragment" />
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/symptoms/calendar/SubmissionSymptomCalendarViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/symptoms/calendar/SubmissionSymptomCalendarViewModelTest.kt
new file mode 100644
index 000000000..83478fd77
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/symptoms/calendar/SubmissionSymptomCalendarViewModelTest.kt
@@ -0,0 +1,114 @@
+package de.rki.coronawarnapp.ui.submission.symptoms.calendar
+
+import de.rki.coronawarnapp.storage.SubmissionRepository
+import de.rki.coronawarnapp.submission.Symptoms
+import de.rki.coronawarnapp.util.preferences.FlowPreference
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.Runs
+import io.mockk.clearAllMocks
+import io.mockk.coEvery
+import io.mockk.coVerifySequence
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.just
+import io.mockk.verify
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
+import org.joda.time.LocalDate
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import testhelpers.BaseTest
+import testhelpers.TestDispatcherProvider
+import testhelpers.extensions.InstantExecutorExtension
+import testhelpers.preferences.mockFlowPreference
+
+@ExtendWith(InstantExecutorExtension::class)
+class SubmissionSymptomCalendarViewModelTest : BaseTest() {
+
+    @MockK lateinit var submissionRepository: SubmissionRepository
+    private lateinit var currentSymptoms: FlowPreference<Symptoms?>
+
+    @BeforeEach
+    fun setUp() {
+        MockKAnnotations.init(this, relaxed = true)
+
+        currentSymptoms = mockFlowPreference(null)
+
+        every { submissionRepository.isSubmissionRunning } returns flowOf(false)
+        coEvery { submissionRepository.startSubmission() } just Runs
+        every { submissionRepository.currentSymptoms } returns currentSymptoms
+    }
+
+    @AfterEach
+    fun tearDown() {
+        clearAllMocks()
+    }
+
+    private fun createViewModel(indication: Symptoms.Indication = Symptoms.Indication.POSITIVE) =
+        SubmissionSymptomCalendarViewModel(
+            symptomIndication = indication,
+            dispatcherProvider = TestDispatcherProvider,
+            submissionRepository = submissionRepository
+        )
+
+    @Test
+    fun `symptom indication is not written to settings`() {
+        createViewModel().apply {
+            onLastSevenDaysStart()
+            onOneToTwoWeeksAgoStart()
+            onMoreThanTwoWeeksStart()
+            onNoInformationStart()
+            onDateSelected(LocalDate.now())
+        }
+
+        verify(exactly = 0) { submissionRepository.currentSymptoms }
+    }
+
+    @Test
+    fun `submission by symptom completion updates symptom data`() {
+        createViewModel().apply {
+            onLastSevenDaysStart()
+            onDone()
+        }
+
+        coVerifySequence {
+            submissionRepository.isSubmissionRunning
+            submissionRepository.currentSymptoms
+            submissionRepository.startSubmission()
+        }
+
+        currentSymptoms.value shouldBe Symptoms(
+            startOfSymptoms = Symptoms.StartOf.LastSevenDays,
+            symptomIndication = Symptoms.Indication.POSITIVE
+        )
+    }
+
+    @Test
+    fun `submission by abort does not write any symptom data`() {
+        createViewModel().onCancelConfirmed()
+
+        coVerifySequence {
+            submissionRepository.isSubmissionRunning
+            submissionRepository.startSubmission()
+        }
+    }
+
+    @Test
+    fun `submission shows upload dialog`() {
+        val uploadStatus = MutableStateFlow(false)
+        every { submissionRepository.isSubmissionRunning } returns uploadStatus
+        createViewModel().apply {
+            showUploadDialog.observeForever { }
+            showUploadDialog.value shouldBe false
+
+            uploadStatus.value = true
+            showUploadDialog.value shouldBe true
+
+            uploadStatus.value = false
+            showUploadDialog.value shouldBe false
+        }
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/symptoms/introduction/SubmissionSymptomIntroductionViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/symptoms/introduction/SubmissionSymptomIntroductionViewModelTest.kt
new file mode 100644
index 000000000..f54546746
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/symptoms/introduction/SubmissionSymptomIntroductionViewModelTest.kt
@@ -0,0 +1,124 @@
+package de.rki.coronawarnapp.ui.submission.symptoms.introduction
+
+import de.rki.coronawarnapp.storage.SubmissionRepository
+import de.rki.coronawarnapp.submission.Symptoms
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.Runs
+import io.mockk.clearAllMocks
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.coVerifySequence
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.just
+import io.mockk.verify
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import testhelpers.BaseTest
+import testhelpers.TestDispatcherProvider
+import testhelpers.extensions.InstantExecutorExtension
+
+@ExtendWith(InstantExecutorExtension::class)
+class SubmissionSymptomIntroductionViewModelTest : BaseTest() {
+
+    @MockK lateinit var submissionRepository: SubmissionRepository
+
+    @BeforeEach
+    fun setUp() {
+        MockKAnnotations.init(this, relaxed = true)
+
+        every { submissionRepository.isSubmissionRunning } returns flowOf(false)
+        coEvery { submissionRepository.startSubmission() } just Runs
+    }
+
+    @AfterEach
+    fun tearDown() {
+        clearAllMocks()
+    }
+
+    private fun createViewModel() = SubmissionSymptomIntroductionViewModel(
+        dispatcherProvider = TestDispatcherProvider,
+        submissionRepository = submissionRepository
+    )
+
+    @Test
+    fun `symptom indication is not written to settings`() {
+        createViewModel().apply {
+            onPositiveSymptomIndication()
+            onNegativeSymptomIndication()
+            onNoInformationSymptomIndication()
+            onNextClicked()
+        }
+
+        verify(exactly = 0) { submissionRepository.currentSymptoms }
+    }
+
+    @Test
+    fun `positive symptom indication is forwarded using navigation arguments`() {
+        createViewModel().apply {
+            onPositiveSymptomIndication()
+            onNextClicked()
+            navigation.value shouldBe SubmissionSymptomIntroductionFragmentDirections
+                .actionSubmissionSymptomIntroductionFragmentToSubmissionSymptomCalendarFragment(
+                    symptomIndication = Symptoms.Indication.POSITIVE
+                )
+        }
+
+        verify(exactly = 0) { submissionRepository.currentSymptoms }
+    }
+
+    @Test
+    fun `negative symptom indication leads to submission`() {
+        createViewModel().apply {
+            onNegativeSymptomIndication()
+            onNextClicked()
+            navigation.value shouldBe SubmissionSymptomIntroductionFragmentDirections
+                .actionSubmissionSymptomIntroductionFragmentToMainFragment()
+        }
+
+        coVerify { submissionRepository.startSubmission() }
+    }
+
+    @Test
+    fun `no information symptom indication leads to cancel dialog`() {
+        createViewModel().apply {
+            onNoInformationSymptomIndication()
+            onNextClicked()
+            navigation.value shouldBe null
+            showCancelDialog.value shouldBe Unit
+        }
+
+        verify(exactly = 0) { submissionRepository.currentSymptoms }
+    }
+
+    @Test
+    fun `submission by abort does not write any symptom data`() {
+        createViewModel().onCancelConfirmed()
+
+        coVerifySequence {
+            submissionRepository.isSubmissionRunning
+            submissionRepository.startSubmission()
+        }
+    }
+
+    @Test
+    fun `submission shows upload dialog`() {
+        val uploadStatus = MutableStateFlow(false)
+        every { submissionRepository.isSubmissionRunning } returns uploadStatus
+        createViewModel().apply {
+            showUploadDialog.observeForever { }
+            showUploadDialog.value shouldBe false
+
+            uploadStatus.value = true
+            showUploadDialog.value shouldBe true
+
+            uploadStatus.value = false
+            showUploadDialog.value shouldBe false
+        }
+    }
+}
-- 
GitLab