Improve clock usage patterns (#6336)

This commit is contained in:
David Perez
2026-01-08 20:54:33 +00:00
committed by GitHub
parent 5d308aa95f
commit 203313eb1d
20 changed files with 168 additions and 181 deletions
@@ -1,6 +1,5 @@
package com.x8bit.bitwarden.data.vault.repository
import com.bitwarden.core.DateTime
import com.bitwarden.core.data.repository.model.DataState
import com.bitwarden.exporters.ExportFormat
import com.bitwarden.fido.Fido2CredentialAutofillView
@@ -24,6 +23,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import java.time.Instant
import javax.crypto.Cipher
/**
@@ -135,7 +135,7 @@ interface VaultRepository :
/**
* Attempt to get the verification code and the period.
*/
suspend fun generateTotp(cipherId: String, time: DateTime): GenerateTotpResult
suspend fun generateTotp(cipherId: String, time: Instant): GenerateTotpResult
/**
* Attempt to get the user's vault data for export.
@@ -1,6 +1,5 @@
package com.x8bit.bitwarden.data.vault.repository
import com.bitwarden.core.DateTime
import com.bitwarden.core.InitUserCryptoMethod
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
import com.bitwarden.core.data.repository.error.MissingPropertyException
@@ -63,6 +62,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import timber.log.Timber
import java.security.GeneralSecurityException
import java.time.Instant
import javax.crypto.Cipher
/**
@@ -408,7 +408,7 @@ class VaultRepositoryImpl(
override suspend fun generateTotp(
cipherId: String,
time: DateTime,
time: Instant,
): GenerateTotpResult {
val userId = activeUserId
?: return GenerateTotpResult.Error(error = NoActiveUserException())
@@ -3,7 +3,6 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.folders.addedit
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.core.DateTime
import com.bitwarden.core.data.repository.model.DataState
import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
@@ -25,6 +24,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import java.time.Clock
import javax.inject.Inject
private const val KEY_STATE = "state"
@@ -37,6 +37,7 @@ private const val KEY_STATE = "state"
@Suppress("TooManyFunctions", "LargeClass")
class FolderAddEditViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val clock: Clock,
private val vaultRepository: VaultRepository,
private val relayManager: SnackbarRelayManager<SnackbarRelay>,
) : BaseViewModel<FolderAddEditState, FolderAddEditEvent, FolderAddEditAction>(
@@ -124,7 +125,7 @@ class FolderAddEditViewModel @Inject constructor(
.orEmpty() +
content.folderName,
id = folderAddEditType.folderId,
revisionDate = DateTime.now(),
revisionDate = clock.instant(),
),
)
sendAction(FolderAddEditAction.Internal.CreateFolderResultReceive(result))
@@ -136,7 +137,7 @@ class FolderAddEditViewModel @Inject constructor(
FolderView(
name = content.folderName,
id = folderAddEditType.folderId,
revisionDate = DateTime.now(),
revisionDate = clock.instant(),
),
)
sendAction(FolderAddEditAction.Internal.UpdateFolderResultReceive(result))
@@ -5,7 +5,6 @@ import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.provider.CallingAppInfo
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.core.DateTime
import com.bitwarden.core.data.manager.toast.ToastManager
import com.bitwarden.core.data.repository.model.DataState
import com.bitwarden.core.data.repository.util.takeUntilLoaded
@@ -415,7 +414,7 @@ class VaultAddEditViewModel @Inject constructor(
handleCreatePublicKeyCredentialRequest(
request = createPublicKeyCredentialRequest,
callingAppInfo = this.callingAppInfo,
cipherView = content.toCipherView(),
cipherView = content.toCipherView(clock = clock),
)
return@onContent
}
@@ -431,7 +430,7 @@ class VaultAddEditViewModel @Inject constructor(
is VaultAddEditType.EditItem -> {
val result = vaultRepository.updateCipher(
cipherId = vaultAddEditType.vaultItemId,
cipherView = content.toCipherView(),
cipherView = content.toCipherView(clock = clock),
)
sendAction(VaultAddEditAction.Internal.UpdateCipherResultReceive(result))
}
@@ -610,7 +609,7 @@ class VaultAddEditViewModel @Inject constructor(
handleCreatePublicKeyCredentialRequest(
request = createPublicKeyCredentialRequest,
callingAppInfo = request.callingAppInfo,
cipherView = content.toCipherView(),
cipherView = content.toCipherView(clock = clock),
)
}
}
@@ -641,7 +640,7 @@ class VaultAddEditViewModel @Inject constructor(
handleCreatePublicKeyCredentialRequest(
request = createPublicKeyCredentialRequest,
callingAppInfo = request.callingAppInfo,
cipherView = content.toCipherView(),
cipherView = content.toCipherView(clock = clock),
)
}
}
@@ -817,7 +816,7 @@ class VaultAddEditViewModel @Inject constructor(
FolderView(
name = action.newFolderName,
id = null,
revisionDate = DateTime.now(),
revisionDate = clock.instant(),
),
)
sendAction(VaultAddEditAction.Internal.AddFolderResultReceive(result = result))
@@ -2191,11 +2190,11 @@ class VaultAddEditViewModel @Inject constructor(
?.map { it.id }
?.let {
vaultRepository.createCipherInOrganization(
cipherView = toCipherView(),
cipherView = toCipherView(clock = clock),
collectionIds = it,
)
}
?: vaultRepository.createCipher(cipherView = toCipherView())
?: vaultRepository.createCipher(cipherView = toCipherView(clock = clock))
}
private fun List<VaultAddEditState.Owner>.toUpdatedOwners(
@@ -2,7 +2,6 @@
package com.x8bit.bitwarden.ui.vault.feature.vault.util
import com.bitwarden.core.DateTime
import com.bitwarden.ui.platform.base.util.orNullIfBlank
import com.bitwarden.vault.CardView
import com.bitwarden.vault.CipherRepromptType
@@ -23,12 +22,13 @@ import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth
import com.x8bit.bitwarden.ui.vault.model.VaultIdentityTitle
import com.x8bit.bitwarden.ui.vault.util.stringLongNameOrNull
import java.time.Clock
import java.time.Instant
/**
* Transforms [VaultAddEditState.ViewState.Content] into [CipherView].
*/
fun VaultAddEditState.ViewState.Content.toCipherView(): CipherView =
fun VaultAddEditState.ViewState.Content.toCipherView(clock: Clock): CipherView =
CipherView(
// Pulled from original cipher when editing, otherwise uses defaults
id = common.originalCipher?.id,
@@ -39,18 +39,18 @@ fun VaultAddEditState.ViewState.Content.toCipherView(): CipherView =
localData = common.originalCipher?.localData,
attachments = common.originalCipher?.attachments,
organizationUseTotp = common.originalCipher?.organizationUseTotp ?: false,
passwordHistory = toPasswordHistory(),
passwordHistory = toPasswordHistory(clock = clock),
permissions = common.originalCipher?.permissions,
creationDate = common.originalCipher?.creationDate ?: Instant.now(),
creationDate = common.originalCipher?.creationDate ?: clock.instant(),
deletedDate = common.originalCipher?.deletedDate,
revisionDate = common.originalCipher?.revisionDate ?: Instant.now(),
revisionDate = common.originalCipher?.revisionDate ?: clock.instant(),
archivedDate = common.originalCipher?.archivedDate,
// Type specific section
type = type.toCipherType(),
identity = type.toIdentityView(),
secureNote = type.toSecureNotesView(),
login = type.toLoginView(common = common),
login = type.toLoginView(common = common, clock = clock),
card = type.toCardView(),
sshKey = type.toSshKeyView(),
@@ -134,21 +134,23 @@ private fun VaultAddEditState.ViewState.Content.ItemType.toIdentityView(): Ident
}
@Suppress("MagicNumber")
private fun VaultAddEditState.ViewState.Content.toPasswordHistory(): List<PasswordHistoryView>? {
private fun VaultAddEditState.ViewState.Content.toPasswordHistory(
clock: Clock,
): List<PasswordHistoryView>? {
val loginCipher = type as? VaultAddEditState.ViewState.Content.ItemType.Login
val oldPassword = common.originalCipher?.login?.password
val timestamp = Instant.now()
val timestamp = clock.instant()
val newPasswordHistory = getPasswordHistory(
loginCipher,
oldPassword,
timestamp,
loginCipher = loginCipher,
oldPassword = oldPassword,
timestamp = timestamp,
)
val newHiddenFieldHistory = getHiddenFieldHistory(
timestamp,
common.originalCipher?.fields,
common.customFieldData,
timestamp = timestamp,
oldFields = common.originalCipher?.fields,
newFields = common.customFieldData,
)
return listOf(
@@ -204,12 +206,16 @@ private fun getHiddenFieldHistory(
private fun VaultAddEditState.ViewState.Content.ItemType.toLoginView(
common: VaultAddEditState.ViewState.Content.Common,
clock: Clock,
): LoginView? =
(this as? VaultAddEditState.ViewState.Content.ItemType.Login)?.let {
LoginView(
username = it.username.orNullIfBlank(),
password = it.password.orNullIfBlank(),
passwordRevisionDate = it.getRevisionDate(common.originalCipher),
passwordRevisionDate = it.getRevisionDate(
originalCipher = common.originalCipher,
clock = clock,
),
uris = it.uriList.toLoginUriView(),
totp = it.totp,
autofillOnPageLoad = common.originalCipher?.login?.autofillOnPageLoad,
@@ -280,12 +286,13 @@ private fun List<UriItem>?.toLoginUriView(): List<LoginUriView>? =
private fun VaultAddEditState.ViewState.Content.ItemType.Login.getRevisionDate(
originalCipher: CipherView?,
): DateTime? {
clock: Clock,
): Instant? {
val isOriginalPasswordNull = originalCipher?.login?.password.isNullOrEmpty()
val hasPasswordHistory = originalCipher?.passwordHistory?.any() ?: false
val isOriginalAndNewPasswordEqual = originalCipher?.login?.password == this.password
return if ((!isOriginalPasswordNull || hasPasswordHistory) && !isOriginalAndNewPasswordEqual) {
Instant.now()
clock.instant()
} else {
originalCipher?.login?.passwordRevisionDate
}
@@ -47,7 +47,7 @@ class PushDiskSourceTest {
)
pushDiskSource.storeLastPushTokenRegistrationDate(
userId = userId,
registrationDate = ZonedDateTime.now(),
registrationDate = ZonedDateTime.parse("2023-10-27T12:00:00Z"),
)
pushDiskSource.clearData(userId = userId)
@@ -138,7 +138,7 @@ class PushDiskSourceTest {
fun `storeLastPushTokenRegistrationDate for null values should clear SharedPreferences`() {
val lastPushTokenBaseKey = "bwPreferencesStorage:pushLastRegistrationDate"
val mockUserId = "mockUserId"
val mockLastPushTokenRegistration = ZonedDateTime.now()
val mockLastPushTokenRegistration = ZonedDateTime.parse("2023-10-27T12:00:00Z")
val lastPushTokenKey = "${lastPushTokenBaseKey}_$mockUserId"
fakeSharedPreferences.edit {
putLong(lastPushTokenKey, mockLastPushTokenRegistration.toEpochSecond())
@@ -506,7 +506,7 @@ private val USER_1_TOTP_CIPHER = mockk<SyncResponseJson.Cipher> {
private val USER_1_DELETED_TOTP_CIPHER = mockk<SyncResponseJson.Cipher> {
every { login?.totp } returns "encryptedTotp1Deleted"
every { login?.username } returns "username"
every { deletedDate } returns ZonedDateTime.now()
every { deletedDate } returns ZonedDateTime.parse("2023-10-27T12:00:00Z")
every { name } returns "cipher1"
}
@@ -20,7 +20,7 @@ fun createMockFido2CredentialView(number: Int): Fido2CredentialView = Fido2Crede
rpName = "mockRpName-$number",
userDisplayName = "mockUserDisplayName-$number",
discoverable = "mockDiscoverable-$number",
creationDate = Instant.now(),
creationDate = Instant.parse("2023-10-27T12:00:00Z"),
)
/**
@@ -933,7 +933,6 @@ class CipherManagerTest {
)
.toEncryptedNetworkCipherResponse()
} returns createMockCipher(number = 1)
val fixedInstant = Instant.parse("2021-01-01T00:00:00Z")
val userId = "mockId-1"
val cipherId = "mockId-1"
val attachmentId = "mockId-1"
@@ -960,8 +959,6 @@ class CipherManagerTest {
)
} just runs
val cipherView = createMockCipherView(number = 1)
mockkStatic(Instant::class)
every { Instant.now() } returns fixedInstant
val result = cipherManager.deleteCipherAttachment(
cipherId = cipherId,
@@ -970,7 +967,6 @@ class CipherManagerTest {
)
assertEquals(DeleteAttachmentResult.Success, result)
unmockkStatic(Instant::class)
unmockkStatic(Cipher::toEncryptedNetworkCipherResponse)
}
@@ -115,7 +115,7 @@ class FolderManagerTest {
val folderView = FolderView(
id = null,
name = "TestName",
revisionDate = Instant.now(FIXED_CLOCK),
revisionDate = FIXED_CLOCK.instant(),
)
val error = IllegalStateException()
@@ -130,7 +130,7 @@ class FolderManagerTest {
@Test
fun `createFolder with folderService failure should return CreateFolderResult failure`() =
runTest {
val date = Instant.now(FIXED_CLOCK)
val date = FIXED_CLOCK.instant()
val testFolderName = "TestName"
fakeAuthDiskSource.userState = MOCK_USER_STATE
@@ -157,7 +157,7 @@ class FolderManagerTest {
fun `createFolder with folderService createFolder should return CreateFolderResult success`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val date = Instant.now(FIXED_CLOCK)
val date = FIXED_CLOCK.instant()
val testFolderName = "TestName"
val folderView = FolderView(
id = null,
@@ -270,7 +270,7 @@ class FolderManagerTest {
val folderView = FolderView(
id = folderId,
name = "TestName",
revisionDate = Instant.now(FIXED_CLOCK),
revisionDate = FIXED_CLOCK.instant(),
)
val error = IllegalStateException()
@@ -286,7 +286,7 @@ class FolderManagerTest {
@Test
fun `updateFolder with folderService failure should return UpdateFolderResult failure`() =
runTest {
val date = Instant.now(FIXED_CLOCK)
val date = FIXED_CLOCK.instant()
val testFolderName = "TestName"
val folderId = "testId"
@@ -317,7 +317,7 @@ class FolderManagerTest {
@Test
fun `updateFolder with folderService updateFolder Invalid response should return UpdateFolderResult Error with a non-null message`() =
runTest {
val date = Instant.now(FIXED_CLOCK)
val date = FIXED_CLOCK.instant()
val testFolderName = "TestName"
val folderId = "testId"
@@ -358,7 +358,7 @@ class FolderManagerTest {
@Test
fun `updateFolder with folderService updateFolder success should return UpdateFolderResult success`() =
runTest {
val date = Instant.now(FIXED_CLOCK)
val date = FIXED_CLOCK.instant()
val testFolderName = "TestName"
val folderId = "testId"
@@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.vault.repository
import app.cash.turbine.test
import com.bitwarden.collections.CollectionView
import com.bitwarden.core.DateTime
import com.bitwarden.core.InitUserCryptoMethod
import com.bitwarden.core.MasterPasswordUnlockData
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
@@ -77,6 +76,7 @@ import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.security.GeneralSecurityException
import java.time.Instant
import java.time.ZonedDateTime
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
@@ -996,7 +996,7 @@ class VaultRepositoryTest {
val result = vaultRepository.generateTotp(
cipherId = "totpCode",
time = DateTime.now(),
time = Instant.parse("2023-10-27T12:00:00Z"),
)
assertEquals(
@@ -1023,7 +1023,7 @@ class VaultRepositoryTest {
val result = vaultRepository.generateTotp(
cipherId = "mockId-1",
time = DateTime.now(),
time = Instant.parse("2023-10-27T12:00:00Z"),
)
assertEquals(
@@ -263,7 +263,7 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
ipAddress = "192.168.0.2",
key = "publicKey",
masterPasswordHash = "verySecureHash",
creationDate = ZonedDateTime.now().minusMinutes(5),
creationDate = ZonedDateTime.now(fixedClock).minusMinutes(5),
responseDate = null,
requestApproved = false,
originUrl = "www.bitwarden.com",
@@ -276,7 +276,7 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
ipAddress = "192.168.0.3",
key = "publicKey",
masterPasswordHash = "verySecureHash",
creationDate = ZonedDateTime.now(),
creationDate = ZonedDateTime.now(fixedClock),
responseDate = null,
requestApproved = false,
originUrl = "www.bitwarden.com",
@@ -291,7 +291,7 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
)
} returns AuthRequestResult.Success(
authRequest1.copy(
responseDate = ZonedDateTime.now(),
responseDate = ZonedDateTime.now(fixedClock),
),
)
coEvery {
@@ -303,7 +303,7 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
)
} returns AuthRequestResult.Success(
authRequest2.copy(
responseDate = ZonedDateTime.now(),
responseDate = ZonedDateTime.now(fixedClock),
),
)
val viewModel = createViewModel()
@@ -1,7 +1,6 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.folders
import app.cash.turbine.test
import com.bitwarden.core.DateTime
import com.bitwarden.core.data.repository.model.DataState
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.ui.platform.base.BaseViewModelTest
@@ -20,6 +19,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.time.Instant
class FoldersViewModelTest : BaseViewModelTest() {
@@ -195,5 +195,9 @@ class FoldersViewModelTest : BaseViewModelTest() {
)
}
private val DEFAULT_FOLDER_VIEW = FolderView("1", "test", revisionDate = DateTime.now())
private val DEFAULT_FOLDER_VIEW = FolderView(
id = "1",
name = "test",
revisionDate = Instant.parse("2025-04-11T10:15:30.00Z"),
)
private val DEFAULT__DISPLAY_FOLDER = FolderDisplayItem("1", "test")
@@ -2,7 +2,6 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.folders.addedit
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.bitwarden.core.DateTime
import com.bitwarden.core.data.repository.model.DataState
import com.bitwarden.ui.platform.base.BaseViewModelTest
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
@@ -32,7 +31,9 @@ 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 java.time.Clock
import java.time.Instant
import java.time.ZoneOffset
@Suppress("LargeClass")
class FolderAddEditViewModelTest : BaseViewModelTest() {
@@ -120,9 +121,9 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
mutableFoldersStateFlow.value =
DataState.Loaded(
FolderView(
DEFAULT_EDIT_ITEM_ID,
DEFAULT_FOLDER_NAME,
DateTime.now(),
id = DEFAULT_EDIT_ITEM_ID,
name = DEFAULT_FOLDER_NAME,
revisionDate = FIXED_CLOCK.instant(),
),
)
@@ -174,9 +175,9 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
mutableFoldersStateFlow.value =
DataState.Loaded(
FolderView(
DEFAULT_EDIT_ITEM_ID,
DEFAULT_FOLDER_NAME,
DateTime.now(),
id = DEFAULT_EDIT_ITEM_ID,
name = DEFAULT_FOLDER_NAME,
revisionDate = FIXED_CLOCK.instant(),
),
)
@@ -253,9 +254,9 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
mutableFoldersStateFlow.value =
DataState.Loaded(
FolderView(
DEFAULT_EDIT_ITEM_ID,
DEFAULT_FOLDER_NAME,
DateTime.now(),
id = DEFAULT_EDIT_ITEM_ID,
name = DEFAULT_FOLDER_NAME,
revisionDate = FIXED_CLOCK.instant(),
),
)
@@ -350,8 +351,6 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
@Test
fun `in add mode, SaveClick createFolder with no parentFolderNamePresent should just create folder with entered name`() =
runTest {
mockkStatic(DateTime::class)
every { DateTime.now() } returns Instant.MIN
val viewModel =
createViewModel(
createSavedStateHandleWithState(
@@ -375,7 +374,7 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
folderView = FolderView(
name = DEFAULT_FOLDER_NAME,
id = null,
revisionDate = Instant.MIN,
revisionDate = FIXED_CLOCK.instant(),
),
)
}
@@ -385,15 +384,12 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
relay = SnackbarRelay.FOLDER_CREATED,
)
}
unmockkStatic(DateTime::class)
}
@Suppress("MaxLineLength")
@Test
fun `in add mode, SaveClick createFolder with a parentFolderNamePresent should prepend the parent folder to the entered name`() =
runTest {
mockkStatic(DateTime::class)
every { DateTime.now() } returns Instant.MIN
val parentFolderName = "parent/folder"
val viewModel =
createViewModel(
@@ -418,7 +414,7 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
folderView = FolderView(
name = "$parentFolderName/$DEFAULT_FOLDER_NAME",
id = null,
revisionDate = Instant.MIN,
revisionDate = FIXED_CLOCK.instant(),
),
)
}
@@ -428,7 +424,6 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
relay = SnackbarRelay.FOLDER_CREATED,
)
}
unmockkStatic(DateTime::class)
}
@Test
@@ -497,9 +492,9 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
mutableFoldersStateFlow.value =
DataState.Loaded(
FolderView(
DEFAULT_EDIT_ITEM_ID,
DEFAULT_FOLDER_NAME,
DateTime.now(),
id = DEFAULT_EDIT_ITEM_ID,
name = DEFAULT_FOLDER_NAME,
revisionDate = FIXED_CLOCK.instant(),
),
)
@@ -542,9 +537,9 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
mutableFoldersStateFlow.value =
DataState.Loaded(
FolderView(
DEFAULT_EDIT_ITEM_ID,
DEFAULT_FOLDER_NAME,
DateTime.now(),
id = DEFAULT_EDIT_ITEM_ID,
name = DEFAULT_FOLDER_NAME,
revisionDate = FIXED_CLOCK.instant(),
),
)
@@ -640,9 +635,9 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
mutableFoldersStateFlow.tryEmit(
DataState.Loaded(
FolderView(
DEFAULT_EDIT_ITEM_ID,
DEFAULT_FOLDER_NAME,
revisionDate = DateTime.now(),
id = DEFAULT_EDIT_ITEM_ID,
name = DEFAULT_FOLDER_NAME,
revisionDate = FIXED_CLOCK.instant(),
),
),
)
@@ -738,9 +733,9 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
mutableFoldersStateFlow.tryEmit(
value = DataState.Pending(
FolderView(
DEFAULT_EDIT_ITEM_ID,
DEFAULT_FOLDER_NAME,
revisionDate = DateTime.now(),
id = DEFAULT_EDIT_ITEM_ID,
name = DEFAULT_FOLDER_NAME,
revisionDate = FIXED_CLOCK.instant(),
),
),
)
@@ -793,6 +788,7 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
savedStateHandle: SavedStateHandle = createSavedStateHandleWithState(),
): FolderAddEditViewModel = FolderAddEditViewModel(
savedStateHandle = savedStateHandle,
clock = FIXED_CLOCK,
vaultRepository = vaultRepository,
relayManager = relayManager,
)
@@ -805,5 +801,10 @@ private val DEFAULT_STATE = FolderAddEditState(
parentFolderName = null,
)
private val FIXED_CLOCK = Clock.fixed(
Instant.parse("2025-04-11T10:15:30.00Z"),
ZoneOffset.UTC,
)
private const val DEFAULT_EDIT_ITEM_ID = "edit_id"
private const val DEFAULT_FOLDER_NAME = "test_name"
@@ -187,7 +187,7 @@ class PasswordHistoryViewModelTest : BaseViewModelTest() {
fun `when password history updates the state updates correctly`() = runTest {
val viewModel = createViewModel()
val passwordHistoryView = PasswordHistoryView("password", Instant.now())
val passwordHistoryView = PasswordHistoryView("password", fixedClock.instant())
fakeGeneratorRepository.storePasswordHistory(passwordHistoryView)
val expectedState = createPasswordHistoryState(
@@ -241,7 +241,7 @@ class PasswordHistoryViewModelTest : BaseViewModelTest() {
fun `PasswordClearClick action should update to Empty ViewState`() = runTest {
val viewModel = createViewModel()
val passwordHistoryView = PasswordHistoryView("password", Instant.now())
val passwordHistoryView = PasswordHistoryView("password", fixedClock.instant())
fakeGeneratorRepository.storePasswordHistory(passwordHistoryView)
viewModel.trySendAction(PasswordHistoryAction.PasswordClearClick)
@@ -7,7 +7,6 @@ import androidx.credentials.provider.ProviderCreateCredentialRequest
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.bitwarden.collections.CollectionView
import com.bitwarden.core.DateTime
import com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager
import com.bitwarden.core.data.manager.toast.ToastManager
import com.bitwarden.core.data.repository.model.DataState
@@ -3363,14 +3362,11 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Test
fun `AddNewFolder action calls create folder from vault repository`() = runTest {
mockkStatic(DateTime::class)
every { DateTime.now() } returns Instant.MIN
val folderName = "folderName"
val expectedFolderResult = FolderView(
id = "123",
name = folderName,
revisionDate = DateTime.now(),
revisionDate = fixedClock.instant(),
)
coEvery {
vaultRepository.createFolder(any())
@@ -3381,23 +3377,20 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
FolderView(
name = folderName,
id = null,
revisionDate = Instant.MIN,
revisionDate = fixedClock.instant(),
),
)
}
unmockkStatic(DateTime::class)
}
@Test
fun `AddNewFolder updates dialog states and selected folder id on success`() = runTest {
val folderId = "123"
val folderName = "folderName"
val expectedFolderResult = FolderView(
id = folderId,
name = folderName,
revisionDate = DateTime.now(),
revisionDate = fixedClock.instant(),
)
coEvery {
vaultRepository.createFolder(any())
@@ -3433,7 +3426,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
data = listOf(
FolderView(
name = "folder",
revisionDate = DateTime.now(),
revisionDate = fixedClock.instant(),
id = null,
),
),
@@ -21,30 +21,19 @@ import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth
import com.x8bit.bitwarden.ui.vault.model.VaultIdentityTitle
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotEquals
import org.junit.jupiter.api.Test
import java.time.Clock
import java.time.Instant
import java.time.ZoneOffset
@Suppress("LargeClass")
class VaultAddItemStateExtensionsTest {
@AfterEach
fun tearDown() {
// Some individual tests call mockkStatic so we will make sure this is always undone.
unmockkStatic(Instant::class)
}
@Suppress("MaxLineLength")
@Test
fun `toCipherView should transform Login ItemType to CipherView`() {
mockkStatic(Instant::class)
every { Instant.now() } returns Instant.MIN
val loginItemType = VaultAddEditState.ViewState.Content(
common = VaultAddEditState.ViewState.Content.Common(
name = "mockName-1",
@@ -71,7 +60,7 @@ class VaultAddItemStateExtensionsTest {
),
)
val result = loginItemType.toCipherView()
val result = loginItemType.toCipherView(clock = FIXED_CLOCK)
assertEquals(
CipherView(
@@ -111,9 +100,9 @@ class VaultAddItemStateExtensionsTest {
fields = emptyList(),
passwordHistory = null,
permissions = null,
creationDate = Instant.MIN,
creationDate = FIXED_CLOCK.instant(),
deletedDate = null,
revisionDate = Instant.MIN,
revisionDate = FIXED_CLOCK.instant(),
archivedDate = null,
sshKey = null,
),
@@ -124,8 +113,6 @@ class VaultAddItemStateExtensionsTest {
@Suppress("MaxLineLength")
@Test
fun `toCipherView should transform Login ItemType to CipherView with original cipher`() {
mockkStatic(Instant::class)
every { Instant.now() } returns Instant.MIN
val cipherView = DEFAULT_LOGIN_CIPHER_VIEW
val viewState = VaultAddEditState.ViewState.Content(
common = VaultAddEditState.ViewState.Content.Common(
@@ -158,7 +145,7 @@ class VaultAddItemStateExtensionsTest {
),
)
val result = viewState.toCipherView()
val result = viewState.toCipherView(clock = FIXED_CLOCK)
assertEquals(
@Suppress("MaxLineLength")
@@ -171,7 +158,7 @@ class VaultAddItemStateExtensionsTest {
login = LoginView(
username = "mockUsername-1",
password = "mockPassword-1",
passwordRevisionDate = Instant.MIN,
passwordRevisionDate = FIXED_CLOCK.instant(),
uris = listOf(
LoginUriView(
uri = "mockUri-1",
@@ -214,15 +201,15 @@ class VaultAddItemStateExtensionsTest {
passwordHistory = listOf(
PasswordHistoryView(
password = "old_password",
lastUsedDate = Instant.MIN,
lastUsedDate = FIXED_CLOCK.instant(),
),
PasswordHistoryView(
password = "password",
lastUsedDate = Instant.MIN,
lastUsedDate = FIXED_CLOCK.instant(),
),
PasswordHistoryView(
password = "hidden: value",
lastUsedDate = Instant.MIN,
lastUsedDate = FIXED_CLOCK.instant(),
),
),
),
@@ -232,8 +219,6 @@ class VaultAddItemStateExtensionsTest {
@Test
fun `toCipherView should transform SecureNotes ItemType to CipherView`() {
mockkStatic(Instant::class)
every { Instant.now() } returns Instant.MIN
val viewState = VaultAddEditState.ViewState.Content(
common = VaultAddEditState.ViewState.Content.Common(
name = "mockName-1",
@@ -252,7 +237,7 @@ class VaultAddItemStateExtensionsTest {
type = VaultAddEditState.ViewState.Content.ItemType.SecureNotes,
)
val result = viewState.toCipherView()
val result = viewState.toCipherView(clock = FIXED_CLOCK)
assertEquals(
CipherView(
@@ -297,9 +282,9 @@ class VaultAddItemStateExtensionsTest {
),
passwordHistory = null,
permissions = null,
creationDate = Instant.MIN,
creationDate = FIXED_CLOCK.instant(),
deletedDate = null,
revisionDate = Instant.MIN,
revisionDate = FIXED_CLOCK.instant(),
archivedDate = null,
sshKey = null,
),
@@ -309,8 +294,6 @@ class VaultAddItemStateExtensionsTest {
@Test
fun `toCipherView should transform SecureNotes ItemType to CipherView with original cipher`() {
mockkStatic(Instant::class)
every { Instant.now() } returns Instant.MIN
val cipherView = DEFAULT_SECURE_NOTES_CIPHER_VIEW.copy(passwordHistory = null)
val viewState = VaultAddEditState.ViewState.Content(
common = VaultAddEditState.ViewState.Content.Common(
@@ -327,7 +310,7 @@ class VaultAddItemStateExtensionsTest {
type = VaultAddEditState.ViewState.Content.ItemType.SecureNotes,
)
val result = viewState.toCipherView()
val result = viewState.toCipherView(clock = FIXED_CLOCK)
assertEquals(
cipherView.copy(
@@ -342,7 +325,7 @@ class VaultAddItemStateExtensionsTest {
passwordHistory = listOf(
PasswordHistoryView(
password = "hidden: value",
lastUsedDate = Instant.MIN,
lastUsedDate = FIXED_CLOCK.instant(),
),
),
),
@@ -352,8 +335,6 @@ class VaultAddItemStateExtensionsTest {
@Test
fun `toCipherView should transform Identity ItemType to CipherView`() {
mockkStatic(Instant::class)
every { Instant.now() } returns Instant.MIN
val viewState = VaultAddEditState.ViewState.Content(
common = VaultAddEditState.ViewState.Content.Common(
name = "mockName-1",
@@ -386,7 +367,7 @@ class VaultAddItemStateExtensionsTest {
),
)
val result = viewState.toCipherView()
val result = viewState.toCipherView(clock = FIXED_CLOCK)
assertEquals(
CipherView(
@@ -431,9 +412,9 @@ class VaultAddItemStateExtensionsTest {
fields = emptyList(),
passwordHistory = null,
permissions = null,
creationDate = Instant.MIN,
creationDate = FIXED_CLOCK.instant(),
deletedDate = null,
revisionDate = Instant.MIN,
revisionDate = FIXED_CLOCK.instant(),
archivedDate = null,
sshKey = null,
),
@@ -443,8 +424,6 @@ class VaultAddItemStateExtensionsTest {
@Test
fun `toCipherView should transform Identity ItemType to CipherView with original cipher`() {
mockkStatic(Instant::class)
every { Instant.now() } returns Instant.MIN
val cipherView = DEFAULT_IDENTITY_CIPHER_VIEW
val viewState = VaultAddEditState.ViewState.Content(
common = VaultAddEditState.ViewState.Content.Common(
@@ -489,7 +468,7 @@ class VaultAddItemStateExtensionsTest {
),
)
val result = viewState.toCipherView()
val result = viewState.toCipherView(clock = FIXED_CLOCK)
assertEquals(
@Suppress("MaxLineLength")
@@ -550,11 +529,11 @@ class VaultAddItemStateExtensionsTest {
passwordHistory = listOf(
PasswordHistoryView(
password = "old_password",
lastUsedDate = Instant.MIN,
lastUsedDate = FIXED_CLOCK.instant(),
),
PasswordHistoryView(
password = "hidden: value",
lastUsedDate = Instant.MIN,
lastUsedDate = FIXED_CLOCK.instant(),
),
),
),
@@ -564,8 +543,6 @@ class VaultAddItemStateExtensionsTest {
@Test
fun `toCipherView should transform Card ItemType to CipherView`() {
mockkStatic(Instant::class)
every { Instant.now() } returns Instant.MIN
val viewState = VaultAddEditState.ViewState.Content(
common = VaultAddEditState.ViewState.Content.Common(
name = "mockName-1",
@@ -586,7 +563,7 @@ class VaultAddItemStateExtensionsTest {
),
)
val result = viewState.toCipherView()
val result = viewState.toCipherView(clock = FIXED_CLOCK)
assertEquals(
CipherView(
@@ -619,9 +596,9 @@ class VaultAddItemStateExtensionsTest {
fields = emptyList(),
passwordHistory = null,
permissions = null,
creationDate = Instant.MIN,
creationDate = FIXED_CLOCK.instant(),
deletedDate = null,
revisionDate = Instant.MIN,
revisionDate = FIXED_CLOCK.instant(),
archivedDate = null,
sshKey = null,
),
@@ -631,8 +608,6 @@ class VaultAddItemStateExtensionsTest {
@Test
fun `toCipherView should transform Card ItemType to CipherView with original cipher`() {
mockkStatic(Instant::class)
every { Instant.now() } returns Instant.MIN
val cipherView = DEFAULT_CARD_CIPHER_VIEW
val viewState = VaultAddEditState.ViewState.Content(
common = VaultAddEditState.ViewState.Content.Common(
@@ -665,7 +640,7 @@ class VaultAddItemStateExtensionsTest {
),
)
val result = viewState.toCipherView()
val result = viewState.toCipherView(clock = FIXED_CLOCK)
assertEquals(
cipherView.copy(
@@ -704,11 +679,11 @@ class VaultAddItemStateExtensionsTest {
passwordHistory = listOf(
PasswordHistoryView(
password = "old_password",
lastUsedDate = Instant.MIN,
lastUsedDate = FIXED_CLOCK.instant(),
),
PasswordHistoryView(
password = "hidden: value",
lastUsedDate = Instant.MIN,
lastUsedDate = FIXED_CLOCK.instant(),
),
),
),
@@ -718,8 +693,6 @@ class VaultAddItemStateExtensionsTest {
@Test
fun `toCipherView should transform SSH Key ItemType to CipherView`() {
mockkStatic(Instant::class)
every { Instant.now() } returns Instant.MIN
val viewState = VaultAddEditState.ViewState.Content(
common = VaultAddEditState.ViewState.Content.Common(
name = "mockName-1",
@@ -737,7 +710,7 @@ class VaultAddItemStateExtensionsTest {
),
)
val result = viewState.toCipherView()
val result = viewState.toCipherView(clock = FIXED_CLOCK)
assertEquals(
CipherView(
@@ -763,9 +736,9 @@ class VaultAddItemStateExtensionsTest {
fields = emptyList(),
passwordHistory = null,
permissions = null,
creationDate = Instant.MIN,
creationDate = FIXED_CLOCK.instant(),
deletedDate = null,
revisionDate = Instant.MIN,
revisionDate = FIXED_CLOCK.instant(),
archivedDate = null,
sshKey = SshKeyView(
publicKey = "mockPublicKey-1",
@@ -787,7 +760,7 @@ class VaultAddItemStateExtensionsTest {
login = LoginView(
username = "mockUsername-1",
password = "mockPassword-1",
passwordRevisionDate = Instant.MIN,
passwordRevisionDate = FIXED_CLOCK.instant(),
uris = null,
totp = null,
autofillOnPageLoad = false,
@@ -811,7 +784,7 @@ class VaultAddItemStateExtensionsTest {
),
)
val result = viewState.toCipherView()
val result = viewState.toCipherView(clock = FIXED_CLOCK)
assertEquals(
cipherView.copy(
@@ -822,7 +795,7 @@ class VaultAddItemStateExtensionsTest {
totp = null,
fido2Credentials = null,
uris = null,
passwordRevisionDate = Instant.MIN,
passwordRevisionDate = FIXED_CLOCK.instant(),
autofillOnPageLoad = false,
),
),
@@ -832,9 +805,6 @@ class VaultAddItemStateExtensionsTest {
@Test
fun `toLoginView should update revision date when password differs`() {
mockkStatic(Instant::class)
every { Instant.now() } returns Instant.MAX
val cipherView = DEFAULT_LOGIN_CIPHER_VIEW
val viewState = VaultAddEditState.ViewState.Content(
@@ -851,8 +821,14 @@ class VaultAddItemStateExtensionsTest {
password = "mockPassword-1",
),
)
val futureClock = Clock.fixed(
Instant.parse("2023-11-27T12:00:00Z"),
ZoneOffset.UTC,
)
val result = viewState.toCipherView()
// We need to pass in a future clock to make sure that when the
// revision date is updated it is updated to a new time
val result = viewState.toCipherView(clock = futureClock)
assertNotEquals(
viewState.common.originalCipher?.login?.passwordRevisionDate,
@@ -862,9 +838,6 @@ class VaultAddItemStateExtensionsTest {
@Test
fun `toLoginView should keep revision date when password is equal`() {
mockkStatic(Instant::class)
every { Instant.now() } returns Instant.MAX
val cipherView = DEFAULT_LOGIN_CIPHER_VIEW
val viewState = VaultAddEditState.ViewState.Content(
@@ -881,8 +854,14 @@ class VaultAddItemStateExtensionsTest {
password = cipherView.login?.password ?: "",
),
)
val futureClock = Clock.fixed(
Instant.parse("2023-11-27T12:00:00Z"),
ZoneOffset.UTC,
)
val result = viewState.toCipherView()
// We need to pass in a future clock to make sure that if the
// revision date were to be updated it would be updated to a new time
val result = viewState.toCipherView(clock = futureClock)
assertEquals(
viewState.common.originalCipher?.login?.passwordRevisionDate,
@@ -892,9 +871,6 @@ class VaultAddItemStateExtensionsTest {
@Test
fun `toLoginView should not update revision date when password is null and has no history`() {
mockkStatic(Instant::class)
every { Instant.now() } returns Instant.MAX
val cipherView = DEFAULT_LOGIN_CIPHER_VIEW.copy(
passwordHistory = null,
login = DEFAULT_LOGIN_CIPHER_VIEW.login?.copy(password = null),
@@ -914,8 +890,14 @@ class VaultAddItemStateExtensionsTest {
password = "updated password",
),
)
val futureClock = Clock.fixed(
Instant.parse("2023-11-27T12:00:00Z"),
ZoneOffset.UTC,
)
val result = viewState.toCipherView()
// We need to pass in a future clock to make sure that if the
// revision date were to be updated it would be updated to a new time
val result = viewState.toCipherView(clock = futureClock)
assertEquals(
viewState.common.originalCipher?.login?.passwordRevisionDate,
@@ -924,6 +906,11 @@ class VaultAddItemStateExtensionsTest {
}
}
private val FIXED_CLOCK: Clock = Clock.fixed(
Instant.parse("2023-10-27T12:00:00Z"),
ZoneOffset.UTC,
)
private val DEFAULT_BASE_CIPHER_VIEW: CipherView = CipherView(
id = "id1234",
organizationId = null,
@@ -979,13 +966,13 @@ private val DEFAULT_BASE_CIPHER_VIEW: CipherView = CipherView(
passwordHistory = listOf(
PasswordHistoryView(
password = "old_password",
lastUsedDate = Instant.MIN,
lastUsedDate = FIXED_CLOCK.instant(),
),
),
permissions = null,
creationDate = Instant.MIN,
creationDate = FIXED_CLOCK.instant(),
deletedDate = null,
revisionDate = Instant.MIN,
revisionDate = FIXED_CLOCK.instant(),
archivedDate = null,
sshKey = null,
)
@@ -995,7 +982,7 @@ private val DEFAULT_LOGIN_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_VIEW.cop
login = LoginView(
username = "username",
password = "password",
passwordRevisionDate = Instant.MIN,
passwordRevisionDate = FIXED_CLOCK.instant(),
uris = listOf(
LoginUriView(
uri = "www.example.com",
@@ -1,7 +1,7 @@
package com.bitwarden.authenticator.data.authenticator.datasource.sdk
import com.bitwarden.core.DateTime
import com.bitwarden.vault.TotpResponse
import java.time.Instant
/**
* Source of authenticator information from the Bitwarden SDK.
@@ -13,7 +13,7 @@ interface AuthenticatorSdkSource {
*/
suspend fun generateTotp(
totp: String,
time: DateTime,
time: Instant,
): Result<TotpResponse>
/**
@@ -1,10 +1,10 @@
package com.bitwarden.authenticator.data.authenticator.datasource.sdk
import com.bitwarden.authenticator.data.platform.manager.SdkClientManager
import com.bitwarden.core.DateTime
import com.bitwarden.generators.PasswordGeneratorRequest
import com.bitwarden.sdk.Client
import com.bitwarden.vault.TotpResponse
import java.time.Instant
import javax.inject.Inject
/**
@@ -16,7 +16,7 @@ class AuthenticatorSdkSourceImpl @Inject constructor(
override suspend fun generateTotp(
totp: String,
time: DateTime,
time: Instant,
): Result<TotpResponse> = runCatching {
getClient()
.vault()
@@ -3,7 +3,6 @@ package com.bitwarden.authenticator.data.authenticator.manager
import com.bitwarden.authenticator.data.authenticator.datasource.sdk.AuthenticatorSdkSource
import com.bitwarden.authenticator.data.authenticator.manager.model.VerificationCodeItem
import com.bitwarden.authenticator.data.authenticator.repository.model.AuthenticatorItem
import com.bitwarden.core.DateTime
import kotlinx.coroutines.cancel
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
@@ -46,7 +45,7 @@ class TotpCodeManagerImpl @Inject constructor(
// If the item is expired or we haven't generated our first item,
// generate a new code using the SDK:
item = authenticatorSdkSource
.generateTotp(otpUri, DateTime.now())
.generateTotp(otpUri, clock.instant())
.getOrNull()
?.let { response ->
VerificationCodeItem(