mirror of
https://github.com/bitwarden/android.git
synced 2025-12-12 08:40:49 -06:00
[PM-23681] Update TotpCodeManager to use CipherListView (#5532)
This commit is contained in:
parent
2d2a5e74da
commit
fca4ebe023
@ -0,0 +1,18 @@
|
|||||||
|
package com.x8bit.bitwarden.data.autofill.util
|
||||||
|
|
||||||
|
import com.bitwarden.vault.CardListView
|
||||||
|
import com.bitwarden.vault.CipherListView
|
||||||
|
import com.bitwarden.vault.CipherListViewType
|
||||||
|
import com.bitwarden.vault.LoginListView
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the [LoginListView] if the cipher is of type [CipherListViewType.Login], otherwise null.
|
||||||
|
*/
|
||||||
|
val CipherListView.login: LoginListView?
|
||||||
|
get() = (this.type as? CipherListViewType.Login)?.v1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the [CardListView] if the cipher is of type [CipherListViewType.Card], otherwise null.
|
||||||
|
*/
|
||||||
|
val CipherListView.card: CardListView?
|
||||||
|
get() = (this.type as? CipherListViewType.Card)?.v1
|
||||||
@ -1,6 +1,7 @@
|
|||||||
package com.x8bit.bitwarden.data.vault.manager
|
package com.x8bit.bitwarden.data.vault.manager
|
||||||
|
|
||||||
import com.bitwarden.core.data.repository.model.DataState
|
import com.bitwarden.core.data.repository.model.DataState
|
||||||
|
import com.bitwarden.vault.CipherListView
|
||||||
import com.bitwarden.vault.CipherView
|
import com.bitwarden.vault.CipherView
|
||||||
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
|
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@ -18,6 +19,15 @@ interface TotpCodeManager {
|
|||||||
cipherList: List<CipherView>,
|
cipherList: List<CipherView>,
|
||||||
): StateFlow<DataState<List<VerificationCodeItem>>>
|
): StateFlow<DataState<List<VerificationCodeItem>>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flow for getting a DataState with multiple verification code items for the given
|
||||||
|
* [cipherListViews].
|
||||||
|
*/
|
||||||
|
fun getTotpCodesForCipherListViewsStateFlow(
|
||||||
|
userId: String,
|
||||||
|
cipherListViews: List<CipherListView>,
|
||||||
|
): StateFlow<DataState<List<VerificationCodeItem>>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flow for getting a DataState with a single verification code item.
|
* Flow for getting a DataState with a single verification code item.
|
||||||
*/
|
*/
|
||||||
@ -25,4 +35,13 @@ interface TotpCodeManager {
|
|||||||
userId: String,
|
userId: String,
|
||||||
cipher: CipherView,
|
cipher: CipherView,
|
||||||
): StateFlow<DataState<VerificationCodeItem?>>
|
): StateFlow<DataState<VerificationCodeItem?>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flow for getting a DataState with a single verification code item for the given
|
||||||
|
* [cipherListView].
|
||||||
|
*/
|
||||||
|
fun getTotpCodeStateFlow(
|
||||||
|
userId: String,
|
||||||
|
cipherListView: CipherListView,
|
||||||
|
): StateFlow<DataState<VerificationCodeItem?>>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,10 @@ package com.x8bit.bitwarden.data.vault.manager
|
|||||||
import com.bitwarden.core.DateTime
|
import com.bitwarden.core.DateTime
|
||||||
import com.bitwarden.core.data.repository.model.DataState
|
import com.bitwarden.core.data.repository.model.DataState
|
||||||
import com.bitwarden.data.manager.DispatcherManager
|
import com.bitwarden.data.manager.DispatcherManager
|
||||||
|
import com.bitwarden.vault.CipherListView
|
||||||
import com.bitwarden.vault.CipherRepromptType
|
import com.bitwarden.vault.CipherRepromptType
|
||||||
import com.bitwarden.vault.CipherView
|
import com.bitwarden.vault.CipherView
|
||||||
|
import com.x8bit.bitwarden.data.autofill.util.login
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||||
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
|
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@ -36,6 +38,9 @@ class TotpCodeManagerImpl(
|
|||||||
private val mutableVerificationCodeStateFlowMap =
|
private val mutableVerificationCodeStateFlowMap =
|
||||||
mutableMapOf<CipherView, StateFlow<DataState<VerificationCodeItem?>>>()
|
mutableMapOf<CipherView, StateFlow<DataState<VerificationCodeItem?>>>()
|
||||||
|
|
||||||
|
private val mutableCipherListViewVerificationCodeStateFlowMap =
|
||||||
|
mutableMapOf<CipherListView, StateFlow<DataState<VerificationCodeItem?>>>()
|
||||||
|
|
||||||
override fun getTotpCodesStateFlow(
|
override fun getTotpCodesStateFlow(
|
||||||
userId: String,
|
userId: String,
|
||||||
cipherList: List<CipherView>,
|
cipherList: List<CipherView>,
|
||||||
@ -73,6 +78,43 @@ class TotpCodeManagerImpl(
|
|||||||
cipher = cipher,
|
cipher = cipher,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
override fun getTotpCodesForCipherListViewsStateFlow(
|
||||||
|
userId: String,
|
||||||
|
cipherListViews: List<CipherListView>,
|
||||||
|
): StateFlow<DataState<List<VerificationCodeItem>>> {
|
||||||
|
// Generate state flows
|
||||||
|
val stateFlows = cipherListViews.map { cipherListView ->
|
||||||
|
getTotpCodeStateFlowInternal(userId, cipherListView)
|
||||||
|
}
|
||||||
|
return combine(stateFlows) { results ->
|
||||||
|
when {
|
||||||
|
results.any { it is DataState.Loading } -> {
|
||||||
|
DataState.Loading
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
DataState.Loaded(
|
||||||
|
data = results.mapNotNull { (it as DataState.Loaded).data },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.stateIn(
|
||||||
|
scope = unconfinedScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(),
|
||||||
|
initialValue = DataState.Loading,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTotpCodeStateFlow(
|
||||||
|
userId: String,
|
||||||
|
cipherListView: CipherListView,
|
||||||
|
): StateFlow<DataState<VerificationCodeItem?>> =
|
||||||
|
getTotpCodeStateFlowInternal(
|
||||||
|
userId = userId,
|
||||||
|
cipherListView = cipherListView,
|
||||||
|
)
|
||||||
|
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
private fun getTotpCodeStateFlowInternal(
|
private fun getTotpCodeStateFlowInternal(
|
||||||
userId: String,
|
userId: String,
|
||||||
@ -108,7 +150,6 @@ class TotpCodeManagerImpl(
|
|||||||
.onSuccess { response ->
|
.onSuccess { response ->
|
||||||
item = VerificationCodeItem(
|
item = VerificationCodeItem(
|
||||||
code = response.code,
|
code = response.code,
|
||||||
totpCode = totpCode,
|
|
||||||
periodSeconds = response.period.toInt(),
|
periodSeconds = response.period.toInt(),
|
||||||
timeLeftSeconds = response.period.toInt() -
|
timeLeftSeconds = response.period.toInt() -
|
||||||
time % response.period.toInt(),
|
time % response.period.toInt(),
|
||||||
@ -129,12 +170,9 @@ class TotpCodeManagerImpl(
|
|||||||
return@flow
|
return@flow
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
item?.let {
|
item = item.copy(
|
||||||
item = it.copy(
|
timeLeftSeconds = item.periodSeconds - (time % item.periodSeconds),
|
||||||
timeLeftSeconds = it.periodSeconds -
|
)
|
||||||
(time % it.periodSeconds),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
item?.let {
|
item?.let {
|
||||||
@ -154,6 +192,78 @@ class TotpCodeManagerImpl(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("LongMethod")
|
||||||
|
private fun getTotpCodeStateFlowInternal(
|
||||||
|
userId: String,
|
||||||
|
cipherListView: CipherListView?,
|
||||||
|
): StateFlow<DataState<VerificationCodeItem?>> {
|
||||||
|
val cipherId = cipherListView?.id ?: return MutableStateFlow(DataState.Loaded(null))
|
||||||
|
cipherListView.login?.totp ?: return MutableStateFlow(DataState.Loaded(null))
|
||||||
|
|
||||||
|
return mutableCipherListViewVerificationCodeStateFlowMap.getOrPut(cipherListView) {
|
||||||
|
// Define a per-item scope so that we can clear the Flow from the scope when it is
|
||||||
|
// no longer needed.
|
||||||
|
val itemScope = CoroutineScope(dispatcherManager.unconfined)
|
||||||
|
|
||||||
|
flow<DataState<VerificationCodeItem?>> {
|
||||||
|
|
||||||
|
var item: VerificationCodeItem? = null
|
||||||
|
while (currentCoroutineContext().isActive) {
|
||||||
|
val dateTime = clock.instant()
|
||||||
|
val time = dateTime.epochSecond.toInt()
|
||||||
|
if (item == null || item.isExpired(clock = clock)) {
|
||||||
|
vaultSdkSource
|
||||||
|
.generateTotpForCipherListView(
|
||||||
|
cipherListView = cipherListView,
|
||||||
|
userId = userId,
|
||||||
|
time = dateTime,
|
||||||
|
)
|
||||||
|
.onSuccess { response ->
|
||||||
|
item = VerificationCodeItem(
|
||||||
|
code = response.code,
|
||||||
|
periodSeconds = response.period.toInt(),
|
||||||
|
timeLeftSeconds = response.period.toInt() -
|
||||||
|
time % response.period.toInt(),
|
||||||
|
issueTime = clock.millis(),
|
||||||
|
uriLoginViewList = cipherListView.login?.uris,
|
||||||
|
id = cipherId,
|
||||||
|
name = cipherListView.name,
|
||||||
|
username = cipherListView.login?.username,
|
||||||
|
hasPasswordReprompt = when (cipherListView.reprompt) {
|
||||||
|
CipherRepromptType.PASSWORD -> true
|
||||||
|
CipherRepromptType.NONE -> false
|
||||||
|
},
|
||||||
|
orgUsesTotp = cipherListView.organizationUseTotp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.onFailure {
|
||||||
|
emit(DataState.Loaded(null))
|
||||||
|
return@flow
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item = item.copy(
|
||||||
|
timeLeftSeconds = item.periodSeconds - (time % item.periodSeconds),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item?.let {
|
||||||
|
emit(DataState.Loaded(it))
|
||||||
|
}
|
||||||
|
delay(ONE_SECOND_MILLISECOND)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onCompletion {
|
||||||
|
mutableCipherListViewVerificationCodeStateFlowMap.remove(cipherListView)
|
||||||
|
itemScope.cancel()
|
||||||
|
}
|
||||||
|
.stateIn(
|
||||||
|
scope = itemScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(),
|
||||||
|
initialValue = DataState.Loading,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun VerificationCodeItem.isExpired(clock: Clock): Boolean {
|
private fun VerificationCodeItem.isExpired(clock: Clock): Boolean {
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import com.bitwarden.vault.LoginUriView
|
|||||||
* Models the items returned by the TotpCodeManager.
|
* Models the items returned by the TotpCodeManager.
|
||||||
*
|
*
|
||||||
* @property code The verification code for the item.
|
* @property code The verification code for the item.
|
||||||
* @property totpCode The totp code for the item.
|
|
||||||
* @property periodSeconds The time span where the code is valid in seconds.
|
* @property periodSeconds The time span where the code is valid in seconds.
|
||||||
* @property timeLeftSeconds The seconds remaining until a new code is required.
|
* @property timeLeftSeconds The seconds remaining until a new code is required.
|
||||||
* @property issueTime The time the verification code was issued.
|
* @property issueTime The time the verification code was issued.
|
||||||
@ -19,7 +18,6 @@ import com.bitwarden.vault.LoginUriView
|
|||||||
*/
|
*/
|
||||||
data class VerificationCodeItem(
|
data class VerificationCodeItem(
|
||||||
val code: String,
|
val code: String,
|
||||||
val totpCode: String,
|
|
||||||
val periodSeconds: Int,
|
val periodSeconds: Int,
|
||||||
val timeLeftSeconds: Int,
|
val timeLeftSeconds: Int,
|
||||||
val issueTime: Long,
|
val issueTime: Long,
|
||||||
|
|||||||
@ -120,7 +120,6 @@ class VaultItemViewModel @Inject constructor(
|
|||||||
TotpCodeItemData(
|
TotpCodeItemData(
|
||||||
periodSeconds = it.periodSeconds,
|
periodSeconds = it.periodSeconds,
|
||||||
timeLeftSeconds = it.timeLeftSeconds,
|
timeLeftSeconds = it.timeLeftSeconds,
|
||||||
totpCode = it.totpCode,
|
|
||||||
verificationCode = it.code,
|
verificationCode = it.code,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,12 +9,10 @@ import kotlinx.parcelize.Parcelize
|
|||||||
* @property periodSeconds The period for the verification code.
|
* @property periodSeconds The period for the verification code.
|
||||||
* @property timeLeftSeconds The time left for the verification timer.
|
* @property timeLeftSeconds The time left for the verification timer.
|
||||||
* @property verificationCode The verification code for the item.
|
* @property verificationCode The verification code for the item.
|
||||||
* @property totpCode The totp code for the item.
|
|
||||||
*/
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class TotpCodeItemData(
|
data class TotpCodeItemData(
|
||||||
val periodSeconds: Int,
|
val periodSeconds: Int,
|
||||||
val timeLeftSeconds: Int,
|
val timeLeftSeconds: Int,
|
||||||
val verificationCode: String,
|
val verificationCode: String,
|
||||||
val totpCode: String,
|
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|||||||
@ -0,0 +1,60 @@
|
|||||||
|
package com.x8bit.bitwarden.data.autofill.util
|
||||||
|
|
||||||
|
import com.bitwarden.vault.CipherListViewType
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCardListView
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherListView
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockLoginListView
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.assertNotNull
|
||||||
|
import org.junit.jupiter.api.assertNull
|
||||||
|
|
||||||
|
class CipherListViewExtensionsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `login should return LoginListView when type is Login`() {
|
||||||
|
val cipherListView = createMockCipherListView(
|
||||||
|
number = 1,
|
||||||
|
type = CipherListViewType.Login(createMockLoginListView(1)),
|
||||||
|
)
|
||||||
|
val loginListView = cipherListView.login
|
||||||
|
assertNotNull(loginListView)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `login should return null when type is not Login`() {
|
||||||
|
val cipherListViews = listOf(
|
||||||
|
createMockCipherListView(number = 1, type = CipherListViewType.SecureNote),
|
||||||
|
createMockCipherListView(
|
||||||
|
number = 2,
|
||||||
|
type = CipherListViewType.Card(createMockCardListView(number = 2)),
|
||||||
|
),
|
||||||
|
createMockCipherListView(number = 3, type = CipherListViewType.SshKey),
|
||||||
|
createMockCipherListView(number = 4, type = CipherListViewType.Identity),
|
||||||
|
)
|
||||||
|
cipherListViews.forEach { assertNull(it.login) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `card should return CardListView when type is Card`() {
|
||||||
|
val cipherListView = createMockCipherListView(
|
||||||
|
number = 1,
|
||||||
|
type = CipherListViewType.Card(createMockCardListView(number = 1)),
|
||||||
|
)
|
||||||
|
val cardListView = cipherListView.card
|
||||||
|
assertNotNull(cardListView)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `card should return null when type is not Card`() {
|
||||||
|
listOf(
|
||||||
|
createMockCipherListView(
|
||||||
|
number = 1,
|
||||||
|
type = CipherListViewType.Login(createMockLoginListView(1)),
|
||||||
|
),
|
||||||
|
createMockCipherListView(number = 2, type = CipherListViewType.SecureNote),
|
||||||
|
createMockCipherListView(number = 3, type = CipherListViewType.SshKey),
|
||||||
|
createMockCipherListView(number = 4, type = CipherListViewType.Identity),
|
||||||
|
)
|
||||||
|
.forEach { assertNull(it.card) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,5 @@
|
|||||||
package com.x8bit.bitwarden.data.vault.datasource.sdk
|
package com.x8bit.bitwarden.data.vault.datasource.sdk
|
||||||
|
|
||||||
import com.bitwarden.core.DateTime
|
|
||||||
import com.bitwarden.core.DeriveKeyConnectorRequest
|
import com.bitwarden.core.DeriveKeyConnectorRequest
|
||||||
import com.bitwarden.core.DerivePinKeyResponse
|
import com.bitwarden.core.DerivePinKeyResponse
|
||||||
import com.bitwarden.core.InitOrgCryptoRequest
|
import com.bitwarden.core.InitOrgCryptoRequest
|
||||||
@ -71,6 +70,9 @@ import org.junit.jupiter.api.Assertions.assertEquals
|
|||||||
import org.junit.jupiter.api.Assertions.assertTrue
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
import java.time.Clock
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
@Suppress("LargeClass")
|
@Suppress("LargeClass")
|
||||||
class VaultSdkSourceTest {
|
class VaultSdkSourceTest {
|
||||||
@ -1030,7 +1032,7 @@ class VaultSdkSourceTest {
|
|||||||
val totpResponse = TotpResponse("TestCode", 30u)
|
val totpResponse = TotpResponse("TestCode", 30u)
|
||||||
coEvery { clientVault.generateTotp(any(), any()) } returns totpResponse
|
coEvery { clientVault.generateTotp(any(), any()) } returns totpResponse
|
||||||
|
|
||||||
val time = DateTime.now()
|
val time = FIXED_CLOCK.instant()
|
||||||
val result = vaultSdkSource.generateTotp(
|
val result = vaultSdkSource.generateTotp(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
totp = "Totp",
|
totp = "Totp",
|
||||||
@ -1469,3 +1471,7 @@ private val DEFAULT_FIDO_2_AUTH_REQUEST = AuthenticateFido2CredentialRequest(
|
|||||||
isUserVerificationSupported = true,
|
isUserVerificationSupported = true,
|
||||||
selectedCipherView = createMockCipherView(number = 1),
|
selectedCipherView = createMockCipherView(number = 1),
|
||||||
)
|
)
|
||||||
|
private val FIXED_CLOCK: Clock = Clock.fixed(
|
||||||
|
Instant.parse("2023-10-27T12:00:00Z"),
|
||||||
|
ZoneOffset.UTC,
|
||||||
|
)
|
||||||
|
|||||||
@ -0,0 +1,123 @@
|
|||||||
|
package com.x8bit.bitwarden.data.vault.datasource.sdk.model
|
||||||
|
|
||||||
|
import com.bitwarden.vault.CardListView
|
||||||
|
import com.bitwarden.vault.CipherListView
|
||||||
|
import com.bitwarden.vault.CipherListViewType
|
||||||
|
import com.bitwarden.vault.CipherPermissions
|
||||||
|
import com.bitwarden.vault.CipherRepromptType
|
||||||
|
import com.bitwarden.vault.CopyableCipherFields
|
||||||
|
import com.bitwarden.vault.Fido2CredentialListView
|
||||||
|
import com.bitwarden.vault.LocalDataView
|
||||||
|
import com.bitwarden.vault.LoginListView
|
||||||
|
import com.bitwarden.vault.LoginUriView
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default date time used for [ZonedDateTime] properties of mock objects.
|
||||||
|
*/
|
||||||
|
private const val DEFAULT_TIMESTAMP = "2023-10-27T12:00:00Z"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a mock [CipherListView] for testing. Defaults to a Login cipher. Set [type] to override
|
||||||
|
* the default behavior.
|
||||||
|
*/
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
fun createMockCipherListView(
|
||||||
|
number: Int,
|
||||||
|
id: String = "mockId-$number",
|
||||||
|
organizationId: String? = "mockOrganizationId-$number",
|
||||||
|
folderId: String? = "mockId-$number",
|
||||||
|
type: CipherListViewType = CipherListViewType.Login(
|
||||||
|
createMockLoginListView(number = 1),
|
||||||
|
),
|
||||||
|
reprompt: CipherRepromptType = CipherRepromptType.NONE,
|
||||||
|
name: String = "mockName-$number",
|
||||||
|
favorite: Boolean = false,
|
||||||
|
collectionIds: List<String> = listOf("mockId-$number"),
|
||||||
|
revisionDate: Instant = Instant.parse(DEFAULT_TIMESTAMP),
|
||||||
|
creationDate: Instant = Instant.parse(DEFAULT_TIMESTAMP),
|
||||||
|
attachments: UInt = 0U,
|
||||||
|
organizationUseTotp: Boolean = false,
|
||||||
|
edit: Boolean = false,
|
||||||
|
viewPassword: Boolean = false,
|
||||||
|
permissions: CipherPermissions? = createMockSdkCipherPermissions(),
|
||||||
|
localData: LocalDataView? = null,
|
||||||
|
key: String = "mockKey-$number",
|
||||||
|
subtitle: String = "mockSubtitle-$number",
|
||||||
|
hasOldAttachments: Boolean = false,
|
||||||
|
copyableFields: List<CopyableCipherFields> = emptyList(),
|
||||||
|
isDeleted: Boolean = false,
|
||||||
|
): CipherListView = CipherListView(
|
||||||
|
id = id,
|
||||||
|
organizationId = organizationId,
|
||||||
|
folderId = folderId,
|
||||||
|
type = type,
|
||||||
|
reprompt = reprompt,
|
||||||
|
name = name,
|
||||||
|
favorite = favorite,
|
||||||
|
collectionIds = collectionIds,
|
||||||
|
revisionDate = revisionDate,
|
||||||
|
creationDate = creationDate,
|
||||||
|
deletedDate = if (isDeleted) Instant.parse(DEFAULT_TIMESTAMP) else null,
|
||||||
|
attachments = attachments,
|
||||||
|
organizationUseTotp = organizationUseTotp,
|
||||||
|
edit = edit,
|
||||||
|
viewPassword = viewPassword,
|
||||||
|
permissions = permissions,
|
||||||
|
localData = localData,
|
||||||
|
key = key,
|
||||||
|
subtitle = subtitle,
|
||||||
|
hasOldAttachments = hasOldAttachments,
|
||||||
|
copyableFields = copyableFields,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a mock [LoginListView] for testing.
|
||||||
|
*/
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
fun createMockLoginListView(
|
||||||
|
number: Int,
|
||||||
|
fido2Credentials: List<Fido2CredentialListView> = listOf(
|
||||||
|
createMockFido2CredentialListView(number = 1),
|
||||||
|
),
|
||||||
|
hasFido2: Boolean = true,
|
||||||
|
username: String = "mockUsername-$number",
|
||||||
|
totp: String? = "mockTotp-$number",
|
||||||
|
uris: List<LoginUriView> = listOf(createMockUriView(number = 1)),
|
||||||
|
): LoginListView = LoginListView(
|
||||||
|
fido2Credentials = fido2Credentials,
|
||||||
|
hasFido2 = hasFido2,
|
||||||
|
username = username,
|
||||||
|
totp = totp,
|
||||||
|
uris = uris,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a mock [Fido2CredentialListView] for testing.
|
||||||
|
*/
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
fun createMockFido2CredentialListView(
|
||||||
|
number: Int,
|
||||||
|
credentialId: String = "mockCredentialId-$number",
|
||||||
|
rpId: String = "mockRpId-$number",
|
||||||
|
userHandle: String = "mockUserHandle-$number",
|
||||||
|
userName: String = "mockUserName-$number",
|
||||||
|
userDisplayName: String = "mockUserDisplayName-$number",
|
||||||
|
): Fido2CredentialListView = Fido2CredentialListView(
|
||||||
|
credentialId = credentialId,
|
||||||
|
rpId = rpId,
|
||||||
|
userHandle = userHandle,
|
||||||
|
userName = userName,
|
||||||
|
userDisplayName = userDisplayName,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a mock [CardListView] for testing.
|
||||||
|
*/
|
||||||
|
fun createMockCardListView(
|
||||||
|
number: Int,
|
||||||
|
brand: String = "mockBrand-$number",
|
||||||
|
): CardListView = CardListView(
|
||||||
|
brand = brand,
|
||||||
|
)
|
||||||
@ -6,10 +6,13 @@ import com.bitwarden.core.data.util.asFailure
|
|||||||
import com.bitwarden.core.data.util.asSuccess
|
import com.bitwarden.core.data.util.asSuccess
|
||||||
import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager
|
import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager
|
||||||
import com.bitwarden.data.manager.DispatcherManager
|
import com.bitwarden.data.manager.DispatcherManager
|
||||||
|
import com.bitwarden.vault.CipherListViewType
|
||||||
import com.bitwarden.vault.CipherRepromptType
|
import com.bitwarden.vault.CipherRepromptType
|
||||||
import com.bitwarden.vault.TotpResponse
|
import com.bitwarden.vault.TotpResponse
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||||
|
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.createMockCipherView
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockLoginListView
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockLoginView
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockLoginView
|
||||||
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
|
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.verificationcode.util.createVerificationCodeItem
|
import com.x8bit.bitwarden.ui.vault.feature.verificationcode.util.createVerificationCodeItem
|
||||||
@ -129,4 +132,125 @@ class TotpCodeManagerTest {
|
|||||||
assertEquals(DataState.Loaded(null), awaitItem())
|
assertEquals(DataState.Loaded(null), awaitItem())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
fun `getTotpCodesForCipherListViewsStateFlow should have loaded data with a valid values passed in`() =
|
||||||
|
runTest {
|
||||||
|
val cipherListViews = listOf(
|
||||||
|
createMockCipherListView(number = 1),
|
||||||
|
)
|
||||||
|
val totpResponse = TotpResponse("123456", 30u)
|
||||||
|
coEvery {
|
||||||
|
vaultSdkSource.generateTotpForCipherListView(
|
||||||
|
userId = any(),
|
||||||
|
cipherListView = any(),
|
||||||
|
time = any(),
|
||||||
|
)
|
||||||
|
} returns totpResponse.asSuccess()
|
||||||
|
|
||||||
|
val expected = createVerificationCodeItem()
|
||||||
|
|
||||||
|
totpCodeManager.getTotpCodesForCipherListViewsStateFlow(userId, cipherListViews).test {
|
||||||
|
assertEquals(DataState.Loaded(listOf(expected)), awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
fun `getTotpCodesForCipherListViewsStateFlow should have loaded data with empty list if no totp code is provided`() =
|
||||||
|
runTest {
|
||||||
|
val totpResponse = TotpResponse("123456", 30u)
|
||||||
|
coEvery {
|
||||||
|
vaultSdkSource.generateTotpForCipherListView(
|
||||||
|
userId = any(),
|
||||||
|
cipherListView = any(),
|
||||||
|
time = any(),
|
||||||
|
)
|
||||||
|
} returns totpResponse.asSuccess()
|
||||||
|
|
||||||
|
val cipherListView = createMockCipherListView(
|
||||||
|
number = 1,
|
||||||
|
type = CipherListViewType.Login(
|
||||||
|
createMockLoginListView(
|
||||||
|
number = 1,
|
||||||
|
totp = null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
totpCodeManager.getTotpCodesForCipherListViewsStateFlow(userId, listOf(cipherListView))
|
||||||
|
.test {
|
||||||
|
assertEquals(DataState.Loaded(emptyList<VerificationCodeItem>()), awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `getTotpCodesForCipherListViewsStateFlow should have loaded data with empty list if unable to generate auth code`() =
|
||||||
|
runTest {
|
||||||
|
coEvery {
|
||||||
|
vaultSdkSource.generateTotpForCipherListView(
|
||||||
|
userId = any(),
|
||||||
|
cipherListView = any(),
|
||||||
|
time = any(),
|
||||||
|
)
|
||||||
|
} returns Exception().asFailure()
|
||||||
|
|
||||||
|
val cipherListView = createMockCipherListView(
|
||||||
|
number = 1,
|
||||||
|
type = CipherListViewType.Login(createMockLoginListView(number = 1)),
|
||||||
|
)
|
||||||
|
|
||||||
|
totpCodeManager.getTotpCodesForCipherListViewsStateFlow(userId, listOf(cipherListView)).test {
|
||||||
|
assertEquals(DataState.Loaded(emptyList<VerificationCodeItem>()), awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
fun `getTotpCodeStateFlow from CipherListView should have loaded item with valid data passed in`() =
|
||||||
|
runTest {
|
||||||
|
val totpResponse = TotpResponse("123456", 30u)
|
||||||
|
coEvery {
|
||||||
|
vaultSdkSource.generateTotpForCipherListView(
|
||||||
|
userId = any(),
|
||||||
|
cipherListView = any(),
|
||||||
|
time = any(),
|
||||||
|
)
|
||||||
|
} returns totpResponse.asSuccess()
|
||||||
|
|
||||||
|
val cipherListView = createMockCipherListView(
|
||||||
|
number = 1,
|
||||||
|
reprompt = CipherRepromptType.PASSWORD,
|
||||||
|
)
|
||||||
|
|
||||||
|
val expected = createVerificationCodeItem().copy(hasPasswordReprompt = true)
|
||||||
|
|
||||||
|
totpCodeManager.getTotpCodeStateFlow(userId, cipherListView).test {
|
||||||
|
assertEquals(DataState.Loaded(expected), awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getTotpCodeFlow from CipherListView should have null data if unable to get item`() =
|
||||||
|
runTest {
|
||||||
|
val totpResponse = TotpResponse("123456", 30u)
|
||||||
|
coEvery {
|
||||||
|
vaultSdkSource.generateTotp(
|
||||||
|
userId = any(),
|
||||||
|
totp = any(),
|
||||||
|
time = any(),
|
||||||
|
)
|
||||||
|
} returns totpResponse.asSuccess()
|
||||||
|
|
||||||
|
val cipherListView = createMockCipherListView(
|
||||||
|
number = 1,
|
||||||
|
type = CipherListViewType.SshKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
totpCodeManager.getTotpCodeStateFlow(userId, cipherListView).test {
|
||||||
|
assertEquals(DataState.Loaded(null), awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3180,7 +3180,7 @@ class VaultRepositoryTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
every {
|
every {
|
||||||
totpCodeManager.getTotpCodeStateFlow(userId = userId, any())
|
totpCodeManager.getTotpCodeStateFlow(userId = userId, cipher = any())
|
||||||
} returns stateFlow
|
} returns stateFlow
|
||||||
|
|
||||||
setupDataStateFlow(userId = userId)
|
setupDataStateFlow(userId = userId)
|
||||||
|
|||||||
@ -2103,7 +2103,6 @@ class VaultItemScreenTest : BitwardenComposeTest() {
|
|||||||
periodSeconds = 30,
|
periodSeconds = 30,
|
||||||
timeLeftSeconds = 15,
|
timeLeftSeconds = 15,
|
||||||
verificationCode = "123456",
|
verificationCode = "123456",
|
||||||
totpCode = "testCode",
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -3226,7 +3225,6 @@ private val DEFAULT_LOGIN: VaultItemState.ViewState.Content.ItemType.Login =
|
|||||||
periodSeconds = 30,
|
periodSeconds = 30,
|
||||||
timeLeftSeconds = 15,
|
timeLeftSeconds = 15,
|
||||||
verificationCode = "123456",
|
verificationCode = "123456",
|
||||||
totpCode = "testCode",
|
|
||||||
),
|
),
|
||||||
fido2CredentialCreationDateText = null,
|
fido2CredentialCreationDateText = null,
|
||||||
canViewTotpCode = true,
|
canViewTotpCode = true,
|
||||||
|
|||||||
@ -2482,7 +2482,6 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||||||
periodSeconds = 30,
|
periodSeconds = 30,
|
||||||
timeLeftSeconds = 30,
|
timeLeftSeconds = 30,
|
||||||
verificationCode = "123456",
|
verificationCode = "123456",
|
||||||
totpCode = "mockTotp-1",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun setupMockUri() {
|
private fun setupMockUri() {
|
||||||
@ -2549,8 +2548,6 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||||||
passwordRevisionDate = R.string.password_last_updated.asText("12/31/69 06:16 PM"),
|
passwordRevisionDate = R.string.password_last_updated.asText("12/31/69 06:16 PM"),
|
||||||
isPremiumUser = true,
|
isPremiumUser = true,
|
||||||
totpCodeItemData = TotpCodeItemData(
|
totpCodeItemData = TotpCodeItemData(
|
||||||
totpCode = "otpauth://totp/Example:alice@google.com" +
|
|
||||||
"?secret=JBSWY3DPEHPK3PXP&issuer=Example",
|
|
||||||
verificationCode = "123456",
|
verificationCode = "123456",
|
||||||
timeLeftSeconds = 15,
|
timeLeftSeconds = 15,
|
||||||
periodSeconds = 30,
|
periodSeconds = 30,
|
||||||
|
|||||||
@ -48,7 +48,6 @@ class CipherViewExtensionsTest {
|
|||||||
periodSeconds = 30,
|
periodSeconds = 30,
|
||||||
timeLeftSeconds = 15,
|
timeLeftSeconds = 15,
|
||||||
verificationCode = "123456",
|
verificationCode = "123456",
|
||||||
totpCode = "testCode",
|
|
||||||
),
|
),
|
||||||
clock = fixedClock,
|
clock = fixedClock,
|
||||||
canDelete = true,
|
canDelete = true,
|
||||||
@ -82,7 +81,6 @@ class CipherViewExtensionsTest {
|
|||||||
periodSeconds = 30,
|
periodSeconds = 30,
|
||||||
timeLeftSeconds = 15,
|
timeLeftSeconds = 15,
|
||||||
verificationCode = "123456",
|
verificationCode = "123456",
|
||||||
totpCode = "testCode",
|
|
||||||
),
|
),
|
||||||
clock = fixedClock,
|
clock = fixedClock,
|
||||||
canDelete = true,
|
canDelete = true,
|
||||||
@ -122,7 +120,6 @@ class CipherViewExtensionsTest {
|
|||||||
periodSeconds = 30,
|
periodSeconds = 30,
|
||||||
timeLeftSeconds = 15,
|
timeLeftSeconds = 15,
|
||||||
verificationCode = "123456",
|
verificationCode = "123456",
|
||||||
totpCode = "testCode",
|
|
||||||
),
|
),
|
||||||
clock = fixedClock,
|
clock = fixedClock,
|
||||||
canDelete = true,
|
canDelete = true,
|
||||||
|
|||||||
@ -278,7 +278,6 @@ fun createLoginContent(isEmpty: Boolean): VaultItemState.ViewState.Content.ItemT
|
|||||||
periodSeconds = 30,
|
periodSeconds = 30,
|
||||||
timeLeftSeconds = 15,
|
timeLeftSeconds = 15,
|
||||||
verificationCode = "123456",
|
verificationCode = "123456",
|
||||||
totpCode = "testCode",
|
|
||||||
)
|
)
|
||||||
.takeUnless { isEmpty },
|
.takeUnless { isEmpty },
|
||||||
fido2CredentialCreationDateText = R.string.created_x
|
fido2CredentialCreationDateText = R.string.created_x
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
|
|||||||
fun createVerificationCodeItem(number: Int = 1) =
|
fun createVerificationCodeItem(number: Int = 1) =
|
||||||
VerificationCodeItem(
|
VerificationCodeItem(
|
||||||
code = "123456",
|
code = "123456",
|
||||||
totpCode = "mockTotp-$number",
|
|
||||||
periodSeconds = 30,
|
periodSeconds = 30,
|
||||||
id = "mockId-$number",
|
id = "mockId-$number",
|
||||||
issueTime = 1698408000000,
|
issueTime = 1698408000000,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user