diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigration.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigration.kt index 1413fb640b79db0c2a5d3ff21ef32606428e30d3..e5f9d81677ee0a601b55e6c9205ddda7050d228a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigration.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigration.kt @@ -3,6 +3,7 @@ package de.rki.coronawarnapp.util.encryptionmigration import android.content.Context import android.content.SharedPreferences import android.database.sqlite.SQLiteDatabase +import androidx.annotation.VisibleForTesting import de.rki.coronawarnapp.bugreporting.reportProblem import de.rki.coronawarnapp.main.CWASettings import de.rki.coronawarnapp.storage.OnboardingSettings @@ -97,7 +98,8 @@ class EncryptedPreferencesMigration @Inject constructor( } } - private class SettingsLocalData(private val sharedPreferences: SharedPreferences) { + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + class SettingsLocalData(private val sharedPreferences: SharedPreferences) { fun wasInteroperabilityShown() = sharedPreferences.getBoolean(PREFERENCE_INTEROPERABILITY_WAS_USED, false) @@ -109,15 +111,22 @@ class EncryptedPreferencesMigration @Inject constructor( sharedPreferences.getInt(PKEY_POSITIVE_TEST_RESULT_REMINDER_COUNT, Int.MIN_VALUE) companion object { - private const val PREFERENCE_INTEROPERABILITY_WAS_USED = "preference_interoperability_is_used_at_least_once" - private const val PKEY_NOTIFICATIONS_RISK_ENABLED = "preference_notifications_risk_enabled" - private const val PKEY_NOTIFICATIONS_TEST_ENABLED = "preference_notifications_test_enabled" - private const val PKEY_POSITIVE_TEST_RESULT_REMINDER_COUNT = - "preference_positive_test_result_reminder_count" + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + const val PREFERENCE_INTEROPERABILITY_WAS_USED = "preference_interoperability_is_used_at_least_once" + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + const val PKEY_NOTIFICATIONS_RISK_ENABLED = "preference_notifications_risk_enabled" + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + const val PKEY_NOTIFICATIONS_TEST_ENABLED = "preference_notifications_test_enabled" + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + const val PKEY_POSITIVE_TEST_RESULT_REMINDER_COUNT = "preference_positive_test_result_reminder_count" } } - private class OnboardingLocalData(private val sharedPreferences: SharedPreferences) { + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + class OnboardingLocalData(private val sharedPreferences: SharedPreferences) { fun onboardingCompletedTimestamp(): Long? { val timestamp = sharedPreferences.getLong(PKEY_ONBOARDING_COMPLETED_TIMESTAMP, 0L) @@ -128,12 +137,16 @@ class EncryptedPreferencesMigration @Inject constructor( fun isBackgroundCheckDone(): Boolean = sharedPreferences.getBoolean(PKEY_BACKGROUND_CHECK_DONE, false) companion object { - private const val PKEY_ONBOARDING_COMPLETED_TIMESTAMP = "preference_onboarding_completed_timestamp" - private const val PKEY_BACKGROUND_CHECK_DONE = "preference_background_check_done" + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + const val PKEY_ONBOARDING_COMPLETED_TIMESTAMP = "preference_onboarding_completed_timestamp" + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + const val PKEY_BACKGROUND_CHECK_DONE = "preference_background_check_done" } } - private class TracingLocalData(private val sharedPreferences: SharedPreferences) { + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + class TracingLocalData(private val sharedPreferences: SharedPreferences) { fun initialPollingForTestResultTimeStamp() = sharedPreferences.getLong(PKEY_POOLING_TEST_RESULT_STARTED, 0L) @@ -144,14 +157,22 @@ class EncryptedPreferencesMigration @Inject constructor( fun initialTracingActivationTimestamp(): Long = sharedPreferences.getLong(PKEY_TRACING_ACTIVATION_TIME, 0L) companion object { - private const val PKEY_POOLING_TEST_RESULT_STARTED = "preference_polling_test_result_started" - private const val PKEY_TEST_RESULT_NOTIFICATION = "preference_test_result_notification" - private const val PKEY_HAS_RISK_STATUS_LOWERED = "preference_has_risk_status_lowered" - private const val PKEY_TRACING_ACTIVATION_TIME = "preference_initial_tracing_activation_time" + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + const val PKEY_POOLING_TEST_RESULT_STARTED = "preference_polling_test_result_started" + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + const val PKEY_TEST_RESULT_NOTIFICATION = "preference_test_result_notification" + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + const val PKEY_HAS_RISK_STATUS_LOWERED = "preference_has_risk_status_lowered" + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + const val PKEY_TRACING_ACTIVATION_TIME = "preference_initial_tracing_activation_time" } } - private class SubmissionLocalData(private val sharedPreferences: SharedPreferences) { + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + class SubmissionLocalData(private val sharedPreferences: SharedPreferences) { fun registrationToken(): String? = sharedPreferences.getString(PKEY_REGISTRATION_TOKEN, null) fun initialTestResultReceivedTimestamp(): Long? { @@ -168,11 +189,20 @@ class EncryptedPreferencesMigration @Inject constructor( fun isAllowedToSubmitDiagnosisKeys(): Boolean = sharedPreferences.getBoolean(PKEY_IS_ALLOWED_TO_SUBMIT, false) companion object { - private const val PKEY_REGISTRATION_TOKEN = "preference_registration_token" - private const val PKEY_INITIAL_RESULT_RECEIVED_TIME = "preference_initial_result_received_time" - private const val PKEY_DEVICE_PARING_SUCCESSFUL_TIME = "preference_device_pairing_successful_time" - private const val PKEY_NUMBER_SUCCESSFUL_SUBMISSIONS = "preference_number_successful_submissions" - private const val PKEY_IS_ALLOWED_TO_SUBMIT = "preference_is_allowed_to_submit_diagnosis_keys" + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + const val PKEY_REGISTRATION_TOKEN = "preference_registration_token" + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + const val PKEY_INITIAL_RESULT_RECEIVED_TIME = "preference_initial_result_received_time" + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + const val PKEY_DEVICE_PARING_SUCCESSFUL_TIME = "preference_device_pairing_successful_time" + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + const val PKEY_NUMBER_SUCCESSFUL_SUBMISSIONS = "preference_number_successful_submissions" + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + const val PKEY_IS_ALLOWED_TO_SUBMIT = "preference_is_allowed_to_submit_diagnosis_keys" } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigrationTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigrationTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..c4b671d9e27857ea7738ea1bf37adce39273d4fc --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigrationTest.kt @@ -0,0 +1,163 @@ +package de.rki.coronawarnapp.util.encryptionmigration + +import android.content.Context +import android.content.SharedPreferences +import androidx.core.content.edit +import de.rki.coronawarnapp.main.CWASettings +import de.rki.coronawarnapp.storage.OnboardingSettings +import de.rki.coronawarnapp.storage.TracingSettings +import de.rki.coronawarnapp.submission.SubmissionSettings +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.mockk +import org.joda.time.Instant +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseIOTest +import testhelpers.preferences.MockSharedPreferences +import testhelpers.preferences.mockFlowPreference +import java.io.File + +class EncryptedPreferencesMigrationTest : BaseIOTest() { + @MockK lateinit var context: Context + @MockK lateinit var encryptedPreferencesHelper: EncryptedPreferencesHelper + @MockK lateinit var cwaSettings: CWASettings + @MockK lateinit var submissionSettings: SubmissionSettings + @MockK lateinit var tracingSettings: TracingSettings + @MockK lateinit var onboardingSettings: OnboardingSettings + @MockK lateinit var encryptedErrorResetTool: EncryptionErrorResetTool + + private val testDir = File(IO_TEST_BASEDIR, this::class.java.simpleName) + private val dbFile = File(testDir, "database.sql") + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + testDir.mkdirs() + } + + @AfterEach + fun teardown() { + testDir.deleteRecursively() + } + + private fun createInstance() = EncryptedPreferencesMigration( + context = context, + encryptedPreferences = encryptedPreferencesHelper, + cwaSettings = cwaSettings, + submissionSettings = submissionSettings, + tracingSettings = tracingSettings, + onboardingSettings = onboardingSettings, + errorResetTool = encryptedErrorResetTool + ) + + private fun createOldPreferences() = MockSharedPreferences().also { + it.edit { + // SettingsLocalData + putBoolean(EncryptedPreferencesMigration.SettingsLocalData.PREFERENCE_INTEROPERABILITY_WAS_USED, true) + putBoolean(EncryptedPreferencesMigration.SettingsLocalData.PKEY_NOTIFICATIONS_RISK_ENABLED, false) + putBoolean(EncryptedPreferencesMigration.SettingsLocalData.PKEY_NOTIFICATIONS_TEST_ENABLED, false) + putInt( + EncryptedPreferencesMigration.SettingsLocalData.PKEY_POSITIVE_TEST_RESULT_REMINDER_COUNT, + Int.MAX_VALUE + ) + + // OnboardingLocalData + putLong(EncryptedPreferencesMigration.OnboardingLocalData.PKEY_ONBOARDING_COMPLETED_TIMESTAMP, 10101010L) + putBoolean(EncryptedPreferencesMigration.OnboardingLocalData.PKEY_BACKGROUND_CHECK_DONE, true) + + // TracingLocalData + putLong(EncryptedPreferencesMigration.TracingLocalData.PKEY_POOLING_TEST_RESULT_STARTED, 10101010L) + putBoolean(EncryptedPreferencesMigration.TracingLocalData.PKEY_TEST_RESULT_NOTIFICATION, true) + putBoolean(EncryptedPreferencesMigration.TracingLocalData.PKEY_HAS_RISK_STATUS_LOWERED, true) + putLong(EncryptedPreferencesMigration.TracingLocalData.PKEY_TRACING_ACTIVATION_TIME, 10101010L) + + // SubmissionLocalData + putString(EncryptedPreferencesMigration.SubmissionLocalData.PKEY_REGISTRATION_TOKEN, "super_secret_token") + putLong(EncryptedPreferencesMigration.SubmissionLocalData.PKEY_INITIAL_RESULT_RECEIVED_TIME, 10101010L) + putLong(EncryptedPreferencesMigration.SubmissionLocalData.PKEY_DEVICE_PARING_SUCCESSFUL_TIME, 10101010L) + putInt(EncryptedPreferencesMigration.SubmissionLocalData.PKEY_NUMBER_SUCCESSFUL_SUBMISSIONS, 1) + putBoolean(EncryptedPreferencesMigration.SubmissionLocalData.PKEY_IS_ALLOWED_TO_SUBMIT, true) + } + } + + @Test + fun `is migration successful`() { + every { context.getDatabasePath("coronawarnapp-db") } returns dbFile + every { encryptedPreferencesHelper.clean() } just Runs + + val oldPreferences = createOldPreferences() + every { encryptedPreferencesHelper.instance } returns oldPreferences + + // SettingsLocalData + every { cwaSettings.wasInteroperabilityShownAtLeastOnce = true } just Runs + val mockRiskPreference = mockFlowPreference(true) + every { cwaSettings.isNotificationsRiskEnabled } returns mockRiskPreference + val mockTestPreference = mockFlowPreference(true) + every { cwaSettings.isNotificationsTestEnabled } returns mockTestPreference + every { cwaSettings.numberOfRemainingSharePositiveTestResultReminders = Int.MAX_VALUE } just Runs + + // OnboardingLocalData + every { onboardingSettings.onboardingCompletedTimestamp = Instant.ofEpochMilli(10101010L) } just Runs + every { onboardingSettings.isBackgroundCheckDone = true } just Runs + + // TracingLocalData + every { tracingSettings.initialPollingForTestResultTimeStamp = 10101010L } just Runs + every { tracingSettings.isTestResultAvailableNotificationSent = true } just Runs + val mockNotificationPreference = mockFlowPreference(false) + every { tracingSettings.isUserToBeNotifiedOfLoweredRiskLevel } returns mockNotificationPreference + every { tracingSettings.isConsentGiven = true } just Runs + + // SubmissionLocalData + val mockRegtokenPreference = mockFlowPreference<String?>(null) + every { submissionSettings.registrationToken } returns mockRegtokenPreference + every { submissionSettings.initialTestResultReceivedAt = Instant.ofEpochMilli(10101010L) } just Runs + every { submissionSettings.devicePairingSuccessfulAt = Instant.ofEpochMilli(10101010L) } just Runs + every { submissionSettings.isSubmissionSuccessful = true } just Runs + every { submissionSettings.isAllowedToSubmitKeys = true } just Runs + + val migrationInstance = createInstance() + + migrationInstance.doMigration() + + // SettingsLocalData + mockRiskPreference.value shouldBe false + mockTestPreference.value shouldBe false + + // TracingLocalData + mockNotificationPreference.value shouldBe true + + // SubmissionLocalData + mockRegtokenPreference.value shouldBe "super_secret_token" + } + + @Test + fun `error during migration will be caught`() { + every { context.getDatabasePath("coronawarnapp-db") } returns dbFile + + val mockPrefs = mockk<SharedPreferences>() + every { + mockPrefs.getBoolean( + EncryptedPreferencesMigration.SettingsLocalData.PREFERENCE_INTEROPERABILITY_WAS_USED, + false + ) + } throws Exception("No one expects the spanish inquisition") + + every { encryptedPreferencesHelper.instance } returns mockPrefs + every { encryptedPreferencesHelper.clean() } just Runs + + every { encryptedErrorResetTool.isResetNoticeToBeShown = true } just Runs + + shouldNotThrowAny { + val instance = createInstance() + instance.doMigration() + } + } +}