mirror of
https://github.com/bitwarden/android.git
synced 2026-10-23 11:33:22 -05:00
Merge branch 'main' into pm-23278/kdf-upgrade-minimum
This commit is contained in:
144 files changed
+3701
-2302
No files matched your search
@@ -249,7 +249,7 @@
|
||||
android:name="com.x8bit.bitwarden.AutofillTileService"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_notification"
|
||||
android:label="@string/autofill"
|
||||
android:label="@string/autofill_title"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
tools:ignore="MissingClass">
|
||||
<intent-filter>
|
||||
|
||||
@@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import timber.log.Timber
|
||||
import java.time.Clock
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
@@ -129,8 +130,8 @@ class PushManagerImpl @Inject constructor(
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
private fun onMessageReceived(notification: BitwardenNotification) {
|
||||
if (authDiskSource.uniqueAppId == notification.contextId) return
|
||||
|
||||
val userId = activeUserId ?: return
|
||||
Timber.d("Push Notification Received: ${notification.notificationType}")
|
||||
|
||||
when (val type = notification.notificationType) {
|
||||
NotificationType.AUTH_REQUEST,
|
||||
@@ -189,9 +190,14 @@ class PushManagerImpl @Inject constructor(
|
||||
.decodeFromString<NotificationPayload.SyncCipherNotification>(
|
||||
string = notification.payload,
|
||||
)
|
||||
.takeIf { isLoggedIn(userId) && it.userMatchesNotification(userId) }
|
||||
?.cipherId
|
||||
?.let { mutableSyncCipherDeleteSharedFlow.tryEmit(SyncCipherDeleteData(it)) }
|
||||
.takeIf { it.userId != null && it.cipherId != null }
|
||||
?.let {
|
||||
SyncCipherDeleteData(
|
||||
userId = requireNotNull(it.userId),
|
||||
cipherId = requireNotNull(it.cipherId),
|
||||
)
|
||||
}
|
||||
?.let { mutableSyncCipherDeleteSharedFlow.tryEmit(it) }
|
||||
}
|
||||
|
||||
NotificationType.SYNC_CIPHERS,
|
||||
@@ -226,9 +232,14 @@ class PushManagerImpl @Inject constructor(
|
||||
.decodeFromString<NotificationPayload.SyncFolderNotification>(
|
||||
string = notification.payload,
|
||||
)
|
||||
.takeIf { isLoggedIn(userId) && it.userMatchesNotification(userId) }
|
||||
?.folderId
|
||||
?.let { mutableSyncFolderDeleteSharedFlow.tryEmit(SyncFolderDeleteData(it)) }
|
||||
.takeIf { it.userId != null && it.folderId != null }
|
||||
?.let {
|
||||
SyncFolderDeleteData(
|
||||
userId = requireNotNull(it.userId),
|
||||
folderId = requireNotNull(it.folderId),
|
||||
)
|
||||
}
|
||||
?.let { mutableSyncFolderDeleteSharedFlow.tryEmit(it) }
|
||||
}
|
||||
|
||||
NotificationType.SYNC_ORG_KEYS -> {
|
||||
@@ -266,9 +277,14 @@ class PushManagerImpl @Inject constructor(
|
||||
.decodeFromString<NotificationPayload.SyncSendNotification>(
|
||||
string = notification.payload,
|
||||
)
|
||||
.takeIf { isLoggedIn(userId) && it.userMatchesNotification(userId) }
|
||||
?.sendId
|
||||
?.let { mutableSyncSendDeleteSharedFlow.tryEmit(SyncSendDeleteData(it)) }
|
||||
.takeIf { it.userId != null && it.sendId != null }
|
||||
?.let {
|
||||
SyncSendDeleteData(
|
||||
userId = requireNotNull(it.userId),
|
||||
sendId = requireNotNull(it.sendId),
|
||||
)
|
||||
}
|
||||
?.let { mutableSyncSendDeleteSharedFlow.tryEmit(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -2,9 +2,8 @@ package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
/**
|
||||
* Required data for sync cipher delete operations.
|
||||
*
|
||||
* @property cipherId The cipher ID.
|
||||
*/
|
||||
data class SyncCipherDeleteData(
|
||||
val userId: String,
|
||||
val cipherId: String,
|
||||
)
|
||||
+1
-2
@@ -2,9 +2,8 @@ package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
/**
|
||||
* Required data for sync folder delete operations.
|
||||
*
|
||||
* @property folderId The folder ID.
|
||||
*/
|
||||
data class SyncFolderDeleteData(
|
||||
val userId: String,
|
||||
val folderId: String,
|
||||
)
|
||||
+1
-2
@@ -2,9 +2,8 @@ package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
/**
|
||||
* Required data for sync send delete operations.
|
||||
*
|
||||
* @property sendId The send ID.
|
||||
*/
|
||||
data class SyncSendDeleteData(
|
||||
val userId: String,
|
||||
val sendId: String,
|
||||
)
|
||||
@@ -24,6 +24,14 @@ interface VaultDiskSource {
|
||||
*/
|
||||
suspend fun getCiphers(userId: String): List<SyncResponseJson.Cipher>
|
||||
|
||||
/**
|
||||
* Retrieves all ciphers with the given [cipherIds] from the data source for a given [userId].
|
||||
*/
|
||||
suspend fun getSelectedCiphers(
|
||||
userId: String,
|
||||
cipherIds: List<String>,
|
||||
): List<SyncResponseJson.Cipher>
|
||||
|
||||
/**
|
||||
* Retrieves all ciphers from the data source for a given [userId] that contain TOTP codes.
|
||||
*/
|
||||
|
||||
+18
@@ -97,6 +97,24 @@ class VaultDiskSourceImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getSelectedCiphers(
|
||||
userId: String,
|
||||
cipherIds: List<String>,
|
||||
): List<SyncResponseJson.Cipher> {
|
||||
val entities = ciphersDao.getSelectedCiphers(userId = userId, cipherIds = cipherIds)
|
||||
return withContext(context = dispatcherManager.default) {
|
||||
entities
|
||||
.map { entity ->
|
||||
async {
|
||||
json.decodeFromStringWithErrorCallback<SyncResponseJson.Cipher>(
|
||||
string = entity.cipherJson,
|
||||
) { Timber.e(it, "Failed to deserialize Cipher in Vault") }
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getTotpCiphers(userId: String): List<SyncResponseJson.Cipher> {
|
||||
val entities = ciphersDao.getAllTotpCiphers(userId = userId)
|
||||
return withContext(context = dispatcherManager.default) {
|
||||
|
||||
@@ -37,6 +37,15 @@ interface CiphersDao {
|
||||
userId: String,
|
||||
): List<CipherEntity>
|
||||
|
||||
/**
|
||||
* Retrieves all ciphers from the database with the given [cipherIds] for a given [userId].
|
||||
*/
|
||||
@Query("SELECT * FROM ciphers WHERE user_id = :userId AND id IN (:cipherIds)")
|
||||
suspend fun getSelectedCiphers(
|
||||
userId: String,
|
||||
cipherIds: List<String>,
|
||||
): List<CipherEntity>
|
||||
|
||||
/**
|
||||
* Retrieves all ciphers from the database for a given [userId].
|
||||
*/
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.bitwarden.core.UpdateKdfResponse
|
||||
import com.bitwarden.core.UpdatePasswordResponse
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.crypto.TrustDeviceResponse
|
||||
import com.bitwarden.exporters.Account
|
||||
import com.bitwarden.exporters.ExportFormat
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.fido.PublicKeyCredentialAuthenticatorAssertionResponse
|
||||
@@ -429,6 +430,23 @@ interface VaultSdkSource {
|
||||
format: ExportFormat,
|
||||
): Result<String>
|
||||
|
||||
/**
|
||||
* Exports the users vault data to a CXF formatted string.
|
||||
*/
|
||||
suspend fun exportVaultDataToCxf(
|
||||
userId: String,
|
||||
account: Account,
|
||||
ciphers: List<Cipher>,
|
||||
): Result<String>
|
||||
|
||||
/**
|
||||
* Imports the given CXF formatted [payload] into the users vault.
|
||||
*
|
||||
* @return Result of the import. If successful, a list of [Cipher]s deciphered from the CXF
|
||||
* payload.
|
||||
*/
|
||||
suspend fun importCxf(userId: String, payload: String): Result<List<Cipher>>
|
||||
|
||||
/**
|
||||
* Register a new FIDO 2 credential to a cipher.
|
||||
*
|
||||
|
||||
+23
@@ -11,6 +11,7 @@ import com.bitwarden.core.UpdatePasswordResponse
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.crypto.TrustDeviceResponse
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.exporters.Account
|
||||
import com.bitwarden.exporters.ExportFormat
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.fido.PublicKeyCredentialAuthenticatorAssertionResponse
|
||||
@@ -492,6 +493,28 @@ class VaultSdkSourceImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun exportVaultDataToCxf(
|
||||
userId: String,
|
||||
account: Account,
|
||||
ciphers: List<Cipher>,
|
||||
): Result<String> = runCatchingWithLogs {
|
||||
getClient(userId = userId)
|
||||
.exporters()
|
||||
.exportCxf(
|
||||
account = account,
|
||||
ciphers = ciphers,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun importCxf(
|
||||
userId: String,
|
||||
payload: String,
|
||||
): Result<List<Cipher>> = runCatchingWithLogs {
|
||||
getClient(userId = userId)
|
||||
.exporters()
|
||||
.importCxf(payload = payload)
|
||||
}
|
||||
|
||||
override suspend fun registerFido2Credential(
|
||||
request: RegisterFido2CredentialRequest,
|
||||
fido2CredentialStore: Fido2CredentialStore,
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DomainsData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.ImportCxfPayloadResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SendData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SyncVaultDataResult
|
||||
@@ -258,6 +259,20 @@ interface VaultRepository : CipherManager, VaultLockManager {
|
||||
restrictedTypes: List<CipherType>,
|
||||
): ExportVaultDataResult
|
||||
|
||||
/**
|
||||
* Attempt to import a CXF payload.
|
||||
*
|
||||
* @param payload The CXF payload to import.
|
||||
*/
|
||||
suspend fun importCxfPayload(payload: String): ImportCxfPayloadResult
|
||||
|
||||
/**
|
||||
* Attempt to export the vault data to a CXF file.
|
||||
*
|
||||
* @param ciphers Ciphers selected for export.
|
||||
*/
|
||||
suspend fun exportVaultDataToCxf(ciphers: List<CipherListView>): Result<String>
|
||||
|
||||
/**
|
||||
* Flow that represents the data for a specific vault list item as found by ID. This may emit
|
||||
* `null` if the item cannot be found.
|
||||
|
||||
+62
-34
@@ -73,6 +73,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DomainsData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.ImportCxfPayloadResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SendData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SyncVaultDataResult
|
||||
@@ -82,7 +83,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.sortAlphabetically
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.sortAlphabeticallyByType
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.sortAlphabeticallyByTypeAndOrganization
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toDomainsData
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkFolder
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkSend
|
||||
@@ -93,6 +94,7 @@ import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolder
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolderList
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSend
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSendList
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toSdkAccount
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toFilteredList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -114,7 +116,6 @@ import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
@@ -929,11 +930,9 @@ class VaultRepositoryImpl(
|
||||
}
|
||||
|
||||
private suspend fun clearFolderIdFromCiphers(folderId: String, userId: String) {
|
||||
vaultDiskSource.getCiphersFlow(userId).firstOrNull()?.forEach {
|
||||
vaultDiskSource.getCiphers(userId = userId).forEach {
|
||||
if (it.folderId == folderId) {
|
||||
vaultDiskSource.saveCipher(
|
||||
userId, it.copy(folderId = null),
|
||||
)
|
||||
vaultDiskSource.saveCipher(userId = userId, cipher = it.copy(folderId = null))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -974,6 +973,42 @@ class VaultRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun importCxfPayload(payload: String): ImportCxfPayloadResult {
|
||||
val userId = activeUserId
|
||||
?: return ImportCxfPayloadResult.Error(error = NoActiveUserException())
|
||||
return vaultSdkSource
|
||||
.importCxf(
|
||||
userId = userId,
|
||||
payload = payload,
|
||||
)
|
||||
.fold(
|
||||
onSuccess = { ImportCxfPayloadResult.Success(it) },
|
||||
onFailure = { ImportCxfPayloadResult.Error(error = it) },
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun exportVaultDataToCxf(
|
||||
ciphers: List<CipherListView>,
|
||||
): Result<String> {
|
||||
val userId = activeUserId
|
||||
?: return NoActiveUserException().asFailure()
|
||||
val account = authDiskSource.userState
|
||||
?.activeAccount
|
||||
?.toSdkAccount()
|
||||
?: return NoActiveUserException().asFailure()
|
||||
|
||||
val ciphers = vaultDiskSource
|
||||
.getSelectedCiphers(userId = userId, cipherIds = ciphers.mapNotNull { it.id })
|
||||
.map { it.toEncryptedSdkCipher() }
|
||||
|
||||
return vaultSdkSource
|
||||
.exportVaultDataToCxf(
|
||||
userId = userId,
|
||||
account = account,
|
||||
ciphers = ciphers,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given [userId] has an associated encrypted PIN key but not a pin-protected user
|
||||
* key. This indicates a scenario in which a user has requested PIN unlocking but requires
|
||||
@@ -1168,7 +1203,11 @@ class VaultRepositoryImpl(
|
||||
.fold(
|
||||
onSuccess = { collections ->
|
||||
DataState.Loaded(
|
||||
collections.sortAlphabeticallyByType(),
|
||||
collections.sortAlphabeticallyByTypeAndOrganization(
|
||||
userOrganizations = authDiskSource
|
||||
.getOrganizations(userId = userId)
|
||||
.orEmpty(),
|
||||
),
|
||||
)
|
||||
},
|
||||
onFailure = { throwable -> DataState.Error(throwable) },
|
||||
@@ -1246,12 +1285,9 @@ class VaultRepositoryImpl(
|
||||
* Deletes the cipher specified by [syncCipherDeleteData] from disk.
|
||||
*/
|
||||
private suspend fun deleteCipher(syncCipherDeleteData: SyncCipherDeleteData) {
|
||||
val userId = activeUserId ?: return
|
||||
|
||||
val cipherId = syncCipherDeleteData.cipherId
|
||||
vaultDiskSource.deleteCipher(
|
||||
userId = userId,
|
||||
cipherId = cipherId,
|
||||
userId = syncCipherDeleteData.userId,
|
||||
cipherId = syncCipherDeleteData.cipherId,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1381,12 +1417,9 @@ class VaultRepositoryImpl(
|
||||
* Deletes the send specified by [syncSendDeleteData] from disk.
|
||||
*/
|
||||
private suspend fun deleteSend(syncSendDeleteData: SyncSendDeleteData) {
|
||||
val userId = activeUserId ?: return
|
||||
|
||||
val sendId = syncSendDeleteData.sendId
|
||||
vaultDiskSource.deleteSend(
|
||||
userId = userId,
|
||||
sendId = sendId,
|
||||
userId = syncSendDeleteData.userId,
|
||||
sendId = syncSendDeleteData.sendId,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1401,15 +1434,14 @@ class VaultRepositoryImpl(
|
||||
val isUpdate = syncSendUpsertData.isUpdate
|
||||
val revisionDate = syncSendUpsertData.revisionDate
|
||||
|
||||
val localSend = sendDataStateFlow
|
||||
.mapNotNull { it.data }
|
||||
val localSend = vaultDiskSource
|
||||
.getSends(userId = userId)
|
||||
.first()
|
||||
.sendViewList
|
||||
.find { it.id == sendId }
|
||||
val isValidCreate = !isUpdate && localSend == null
|
||||
val isValidUpdate = isUpdate &&
|
||||
localSend != null &&
|
||||
localSend.revisionDate.epochSecond < revisionDate.toEpochSecond()
|
||||
localSend.revisionDate.toEpochSecond() < revisionDate.toEpochSecond()
|
||||
|
||||
if (!isValidCreate && !isValidUpdate) return
|
||||
|
||||
@@ -1432,16 +1464,13 @@ class VaultRepositoryImpl(
|
||||
* Deletes the folder specified by [syncFolderDeleteData] from disk.
|
||||
*/
|
||||
private suspend fun deleteFolder(syncFolderDeleteData: SyncFolderDeleteData) {
|
||||
val userId = activeUserId ?: return
|
||||
|
||||
val folderId = syncFolderDeleteData.folderId
|
||||
clearFolderIdFromCiphers(
|
||||
folderId = folderId,
|
||||
userId = userId,
|
||||
folderId = syncFolderDeleteData.folderId,
|
||||
userId = syncFolderDeleteData.userId,
|
||||
)
|
||||
vaultDiskSource.deleteFolder(
|
||||
folderId = folderId,
|
||||
userId = userId,
|
||||
folderId = syncFolderDeleteData.folderId,
|
||||
userId = syncFolderDeleteData.userId,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1454,21 +1483,20 @@ class VaultRepositoryImpl(
|
||||
val folderId = syncFolderUpsertData.folderId
|
||||
val isUpdate = syncFolderUpsertData.isUpdate
|
||||
val revisionDate = syncFolderUpsertData.revisionDate
|
||||
|
||||
val localFolder = foldersStateFlow
|
||||
.mapNotNull { it.data }
|
||||
val localFolder = vaultDiskSource
|
||||
.getFolders(userId = userId)
|
||||
.first()
|
||||
.find { it.id == folderId }
|
||||
val isValidCreate = !isUpdate && localFolder == null
|
||||
val isValidUpdate = isUpdate &&
|
||||
localFolder != null &&
|
||||
localFolder.revisionDate.epochSecond < revisionDate.toEpochSecond()
|
||||
localFolder.revisionDate.toEpochSecond() < revisionDate.toEpochSecond()
|
||||
|
||||
if (!isValidCreate && !isValidUpdate) return
|
||||
|
||||
folderService
|
||||
.getFolder(folderId)
|
||||
.onSuccess { vaultDiskSource.saveFolder(userId, it) }
|
||||
.getFolder(folderId = folderId)
|
||||
.onSuccess { vaultDiskSource.saveFolder(userId = userId, folder = it) }
|
||||
}
|
||||
//endregion Push Notification helpers
|
||||
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.x8bit.bitwarden.data.vault.repository.model
|
||||
|
||||
import com.bitwarden.vault.Cipher
|
||||
|
||||
/**
|
||||
* Models result of the vault data being imported from a CXF payload.
|
||||
*/
|
||||
sealed class ImportCxfPayloadResult {
|
||||
|
||||
/**
|
||||
* The vault data has been successfully imported.
|
||||
*/
|
||||
data class Success(val ciphers: List<Cipher>) : ImportCxfPayloadResult()
|
||||
|
||||
/**
|
||||
* There was an error importing the vault data.
|
||||
*
|
||||
* @param error The error that occurred during import.
|
||||
*/
|
||||
data class Error(val error: Throwable) : ImportCxfPayloadResult()
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.x8bit.bitwarden.data.vault.repository.util
|
||||
|
||||
import com.bitwarden.exporters.Account
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
|
||||
/**
|
||||
* Converts a [AccountJson] to a [Account] for use in the SDK.
|
||||
*/
|
||||
fun AccountJson.toSdkAccount(): Account = Account(
|
||||
id = profile.userId,
|
||||
email = profile.email,
|
||||
name = profile.name,
|
||||
)
|
||||
+33
-9
@@ -32,21 +32,45 @@ fun List<SyncResponseJson.Collection>.toEncryptedSdkCollectionList(): List<Colle
|
||||
map { it.toEncryptedSdkCollection() }
|
||||
|
||||
/**
|
||||
* Sorts the collections, grouping them by type, with `DEFAULT_USER_COLLECTION` types displayed
|
||||
* first. Within each group, collections are sorted alphabetically by name.
|
||||
* Sorts a list of [CollectionView] objects based on a multi-level sorting logic.
|
||||
*
|
||||
* The sorting criteria are as follows, in order of precedence:
|
||||
* 1. Collections of type `DEFAULT_USER_COLLECTION` are placed first.
|
||||
* 2. All other collections are grouped by their `CollectionType`.
|
||||
* 3. Within each group, collections are sorted alphabetically by name. For collections of
|
||||
* type `DEFAULT_USER_COLLECTION`, the corresponding organization's name is used for sorting
|
||||
* instead of the collection's own name.
|
||||
*
|
||||
* This function uses a [SpecialCharWithPrecedenceComparator] for the alphabetical sort.
|
||||
*
|
||||
* @param userOrganizations A list of the user's organizations, used to find the name for
|
||||
* `DEFAULT_USER_COLLECTION` types.
|
||||
* @return A new list containing the sorted [CollectionView] objects.
|
||||
*/
|
||||
@JvmName("toAlphabeticallySortedCollectionList")
|
||||
fun List<CollectionView>.sortAlphabeticallyByType(): List<CollectionView> {
|
||||
fun List<CollectionView>.sortAlphabeticallyByTypeAndOrganization(
|
||||
userOrganizations: List<SyncResponseJson.Profile.Organization>,
|
||||
): List<CollectionView> {
|
||||
return this.sortedWith(
|
||||
// DEFAULT_USER_COLLECTION come first
|
||||
comparator = compareBy<CollectionView> { it.type != CollectionType.DEFAULT_USER_COLLECTION }
|
||||
// Then sort by other CollectionType ordinals
|
||||
.thenBy { it.type }
|
||||
// Finally, sort by name within each group
|
||||
.thenComparing(
|
||||
CollectionView::name,
|
||||
SpecialCharWithPrecedenceComparator,
|
||||
),
|
||||
// Finally, sort within each group. For default collections, use the
|
||||
// organization's name; for others, use the collection's name.
|
||||
.thenBy(SpecialCharWithPrecedenceComparator) {
|
||||
if (it.type == CollectionType.DEFAULT_USER_COLLECTION) {
|
||||
// For default collections, sort by the organization's name
|
||||
userOrganizations
|
||||
.find { org -> org.id == it.organizationId }
|
||||
?.name
|
||||
?: it.name
|
||||
} else {
|
||||
// For other collections, sort by the collection's name
|
||||
it.name
|
||||
}
|
||||
}
|
||||
// As a final fallback if names are identical, sort by ID to ensure a stable order
|
||||
.thenBy { it.id },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+7
-2
@@ -5,8 +5,10 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.base.util.isValidEmail
|
||||
import com.bitwarden.ui.platform.resource.BitwardenPlurals
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asPluralsText
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
|
||||
@@ -321,8 +323,11 @@ class CompleteRegistrationViewModel @Inject constructor(
|
||||
it.copy(
|
||||
dialog = CompleteRegistrationDialog.Error(
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.master_password_length_val_message_x
|
||||
.asText(MIN_PASSWORD_LENGTH),
|
||||
message = BitwardenPlurals.master_password_length_val_message_x
|
||||
.asPluralsText(
|
||||
quantity = MIN_PASSWORD_LENGTH,
|
||||
args = arrayOf(MIN_PASSWORD_LENGTH),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
+7
-2
@@ -24,11 +24,12 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenPlurals
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.bitwarden.ui.util.asText
|
||||
@@ -154,7 +155,11 @@ private fun MinimumCharacterCount(
|
||||
}
|
||||
Spacer(modifier = Modifier.width(2.dp))
|
||||
Text(
|
||||
text = stringResource(BitwardenString.minimum_characters, minimumCharacterCount),
|
||||
text = pluralStringResource(
|
||||
id = BitwardenPlurals.minimum_characters,
|
||||
count = minimumCharacterCount,
|
||||
formatArgs = arrayOf(minimumCharacterCount),
|
||||
),
|
||||
color = BitwardenTheme.colorScheme.text.secondary,
|
||||
style = BitwardenTheme.typography.labelSmall,
|
||||
)
|
||||
|
||||
+7
-2
@@ -5,8 +5,10 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.base.util.orNullIfBlank
|
||||
import com.bitwarden.ui.platform.resource.BitwardenPlurals
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asPluralsText
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
|
||||
@@ -199,8 +201,11 @@ class ResetPasswordViewModel @Inject constructor(
|
||||
it.copy(
|
||||
dialogState = ResetPasswordState.DialogState.Error(
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.master_password_length_val_message_x
|
||||
.asText(MIN_PASSWORD_LENGTH),
|
||||
message = BitwardenPlurals.master_password_length_val_message_x
|
||||
.asPluralsText(
|
||||
quantity = MIN_PASSWORD_LENGTH,
|
||||
args = arrayOf(MIN_PASSWORD_LENGTH),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
+7
-2
@@ -4,8 +4,10 @@ import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.resource.BitwardenPlurals
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asPluralsText
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
|
||||
@@ -111,8 +113,11 @@ class SetPasswordViewModel @Inject constructor(
|
||||
it.copy(
|
||||
dialogState = SetPasswordState.DialogState.Error(
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.master_password_length_val_message_x
|
||||
.asText(MIN_PASSWORD_LENGTH),
|
||||
message = BitwardenPlurals.master_password_length_val_message_x
|
||||
.asPluralsText(
|
||||
quantity = MIN_PASSWORD_LENGTH,
|
||||
args = arrayOf(MIN_PASSWORD_LENGTH),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ private fun AutofillSelectionDialog(
|
||||
selectionItems = {
|
||||
if (AutofillSelectionOption.AUTOFILL in displayItem.autofillSelectionOptions) {
|
||||
BitwardenBasicDialogRow(
|
||||
text = stringResource(id = BitwardenString.autofill),
|
||||
text = stringResource(id = BitwardenString.autofill_title),
|
||||
onClick = {
|
||||
selectionCallback(
|
||||
displayItem,
|
||||
|
||||
+1
-1
@@ -241,7 +241,7 @@ enum class Settings(
|
||||
testTag = "AccountSecuritySettingsButton",
|
||||
),
|
||||
AUTO_FILL(
|
||||
text = BitwardenString.autofill.asText(),
|
||||
text = BitwardenString.autofill_title.asText(),
|
||||
vectorIconRes = BitwardenDrawable.ic_check_mark,
|
||||
testTag = "AutofillSettingsButton",
|
||||
),
|
||||
|
||||
+2
-2
@@ -141,7 +141,7 @@ fun AutoFillScreen(
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = BitwardenString.autofill),
|
||||
title = stringResource(id = BitwardenString.autofill_title),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = rememberVectorPainter(id = BitwardenDrawable.ic_back),
|
||||
navigationIconContentDescription = stringResource(id = BitwardenString.back),
|
||||
@@ -187,7 +187,7 @@ private fun AutoFillScreenContent(
|
||||
)
|
||||
}
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = BitwardenString.autofill),
|
||||
label = stringResource(id = BitwardenString.autofill_title),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
|
||||
+6
-4
@@ -24,6 +24,7 @@ import com.bitwarden.ui.platform.components.navigation.model.NavigationItem
|
||||
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.ui.platform.components.scaffold.model.ScaffoldNavigationData
|
||||
import com.bitwarden.ui.platform.theme.RootTransitionProviders
|
||||
import com.bitwarden.ui.platform.util.toObjectNavigationRoute
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberBitwardenNavController
|
||||
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.about.navigateToAbout
|
||||
@@ -272,14 +273,15 @@ private fun VaultUnlockedNavBarScaffold(
|
||||
* If direct navigation is required, the [navigate] lambda will be invoked with the appropriate
|
||||
* [NavOptions].
|
||||
*/
|
||||
@Suppress("MaxLineLength")
|
||||
private fun NavController.navigateToTabOrRoot(
|
||||
tabToNavigateTo: VaultUnlockedNavBarTab,
|
||||
navigate: (NavOptions) -> Unit,
|
||||
) {
|
||||
if (tabToNavigateTo.startDestinationRoute == currentDestination?.route) {
|
||||
if (tabToNavigateTo.startDestinationRoute.toObjectNavigationRoute() == currentDestination?.route) {
|
||||
// We are at the start destination already, so nothing to do.
|
||||
return
|
||||
} else if (currentDestination?.parent?.route == tabToNavigateTo.graphRoute) {
|
||||
} else if (currentDestination?.parent?.route == tabToNavigateTo.graphRoute.toObjectNavigationRoute()) {
|
||||
// We are not at the start destination but we are in the correct graph,
|
||||
// so lets pop up to the start destination.
|
||||
popBackStack(route = tabToNavigateTo.startDestinationRoute, inclusive = false)
|
||||
@@ -300,8 +302,8 @@ private fun NavController.navigateToTabOrRoot(
|
||||
/**
|
||||
* Determine if the current destination is the same as the given tab.
|
||||
*/
|
||||
private fun NavBackStackEntry?.isCurrentRoute(route: String): Boolean =
|
||||
private fun NavBackStackEntry?.isCurrentRoute(route: Any): Boolean =
|
||||
this
|
||||
?.destination
|
||||
?.hierarchy
|
||||
?.any { it.route == route } == true
|
||||
?.any { it.route == route.toObjectNavigationRoute() } == true
|
||||
+8
-9
@@ -4,7 +4,6 @@ import android.os.Parcelable
|
||||
import com.bitwarden.ui.platform.components.navigation.model.NavigationItem
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.util.toObjectNavigationRoute
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.SettingsGraphRoute
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.SettingsRoute
|
||||
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorGraphRoute
|
||||
@@ -35,8 +34,8 @@ sealed class VaultUnlockedNavBarTab : NavigationItem, Parcelable {
|
||||
override val iconRes get() = BitwardenDrawable.ic_generator
|
||||
override val labelRes get() = BitwardenString.generator
|
||||
override val contentDescriptionRes get() = BitwardenString.generator
|
||||
override val graphRoute get() = GeneratorGraphRoute.toObjectNavigationRoute()
|
||||
override val startDestinationRoute get() = GeneratorRoute.Standard.toObjectNavigationRoute()
|
||||
override val graphRoute get() = GeneratorGraphRoute
|
||||
override val startDestinationRoute get() = GeneratorRoute.Standard
|
||||
override val testTag get() = "GeneratorTab"
|
||||
override val notificationCount get() = 0
|
||||
}
|
||||
@@ -50,8 +49,8 @@ sealed class VaultUnlockedNavBarTab : NavigationItem, Parcelable {
|
||||
override val iconRes get() = BitwardenDrawable.ic_send
|
||||
override val labelRes get() = BitwardenString.send
|
||||
override val contentDescriptionRes get() = BitwardenString.send
|
||||
override val graphRoute get() = SendGraphRoute.toObjectNavigationRoute()
|
||||
override val startDestinationRoute get() = SendRoute.toObjectNavigationRoute()
|
||||
override val graphRoute get() = SendGraphRoute
|
||||
override val startDestinationRoute get() = SendRoute
|
||||
override val testTag get() = "SendTab"
|
||||
override val notificationCount get() = 0
|
||||
}
|
||||
@@ -66,8 +65,8 @@ sealed class VaultUnlockedNavBarTab : NavigationItem, Parcelable {
|
||||
) : VaultUnlockedNavBarTab() {
|
||||
override val iconResSelected get() = BitwardenDrawable.ic_vault_filled
|
||||
override val iconRes get() = BitwardenDrawable.ic_vault
|
||||
override val graphRoute get() = VaultGraphRoute.toObjectNavigationRoute()
|
||||
override val startDestinationRoute get() = VaultRoute.toObjectNavigationRoute()
|
||||
override val graphRoute get() = VaultGraphRoute
|
||||
override val startDestinationRoute get() = VaultRoute
|
||||
override val testTag get() = "VaultTab"
|
||||
override val notificationCount get() = 0
|
||||
}
|
||||
@@ -83,8 +82,8 @@ sealed class VaultUnlockedNavBarTab : NavigationItem, Parcelable {
|
||||
override val iconRes get() = BitwardenDrawable.ic_settings
|
||||
override val labelRes get() = BitwardenString.settings
|
||||
override val contentDescriptionRes get() = BitwardenString.settings
|
||||
override val graphRoute get() = SettingsGraphRoute.toObjectNavigationRoute()
|
||||
override val startDestinationRoute get() = SettingsRoute.Standard.toObjectNavigationRoute()
|
||||
override val graphRoute get() = SettingsGraphRoute
|
||||
override val startDestinationRoute get() = SettingsRoute.Standard
|
||||
override val testTag get() = "SettingsTab"
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -14,8 +14,10 @@ import com.bitwarden.network.model.PolicyTypeJson
|
||||
import com.bitwarden.ui.platform.base.BackgroundEvent
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
|
||||
import com.bitwarden.ui.platform.resource.BitwardenPlurals
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asPluralsText
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.bitwarden.vault.DecryptCipherListResult
|
||||
@@ -1945,7 +1947,11 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
is BreachCountResult.Success -> {
|
||||
VaultAddEditState.DialogState.Generic(
|
||||
message = if (result.breachCount > 0) {
|
||||
BitwardenString.password_exposed.asText(result.breachCount)
|
||||
BitwardenPlurals.password_exposed
|
||||
.asPluralsText(
|
||||
quantity = result.breachCount,
|
||||
args = arrayOf(result.breachCount),
|
||||
)
|
||||
} else {
|
||||
BitwardenString.password_safe.asText()
|
||||
},
|
||||
|
||||
@@ -13,8 +13,10 @@ import com.bitwarden.ui.platform.base.BackgroundEvent
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.components.icon.model.IconData
|
||||
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
|
||||
import com.bitwarden.ui.platform.resource.BitwardenPlurals
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asPluralsText
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.ui.util.concat
|
||||
import com.bitwarden.vault.CipherView
|
||||
@@ -978,7 +980,10 @@ class VaultItemViewModel @Inject constructor(
|
||||
is BreachCountResult.Success -> {
|
||||
VaultItemState.DialogState.Generic(
|
||||
message = if (result.breachCount > 0) {
|
||||
BitwardenString.password_exposed.asText(result.breachCount)
|
||||
BitwardenPlurals.password_exposed.asPluralsText(
|
||||
quantity = result.breachCount,
|
||||
args = arrayOf(result.breachCount),
|
||||
)
|
||||
} else {
|
||||
BitwardenString.password_safe.asText()
|
||||
},
|
||||
|
||||
+4
@@ -2513,6 +2513,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
decryptCipherListResult = vaultData.decryptCipherListResult.copy(
|
||||
successes = vaultData.decryptCipherListResult.successes
|
||||
.filter { it.type is CipherListViewType.Card },
|
||||
failures = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -2530,6 +2531,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
cipherListViews = vaultData.decryptCipherListResult.successes,
|
||||
matchUri = matchUri,
|
||||
),
|
||||
failures = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -2553,6 +2555,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
cipherListViews = vaultData.decryptCipherListResult.successes,
|
||||
matchUri = matchUri,
|
||||
),
|
||||
failures = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -2574,6 +2577,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
searchTypeData = SearchTypeData.Vault.Logins,
|
||||
searchTerm = query,
|
||||
),
|
||||
failures = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -913,7 +913,9 @@ class VaultViewModel @Inject constructor(
|
||||
|
||||
updateVaultState(
|
||||
vaultData = vaultData.data,
|
||||
dialog = if (shouldShowDecryptionAlert) {
|
||||
dialog = if (shouldShowDecryptionAlert ||
|
||||
state.dialog is VaultState.DialogState.VaultLoadCipherDecryptionError
|
||||
) {
|
||||
VaultState.DialogState.VaultLoadCipherDecryptionError(
|
||||
title = BitwardenString.decryption_error.asText(),
|
||||
cipherCount = vaultData.data.decryptCipherListResult.failures.size,
|
||||
|
||||
@@ -237,6 +237,7 @@ class PushManagerTest {
|
||||
pushManager.onMessageReceived(SYNC_CIPHER_DELETE_NOTIFICATION_MAP)
|
||||
assertEquals(
|
||||
SyncCipherDeleteData(
|
||||
userId = "078966a2-93c2-4618-ae2a-0a2394c88d37",
|
||||
cipherId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321",
|
||||
),
|
||||
awaitItem(),
|
||||
@@ -285,6 +286,7 @@ class PushManagerTest {
|
||||
pushManager.onMessageReceived(SYNC_FOLDER_DELETE_NOTIFICATION_MAP)
|
||||
assertEquals(
|
||||
SyncFolderDeleteData(
|
||||
userId = "078966a2-93c2-4618-ae2a-0a2394c88d37",
|
||||
folderId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321",
|
||||
),
|
||||
awaitItem(),
|
||||
@@ -315,6 +317,7 @@ class PushManagerTest {
|
||||
pushManager.onMessageReceived(SYNC_LOGIN_DELETE_NOTIFICATION_MAP)
|
||||
assertEquals(
|
||||
SyncCipherDeleteData(
|
||||
userId = "078966a2-93c2-4618-ae2a-0a2394c88d37",
|
||||
cipherId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321",
|
||||
),
|
||||
awaitItem(),
|
||||
@@ -343,6 +346,7 @@ class PushManagerTest {
|
||||
pushManager.onMessageReceived(SYNC_SEND_DELETE_NOTIFICATION_MAP)
|
||||
assertEquals(
|
||||
SyncSendDeleteData(
|
||||
userId = "078966a2-93c2-4618-ae2a-0a2394c88d37",
|
||||
sendId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321",
|
||||
),
|
||||
awaitItem(),
|
||||
@@ -403,7 +407,13 @@ class PushManagerTest {
|
||||
fun `onMessageReceived with sync cipher delete does nothing`() = runTest {
|
||||
pushManager.syncCipherDeleteFlow.test {
|
||||
pushManager.onMessageReceived(SYNC_CIPHER_DELETE_NOTIFICATION_MAP)
|
||||
expectNoEvents()
|
||||
assertEquals(
|
||||
SyncCipherDeleteData(
|
||||
userId = "078966a2-93c2-4618-ae2a-0a2394c88d37",
|
||||
cipherId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321",
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,12 +434,19 @@ class PushManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onMessageReceived with sync folder delete does nothing`() = runTest {
|
||||
pushManager.syncFolderDeleteFlow.test {
|
||||
pushManager.onMessageReceived(SYNC_FOLDER_DELETE_NOTIFICATION_MAP)
|
||||
expectNoEvents()
|
||||
fun `onMessageReceived with sync folder delete emits to syncFolderDeleteFlow`() =
|
||||
runTest {
|
||||
pushManager.syncFolderDeleteFlow.test {
|
||||
pushManager.onMessageReceived(SYNC_FOLDER_DELETE_NOTIFICATION_MAP)
|
||||
assertEquals(
|
||||
SyncFolderDeleteData(
|
||||
userId = "078966a2-93c2-4618-ae2a-0a2394c88d37",
|
||||
folderId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321",
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onMessageReceived with sync folder update does nothing`() = runTest {
|
||||
@@ -440,12 +457,19 @@ class PushManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onMessageReceived with sync login delete does nothing`() = runTest {
|
||||
pushManager.syncCipherDeleteFlow.test {
|
||||
pushManager.onMessageReceived(SYNC_LOGIN_DELETE_NOTIFICATION_MAP)
|
||||
expectNoEvents()
|
||||
fun `onMessageReceived with sync login delete emits to syncCipherDeleteFlow`() =
|
||||
runTest {
|
||||
pushManager.syncCipherDeleteFlow.test {
|
||||
pushManager.onMessageReceived(SYNC_LOGIN_DELETE_NOTIFICATION_MAP)
|
||||
assertEquals(
|
||||
SyncCipherDeleteData(
|
||||
userId = "078966a2-93c2-4618-ae2a-0a2394c88d37",
|
||||
cipherId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321",
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onMessageReceived with sync send create does nothing`() = runTest {
|
||||
@@ -456,10 +480,16 @@ class PushManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onMessageReceived with sync send delete does nothing`() = runTest {
|
||||
fun `onMessageReceived with sync send delete emits to syncSendDeleteFlow`() = runTest {
|
||||
pushManager.syncSendDeleteFlow.test {
|
||||
pushManager.onMessageReceived(SYNC_SEND_DELETE_NOTIFICATION_MAP)
|
||||
expectNoEvents()
|
||||
assertEquals(
|
||||
SyncSendDeleteData(
|
||||
userId = "078966a2-93c2-4618-ae2a-0a2394c88d37",
|
||||
sendId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321",
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+16
@@ -105,6 +105,22 @@ class VaultDiskSourceTest {
|
||||
assertEquals(ciphers, result2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getSelectedCiphers should return selected CiphersDao ciphers`() = runTest {
|
||||
val cipherEntities = listOf(
|
||||
CIPHER_ENTITY,
|
||||
CIPHER_ENTITY.copy(id = "otherCipherId"),
|
||||
)
|
||||
val ciphers = listOf(CIPHER_1)
|
||||
val cipherIds = listOf("mockId-1")
|
||||
|
||||
val result1 = vaultDiskSource.getSelectedCiphers(USER_ID, cipherIds)
|
||||
assertEquals(emptyList<SyncResponseJson.Cipher>(), result1)
|
||||
ciphersDao.insertCiphers(cipherEntities)
|
||||
val result2 = vaultDiskSource.getSelectedCiphers(USER_ID, cipherIds)
|
||||
assertEquals(ciphers, result2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getTotpCiphers should return all CiphersDao totp ciphers`() = runTest {
|
||||
val cipherEntities = listOf(
|
||||
|
||||
+6
@@ -41,6 +41,12 @@ class FakeCiphersDao : CiphersDao {
|
||||
override suspend fun getAllCiphers(userId: String): List<CipherEntity> =
|
||||
storedCiphers.filter { it.userId == userId }
|
||||
|
||||
override suspend fun getSelectedCiphers(
|
||||
userId: String,
|
||||
cipherIds: List<String>,
|
||||
): List<CipherEntity> =
|
||||
storedCiphers.filter { it.userId == userId && it.id in cipherIds }
|
||||
|
||||
override suspend fun getAllTotpCiphers(userId: String): List<CipherEntity> =
|
||||
storedCiphers.filter { it.userId == userId && it.hasTotp }
|
||||
|
||||
|
||||
+64
@@ -15,6 +15,7 @@ import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.crypto.TrustDeviceResponse
|
||||
import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager
|
||||
import com.bitwarden.exporters.Account
|
||||
import com.bitwarden.exporters.ExportFormat
|
||||
import com.bitwarden.fido.ClientData
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
@@ -1135,6 +1136,69 @@ class VaultSdkSourceTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exportVaultDataToCxf should call SDK and return a Result with the correct data`() =
|
||||
runTest {
|
||||
val userId = "userId"
|
||||
val account = mockk<Account>()
|
||||
val ciphers = listOf(createMockSdkCipher(1))
|
||||
val expected = "TestResult"
|
||||
|
||||
coEvery {
|
||||
clientExporters.exportCxf(
|
||||
account = account,
|
||||
ciphers = ciphers,
|
||||
)
|
||||
} returns expected
|
||||
|
||||
val result = vaultSdkSource.exportVaultDataToCxf(
|
||||
userId = userId,
|
||||
account = account,
|
||||
ciphers = ciphers,
|
||||
)
|
||||
|
||||
coVerify {
|
||||
clientExporters.exportCxf(
|
||||
account = account,
|
||||
ciphers = ciphers,
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
expected.asSuccess(),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `importCxf should call SDK and return a Result with the correct data`() = runTest {
|
||||
val userId = "userId"
|
||||
val expected = listOf(createMockSdkCipher(number = 1))
|
||||
val cxf = "cxf"
|
||||
|
||||
coEvery {
|
||||
clientExporters.importCxf(
|
||||
payload = cxf,
|
||||
)
|
||||
} returns expected
|
||||
|
||||
val result = vaultSdkSource.importCxf(
|
||||
userId = userId,
|
||||
payload = cxf,
|
||||
)
|
||||
|
||||
coVerify {
|
||||
clientExporters.importCxf(
|
||||
payload = cxf,
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
expected.asSuccess(),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `registerFido2Credential should return attestation response when registration completes`() =
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.sdk.model
|
||||
|
||||
import com.bitwarden.exporters.Account
|
||||
|
||||
/**
|
||||
* Creates a mock [com.bitwarden.exporters.Account] for testing purposes
|
||||
*/
|
||||
fun createMockAccount(
|
||||
number: Int,
|
||||
email: String = "mockEmail-$number",
|
||||
name: String? = "mockName-$number",
|
||||
): Account = Account(
|
||||
id = "mockId-$number",
|
||||
email = email,
|
||||
name = name,
|
||||
)
|
||||
+220
-260
@@ -71,6 +71,8 @@ import com.x8bit.bitwarden.data.platform.manager.model.SyncSendUpsertData
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockAccount
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherListView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockDecryptCipherListResult
|
||||
@@ -92,6 +94,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DomainsData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.ImportCxfPayloadResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SendData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SyncVaultDataResult
|
||||
@@ -2834,46 +2837,26 @@ class VaultRepositoryTest {
|
||||
fun `DeleteFolder with folderService Delete success should return DeleteFolderResult Success and update ciphers`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val folderId = "mockFolderId-1"
|
||||
val mockCipher = createMockCipher(number = 1)
|
||||
val ciphers = listOf(mockCipher, createMockCipher(number = 2))
|
||||
coEvery { folderService.deleteFolder(folderId) } returns Unit.asSuccess()
|
||||
coEvery {
|
||||
vaultDiskSource.deleteFolder(
|
||||
MOCK_USER_STATE.activeUserId,
|
||||
folderId,
|
||||
)
|
||||
} just runs
|
||||
|
||||
val mockCipher = createMockCipher(1)
|
||||
|
||||
val mutableCiphersStateFlow =
|
||||
MutableStateFlow(
|
||||
listOf(
|
||||
mockCipher,
|
||||
createMockCipher(2),
|
||||
),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
vaultDiskSource.getCiphersFlow(MOCK_USER_STATE.activeUserId)
|
||||
} returns mutableCiphersStateFlow
|
||||
|
||||
coEvery { vaultDiskSource.deleteFolder(userId = userId, folderId = folderId) } just runs
|
||||
coEvery { vaultDiskSource.getCiphers(userId = userId) } returns ciphers
|
||||
coEvery {
|
||||
vaultDiskSource.saveCipher(
|
||||
MOCK_USER_STATE.activeUserId,
|
||||
mockCipher.copy(
|
||||
folderId = null,
|
||||
),
|
||||
userId = userId,
|
||||
cipher = mockCipher.copy(folderId = null),
|
||||
)
|
||||
} just runs
|
||||
|
||||
val result = vaultRepository.deleteFolder(folderId)
|
||||
val result = vaultRepository.deleteFolder(folderId = folderId)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultDiskSource.saveCipher(
|
||||
MOCK_USER_STATE.activeUserId,
|
||||
mockCipher.copy(
|
||||
folderId = null,
|
||||
),
|
||||
userId = userId,
|
||||
cipher = mockCipher.copy(folderId = null),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3337,11 +3320,10 @@ class VaultRepositoryTest {
|
||||
val userId = "mockId-1"
|
||||
val cipherId = "mockId-1"
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery { vaultDiskSource.deleteCipher(userId = userId, cipherId = cipherId) } just runs
|
||||
|
||||
mutableSyncCipherDeleteFlow.tryEmit(
|
||||
SyncCipherDeleteData(cipherId = cipherId),
|
||||
SyncCipherDeleteData(userId = userId, cipherId = cipherId),
|
||||
)
|
||||
|
||||
coVerify { vaultDiskSource.deleteCipher(userId = userId, cipherId = cipherId) }
|
||||
@@ -3679,13 +3661,9 @@ class VaultRepositoryTest {
|
||||
fun `syncSendDeleteFlow should delete send from disk`() {
|
||||
val userId = "mockId-1"
|
||||
val sendId = "mockId-1"
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery { vaultDiskSource.deleteSend(userId = userId, sendId = sendId) } just runs
|
||||
|
||||
mutableSyncSendDeleteFlow.tryEmit(
|
||||
SyncSendDeleteData(sendId = sendId),
|
||||
)
|
||||
mutableSyncSendDeleteFlow.tryEmit(SyncSendDeleteData(userId = userId, sendId = sendId))
|
||||
|
||||
coVerify { vaultDiskSource.deleteSend(userId = userId, sendId = sendId) }
|
||||
}
|
||||
@@ -3813,53 +3791,37 @@ class VaultRepositoryTest {
|
||||
fun `syncSendUpsertFlow update failure with 404 code should make a request for a send and then delete it`() =
|
||||
runTest {
|
||||
val number = 1
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val sendId = "mockId-$number"
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val response: HttpException = mockk {
|
||||
every { code() } returns 404
|
||||
}
|
||||
coEvery { sendsService.getSend(sendId = sendId) } returns response.asFailure()
|
||||
coEvery {
|
||||
sendsService.getSend(sendId)
|
||||
} returns response.asFailure()
|
||||
|
||||
coEvery {
|
||||
vaultDiskSource.deleteSend(any(), any())
|
||||
vaultDiskSource.deleteSend(userId = userId, sendId = sendId)
|
||||
} just runs
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
setVaultToUnlocked(userId = MOCK_USER_STATE.activeUserId)
|
||||
val sendView = createMockSendView(number = number)
|
||||
val sendView = createMockSend(
|
||||
number = number,
|
||||
revisionDate = ZonedDateTime.now(clock).minus(5, ChronoUnit.MINUTES),
|
||||
)
|
||||
coEvery {
|
||||
vaultSdkSource.decryptSendList(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
sendList = listOf(createMockSdkSend(number = number)),
|
||||
)
|
||||
} returns listOf(sendView).asSuccess()
|
||||
vaultDiskSource.getSends(userId = userId)
|
||||
} returns MutableStateFlow(listOf(sendView))
|
||||
|
||||
val sendsFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Send>>()
|
||||
setupVaultDiskSourceFlows(sendsFlow = sendsFlow)
|
||||
|
||||
vaultRepository.sendDataStateFlow.test {
|
||||
// Populate and consume items related to the sends flow
|
||||
awaitItem()
|
||||
sendsFlow.tryEmit(listOf(createMockSend(number = number)))
|
||||
awaitItem()
|
||||
|
||||
mutableSyncSendUpsertFlow.tryEmit(
|
||||
SyncSendUpsertData(
|
||||
sendId = sendId,
|
||||
revisionDate = ZonedDateTime.now(),
|
||||
isUpdate = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
mutableSyncSendUpsertFlow.tryEmit(
|
||||
SyncSendUpsertData(
|
||||
sendId = sendId,
|
||||
revisionDate = ZonedDateTime.now(clock),
|
||||
isUpdate = true,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
sendsService.getSend(sendId)
|
||||
vaultDiskSource.deleteSend(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
sendId = sendId,
|
||||
)
|
||||
sendsService.getSend(sendId = sendId)
|
||||
vaultDiskSource.deleteSend(userId = userId, sendId = sendId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3867,51 +3829,31 @@ class VaultRepositoryTest {
|
||||
@Test
|
||||
fun `syncSendUpsertFlow create failure with 404 code should make a request for a send and do nothing`() =
|
||||
runTest {
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val sendId = "mockId-1"
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
setVaultToUnlocked(userId = MOCK_USER_STATE.activeUserId)
|
||||
|
||||
val response: HttpException = mockk {
|
||||
every { code() } returns 404
|
||||
}
|
||||
coEvery { sendsService.getSend(sendId = sendId) } returns response.asFailure()
|
||||
coEvery {
|
||||
sendsService.getSend(sendId)
|
||||
} returns response.asFailure()
|
||||
vaultDiskSource.getSends(userId = userId)
|
||||
} returns MutableStateFlow(emptyList())
|
||||
|
||||
coEvery {
|
||||
vaultSdkSource.decryptSendList(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
sendList = listOf(),
|
||||
)
|
||||
} returns emptyList<SendView>().asSuccess()
|
||||
|
||||
val sendsFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Send>>()
|
||||
setupVaultDiskSourceFlows(sendsFlow = sendsFlow)
|
||||
|
||||
vaultRepository.sendDataStateFlow.test {
|
||||
// Populate and consume items related to the sends flow
|
||||
awaitItem()
|
||||
sendsFlow.tryEmit(emptyList())
|
||||
awaitItem()
|
||||
|
||||
mutableSyncSendUpsertFlow.tryEmit(
|
||||
SyncSendUpsertData(
|
||||
sendId = sendId,
|
||||
revisionDate = ZonedDateTime.now(),
|
||||
isUpdate = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
mutableSyncSendUpsertFlow.tryEmit(
|
||||
SyncSendUpsertData(
|
||||
sendId = sendId,
|
||||
revisionDate = ZonedDateTime.now(clock),
|
||||
isUpdate = false,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
sendsService.getSend(sendId)
|
||||
sendsService.getSend(sendId = sendId)
|
||||
}
|
||||
coVerify(exactly = 0) {
|
||||
vaultDiskSource.deleteSend(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
sendId = sendId,
|
||||
)
|
||||
vaultDiskSource.deleteSend(userId = userId, sendId = sendId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3920,50 +3862,28 @@ class VaultRepositoryTest {
|
||||
fun `syncSendUpsertFlow valid create success should make a request for a send and then store it`() =
|
||||
runTest {
|
||||
val number = 1
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val sendId = "mockId-$number"
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
setVaultToUnlocked(userId = MOCK_USER_STATE.activeUserId)
|
||||
coEvery {
|
||||
vaultSdkSource.decryptSendList(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
sendList = listOf(),
|
||||
)
|
||||
} returns listOf<SendView>().asSuccess()
|
||||
vaultDiskSource.getSends(userId = userId)
|
||||
} returns MutableStateFlow(emptyList())
|
||||
val send = mockk<SyncResponseJson.Send>()
|
||||
coEvery { sendsService.getSend(sendId = sendId) } returns send.asSuccess()
|
||||
coEvery { vaultDiskSource.saveSend(userId = userId, send = send) } just runs
|
||||
|
||||
val sendsFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Send>>()
|
||||
setupVaultDiskSourceFlows(sendsFlow = sendsFlow)
|
||||
|
||||
val send: SyncResponseJson.Send = mockk()
|
||||
coEvery {
|
||||
sendsService.getSend(sendId)
|
||||
} returns send.asSuccess()
|
||||
|
||||
coEvery {
|
||||
vaultDiskSource.saveSend(any(), any())
|
||||
} just runs
|
||||
|
||||
vaultRepository.sendDataStateFlow.test {
|
||||
// Populate and consume items related to the sends flow
|
||||
awaitItem()
|
||||
sendsFlow.tryEmit(listOf())
|
||||
awaitItem()
|
||||
|
||||
mutableSyncSendUpsertFlow.tryEmit(
|
||||
SyncSendUpsertData(
|
||||
sendId = sendId,
|
||||
revisionDate = ZonedDateTime.now(),
|
||||
isUpdate = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
mutableSyncSendUpsertFlow.tryEmit(
|
||||
SyncSendUpsertData(
|
||||
sendId = sendId,
|
||||
revisionDate = ZonedDateTime.now(clock),
|
||||
isUpdate = false,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
sendsService.getSend(sendId)
|
||||
vaultDiskSource.saveSend(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
send = send,
|
||||
)
|
||||
sendsService.getSend(sendId = sendId)
|
||||
vaultDiskSource.saveSend(userId = userId, send = send)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3972,51 +3892,33 @@ class VaultRepositoryTest {
|
||||
fun `syncSendUpsertFlow valid update success should make a request for a send and then store it`() =
|
||||
runTest {
|
||||
val number = 1
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val sendId = "mockId-$number"
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
setVaultToUnlocked(userId = MOCK_USER_STATE.activeUserId)
|
||||
val sendView = createMockSendView(number = number)
|
||||
val sendView = createMockSend(
|
||||
number = number,
|
||||
revisionDate = ZonedDateTime.now(clock).minus(5, ChronoUnit.MINUTES),
|
||||
)
|
||||
coEvery {
|
||||
vaultSdkSource.decryptSendList(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
sendList = listOf(createMockSdkSend(number = number)),
|
||||
)
|
||||
} returns listOf(sendView).asSuccess()
|
||||
vaultDiskSource.getSends(userId = userId)
|
||||
} returns MutableStateFlow(listOf(sendView))
|
||||
|
||||
val sendsFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Send>>()
|
||||
setupVaultDiskSourceFlows(sendsFlow = sendsFlow)
|
||||
val send = mockk<SyncResponseJson.Send>()
|
||||
coEvery { sendsService.getSend(sendId = sendId) } returns send.asSuccess()
|
||||
coEvery { vaultDiskSource.saveSend(userId = userId, send = send) } just runs
|
||||
|
||||
val send: SyncResponseJson.Send = mockk()
|
||||
coEvery {
|
||||
sendsService.getSend(sendId)
|
||||
} returns send.asSuccess()
|
||||
|
||||
coEvery {
|
||||
vaultDiskSource.saveSend(any(), any())
|
||||
} just runs
|
||||
|
||||
vaultRepository.sendDataStateFlow.test {
|
||||
// Populate and consume items related to the sends flow
|
||||
awaitItem()
|
||||
sendsFlow.tryEmit(listOf(createMockSend(number = number)))
|
||||
awaitItem()
|
||||
|
||||
mutableSyncSendUpsertFlow.tryEmit(
|
||||
SyncSendUpsertData(
|
||||
sendId = sendId,
|
||||
revisionDate = ZonedDateTime.now(),
|
||||
isUpdate = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
mutableSyncSendUpsertFlow.tryEmit(
|
||||
SyncSendUpsertData(
|
||||
sendId = sendId,
|
||||
revisionDate = ZonedDateTime.now(clock),
|
||||
isUpdate = true,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
sendsService.getSend(sendId)
|
||||
vaultDiskSource.saveSend(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
send = send,
|
||||
)
|
||||
sendsService.getSend(sendId = sendId)
|
||||
vaultDiskSource.saveSend(userId = userId, send = send)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4024,20 +3926,21 @@ class VaultRepositoryTest {
|
||||
fun `syncFolderDeleteFlow should delete folder from disk and update ciphers`() {
|
||||
val userId = "mockId-1"
|
||||
val folderId = "mockId-1"
|
||||
val cipher = createMockCipher(number = 1, folderId = folderId)
|
||||
val updatedCipher = createMockCipher(number = 1, folderId = null)
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery { vaultDiskSource.deleteFolder(userId = userId, folderId = folderId) } just runs
|
||||
coEvery {
|
||||
vaultDiskSource.getCiphersFlow(userId)
|
||||
} returns flowOf()
|
||||
coEvery { vaultDiskSource.getCiphers(userId = userId) } returns listOf(cipher)
|
||||
coEvery { vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher) } just runs
|
||||
|
||||
mutableSyncFolderDeleteFlow.tryEmit(
|
||||
SyncFolderDeleteData(folderId = folderId),
|
||||
SyncFolderDeleteData(userId = userId, folderId = folderId),
|
||||
)
|
||||
|
||||
coVerify {
|
||||
coVerify(exactly = 1) {
|
||||
vaultDiskSource.deleteFolder(userId = userId, folderId = folderId)
|
||||
vaultDiskSource.getCiphersFlow(userId)
|
||||
vaultDiskSource.getCiphers(userId = userId)
|
||||
vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4164,50 +4067,28 @@ class VaultRepositoryTest {
|
||||
fun `syncFolderUpsertFlow valid create success should make a request for a folder and then store it`() =
|
||||
runTest {
|
||||
val number = 1
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val folderId = "mockId-$number"
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
setVaultToUnlocked(userId = MOCK_USER_STATE.activeUserId)
|
||||
coEvery {
|
||||
vaultSdkSource.decryptFolderList(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
folderList = listOf(),
|
||||
)
|
||||
} returns listOf<FolderView>().asSuccess()
|
||||
vaultDiskSource.getFolders(userId = userId)
|
||||
} returns MutableStateFlow(emptyList())
|
||||
val folder = mockk<SyncResponseJson.Folder>()
|
||||
coEvery { folderService.getFolder(folderId = folderId) } returns folder.asSuccess()
|
||||
coEvery { vaultDiskSource.saveFolder(userId = userId, folder = folder) } just runs
|
||||
|
||||
val foldersFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Folder>>()
|
||||
setupVaultDiskSourceFlows(foldersFlow = foldersFlow)
|
||||
|
||||
val folder: SyncResponseJson.Folder = mockk()
|
||||
coEvery {
|
||||
folderService.getFolder(folderId)
|
||||
} returns folder.asSuccess()
|
||||
|
||||
coEvery {
|
||||
vaultDiskSource.saveFolder(any(), any())
|
||||
} just runs
|
||||
|
||||
vaultRepository.foldersStateFlow.test {
|
||||
// Populate and consume items related to the folders flow
|
||||
awaitItem()
|
||||
foldersFlow.tryEmit(listOf())
|
||||
awaitItem()
|
||||
|
||||
mutableSyncFolderUpsertFlow.tryEmit(
|
||||
SyncFolderUpsertData(
|
||||
folderId = folderId,
|
||||
revisionDate = ZonedDateTime.now(),
|
||||
isUpdate = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
mutableSyncFolderUpsertFlow.tryEmit(
|
||||
SyncFolderUpsertData(
|
||||
folderId = folderId,
|
||||
revisionDate = ZonedDateTime.now(clock),
|
||||
isUpdate = false,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
folderService.getFolder(folderId)
|
||||
vaultDiskSource.saveFolder(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
folder = folder,
|
||||
)
|
||||
folderService.getFolder(folderId = folderId)
|
||||
vaultDiskSource.saveFolder(userId = userId, folder = folder)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4216,51 +4097,32 @@ class VaultRepositoryTest {
|
||||
fun `syncFolderUpsertFlow valid update success should make a request for a folder and then store it`() =
|
||||
runTest {
|
||||
val number = 1
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val folderId = "mockId-$number"
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
setVaultToUnlocked(userId = MOCK_USER_STATE.activeUserId)
|
||||
val folderView = createMockFolderView(number = number)
|
||||
val folderView = createMockFolder(
|
||||
number = number,
|
||||
revisionDate = ZonedDateTime.now(clock).minus(5, ChronoUnit.MINUTES),
|
||||
)
|
||||
coEvery {
|
||||
vaultSdkSource.decryptFolderList(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
folderList = listOf(createMockSdkFolder(number = number)),
|
||||
)
|
||||
} returns listOf(folderView).asSuccess()
|
||||
vaultDiskSource.getFolders(userId = userId)
|
||||
} returns MutableStateFlow(listOf(folderView))
|
||||
val folder = mockk<SyncResponseJson.Folder>()
|
||||
coEvery { folderService.getFolder(folderId = folderId) } returns folder.asSuccess()
|
||||
coEvery { vaultDiskSource.saveFolder(userId = userId, folder = folder) } just runs
|
||||
|
||||
val foldersFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Folder>>()
|
||||
setupVaultDiskSourceFlows(foldersFlow = foldersFlow)
|
||||
|
||||
val folder: SyncResponseJson.Folder = mockk()
|
||||
coEvery {
|
||||
folderService.getFolder(folderId)
|
||||
} returns folder.asSuccess()
|
||||
|
||||
coEvery {
|
||||
vaultDiskSource.saveFolder(any(), any())
|
||||
} just runs
|
||||
|
||||
vaultRepository.foldersStateFlow.test {
|
||||
// Populate and consume items related to the folders flow
|
||||
awaitItem()
|
||||
foldersFlow.tryEmit(listOf(createMockFolder(number = number)))
|
||||
awaitItem()
|
||||
|
||||
mutableSyncFolderUpsertFlow.tryEmit(
|
||||
SyncFolderUpsertData(
|
||||
folderId = folderId,
|
||||
revisionDate = ZonedDateTime.now(),
|
||||
isUpdate = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
mutableSyncFolderUpsertFlow.tryEmit(
|
||||
SyncFolderUpsertData(
|
||||
folderId = folderId,
|
||||
revisionDate = ZonedDateTime.now(clock),
|
||||
isUpdate = true,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
folderService.getFolder(folderId)
|
||||
vaultDiskSource.saveFolder(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
folder = folder,
|
||||
)
|
||||
vaultDiskSource.saveFolder(userId = userId, folder = folder)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4396,6 +4258,104 @@ class VaultRepositoryTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `importCxfPayload should return success result`() = runTest {
|
||||
val userId = "mockId-1"
|
||||
val payload = "payload"
|
||||
val ciphers = listOf(createMockSdkCipher(number = 1))
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
|
||||
coEvery {
|
||||
vaultSdkSource.importCxf(
|
||||
userId = userId,
|
||||
payload = payload,
|
||||
)
|
||||
} returns ciphers.asSuccess()
|
||||
val result = vaultRepository.importCxfPayload(payload)
|
||||
|
||||
assertEquals(
|
||||
ImportCxfPayloadResult.Success(ciphers),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `importCxfPayload should return error result`() = runTest {
|
||||
val userId = "mockId-1"
|
||||
val payload = "payload"
|
||||
val expected = Throwable()
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
|
||||
coEvery {
|
||||
vaultSdkSource.importCxf(
|
||||
userId = userId,
|
||||
payload = payload,
|
||||
)
|
||||
} returns expected.asFailure()
|
||||
val result = vaultRepository.importCxfPayload(payload)
|
||||
|
||||
assertEquals(
|
||||
ImportCxfPayloadResult.Error(expected),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exportVaultDataToCxf should return success result`() = runTest {
|
||||
val userId = "mockId-1"
|
||||
val account = createMockAccount(number = 1, email = "email", name = null)
|
||||
val cipherListViews = listOf(createMockCipherListView(number = 1))
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
|
||||
coEvery {
|
||||
vaultSdkSource.exportVaultDataToCxf(
|
||||
userId = userId,
|
||||
account = account,
|
||||
ciphers = any(),
|
||||
)
|
||||
} returns "TestResult".asSuccess()
|
||||
coEvery {
|
||||
vaultDiskSource.getSelectedCiphers(
|
||||
userId = userId,
|
||||
cipherIds = cipherListViews.mapNotNull { it.id },
|
||||
)
|
||||
} returns listOf(createMockCipher(number = 1))
|
||||
|
||||
val result = vaultRepository.exportVaultDataToCxf(ciphers = cipherListViews)
|
||||
|
||||
assertEquals("TestResult".asSuccess(), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exportVaultDataToCxf should return error result when exportVaultDataToCxf fails`() =
|
||||
runTest {
|
||||
val userId = "mockId-1"
|
||||
val account = createMockAccount(number = 1, email = "email", name = null)
|
||||
val cipherListViews = listOf(createMockCipherListView(number = 1))
|
||||
val throwable = Throwable()
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
|
||||
coEvery {
|
||||
vaultSdkSource.exportVaultDataToCxf(
|
||||
userId = userId,
|
||||
account = account,
|
||||
ciphers = any(),
|
||||
)
|
||||
} returns throwable.asFailure()
|
||||
|
||||
coEvery {
|
||||
vaultDiskSource.getSelectedCiphers(
|
||||
userId = userId,
|
||||
cipherIds = cipherListViews.mapNotNull { it.id },
|
||||
)
|
||||
} returns listOf(createMockCipher(number = 1))
|
||||
|
||||
val result = vaultRepository.exportVaultDataToCxf(ciphers = cipherListViews)
|
||||
|
||||
assertEquals(throwable.asFailure(), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `silentlyDiscoverCredentials should return result`() = runTest {
|
||||
val userId = "userId"
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package com.x8bit.bitwarden.data.vault.repository.model
|
||||
|
||||
import com.bitwarden.network.model.KdfTypeJson
|
||||
import com.bitwarden.network.model.UserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
/**
|
||||
* Creates a mock [AccountJson.Profile] for testing purposes.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
fun createMockAccountJsonProfile(
|
||||
number: Int,
|
||||
userId: String = "mockId-$number",
|
||||
email: String = "mockEmail-$number",
|
||||
isEmailVerified: Boolean = true,
|
||||
name: String? = "mockName-$number",
|
||||
stamp: String = "mockSecurityStamp-$number",
|
||||
organizationId: String? = "mockOrganizationId-$number",
|
||||
avatarColorHex: String? = "mockAvatarColorHex-$number",
|
||||
hasPremium: Boolean = false,
|
||||
forcePasswordResetReason: ForcePasswordResetReason? = null,
|
||||
kdfType: KdfTypeJson? = null,
|
||||
kdfIterations: Int? = null,
|
||||
kdfMemory: Int? = null,
|
||||
kdfParallelism: Int? = null,
|
||||
userDecryptionOptions: UserDecryptionOptionsJson? = null,
|
||||
isTwoFactorEnabled: Boolean = false,
|
||||
creationDate: ZonedDateTime = ZonedDateTime.parse("2024-09-13T01:00:00.00Z"),
|
||||
): AccountJson.Profile = AccountJson.Profile(
|
||||
userId = userId,
|
||||
email = email,
|
||||
isEmailVerified = isEmailVerified,
|
||||
name = name,
|
||||
stamp = stamp,
|
||||
organizationId = organizationId,
|
||||
avatarColorHex = avatarColorHex,
|
||||
hasPremium = hasPremium,
|
||||
forcePasswordResetReason = forcePasswordResetReason,
|
||||
kdfType = kdfType,
|
||||
kdfIterations = kdfIterations,
|
||||
kdfMemory = kdfMemory,
|
||||
kdfParallelism = kdfParallelism,
|
||||
userDecryptionOptions = userDecryptionOptions,
|
||||
isTwoFactorEnabled = isTwoFactorEnabled,
|
||||
creationDate = creationDate,
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a mock [AccountJson] for testing purposes.
|
||||
*/
|
||||
fun createMockAccountJson(
|
||||
number: Int,
|
||||
profile: AccountJson.Profile = createMockAccountJsonProfile(number),
|
||||
tokens: AccountTokensJson = AccountTokensJson(
|
||||
accessToken = "accessToken-$number",
|
||||
refreshToken = "refreshToken-$number",
|
||||
),
|
||||
settings: AccountJson.Settings = AccountJson.Settings(
|
||||
environmentUrlData = null,
|
||||
),
|
||||
): AccountJson = AccountJson(
|
||||
profile = profile,
|
||||
tokens = tokens,
|
||||
settings = settings,
|
||||
)
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.x8bit.bitwarden.data.vault.repository.util
|
||||
|
||||
import com.bitwarden.exporters.Account
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.createMockAccountJson
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AccountJsonExtensionsTest {
|
||||
|
||||
@Test
|
||||
fun `toAccountData returns populated AccountData when account is non-null`() {
|
||||
val account = createMockAccountJson(number = 1)
|
||||
assertEquals(
|
||||
Account(
|
||||
id = "mockId-1",
|
||||
email = "mockEmail-1",
|
||||
name = "mockName-1",
|
||||
),
|
||||
account.toSdkAccount(),
|
||||
)
|
||||
}
|
||||
}
|
||||
+23
-5
@@ -4,6 +4,7 @@ import com.bitwarden.collections.Collection
|
||||
import com.bitwarden.collections.CollectionType
|
||||
import com.bitwarden.network.model.CollectionTypeJson
|
||||
import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.bitwarden.network.model.createMockOrganization
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
@@ -103,7 +104,7 @@ class VaultSdkCollectionExtensionsTest {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toSortAlphabetically should sort collections by type and name`() {
|
||||
fun `toSortAlphabeticallyByTypeAndOrganization should sort collections by type and name`() {
|
||||
val list = listOf(
|
||||
createMockCollectionView(1).copy(name = "c"),
|
||||
createMockCollectionView(1).copy(name = "B"),
|
||||
@@ -111,16 +112,28 @@ class VaultSdkCollectionExtensionsTest {
|
||||
createMockCollectionView(1).copy(name = "4"),
|
||||
createMockCollectionView(1).copy(name = "A"),
|
||||
createMockCollectionView(1).copy(name = "#"),
|
||||
createMockCollectionView(1).copy(
|
||||
name = "D",
|
||||
createMockCollectionView(2).copy(
|
||||
name = "Org2 items",
|
||||
type = CollectionType.DEFAULT_USER_COLLECTION,
|
||||
organizationId = "mockId-2",
|
||||
),
|
||||
createMockCollectionView(1).copy(
|
||||
name = "Org1 items",
|
||||
type = CollectionType.DEFAULT_USER_COLLECTION,
|
||||
organizationId = "mockId-1",
|
||||
),
|
||||
)
|
||||
|
||||
val expected = listOf(
|
||||
createMockCollectionView(1).copy(
|
||||
name = "D",
|
||||
name = "Org1 items",
|
||||
type = CollectionType.DEFAULT_USER_COLLECTION,
|
||||
organizationId = "mockId-1",
|
||||
),
|
||||
createMockCollectionView(2).copy(
|
||||
name = "Org2 items",
|
||||
type = CollectionType.DEFAULT_USER_COLLECTION,
|
||||
organizationId = "mockId-2",
|
||||
),
|
||||
createMockCollectionView(1).copy(name = "#"),
|
||||
createMockCollectionView(1).copy(name = "4"),
|
||||
@@ -132,7 +145,12 @@ class VaultSdkCollectionExtensionsTest {
|
||||
|
||||
assertEquals(
|
||||
expected,
|
||||
list.sortAlphabeticallyByType(),
|
||||
list.sortAlphabeticallyByTypeAndOrganization(
|
||||
userOrganizations = listOf(
|
||||
createMockOrganization(number = 1),
|
||||
createMockOrganization(number = 2),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+6
-1
@@ -5,7 +5,9 @@ import app.cash.turbine.test
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager
|
||||
import com.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.bitwarden.ui.platform.resource.BitwardenPlurals
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.asPluralsText
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_0
|
||||
@@ -590,7 +592,10 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
||||
passwordInput = input,
|
||||
dialog = CompleteRegistrationDialog.Error(
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.master_password_length_val_message_x.asText(12),
|
||||
message = BitwardenPlurals.master_password_length_val_message_x.asPluralsText(
|
||||
quantity = 12,
|
||||
args = arrayOf(12),
|
||||
),
|
||||
),
|
||||
)
|
||||
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
|
||||
|
||||
+6
-2
@@ -3,7 +3,9 @@ package com.x8bit.bitwarden.ui.auth.feature.resetPassword
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.bitwarden.ui.platform.resource.BitwardenPlurals
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.asPluralsText
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
|
||||
@@ -133,8 +135,10 @@ class ResetPasswordViewModelTest : BaseViewModelTest() {
|
||||
resetReason = ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET,
|
||||
dialogState = ResetPasswordState.DialogState.Error(
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.master_password_length_val_message_x
|
||||
.asText(MIN_PASSWORD_LENGTH),
|
||||
message = BitwardenPlurals.master_password_length_val_message_x.asPluralsText(
|
||||
quantity = MIN_PASSWORD_LENGTH,
|
||||
args = arrayOf(MIN_PASSWORD_LENGTH),
|
||||
),
|
||||
),
|
||||
passwordInput = password,
|
||||
passwordStrengthState = PasswordStrengthState.WEAK_1,
|
||||
|
||||
+6
-2
@@ -3,7 +3,9 @@ package com.x8bit.bitwarden.ui.auth.feature.setpassword
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.bitwarden.ui.platform.resource.BitwardenPlurals
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.asPluralsText
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
|
||||
@@ -87,8 +89,10 @@ class SetPasswordViewModelTest : BaseViewModelTest() {
|
||||
DEFAULT_STATE.copy(
|
||||
dialogState = SetPasswordState.DialogState.Error(
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.master_password_length_val_message_x
|
||||
.asText(MIN_PASSWORD_LENGTH),
|
||||
message = BitwardenPlurals.master_password_length_val_message_x.asPluralsText(
|
||||
quantity = MIN_PASSWORD_LENGTH,
|
||||
args = arrayOf(MIN_PASSWORD_LENGTH),
|
||||
),
|
||||
),
|
||||
passwordInput = password,
|
||||
retypePasswordInput = password,
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ class FlightRecorderDataSetExtensionsTest {
|
||||
),
|
||||
)
|
||||
val result = dataset.getStopsLoggingStringForActiveLog(clock = FIXED_CLOCK)
|
||||
assertEquals(BitwardenString.stops_logging_on.asText("1/21/70", "4:27 AM"), result)
|
||||
assertEquals(BitwardenString.stops_logging_on.asText("1/21/70", "4:27\u202FAM"), result)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -195,7 +195,7 @@ class LoginApprovalViewModelTest : BaseViewModelTest() {
|
||||
email = EMAIL,
|
||||
fingerprint = AUTH_REQUEST.fingerprint,
|
||||
ipAddress = AUTH_REQUEST.ipAddress,
|
||||
time = "9/13/24, 12:00 AM",
|
||||
time = "9/13/24, 12:00\u202FAM",
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
|
||||
+1
-1
@@ -124,7 +124,7 @@ class FlightRecorderDataSetExtensionsTest {
|
||||
id = "51",
|
||||
title = "2015-10-10T02:55:52 – 2015-10-10T03:55:52".asText(),
|
||||
subtextStart = "1.00 KB".asText(),
|
||||
subtextEnd = BitwardenString.expires_at.asText("10:15 PM"),
|
||||
subtextEnd = BitwardenString.expires_at.asText("10:15\u202FPM"),
|
||||
isDeletedEnabled = true,
|
||||
),
|
||||
RecordedLogsState.DisplayItem(
|
||||
|
||||
+2
-2
@@ -157,7 +157,7 @@ class OtherViewModelTest : BaseViewModelTest() {
|
||||
mutableVaultLastSyncStateFlow.tryEmit(newSyncTime)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
lastSyncTime = "Oct 27, 2023, 12:00 PM",
|
||||
lastSyncTime = "Oct 27, 2023, 12:00\u202FPM",
|
||||
dialogState = null,
|
||||
),
|
||||
awaitItem(),
|
||||
@@ -236,6 +236,6 @@ private val DEFAULT_STATE = OtherState(
|
||||
allowScreenCapture = false,
|
||||
allowSyncOnRefresh = false,
|
||||
clearClipboardFrequency = ClearClipboardFrequency.NEVER,
|
||||
lastSyncTime = "Oct 26, 2023, 12:00 PM",
|
||||
lastSyncTime = "Oct 26, 2023, 12:00\u202FPM",
|
||||
dialogState = null,
|
||||
)
|
||||
+1
-1
@@ -172,7 +172,7 @@ class PasswordHistoryViewModelTest : BaseViewModelTest() {
|
||||
passwords = listOf(
|
||||
PasswordHistoryState.GeneratedPassword(
|
||||
password = "mockPassword-1",
|
||||
date = "10/27/23, 12:00 PM",
|
||||
date = "10/27/23, 12:00\u202FPM",
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
+2
-2
@@ -78,7 +78,7 @@ class SendDataExtensionsTest {
|
||||
SendState.ViewState.Content.SendItem(
|
||||
id = "mockId-1",
|
||||
name = "mockName-1",
|
||||
deletionDate = "Oct 27, 2023, 12:00 PM",
|
||||
deletionDate = "Oct 27, 2023, 12:00\u202FPM",
|
||||
type = SendState.ViewState.Content.SendItem.Type.FILE,
|
||||
iconList = DEFAULT_SEND_STATUS_ICONS,
|
||||
shareUrl = "www.test.com/#/send/mockAccessId-1/mockKey-1",
|
||||
@@ -87,7 +87,7 @@ class SendDataExtensionsTest {
|
||||
SendState.ViewState.Content.SendItem(
|
||||
id = "mockId-2",
|
||||
name = "mockName-2",
|
||||
deletionDate = "Oct 27, 2023, 12:00 PM",
|
||||
deletionDate = "Oct 27, 2023, 12:00\u202FPM",
|
||||
type = SendState.ViewState.Content.SendItem.Type.TEXT,
|
||||
iconList = DEFAULT_SEND_STATUS_ICONS,
|
||||
shareUrl = "www.test.com/#/send/mockAccessId-2/mockKey-2",
|
||||
|
||||
+2
-2
@@ -48,7 +48,7 @@ class SendViewExtensionsTest {
|
||||
),
|
||||
shareLink = sendUrl,
|
||||
sendName = "mockName-1",
|
||||
deletionDate = "Oct 27, 2023, 12:00 PM",
|
||||
deletionDate = "Oct 27, 2023, 12:00\u202FPM",
|
||||
maxAccessCount = 1,
|
||||
currentAccessCount = 1,
|
||||
notes = "mockNotes-1",
|
||||
@@ -76,7 +76,7 @@ class SendViewExtensionsTest {
|
||||
),
|
||||
shareLink = sendUrl,
|
||||
sendName = "mockName-2",
|
||||
deletionDate = "Oct 27, 2023, 12:00 PM",
|
||||
deletionDate = "Oct 27, 2023, 12:00\u202FPM",
|
||||
maxAccessCount = 1,
|
||||
currentAccessCount = 1,
|
||||
notes = "mockNotes-2",
|
||||
|
||||
+6
-1
@@ -20,8 +20,10 @@ import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.bitwarden.send.SendView
|
||||
import com.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
|
||||
import com.bitwarden.ui.platform.resource.BitwardenPlurals
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asPluralsText
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.vault.CipherListView
|
||||
import com.bitwarden.vault.CipherView
|
||||
@@ -2439,7 +2441,10 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
assertEquals(
|
||||
loginState.copy(
|
||||
dialog = VaultAddEditState.DialogState.Generic(
|
||||
message = BitwardenString.password_exposed.asText(breachCount),
|
||||
message = BitwardenPlurals.password_exposed.asPluralsText(
|
||||
quantity = breachCount,
|
||||
args = arrayOf(breachCount),
|
||||
),
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
|
||||
+2
-2
@@ -217,7 +217,7 @@ class CipherViewExtensionsTest {
|
||||
totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example",
|
||||
canViewPassword = false,
|
||||
fido2CredentialCreationDateTime = BitwardenString.created_x.asText(
|
||||
"Oct 27, 2023, 12:00 PM",
|
||||
"Oct 27, 2023, 12:00\u202FPM",
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -278,7 +278,7 @@ class CipherViewExtensionsTest {
|
||||
totp = totp,
|
||||
canViewPassword = false,
|
||||
fido2CredentialCreationDateTime = BitwardenString.created_x.asText(
|
||||
"Oct 27, 2023, 12:00 PM",
|
||||
"Oct 27, 2023, 12:00\u202FPM",
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
+1
-1
@@ -353,7 +353,7 @@ class ImportLoginsScreenTest : BitwardenComposeTest() {
|
||||
it.copy(dialogState = ImportLoginsState.DialogState.Syncing)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Syncing logins...")
|
||||
.onNodeWithText(text = "Syncing logins…")
|
||||
.assertIsDisplayed()
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
}
|
||||
|
||||
+1
-1
@@ -431,7 +431,7 @@ class VaultItemScreenTest : BitwardenComposeTest() {
|
||||
|
||||
// Verify only the first collection name is shown
|
||||
composeTestRule
|
||||
.onNodeWithText("My collection...")
|
||||
.onNodeWithText("My collection…")
|
||||
.assertIsDisplayed()
|
||||
|
||||
// Verify other collection names are not shown by default.
|
||||
|
||||
+6
-1
@@ -13,8 +13,10 @@ import com.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.bitwarden.ui.platform.components.icon.model.IconData
|
||||
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenPlurals
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asPluralsText
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.ui.util.concat
|
||||
import com.bitwarden.vault.CipherView
|
||||
@@ -1289,7 +1291,10 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
||||
assertEquals(
|
||||
loginState.copy(
|
||||
dialog = VaultItemState.DialogState.Generic(
|
||||
message = BitwardenString.password_exposed.asText(breachCount),
|
||||
message = BitwardenPlurals.password_exposed.asPluralsText(
|
||||
quantity = breachCount,
|
||||
args = arrayOf(breachCount),
|
||||
),
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
|
||||
+6
-6
@@ -170,8 +170,8 @@ fun createCommonContent(
|
||||
if (isEmpty) {
|
||||
VaultItemState.ViewState.Content.Common(
|
||||
name = "mockName",
|
||||
created = BitwardenString.created.asText("Jan 1, 1970, 12:16 AM"),
|
||||
lastUpdated = BitwardenString.last_edited.asText("Jan 1, 1970, 12:16 AM"),
|
||||
created = BitwardenString.created.asText("Jan 1, 1970, 12:16\u202FAM"),
|
||||
lastUpdated = BitwardenString.last_edited.asText("Jan 1, 1970, 12:16\u202FAM"),
|
||||
notes = null,
|
||||
customFields = emptyList(),
|
||||
requiresCloneConfirmation = false,
|
||||
@@ -188,8 +188,8 @@ fun createCommonContent(
|
||||
} else {
|
||||
VaultItemState.ViewState.Content.Common(
|
||||
name = "mockName",
|
||||
created = BitwardenString.created.asText("Jan 1, 1970, 12:16 AM"),
|
||||
lastUpdated = BitwardenString.last_edited.asText("Jan 1, 1970, 12:16 AM"),
|
||||
created = BitwardenString.created.asText("Jan 1, 1970, 12:16\u202FAM"),
|
||||
lastUpdated = BitwardenString.last_edited.asText("Jan 1, 1970, 12:16\u202FAM"),
|
||||
notes = "Lots of notes",
|
||||
customFields = listOf(
|
||||
FieldView(
|
||||
@@ -271,7 +271,7 @@ fun createLoginContent(isEmpty: Boolean): VaultItemState.ViewState.Content.ItemT
|
||||
)
|
||||
},
|
||||
passwordRevisionDate = BitwardenString.password_last_updated
|
||||
.asText("Jan 1, 1970, 12:16 AM")
|
||||
.asText("Jan 1, 1970, 12:16\u202FAM")
|
||||
.takeUnless { isEmpty },
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = TotpCodeItemData(
|
||||
@@ -281,7 +281,7 @@ fun createLoginContent(isEmpty: Boolean): VaultItemState.ViewState.Content.ItemT
|
||||
)
|
||||
.takeUnless { isEmpty },
|
||||
fido2CredentialCreationDateText = BitwardenString.created_x
|
||||
.asText("Oct 27, 2023, 12:00 PM")
|
||||
.asText("Oct 27, 2023, 12:00\u202FPM")
|
||||
.takeUnless { isEmpty },
|
||||
canViewTotpCode = true,
|
||||
)
|
||||
|
||||
@@ -878,7 +878,7 @@ class VaultScreenTest : BitwardenComposeTest() {
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Syncing...")
|
||||
.onNodeWithText("Syncing…")
|
||||
.assertIsDisplayed()
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
}
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ class FlightRecorderDataSetExtensionsTest {
|
||||
assertEquals(
|
||||
BitwardenSnackbarData(
|
||||
message = BitwardenString.flight_recorder_banner_message
|
||||
.asText("4/12/25", "9:15 AM"),
|
||||
.asText("4/12/25", "9:15\u202FAM"),
|
||||
messageHeader = BitwardenString.flight_recorder_banner_title.asText(),
|
||||
actionLabel = BitwardenString.go_to_settings.asText(),
|
||||
withDismissAction = true,
|
||||
|
||||
Reference in new issue
Block a user