diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/edititem/EditItemScreen.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/edititem/EditItemScreen.kt index 7e549e81d27..47188ac34d3 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/edititem/EditItemScreen.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/edititem/EditItemScreen.kt @@ -156,7 +156,7 @@ fun EditItemContent( onTypeOptionClicked: (AuthenticatorItemType) -> Unit = {}, onTotpCodeTextChange: (String) -> Unit = {}, onAlgorithmOptionClicked: (AuthenticatorItemAlgorithm) -> Unit = {}, - onRefreshPeriodOptionClicked: (AuthenticatorRefreshPeriodOption) -> Unit = {}, + onRefreshPeriodOptionClicked: (Int) -> Unit = {}, onNumberOfDigitsChanged: (Int) -> Unit = {}, onExpandAdvancedOptionsClicked: () -> Unit = {}, ) { @@ -261,7 +261,7 @@ private fun LazyListScope.advancedOptions( viewState: EditItemState.ViewState.Content, onAlgorithmOptionClicked: (AuthenticatorItemAlgorithm) -> Unit, onTypeOptionClicked: (AuthenticatorItemType) -> Unit, - onRefreshPeriodOptionClicked: (AuthenticatorRefreshPeriodOption) -> Unit, + onRefreshPeriodOptionClicked: (Int) -> Unit, onNumberOfDigitsChanged: (Int) -> Unit, ) { item(key = "OtpItemTypeSelector") { @@ -312,11 +312,16 @@ private fun LazyListScope.advancedOptions( item(key = "RefreshPeriodItemTypePicker") { val possibleRefreshPeriodOptions = AuthenticatorRefreshPeriodOption.entries + .map { it.seconds } + .plus(viewState.itemData.originalRefreshPeriod) + .plus(viewState.itemData.refreshPeriod) + .distinct() + .sorted() val refreshPeriodOptionsWithStrings = possibleRefreshPeriodOptions.associateWith { pluralStringResource( id = BitwardenPlurals.refresh_period_seconds, - count = it.seconds, - formatArgs = arrayOf(it.seconds), + count = it, + formatArgs = arrayOf(it), ) } BitwardenMultiSelectButton( @@ -406,7 +411,8 @@ private fun EditItemContentExpandedOptionsPreview() { viewState = EditItemState.ViewState.Content( isAdvancedOptionsExpanded = true, itemData = EditItemData( - refreshPeriod = AuthenticatorRefreshPeriodOption.THIRTY, + refreshPeriod = 45, + originalRefreshPeriod = 45, totpCode = "123456", type = AuthenticatorItemType.TOTP, username = "account name", @@ -428,7 +434,8 @@ private fun EditItemContentCollapsedOptionsPreview() { viewState = EditItemState.ViewState.Content( isAdvancedOptionsExpanded = false, itemData = EditItemData( - refreshPeriod = AuthenticatorRefreshPeriodOption.THIRTY, + refreshPeriod = 30, + originalRefreshPeriod = 30, totpCode = "123456", type = AuthenticatorItemType.TOTP, username = "account name", diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/edititem/EditItemViewModel.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/edititem/EditItemViewModel.kt index 45d439f9e9d..86fdbdd40ba 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/edititem/EditItemViewModel.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/edititem/EditItemViewModel.kt @@ -136,7 +136,7 @@ class EditItemViewModel @Inject constructor( accountName = content.itemData.username?.trim(), type = content.itemData.type, algorithm = content.itemData.algorithm, - period = content.itemData.refreshPeriod.seconds, + period = content.itemData.refreshPeriod, digits = content.itemData.digits, issuer = content.itemData.issuer.trim(), favorite = content.itemData.favorite, @@ -354,8 +354,8 @@ class EditItemViewModel @Inject constructor( minDigitsAllowed = MIN_ALLOWED_CODE_DIGITS, maxDigitsAllowed = MAX_ALLOWED_CODE_DIGITS, itemData = EditItemData( - refreshPeriod = AuthenticatorRefreshPeriodOption.fromSeconds(period) - ?: AuthenticatorRefreshPeriodOption.THIRTY, + refreshPeriod = period, + originalRefreshPeriod = period, totpCode = key.toUpperCase(Locale.current), type = type, username = accountName, @@ -504,7 +504,7 @@ sealed class EditItemAction { * The user has selected a refresh period option. */ data class RefreshPeriodOptionClick( - val period: AuthenticatorRefreshPeriodOption, + val period: Int, ) : EditItemAction() /** @@ -552,13 +552,4 @@ enum class AuthenticatorRefreshPeriodOption(val seconds: Int) { THIRTY(seconds = 30), SIXTY(seconds = 60), NINETY(seconds = 90), - ; - - @Suppress("UndocumentedPublicClass") - companion object { - /** - * Returns a [AuthenticatorRefreshPeriodOption] with the provided [seconds], or null. - */ - fun fromSeconds(seconds: Int) = entries.find { it.seconds == seconds } - } } diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/edititem/model/EditItemData.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/edititem/model/EditItemData.kt index 369ddd60c01..3b4ff88a8cd 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/edititem/model/EditItemData.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/edititem/model/EditItemData.kt @@ -3,13 +3,13 @@ package com.bitwarden.authenticator.ui.authenticator.feature.edititem.model import android.os.Parcelable import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemAlgorithm import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemType -import com.bitwarden.authenticator.ui.authenticator.feature.edititem.AuthenticatorRefreshPeriodOption import kotlinx.parcelize.Parcelize /** * The data relating to the verification code. * - * @property refreshPeriod The period for the verification code. + * @property refreshPeriod The selected period for the verification code in seconds. + * @property originalRefreshPeriod The loaded period in seconds, retained as a picker option. * @property totpCode The totp code for the item. * @property username Account or username for this item. * @property issuer Name of the item provider. @@ -18,7 +18,8 @@ import kotlinx.parcelize.Parcelize */ @Parcelize data class EditItemData( - val refreshPeriod: AuthenticatorRefreshPeriodOption, + val refreshPeriod: Int, + val originalRefreshPeriod: Int, val totpCode: String, val type: AuthenticatorItemType, val username: String?, diff --git a/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/edititem/EditItemScreenTest.kt b/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/edititem/EditItemScreenTest.kt index 9b7fb4ccd2b..2080381135f 100644 --- a/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/edititem/EditItemScreenTest.kt +++ b/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/edititem/EditItemScreenTest.kt @@ -1,10 +1,15 @@ package com.bitwarden.authenticator.ui.authenticator.feature.edititem import androidx.compose.ui.test.assert +import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsSelected +import androidx.compose.ui.test.filter import androidx.compose.ui.test.filterToOne import androidx.compose.ui.test.hasAnyAncestor import androidx.compose.ui.test.hasContentDescription +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.isSelectable import androidx.compose.ui.test.isDialog import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onChildren @@ -318,6 +323,15 @@ class EditItemScreenTest : AuthenticatorComposeTest() { .onAllNodesWithText(text = "Refresh period") .filterToOne(hasAnyAncestor(isDialog())) .assertIsDisplayed() + composeTestRule + .onAllNodes(isSelectable() and hasAnyAncestor(isDialog())) + .assertCountEquals(3) + listOf(30, 60, 90).forEach { period -> + composeTestRule + .onAllNodesWithText(text = "$period seconds") + .filter(hasAnyAncestor(isDialog())) + .assertCountEquals(1) + } composeTestRule .onNodeWithText(text = "60 seconds") .performClick() @@ -325,18 +339,95 @@ class EditItemScreenTest : AuthenticatorComposeTest() { verify(exactly = 1) { viewModel.trySendAction( - EditItemAction.RefreshPeriodOptionClick(AuthenticatorRefreshPeriodOption.SIXTY), + EditItemAction.RefreshPeriodOptionClick(60), ) } } @Test - fun `refresh period click should display dialog and cancel should dismiss the dialog`() { + fun `custom refresh period should remain selectable after choosing a preset`() { + val content = DEFAULT_CONTENT.copy( + isAdvancedOptionsExpanded = true, + itemData = DEFAULT_ITEM_DATA.copy(refreshPeriod = 45, originalRefreshPeriod = 45), + ) + mutableStateFlow.update { it.copy(viewState = content) } + composeTestRule + .onNodeWithContentDescriptionAfterScroll(label = "45 seconds. Refresh period") + .assertIsDisplayed() + composeTestRule.onNodeWithText(text = "45 seconds").assertIsDisplayed() + composeTestRule + .onNodeWithContentDescription(label = "45 seconds. Refresh period") + .performClick() + + val options = composeTestRule + .onAllNodes(isSelectable() and hasAnyAncestor(isDialog())) + .assertCountEquals(4) + listOf(30, 45, 60, 90).forEachIndexed { index, period -> + options[index].assert(hasText("$period seconds")) + } + options[1].assertIsSelected() + options[2].performClick() + composeTestRule.assertNoDialogExists() + verify(exactly = 1) { + viewModel.trySendAction(EditItemAction.RefreshPeriodOptionClick(60)) + } + mutableStateFlow.update { - it.copy(viewState = DEFAULT_CONTENT.copy(isAdvancedOptionsExpanded = true)) + it.copy(viewState = content.copy(itemData = content.itemData.copy(refreshPeriod = 60))) } composeTestRule - .onNodeWithContentDescriptionAfterScroll(label = "30 seconds. Refresh period") + .onNodeWithContentDescriptionAfterScroll(label = "60 seconds. Refresh period") + .performClick() + composeTestRule + .onAllNodesWithText(text = "60 seconds") + .filterToOne(hasAnyAncestor(isDialog())) + .assertIsSelected() + composeTestRule.onNodeWithText(text = "45 seconds").performClick() + composeTestRule.assertNoDialogExists() + verify(exactly = 1) { + viewModel.trySendAction(EditItemAction.RefreshPeriodOptionClick(45)) + } + + mutableStateFlow.update { it.copy(viewState = content) } + composeTestRule + .onNodeWithContentDescriptionAfterScroll(label = "45 seconds. Refresh period") + .assertIsDisplayed() + } + + @Test + fun `restored selection should be included alongside the original custom period`() { + mutableStateFlow.update { + it.copy( + viewState = DEFAULT_CONTENT.copy( + isAdvancedOptionsExpanded = true, + itemData = DEFAULT_ITEM_DATA.copy( + refreshPeriod = 120, + originalRefreshPeriod = 45, + ), + ), + ) + } + composeTestRule + .onNodeWithContentDescriptionAfterScroll(label = "120 seconds. Refresh period") + .performClick() + val options = composeTestRule + .onAllNodes(isSelectable() and hasAnyAncestor(isDialog())) + .assertCountEquals(5) + listOf(30, 45, 60, 90, 120).forEachIndexed { index, period -> + options[index].assert(hasText("$period seconds")) + } + options[4].assertIsSelected() + } + + @Test + fun `refresh period click should display dialog and cancel should dismiss the dialog`() { + val content = DEFAULT_CONTENT.copy( + isAdvancedOptionsExpanded = true, + itemData = DEFAULT_ITEM_DATA.copy(refreshPeriod = 45, originalRefreshPeriod = 45), + ) + mutableStateFlow.update { it.copy(viewState = content) } + composeTestRule + .onNodeWithContentDescriptionAfterScroll(label = "45 seconds. Refresh period") .performClick() composeTestRule @@ -347,6 +438,36 @@ class EditItemScreenTest : AuthenticatorComposeTest() { .onNodeWithText(text = "Cancel") .performClick() composeTestRule.assertNoDialogExists() + composeTestRule + .onNodeWithTextAfterScroll(text = "Additional options") + .performClick() + mutableStateFlow.update { + it.copy(viewState = content.copy(isAdvancedOptionsExpanded = false)) + } + composeTestRule.onNodeWithText(text = "45 seconds").assertDoesNotExist() + composeTestRule + .onNodeWithTextAfterScroll(text = "Name") + .performTextInput(text = "New issuer") + val renamedContent = content.copy(itemData = content.itemData.copy(issuer = "New issuer")) + mutableStateFlow.update { + it.copy(viewState = renamedContent.copy(isAdvancedOptionsExpanded = false)) + } + composeTestRule + .onNodeWithTextAfterScroll(text = "Additional options") + .performClick() + mutableStateFlow.update { it.copy(viewState = renamedContent) } + composeTestRule + .onNodeWithContentDescriptionAfterScroll(label = "45 seconds. Refresh period") + .assertIsDisplayed() + verify(exactly = 0) { + viewModel.trySendAction(any()) + } + verify(exactly = 2) { + viewModel.trySendAction(EditItemAction.ExpandAdvancedOptionsClick) + } + verify(exactly = 1) { + viewModel.trySendAction(EditItemAction.IssuerNameTextChange("New issuer")) + } } @Test @@ -391,7 +512,8 @@ private val DEFAULT_STATE: EditItemState = private val DEFAULT_ITEM_DATA: EditItemData = EditItemData( - refreshPeriod = AuthenticatorRefreshPeriodOption.THIRTY, + refreshPeriod = 30, + originalRefreshPeriod = 30, totpCode = "", type = AuthenticatorItemType.TOTP, username = null, diff --git a/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/edititem/EditItemViewModelTest.kt b/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/edititem/EditItemViewModelTest.kt index 6f0d1c0c599..1cc3553b915 100644 --- a/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/edititem/EditItemViewModelTest.kt +++ b/authenticator/src/test/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/edititem/EditItemViewModelTest.kt @@ -26,11 +26,14 @@ import io.mockk.runs import io.mockk.unmockkStatic import io.mockk.verify import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource class EditItemViewModelTest : BaseViewModelTest() { private val mutableItemStateFlow = @@ -63,6 +66,109 @@ class EditItemViewModelTest : BaseViewModelTest() { assertEquals(DEFAULT_STATE, viewModel.stateFlow.value) } + @ParameterizedTest + @ValueSource(ints = [1, 30, 45, 60, 90, 120]) + fun `saving metadata edits should preserve the loaded period`(period: Int) { + val entity = DEFAULT_AUTHENTICATOR_ENTITY.copy( + period = period, + algorithm = AuthenticatorItemAlgorithm.SHA256, + digits = 8, + favorite = true, + ) + coEvery { + authenticatorRepository.createItem(item = any()) + } returns CreateItemResult.Success + val viewModel = createViewModel() + assertEquals(DEFAULT_STATE, viewModel.stateFlow.value) + mutableItemStateFlow.tryEmit(DataState.Loaded(entity)) + + viewModel.trySendAction(EditItemAction.IssuerNameTextChange("New issuer")) + viewModel.trySendAction(EditItemAction.UsernameTextChange("New username")) + viewModel.trySendAction(EditItemAction.SaveClick) + + coVerify(exactly = 1) { + authenticatorRepository.createItem( + item = entity.copy(issuer = "New issuer", accountName = "New username"), + ) + } + } + + @ParameterizedTest + @ValueSource(ints = [1, 30, 45, 60, 90, 120]) + fun `saving without edits should preserve the loaded period`(period: Int) { + val entity = DEFAULT_AUTHENTICATOR_ENTITY.copy(period = period) + coEvery { + authenticatorRepository.createItem(item = any()) + } returns CreateItemResult.Success + val viewModel = createViewModel() + mutableItemStateFlow.tryEmit(DataState.Loaded(entity)) + + viewModel.trySendAction(EditItemAction.SaveClick) + + coVerify(exactly = 1) { + authenticatorRepository.createItem(item = entity) + } + } + + @ParameterizedTest + @ValueSource(ints = [60, 45]) + fun `selecting a period should retain the original option and save the selection`(period: Int) { + val entity = DEFAULT_AUTHENTICATOR_ENTITY.copy(period = 45) + coEvery { + authenticatorRepository.createItem(item = any()) + } returns CreateItemResult.Success + val viewModel = createViewModel() + mutableItemStateFlow.tryEmit(DataState.Loaded(entity)) + + viewModel.trySendAction(EditItemAction.ExpandAdvancedOptionsClick) + viewModel.trySendAction(EditItemAction.RefreshPeriodOptionClick(60)) + viewModel.trySendAction(EditItemAction.RefreshPeriodOptionClick(period)) + viewModel.trySendAction(EditItemAction.ExpandAdvancedOptionsClick) + viewModel.trySendAction(EditItemAction.ExpandAdvancedOptionsClick) + viewModel.trySendAction(EditItemAction.IssuerNameTextChange("New issuer")) + + assertEquals( + DEFAULT_CONTENT.copy( + isAdvancedOptionsExpanded = true, + itemData = DEFAULT_ITEM_DATA.copy( + refreshPeriod = period, + originalRefreshPeriod = 45, + issuer = "New issuer", + ), + ), + viewModel.stateFlow.value.viewState, + ) + viewModel.trySendAction(EditItemAction.SaveClick) + coVerify(exactly = 1) { + authenticatorRepository.createItem( + item = entity.copy(period = period, issuer = "New issuer"), + ) + } + } + + @Test + fun `saved custom period content should be accepted and saved`() { + every { authenticatorRepository.getItemStateFlow(DEFAULT_ITEM_ID) } returns emptyFlow() + coEvery { + authenticatorRepository.createItem(item = any()) + } returns CreateItemResult.Success + val state = DEFAULT_STATE.copy( + viewState = DEFAULT_CONTENT.copy( + itemData = DEFAULT_ITEM_DATA.copy(refreshPeriod = 120, originalRefreshPeriod = 45), + ), + ) + val viewModel = createViewModel(state = state) + assertEquals(state, viewModel.stateFlow.value) + + viewModel.trySendAction(EditItemAction.SaveClick) + + coVerify(exactly = 1) { + authenticatorRepository.createItem( + item = DEFAULT_AUTHENTICATOR_ENTITY.copy(period = 120), + ) + } + } + @Test fun `on DismissDialog should clear the dialog state`() = runTest { val state = DEFAULT_STATE.copy( @@ -175,7 +281,7 @@ class EditItemViewModelTest : BaseViewModelTest() { mutableItemStateFlow.tryEmit(DataState.Loaded(DEFAULT_AUTHENTICATOR_ENTITY)) val state = DEFAULT_STATE.copy(viewState = DEFAULT_CONTENT) val viewModel = createViewModel(state = state) - val period = AuthenticatorRefreshPeriodOption.NINETY + val period = 90 viewModel.trySendAction(EditItemAction.RefreshPeriodOptionClick(period)) assertEquals( state.copy( @@ -224,11 +330,15 @@ class EditItemViewModelTest : BaseViewModelTest() { @Test fun `on SaveClick with blank issuer should display an error dialog`() { mutableItemStateFlow.tryEmit( - DataState.Loaded(DEFAULT_AUTHENTICATOR_ENTITY.copy(issuer = "")), + DataState.Loaded(DEFAULT_AUTHENTICATOR_ENTITY.copy(issuer = "", period = 45)), ) val state = DEFAULT_STATE.copy( viewState = DEFAULT_CONTENT.copy( - itemData = DEFAULT_ITEM_DATA.copy(issuer = ""), + itemData = DEFAULT_ITEM_DATA.copy( + issuer = "", + refreshPeriod = 45, + originalRefreshPeriod = 45, + ), ), ) val viewModel = createViewModel(state = state) @@ -243,16 +353,21 @@ class EditItemViewModelTest : BaseViewModelTest() { ), viewModel.stateFlow.value, ) + coVerify(exactly = 0) { authenticatorRepository.createItem(item = any()) } } @Test fun `on SaveClick with blank totp code should display an error dialog`() { mutableItemStateFlow.tryEmit( - DataState.Loaded(DEFAULT_AUTHENTICATOR_ENTITY.copy(key = "")), + DataState.Loaded(DEFAULT_AUTHENTICATOR_ENTITY.copy(key = "", period = 45)), ) val state = DEFAULT_STATE.copy( viewState = DEFAULT_CONTENT.copy( - itemData = DEFAULT_ITEM_DATA.copy(totpCode = ""), + itemData = DEFAULT_ITEM_DATA.copy( + totpCode = "", + refreshPeriod = 45, + originalRefreshPeriod = 45, + ), ), ) val viewModel = createViewModel(state = state) @@ -267,16 +382,21 @@ class EditItemViewModelTest : BaseViewModelTest() { ), viewModel.stateFlow.value, ) + coVerify(exactly = 0) { authenticatorRepository.createItem(item = any()) } } @Test fun `on SaveClick with non-base32 totp code should display an error dialog`() { mutableItemStateFlow.tryEmit( - DataState.Loaded(DEFAULT_AUTHENTICATOR_ENTITY.copy(key = "111%")), + DataState.Loaded(DEFAULT_AUTHENTICATOR_ENTITY.copy(key = "111%", period = 45)), ) val state = DEFAULT_STATE.copy( viewState = DEFAULT_CONTENT.copy( - itemData = DEFAULT_ITEM_DATA.copy(totpCode = "111%"), + itemData = DEFAULT_ITEM_DATA.copy( + totpCode = "111%", + refreshPeriod = 45, + originalRefreshPeriod = 45, + ), ), ) val viewModel = createViewModel(state = state) @@ -290,15 +410,23 @@ class EditItemViewModelTest : BaseViewModelTest() { ), viewModel.stateFlow.value, ) + coVerify(exactly = 0) { authenticatorRepository.createItem(item = any()) } } @Test fun `on SaveClick with valid data and createItem error should display error dialog`() = runTest { mutableItemStateFlow.tryEmit( - DataState.Loaded(DEFAULT_AUTHENTICATOR_ENTITY), + DataState.Loaded(DEFAULT_AUTHENTICATOR_ENTITY.copy(period = 45)), + ) + val state = DEFAULT_STATE.copy( + viewState = DEFAULT_CONTENT.copy( + itemData = DEFAULT_ITEM_DATA.copy( + refreshPeriod = 45, + originalRefreshPeriod = 45, + ), + ), ) - val state = DEFAULT_STATE.copy(viewState = DEFAULT_CONTENT) coEvery { authenticatorRepository.createItem(item = any()) } returns CreateItemResult.Error @@ -324,7 +452,16 @@ class EditItemViewModelTest : BaseViewModelTest() { awaitItem(), ) } - coVerify(exactly = 1) { + viewModel.trySendAction(EditItemAction.DismissDialog) + assertEquals(state, viewModel.stateFlow.value) + coEvery { + authenticatorRepository.createItem(item = any()) + } returns CreateItemResult.Success + viewModel.eventFlow.test { + viewModel.trySendAction(EditItemAction.SaveClick) + assertEquals(EditItemEvent.NavigateBack, awaitItem()) + } + coVerify(exactly = 2) { authenticatorRepository.createItem( item = AuthenticatorItemEntity( id = DEFAULT_ITEM_ID, @@ -332,7 +469,7 @@ class EditItemViewModelTest : BaseViewModelTest() { accountName = "mockAccountName", type = AuthenticatorItemType.TOTP, algorithm = AuthenticatorItemAlgorithm.SHA1, - period = 30, + period = 45, digits = 6, issuer = "mockIssuer", favorite = false, @@ -344,9 +481,13 @@ class EditItemViewModelTest : BaseViewModelTest() { @Test fun `on SaveClick with valid data and createItem success navigate back`() = runTest { mutableItemStateFlow.tryEmit( - DataState.Loaded(DEFAULT_AUTHENTICATOR_ENTITY), + DataState.Loaded(DEFAULT_AUTHENTICATOR_ENTITY.copy(period = 45)), + ) + val state = DEFAULT_STATE.copy( + viewState = DEFAULT_CONTENT.copy( + itemData = DEFAULT_ITEM_DATA.copy(refreshPeriod = 45, originalRefreshPeriod = 45), + ), ) - val state = DEFAULT_STATE.copy(viewState = DEFAULT_CONTENT) coEvery { authenticatorRepository.createItem(item = any()) } returns CreateItemResult.Success @@ -378,7 +519,7 @@ class EditItemViewModelTest : BaseViewModelTest() { accountName = "mockAccountName", type = AuthenticatorItemType.TOTP, algorithm = AuthenticatorItemAlgorithm.SHA1, - period = 30, + period = 45, digits = 6, issuer = "mockIssuer", favorite = false, @@ -480,7 +621,8 @@ private val DEFAULT_STATE: EditItemState = private val DEFAULT_ITEM_DATA: EditItemData = EditItemData( - refreshPeriod = AuthenticatorRefreshPeriodOption.THIRTY, + refreshPeriod = 30, + originalRefreshPeriod = 30, totpCode = "ABCD", type = AuthenticatorItemType.TOTP, username = "mockAccountName",