mirror of
https://github.com/bitwarden/android.git
synced 2026-08-02 21:52:21 -05:00
[PM-13626] Remember last opened view for 5 minutes (#4574)
Signed-off-by: Andre Rosado <arosado@bitwarden.com> Co-authored-by: Dave Severns <dseverns@livefront.com>
This commit is contained in:
co-authored by
Dave Severns
parent
3f1f9983e3
commit
fe06bf48e7
@@ -21,8 +21,8 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialReques
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CreateCredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2AssertionRequestOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2CreateCredentialRequestOrNull
|
||||
@@ -34,9 +34,11 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
|
||||
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppResumeManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData
|
||||
@@ -130,6 +132,11 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
private val savedStateHandle = SavedStateHandle()
|
||||
|
||||
private val appResumeManager: AppResumeManager = mockk {
|
||||
every { setResumeScreen(any()) } just runs
|
||||
every { clearResumeScreen() } just runs
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkStatic(
|
||||
@@ -1061,6 +1068,28 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
verify { authRepository.switchAccount(userId) }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on ResumeScreenDataReceived with null value, should call AppResumeManager clearResumeScreen`() {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(
|
||||
MainAction.ResumeScreenDataReceived(screenResumeData = null),
|
||||
)
|
||||
|
||||
verify { appResumeManager.clearResumeScreen() }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on ResumeScreenDataReceived with data value, should call AppResumeManager setResumeScreen`() {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(
|
||||
MainAction.ResumeScreenDataReceived(screenResumeData = AppResumeScreenData.GeneratorScreen),
|
||||
)
|
||||
|
||||
verify { appResumeManager.setResumeScreen(AppResumeScreenData.GeneratorScreen) }
|
||||
}
|
||||
|
||||
private fun createViewModel(
|
||||
initialSpecialCircumstance: SpecialCircumstance? = null,
|
||||
) = MainViewModel(
|
||||
@@ -1079,6 +1108,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
savedStateHandle = savedStateHandle.apply {
|
||||
set(SPECIAL_CIRCUMSTANCE_KEY, initialSpecialCircumstance)
|
||||
},
|
||||
appResumeManager = appResumeManager,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+57
-1
@@ -27,13 +27,13 @@ import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.encodeToJsonElement
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@Suppress("LargeClass")
|
||||
@@ -1335,6 +1335,62 @@ class AuthDiskSourceTest {
|
||||
actual,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getLastLockTimestamp should pull from SharedPreferences`() {
|
||||
val storeKey = "bwPreferencesStorage:lastLockTimestamp"
|
||||
val mockUserId = "mockUserId"
|
||||
val expectedState = Instant.parse("2025-01-13T12:00:00Z")
|
||||
fakeSharedPreferences.edit {
|
||||
putLong(
|
||||
"${storeKey}_$mockUserId",
|
||||
expectedState.toEpochMilli(),
|
||||
)
|
||||
}
|
||||
val actual = authDiskSource.getLastLockTimestamp(userId = mockUserId)
|
||||
assertEquals(
|
||||
expectedState,
|
||||
actual,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getLastLockTimestamp should pull null from SharedPreferences if there is no data`() {
|
||||
val mockUserId = "mockUserId"
|
||||
val expectedState = null
|
||||
val actual = authDiskSource.getLastLockTimestamp(userId = mockUserId)
|
||||
assertEquals(
|
||||
expectedState,
|
||||
actual,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setLastLockTimestamp should update SharedPreferences`() {
|
||||
val mockUserId = "mockUserId"
|
||||
val expectedState = Instant.parse("2025-01-13T12:00:00Z")
|
||||
authDiskSource.storeLastLockTimestamp(
|
||||
userId = mockUserId,
|
||||
expectedState,
|
||||
)
|
||||
val actual = authDiskSource.getLastLockTimestamp(userId = mockUserId)
|
||||
assertEquals(
|
||||
expectedState,
|
||||
actual,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setLastLockTimestamp should clear SharedPreferences when null is passed`() {
|
||||
val mockUserId = "mockUserId"
|
||||
val expectedState = null
|
||||
authDiskSource.storeLastLockTimestamp(
|
||||
userId = mockUserId,
|
||||
expectedState,
|
||||
)
|
||||
val actual = authDiskSource.getLastLockTimestamp(userId = mockUserId)
|
||||
assertNull(actual)
|
||||
}
|
||||
}
|
||||
|
||||
private const val USER_STATE_JSON = """
|
||||
|
||||
+18
-1
@@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.onSubscription
|
||||
import org.junit.Assert.assertEquals
|
||||
import java.time.Instant
|
||||
|
||||
class FakeAuthDiskSource : AuthDiskSource {
|
||||
|
||||
@@ -64,6 +65,7 @@ class FakeAuthDiskSource : AuthDiskSource {
|
||||
private val storedOnboardingStatus = mutableMapOf<String, OnboardingStatus?>()
|
||||
private val storedShowImportLogins = mutableMapOf<String, Boolean?>()
|
||||
private val storedNewDeviceNoticeState = mutableMapOf<String, NewDeviceNoticeState?>()
|
||||
private val storedLastLockTimestampState = mutableMapOf<String, Instant?>()
|
||||
|
||||
override var userState: UserStateJson? = null
|
||||
set(value) {
|
||||
@@ -314,13 +316,21 @@ class FakeAuthDiskSource : AuthDiskSource {
|
||||
return storedNewDeviceNoticeState[userId] ?: NewDeviceNoticeState(
|
||||
displayStatus = NewDeviceNoticeDisplayStatus.HAS_NOT_SEEN,
|
||||
lastSeenDate = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun storeNewDeviceNoticeState(userId: String, newState: NewDeviceNoticeState?) {
|
||||
storedNewDeviceNoticeState[userId] = newState
|
||||
}
|
||||
|
||||
override fun getLastLockTimestamp(userId: String): Instant? {
|
||||
return storedLastLockTimestampState[userId]
|
||||
}
|
||||
|
||||
override fun storeLastLockTimestamp(userId: String, lastLockTimestamp: Instant?) {
|
||||
storedLastLockTimestampState[userId] = lastLockTimestamp
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the the [isTdeLoginComplete] was stored successfully using the [userId].
|
||||
*/
|
||||
@@ -471,6 +481,13 @@ class FakeAuthDiskSource : AuthDiskSource {
|
||||
assertEquals(policies, storedPolicies[userId])
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the [lastLockTimestamp] was stored successfully using the [userId].
|
||||
*/
|
||||
fun assertLastLockTimestamp(userId: String, expectedValue: Instant?) {
|
||||
assertEquals(expectedValue, storedLastLockTimestampState[userId])
|
||||
}
|
||||
|
||||
//region Private helper functions
|
||||
|
||||
private fun getMutableShouldUseKeyConnectorFlow(
|
||||
|
||||
+51
@@ -4,12 +4,15 @@ import androidx.core.content.edit
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.platform.base.FakeSharedPreferences
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.di.PlatformNetworkModule
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.ClearClipboardFrequency
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
import com.x8bit.bitwarden.data.platform.util.decodeFromStringOrNull
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
@@ -1314,4 +1317,52 @@ class SettingsDiskSourceTest {
|
||||
assertTrue(awaitItem() ?: false)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getAppResumeScreen should pull from SharedPreferences`() {
|
||||
val mockUserId = "mockUserId"
|
||||
val resumeScreenKey = "bwPreferencesStorage:resumeScreen_$mockUserId"
|
||||
val expectedData = AppResumeScreenData.GeneratorScreen
|
||||
fakeSharedPreferences.edit {
|
||||
putString(
|
||||
resumeScreenKey,
|
||||
json.encodeToString<AppResumeScreenData>(expectedData),
|
||||
)
|
||||
}
|
||||
assertEquals(expectedData, settingsDiskSource.getAppResumeScreen(mockUserId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `storeAppResumeScreen should update SharedPreferences`() {
|
||||
val mockUserId = "mockUserId"
|
||||
val resumeScreenKey = "bwPreferencesStorage:resumeScreen_$mockUserId"
|
||||
val expectedData = AppResumeScreenData.GeneratorScreen
|
||||
settingsDiskSource.storeAppResumeScreen(mockUserId, expectedData)
|
||||
assertEquals(
|
||||
expectedData,
|
||||
fakeSharedPreferences.getString(resumeScreenKey, "")?.let {
|
||||
Json.decodeFromStringOrNull<AppResumeScreenData>(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `storeAppResumeScreen should save null when passed`() {
|
||||
val mockUserId = "mockUserId"
|
||||
val resumeScreenKey = "bwPreferencesStorage:resumeScreen_$mockUserId"
|
||||
val expectedData = AppResumeScreenData.GeneratorScreen
|
||||
settingsDiskSource.storeAppResumeScreen(mockUserId, expectedData)
|
||||
assertEquals(
|
||||
expectedData,
|
||||
fakeSharedPreferences.getString(resumeScreenKey, "")?.let {
|
||||
Json.decodeFromStringOrNull<AppResumeScreenData>(it)
|
||||
},
|
||||
)
|
||||
settingsDiskSource.storeAppResumeScreen(mockUserId, null)
|
||||
assertNull(
|
||||
fakeSharedPreferences.getString(resumeScreenKey, "")?.let {
|
||||
Json.decodeFromStringOrNull<AppResumeScreenData>(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -1,14 +1,17 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.disk.util
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.platform.util.decodeFromStringOrNull
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.onSubscription
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
@@ -67,6 +70,7 @@ class FakeSettingsDiskSource : SettingsDiskSource {
|
||||
private val storedScreenCaptureAllowed = mutableMapOf<String, Boolean?>()
|
||||
private var storedSystemBiometricIntegritySource: String? = null
|
||||
private val storedAccountBiometricIntegrityValidity = mutableMapOf<String, Boolean?>()
|
||||
private val storedAppResumeScreenData = mutableMapOf<String, String?>()
|
||||
private val userSignIns = mutableMapOf<String, Boolean>()
|
||||
private val userShowAutoFillBadge = mutableMapOf<String, Boolean?>()
|
||||
private val userShowUnlockBadge = mutableMapOf<String, Boolean?>()
|
||||
@@ -424,6 +428,14 @@ class FakeSettingsDiskSource : SettingsDiskSource {
|
||||
emit(hasSeenGeneratorCoachMark)
|
||||
}
|
||||
|
||||
override fun storeAppResumeScreen(userId: String, screenData: AppResumeScreenData?) {
|
||||
storedAppResumeScreenData[userId] = screenData.let { Json.encodeToString(it) }
|
||||
}
|
||||
|
||||
override fun getAppResumeScreen(userId: String): AppResumeScreenData? {
|
||||
return storedAppResumeScreenData[userId]?.let { Json.decodeFromStringOrNull(it) }
|
||||
}
|
||||
|
||||
//region Private helper functions
|
||||
private fun getMutableScreenCaptureAllowedFlow(userId: String): MutableSharedFlow<Boolean?> {
|
||||
return mutableScreenCaptureAllowedFlowMap.getOrPut(userId) {
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.util.FakeSettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
|
||||
class AppResumeManagerTest {
|
||||
private val fakeSettingsDiskSource: SettingsDiskSource = FakeSettingsDiskSource()
|
||||
private val authRepository = mockk<AuthRepository> {
|
||||
every { activeUserId } returns USER_ID
|
||||
}
|
||||
private val vaultLockManager: VaultLockManager = mockk {
|
||||
every { isVaultUnlocked(USER_ID) } returns true
|
||||
}
|
||||
|
||||
private val fixedClock: Clock = Clock.fixed(
|
||||
Instant.parse("2023-10-27T12:00:00Z"),
|
||||
ZoneOffset.UTC,
|
||||
)
|
||||
|
||||
private val fakeAuthDiskSource = FakeAuthDiskSource()
|
||||
|
||||
private val appResumeManager = AppResumeManagerImpl(
|
||||
settingsDiskSource = fakeSettingsDiskSource,
|
||||
authDiskSource = fakeAuthDiskSource,
|
||||
authRepository = authRepository,
|
||||
vaultLockManager = vaultLockManager,
|
||||
clock = fixedClock,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `setResumeScreen should update the app resume screen in the settings disk source`() =
|
||||
runTest {
|
||||
val expectedValue = AppResumeScreenData.SendScreen
|
||||
appResumeManager.setResumeScreen(expectedValue)
|
||||
val actualValue = fakeSettingsDiskSource.getAppResumeScreen(USER_ID)
|
||||
|
||||
assertEquals(expectedValue, actualValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getResumeScreen should return null when there is no app resume screen saved`() =
|
||||
runTest {
|
||||
val actualValue = appResumeManager.getResumeScreen()
|
||||
assertNull(actualValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getResumeScreen should return the saved AppResumeScreen`() =
|
||||
runTest {
|
||||
val expectedValue = AppResumeScreenData.GeneratorScreen
|
||||
fakeSettingsDiskSource.storeAppResumeScreen(
|
||||
userId = USER_ID,
|
||||
screenData = expectedValue,
|
||||
)
|
||||
val actualValue = appResumeManager.getResumeScreen()
|
||||
assertEquals(expectedValue, actualValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clearResumeScreen should clear the app resume screen in the settings disk source`() =
|
||||
runTest {
|
||||
fakeSettingsDiskSource.storeAppResumeScreen(
|
||||
userId = USER_ID,
|
||||
screenData = AppResumeScreenData.GeneratorScreen,
|
||||
)
|
||||
appResumeManager.clearResumeScreen()
|
||||
val actualValue = fakeSettingsDiskSource.getAppResumeScreen(USER_ID)
|
||||
assertNull(actualValue)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getResumeSpecialCircumstance should return GeneratorShortcut when the resume screen is GeneratorScreen`() =
|
||||
runTest {
|
||||
fakeSettingsDiskSource.storeAppResumeScreen(
|
||||
userId = USER_ID,
|
||||
screenData = AppResumeScreenData.GeneratorScreen,
|
||||
)
|
||||
fakeAuthDiskSource.storeLastLockTimestamp(USER_ID, fixedClock.instant())
|
||||
val expectedValue = SpecialCircumstance.GeneratorShortcut
|
||||
val actualValue = appResumeManager.getResumeSpecialCircumstance()
|
||||
assertEquals(expectedValue, actualValue)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getResumeSpecialCircumstance should return SendShortcut when the resume screen is SendScreen`() =
|
||||
runTest {
|
||||
fakeSettingsDiskSource.storeAppResumeScreen(
|
||||
userId = USER_ID,
|
||||
screenData = AppResumeScreenData.SendScreen,
|
||||
)
|
||||
fakeAuthDiskSource.storeLastLockTimestamp(USER_ID, fixedClock.instant())
|
||||
val expectedValue = SpecialCircumstance.SendShortcut
|
||||
val actualValue = appResumeManager.getResumeSpecialCircumstance()
|
||||
assertEquals(expectedValue, actualValue)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getResumeSpecialCircumstance should return VerificationCodeShortcut when the resume screen is VerificationCodeScreen`() =
|
||||
runTest {
|
||||
fakeSettingsDiskSource.storeAppResumeScreen(
|
||||
userId = USER_ID,
|
||||
screenData = AppResumeScreenData.VerificationCodeScreen,
|
||||
)
|
||||
fakeAuthDiskSource.storeLastLockTimestamp(USER_ID, fixedClock.instant())
|
||||
val expectedValue = SpecialCircumstance.VerificationCodeShortcut
|
||||
val actualValue = appResumeManager.getResumeSpecialCircumstance()
|
||||
assertEquals(expectedValue, actualValue)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getResumeSpecialCircumstance should return SearchShortcut when the resume screen is SearchScreen`() =
|
||||
runTest {
|
||||
fakeSettingsDiskSource.storeAppResumeScreen(
|
||||
userId = USER_ID,
|
||||
screenData = AppResumeScreenData.SearchScreen("test"),
|
||||
)
|
||||
fakeAuthDiskSource.storeLastLockTimestamp(USER_ID, fixedClock.instant())
|
||||
val expectedValue = SpecialCircumstance.SearchShortcut("test")
|
||||
val actualValue = appResumeManager.getResumeSpecialCircumstance()
|
||||
assertEquals(expectedValue, actualValue)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getResumeSpecialCircumstance should should clear app resume screen if have passed 5 minutes`() {
|
||||
val delayedAuthDiskSource: AuthDiskSource = mockk {
|
||||
every { getLastLockTimestamp(any()) } returns fixedClock.instant()
|
||||
.minusSeconds(5 * 60 + 1)
|
||||
}
|
||||
|
||||
val delayedAppResumeManager = AppResumeManagerImpl(
|
||||
settingsDiskSource = fakeSettingsDiskSource,
|
||||
authDiskSource = delayedAuthDiskSource,
|
||||
authRepository = authRepository,
|
||||
vaultLockManager = vaultLockManager,
|
||||
clock = fixedClock,
|
||||
)
|
||||
fakeSettingsDiskSource.storeAppResumeScreen(
|
||||
userId = USER_ID,
|
||||
screenData = AppResumeScreenData.GeneratorScreen,
|
||||
)
|
||||
val actualValue = delayedAppResumeManager.getResumeSpecialCircumstance()
|
||||
assertNull(actualValue)
|
||||
|
||||
val actualSettingsValue = fakeSettingsDiskSource.getAppResumeScreen(
|
||||
userId = USER_ID,
|
||||
)
|
||||
assertNull(actualSettingsValue)
|
||||
}
|
||||
}
|
||||
|
||||
private const val USER_ID = "user_id"
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.util
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
class AppResumeStateManagerTest {
|
||||
private val appStateManager = AppResumeStateManagerImpl()
|
||||
|
||||
@Test
|
||||
fun `AppResumeStateManagerImpl should update and retrieve screen data`() {
|
||||
val screenData = AppResumeScreenData.GeneratorScreen
|
||||
|
||||
appStateManager.updateScreenData(screenData)
|
||||
Assert.assertEquals(screenData, appStateManager.appResumeState.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AppResumeStateManagerImpl should retrieve null if not set`() {
|
||||
Assert.assertEquals(null, appStateManager.appResumeState.value)
|
||||
}
|
||||
}
|
||||
@@ -167,6 +167,10 @@ class VaultLockManagerTest {
|
||||
vaultLockManager.vaultStateEventFlow.test {
|
||||
vaultLockManager.lockVault(userId = USER_ID)
|
||||
assertEquals(VaultStateEvent.Locked(userId = USER_ID), awaitItem())
|
||||
fakeAuthDiskSource.assertLastLockTimestamp(
|
||||
userId = USER_ID,
|
||||
FIXED_CLOCK.instant(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
@@ -12,6 +12,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppResumeManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
@@ -80,8 +81,14 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
||||
every { isUserVerified } returns true
|
||||
every { isUserVerified = any() } just runs
|
||||
}
|
||||
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager = mockk {
|
||||
every { specialCircumstance } returns null
|
||||
every { specialCircumstance = any() } answers { }
|
||||
}
|
||||
|
||||
private val appResumeManager: AppResumeManager = mockk {
|
||||
every { getResumeSpecialCircumstance() } returns null
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1248,6 +1255,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
||||
biometricsEncryptionManager = biometricsEncryptionManager,
|
||||
fido2CredentialManager = fido2CredentialManager,
|
||||
specialCircumstanceManager = specialCircumstanceManager,
|
||||
appResumeManager = appResumeManager,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+65
-28
@@ -762,34 +762,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
||||
val fido2GetCredentialsRequest = createMockFido2GetCredentialsRequest(number = 1)
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2GetCredentials(fido2GetCredentialsRequest)
|
||||
mutableUserStateFlow.tryEmit(
|
||||
UserState(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = listOf(
|
||||
UserState.Account(
|
||||
userId = "activeUserId",
|
||||
name = "name",
|
||||
email = "email",
|
||||
avatarColorHex = "avatarHexColor",
|
||||
environment = Environment.Us,
|
||||
isPremium = true,
|
||||
isLoggedIn = true,
|
||||
isVaultUnlocked = true,
|
||||
needsPasswordReset = false,
|
||||
isBiometricsEnabled = false,
|
||||
organizations = emptyList(),
|
||||
needsMasterPassword = false,
|
||||
trustedDevice = null,
|
||||
hasMasterPassword = true,
|
||||
isUsingKeyConnector = false,
|
||||
onboardingStatus = OnboardingStatus.COMPLETE,
|
||||
firstTimeState = FirstTimeState(
|
||||
showImportLoginsCard = true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
mutableUserStateFlow.tryEmit(MOCK_VAULT_UNLOCKED_USER_STATE)
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(
|
||||
RootNavState.VaultUnlockedForFido2GetCredentials(
|
||||
@@ -800,6 +773,45 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when the active user has an unlocked vault but there is an SendShortcut special circumstance the nav state should be VaultUnlocked`() {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.SendShortcut
|
||||
mutableUserStateFlow.tryEmit(MOCK_VAULT_UNLOCKED_USER_STATE)
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(
|
||||
RootNavState.VaultUnlocked(activeUserId = "activeUserId"),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when the active user has an unlocked vault but there is an VerificationCodeShortcut special circumstance the nav state should be VaultUnlocked`() {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.VerificationCodeShortcut
|
||||
mutableUserStateFlow.tryEmit(MOCK_VAULT_UNLOCKED_USER_STATE)
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(
|
||||
RootNavState.VaultUnlocked(activeUserId = "activeUserId"),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when the active user has an unlocked vault but there is an SearchShortcut special circumstance the nav state should be VaultUnlocked`() {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.SearchShortcut("")
|
||||
mutableUserStateFlow.tryEmit(MOCK_VAULT_UNLOCKED_USER_STATE)
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(
|
||||
RootNavState.VaultUnlocked(activeUserId = "activeUserId"),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when there are no accounts but there is a CompleteRegistration special circumstance the nav state should be CompleteRegistration`() {
|
||||
@@ -1386,3 +1398,28 @@ private val FIXED_CLOCK: Clock = Clock.fixed(
|
||||
)
|
||||
|
||||
private const val ACCESS_TOKEN: String = "access_token"
|
||||
|
||||
private val MOCK_VAULT_UNLOCKED_USER_STATE = UserState(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = listOf(
|
||||
UserState.Account(
|
||||
userId = "activeUserId",
|
||||
name = "name",
|
||||
email = "email",
|
||||
avatarColorHex = "avatarColorHex",
|
||||
environment = Environment.Us,
|
||||
isPremium = true,
|
||||
isLoggedIn = true,
|
||||
isVaultUnlocked = true,
|
||||
needsPasswordReset = false,
|
||||
isBiometricsEnabled = false,
|
||||
organizations = emptyList(),
|
||||
needsMasterPassword = false,
|
||||
trustedDevice = null,
|
||||
hasMasterPassword = true,
|
||||
isUsingKeyConnector = false,
|
||||
firstTimeState = FirstTimeState(false),
|
||||
onboardingStatus = OnboardingStatus.COMPLETE,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.compose.ui.test.performScrollToNode
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.core.net.toUri
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.AppResumeStateManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
@@ -55,6 +56,8 @@ class SearchScreenTest : BaseComposeTest() {
|
||||
every { launchUri(any()) } just runs
|
||||
}
|
||||
|
||||
private val appResumeStateManager: AppResumeStateManager = mockk(relaxed = true)
|
||||
|
||||
private var onNavigateBackCalled = false
|
||||
private var onNavigateToEditSendId: String? = null
|
||||
private var onNavigateToEditCipherId: String? = null
|
||||
@@ -70,6 +73,7 @@ class SearchScreenTest : BaseComposeTest() {
|
||||
onNavigateToEditSend = { onNavigateToEditSendId = it },
|
||||
onNavigateToEditCipher = { onNavigateToEditCipherId = it },
|
||||
onNavigateToViewCipher = { onNavigateToViewCipherId = it },
|
||||
appResumeStateManager = appResumeStateManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+69
@@ -248,6 +248,75 @@ class VaultUnlockedNavBarViewModelTest : BaseViewModelTest() {
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on init with SendShortcut special circumstance should navigate to the send screen with shortcut event`() =
|
||||
runTest {
|
||||
every {
|
||||
specialCircumstancesManager.specialCircumstance
|
||||
} returns SpecialCircumstance.SendShortcut
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
VaultUnlockedNavBarEvent.Shortcut.NavigateToSendScreen,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
verify(exactly = 1) {
|
||||
specialCircumstancesManager.specialCircumstance
|
||||
specialCircumstancesManager.specialCircumstance = null
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on init with VerificationCodeShortcut special circumstance should navigate to the Vault screen with shortcut event`() =
|
||||
runTest {
|
||||
every {
|
||||
specialCircumstancesManager.specialCircumstance
|
||||
} returns SpecialCircumstance.VerificationCodeShortcut
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
VaultUnlockedNavBarEvent.Shortcut.NavigateToVaultScreen(
|
||||
labelRes = R.string.my_vault,
|
||||
contentDescRes = R.string.my_vault,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
verify(exactly = 1) {
|
||||
specialCircumstancesManager.specialCircumstance
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on init with SearchShortcut special circumstance should navigate to the Vault screen with shortcut event`() =
|
||||
runTest {
|
||||
every {
|
||||
specialCircumstancesManager.specialCircumstance
|
||||
} returns SpecialCircumstance.SearchShortcut("")
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
VaultUnlockedNavBarEvent.Shortcut.NavigateToVaultScreen(
|
||||
labelRes = R.string.my_vault,
|
||||
contentDescRes = R.string.my_vault,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
verify(exactly = 1) {
|
||||
specialCircumstancesManager.specialCircumstance
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel() =
|
||||
VaultUnlockedNavBarViewModel(
|
||||
|
||||
+6
-1
@@ -29,6 +29,7 @@ import androidx.compose.ui.test.performTouchInput
|
||||
import androidx.compose.ui.test.swipeRight
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.core.net.toUri
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.AppResumeStateManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
@@ -60,16 +61,20 @@ class GeneratorScreenTest : BaseComposeTest() {
|
||||
private val intentManager: IntentManager = mockk {
|
||||
every { launchUri(any()) } just runs
|
||||
}
|
||||
private val appResumeStateManager: AppResumeStateManager = mockk(relaxed = true)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
composeTestRule.setContent {
|
||||
GeneratorScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateToPasswordHistory = { onNavigateToPasswordHistoryScreenCalled = true },
|
||||
onNavigateToPasswordHistory = {
|
||||
onNavigateToPasswordHistoryScreenCalled = true
|
||||
},
|
||||
onNavigateBack = {},
|
||||
onDimNavBarRequest = { onDimNavBarRequest = it },
|
||||
intentManager = intentManager,
|
||||
appResumeStateManager = appResumeStateManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.compose.ui.test.performScrollToNode
|
||||
import androidx.core.net.toUri
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.AppResumeStateManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
@@ -60,6 +61,7 @@ class SendScreenTest : BaseComposeTest() {
|
||||
every { eventFlow } returns mutableEventFlow
|
||||
every { stateFlow } returns mutableStateFlow
|
||||
}
|
||||
private val appResumeStateManager: AppResumeStateManager = mockk(relaxed = true)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
@@ -72,6 +74,7 @@ class SendScreenTest : BaseComposeTest() {
|
||||
onNavigateToSendTextList = { onNavigateToSendTextListCalled = true },
|
||||
onNavigateToSearchSend = { onNavigateToSendSearchCalled = true },
|
||||
intentManager = intentManager,
|
||||
appResumeStateManager = appResumeStateManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,13 @@ import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
@@ -145,6 +147,11 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
} returns mutableSshKeyVaultItemsEnabledFlow.value
|
||||
}
|
||||
private val reviewPromptManager: ReviewPromptManager = mockk()
|
||||
private val mockAuthRepository = mockk<AuthRepository>(relaxed = true)
|
||||
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager = mockk {
|
||||
every { specialCircumstance } returns null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct and should trigger a syncIfNecessary call`() {
|
||||
@@ -1841,6 +1848,41 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `init should send NavigateToVerificationCodeScreen when special circumstance is VerificationCodeShortcut`() =
|
||||
runTest {
|
||||
every {
|
||||
specialCircumstanceManager.specialCircumstance
|
||||
} returns SpecialCircumstance.VerificationCodeShortcut
|
||||
every { specialCircumstanceManager.specialCircumstance = null } just runs
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultAction.LifecycleResumed)
|
||||
assertEquals(
|
||||
VaultEvent.NavigateToVerificationCodeScreen, awaitItem(),
|
||||
)
|
||||
}
|
||||
verify { specialCircumstanceManager.specialCircumstance = null }
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `init should send NavigateToVaultSearchScreen when special circumstance is SearchShortcut`() =
|
||||
runTest {
|
||||
every {
|
||||
specialCircumstanceManager.specialCircumstance
|
||||
} returns SpecialCircumstance.SearchShortcut("")
|
||||
every { specialCircumstanceManager.specialCircumstance = null } just runs
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultAction.LifecycleResumed)
|
||||
assertEquals(
|
||||
VaultEvent.NavigateToVaultSearchScreen, awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(): VaultViewModel =
|
||||
VaultViewModel(
|
||||
authRepository = authRepository,
|
||||
@@ -1854,6 +1896,7 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
firstTimeActionManager = firstTimeActionManager,
|
||||
snackbarRelayManager = snackbarRelayManager,
|
||||
reviewPromptManager = reviewPromptManager,
|
||||
specialCircumstanceManager = specialCircumstanceManager,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -12,6 +12,7 @@ import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.AppResumeStateManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
@@ -41,6 +42,7 @@ class VerificationCodeScreenTest : BaseComposeTest() {
|
||||
every { eventFlow } returns mutableEventFlow
|
||||
every { stateFlow } returns mutableStateFlow
|
||||
}
|
||||
private val appResumeStateManager: AppResumeStateManager = mockk(relaxed = true)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
@@ -50,6 +52,7 @@ class VerificationCodeScreenTest : BaseComposeTest() {
|
||||
onNavigateBack = { onNavigateBackCalled = true },
|
||||
onNavigateToVaultItemScreen = { onNavigateToVaultItemId = it },
|
||||
onNavigateToSearch = { onNavigateToSearchCalled = true },
|
||||
appResumeStateManager = appResumeStateManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user