BIT-897 Decrypt sync response (#181)

This commit is contained in:
Ramsey Smith 2023-10-31 08:06:36 -06:00 committed by Álison Fernandes
parent 4a3e88f939
commit b31b859516
27 changed files with 1814 additions and 47 deletions

View file

@ -1,7 +1,8 @@
package com.x8bit.bitwarden.data.vault.datasource.network.di
import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService
import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncServiceImpl
import com.x8bit.bitwarden.data.platform.datasource.network.retrofit.Retrofits
import com.x8bit.bitwarden.data.vault.datasource.network.api.SyncApi
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -18,7 +19,9 @@ object VaultNetworkModule {
@Provides
@Singleton
fun provideSyncApiService(
fun provideSyncService(
retrofits: Retrofits,
): SyncApi = retrofits.authenticatedApiRetrofit.create()
): SyncService = SyncServiceImpl(
syncApi = retrofits.authenticatedApiRetrofit.create(),
)
}

View file

@ -8,19 +8,19 @@ import kotlinx.serialization.Serializable
* Represents different fields that a custom cipher field can be linked to.
*/
@Serializable(LinkedIdTypeSerializer::class)
enum class LinkedIdTypeJson {
enum class LinkedIdTypeJson(val value: UInt) {
// region LOGIN
/**
* The field is linked to the login's username.
*/
@SerialName("100")
LOGIN_USERNAME,
LOGIN_USERNAME(value = 100U),
/**
* The field is linked to the login's password.
*/
@SerialName("101")
LOGIN_PASSWORD,
LOGIN_PASSWORD(value = 101U),
// endregion LOGIN
// region CARD
@ -28,37 +28,37 @@ enum class LinkedIdTypeJson {
* The field is linked to the card's cardholder name.
*/
@SerialName("300")
CARD_CARDHOLDER_NAME,
CARD_CARDHOLDER_NAME(value = 300U),
/**
* The field is linked to the card's expiration month.
*/
@SerialName("301")
CARD_EXP_MONTH,
CARD_EXP_MONTH(value = 301U),
/**
* The field is linked to the card's expiration year.
*/
@SerialName("302")
CARD_EXP_YEAR,
CARD_EXP_YEAR(value = 302U),
/**
* The field is linked to the card's code.
*/
@SerialName("303")
CARD_CODE,
CARD_CODE(value = 303U),
/**
* The field is linked to the card's brand.
*/
@SerialName("304")
CARD_BRAND,
CARD_BRAND(value = 304U),
/**
* The field is linked to the card's number.
*/
@SerialName("305")
CARD_NUMBER,
CARD_NUMBER(value = 305U),
// endregion CARD
// region IDENTITY
@ -66,115 +66,115 @@ enum class LinkedIdTypeJson {
* The field is linked to the identity's title.
*/
@SerialName("400")
IDENTITY_TITLE,
IDENTITY_TITLE(value = 400U),
/**
* The field is linked to the identity's middle name.
*/
@SerialName("401")
IDENTITY_MIDDLE_NAME,
IDENTITY_MIDDLE_NAME(value = 401U),
/**
* The field is linked to the identity's address line 1.
*/
@SerialName("402")
IDENTITY_ADDRESS_1,
IDENTITY_ADDRESS_1(value = 402U),
/**
* The field is linked to the identity's address line 2.
*/
@SerialName("403")
IDENTITY_ADDRESS_2,
IDENTITY_ADDRESS_2(value = 403U),
/**
* The field is linked to the identity's address line 3.
*/
@SerialName("404")
IDENTITY_ADDRESS_3,
IDENTITY_ADDRESS_3(value = 404U),
/**
* The field is linked to the identity's city.
*/
@SerialName("405")
IDENTITY_CITY,
IDENTITY_CITY(value = 405U),
/**
* The field is linked to the identity's state.
*/
@SerialName("406")
IDENTITY_STATE,
IDENTITY_STATE(value = 406U),
/**
* The field is linked to the identity's postal code
*/
@SerialName("407")
IDENTITY_POSTAL_CODE,
IDENTITY_POSTAL_CODE(value = 407U),
/**
* The field is linked to the identity's country.
*/
@SerialName("408")
IDENTITY_COUNTRY,
IDENTITY_COUNTRY(value = 408U),
/**
* The field is linked to the identity's company.
*/
@SerialName("409")
IDENTITY_COMPANY,
IDENTITY_COMPANY(value = 409U),
/**
* The field is linked to the identity's email.
*/
@SerialName("410")
IDENTITY_EMAIL,
IDENTITY_EMAIL(value = 410U),
/**
* The field is linked to the identity's phone.
*/
@SerialName("411")
IDENTITY_PHONE,
IDENTITY_PHONE(value = 411U),
/**
* The field is linked to the identity's SSN.
*/
@SerialName("412")
IDENTITY_SSN,
IDENTITY_SSN(value = 412U),
/**
* The field is linked to the identity's username.
*/
@SerialName("413")
IDENTITY_USERNAME,
IDENTITY_USERNAME(value = 413U),
/**
* The field is linked to the identity's passport number.
*/
@SerialName("414")
IDENTITY_PASSPORT_NUMBER,
IDENTITY_PASSPORT_NUMBER(value = 414U),
/**
* The field is linked to the identity's license number.
*/
@SerialName("415")
IDENTITY_LICENSE_NUMBER,
IDENTITY_LICENSE_NUMBER(value = 415U),
/**
* The field is linked to the identity's first name.
*/
@SerialName("416")
IDENTITY_FIRST_NAME,
IDENTITY_FIRST_NAME(value = 416U),
/**
* The field is linked to the identity's last name.
*/
@SerialName("417")
IDENTITY_LAST_NAME,
IDENTITY_LAST_NAME(value = 417U),
/**
* The field is linked to the identity's full name.
*/
@SerialName("418")
IDENTITY_FULL_NAME,
IDENTITY_FULL_NAME(value = 418U),
// endregion IDENTITY
}

View file

@ -77,7 +77,7 @@ data class SyncResponseJson(
/**
* Represents a folder in the vault response.
*
* @property revisionDate The revision date of the folder (nullable).
* @property revisionDate The revision date of the folder.
* @property name The name of the folder (nullable).
* @property id The ID of the folder.
*/
@ -85,7 +85,7 @@ data class SyncResponseJson(
data class Folder(
@SerialName("revisionDate")
@Contextual
val revisionDate: LocalDateTime?, // Date
val revisionDate: LocalDateTime,
@SerialName("name")
val name: String?,
@ -236,7 +236,7 @@ data class SyncResponseJson(
* @property shouldUseActivateAutofillPolicy If the organization should
* use auto fill policy.
* @property shouldUseEvents If the organization should use events.
* @property isFamilySponsorshipFriendlyName If the family sponsorship is a friendly name.
* @property familySponsorshipFriendlyName If the family sponsorship is a friendly name.
* @property isKeyConnectorEnabled If the key connector is enabled.
* @property shouldUseTotp If he organization should use TOTP.
* @property familySponsorshipLastSyncDate The last date the family sponsorship
@ -351,7 +351,7 @@ data class SyncResponseJson(
val shouldUseEvents: Boolean,
@SerialName("familySponsorshipFriendlyName")
val isFamilySponsorshipFriendlyName: String?,
val familySponsorshipFriendlyName: String?,
@SerialName("keyConnectorEnabled")
val isKeyConnectorEnabled: Boolean,
@ -376,7 +376,8 @@ data class SyncResponseJson(
val isSsoBound: Boolean,
@SerialName("familySponsorshipValidUntil")
val familySponsorshipValidUntil: String?,
@Contextual
val familySponsorshipValidUntil: LocalDateTime?,
@SerialName("status")
val status: Int,
@ -499,10 +500,10 @@ data class SyncResponseJson(
* @property shouldEdit If the cipher can edit.
* @property passwordHistory A list of password history objects
* associated with the cipher (nullable).
* @property revisionDate The revision date of the cipher (nullable).
* @property revisionDate The revision date of the cipher.
* @property type The type of cipher.
* @property login The login of the cipher.
* @property creationDate The creation date of the cipher (nullable).
* @property creationDate The creation date of the cipher.
* @property secureNote The secure note of the cipher.
* @property folderId The folder ID of the cipher (nullable).
* @property organizationId The organization ID of the cipher (nullable).
@ -538,20 +539,20 @@ data class SyncResponseJson(
@SerialName("revisionDate")
@Contextual
val revisionDate: LocalDateTime?,
val revisionDate: LocalDateTime,
@SerialName("type")
val type: CipherTypeJson,
@SerialName("login")
val login: Login,
val login: Login?,
@SerialName("creationDate")
@Contextual
val creationDate: LocalDateTime?,
val creationDate: LocalDateTime,
@SerialName("secureNote")
val secureNote: SecureNote,
val secureNote: SecureNote?,
@SerialName("folderId")
val folderId: String?,
@ -564,7 +565,7 @@ data class SyncResponseJson(
val deletedDate: LocalDateTime?,
@SerialName("identity")
val identity: Identity,
val identity: Identity?,
@SerialName("collectionIds")
val collectionIds: List<String>?,
@ -585,7 +586,7 @@ data class SyncResponseJson(
val isFavorite: Boolean,
@SerialName("card")
val card: Card,
val card: Card?,
) {
/**
* Represents an attachment in the vault response.
@ -795,7 +796,7 @@ data class SyncResponseJson(
@Serializable
data class Uri(
@SerialName("match")
val uriMatchType: UriMatchTypeJson,
val uriMatchType: UriMatchTypeJson?,
@SerialName("uri")
val uri: String?,

View file

@ -0,0 +1,13 @@
package com.x8bit.bitwarden.data.vault.datasource.network.service
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
/**
* Provides an API for querying sync endpoints.
*/
interface SyncService {
/**
* Make sync request to get vault items.
*/
suspend fun sync(): Result<SyncResponseJson>
}

View file

@ -0,0 +1,10 @@
package com.x8bit.bitwarden.data.vault.datasource.network.service
import com.x8bit.bitwarden.data.vault.datasource.network.api.SyncApi
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
class SyncServiceImpl constructor(
private val syncApi: SyncApi,
) : SyncService {
override suspend fun sync(): Result<SyncResponseJson> = syncApi.sync()
}

View file

@ -0,0 +1,32 @@
package com.x8bit.bitwarden.data.vault.datasource.sdk
import com.bitwarden.core.Cipher
import com.bitwarden.core.CipherListView
import com.bitwarden.core.CipherView
import com.bitwarden.core.Folder
import com.bitwarden.core.FolderView
/**
* Source of vault information and functionality from the Bitwarden SDK.
*/
interface VaultSdkSource {
/**
* Decrypts a [Cipher] returning a [CipherView] wrapped in a [Result].
*/
suspend fun decryptCipher(cipher: Cipher): Result<CipherView>
/**
* Decrypts a list of [Cipher]s returning a list of [CipherListView] wrapped in a [Result].
*/
suspend fun decryptCipherList(cipherList: List<Cipher>): Result<List<CipherListView>>
/**
* Decrypts a [Folder] returning a [FolderView] wrapped in a [Result].
*/
suspend fun decryptFolder(folder: Folder): Result<FolderView>
/**
* Decrypts a list of [Folder]s returning a list of [FolderView] wrapped in a [Result].
*/
suspend fun decryptFolderList(folderList: List<Folder>): Result<List<FolderView>>
}

View file

@ -0,0 +1,29 @@
package com.x8bit.bitwarden.data.vault.datasource.sdk
import com.bitwarden.core.Cipher
import com.bitwarden.core.CipherListView
import com.bitwarden.core.CipherView
import com.bitwarden.core.Folder
import com.bitwarden.core.FolderView
import com.bitwarden.sdk.ClientVault
/**
* Primary implementation of [VaultSdkSource] that serves as a convenience wrapper around a
* [ClientVault].
*/
class VaultSdkSourceImpl(
private val clientVault: ClientVault,
) : VaultSdkSource {
override suspend fun decryptCipher(cipher: Cipher): Result<CipherView> =
runCatching { clientVault.ciphers().decrypt(cipher) }
override suspend fun decryptCipherList(cipherList: List<Cipher>): Result<List<CipherListView>> =
runCatching { clientVault.ciphers().decryptList(cipherList) }
override suspend fun decryptFolder(folder: Folder): Result<FolderView> =
runCatching { clientVault.folders().decrypt(folder) }
override suspend fun decryptFolderList(folderList: List<Folder>): Result<List<FolderView>> =
runCatching { clientVault.folders().decryptList(folderList) }
}

View file

@ -0,0 +1,24 @@
package com.x8bit.bitwarden.data.vault.datasource.sdk.di
import com.bitwarden.sdk.Client
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSourceImpl
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
/**
* Provides SDK-related dependencies for the vault package.
*/
@Module
@InstallIn(SingletonComponent::class)
class VaultSdkModule {
@Provides
@Singleton
fun providesVaultSdkSource(
client: Client,
): VaultSdkSource = VaultSdkSourceImpl(clientVault = client.vault())
}

View file

@ -0,0 +1,12 @@
package com.x8bit.bitwarden.data.vault.repository
/**
* Responsible for managing vault data inside the network layer.
*/
interface VaultRepository {
/**
* Attempt to sync the vault data.
*/
suspend fun sync()
}

View file

@ -0,0 +1,52 @@
package com.x8bit.bitwarden.data.vault.repository
import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipherList
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolderList
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
/**
* Default implementation of [VaultRepository].
*/
class VaultRepositoryImpl constructor(
private val syncService: SyncService,
private val vaultSdkSource: VaultSdkSource,
dispatcher: CoroutineDispatcher,
) : VaultRepository {
private val scope = CoroutineScope(dispatcher)
private var syncJob: Job = Job().apply { complete() }
override suspend fun sync() {
if (!syncJob.isCompleted) return
syncJob = scope.launch {
syncService
.sync()
.fold(
onSuccess = { syncResponse ->
// TODO transform into domain object consumable by VaultViewModel BIT-205.
// TODO initialize crypto in BIT-990
syncResponse.ciphers?.let { networkCiphers ->
vaultSdkSource.decryptCipherList(
cipherList = networkCiphers.toEncryptedSdkCipherList(),
)
}
syncResponse.folders?.let { networkFolders ->
vaultSdkSource.decryptFolderList(
folderList = networkFolders.toEncryptedSdkFolderList(),
)
}
},
onFailure = {
// TODO handle failure BIT-205.
},
)
}
}
}

View file

@ -0,0 +1,31 @@
package com.x8bit.bitwarden.data.vault.repository.di
import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.VaultRepositoryImpl
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.Dispatchers
import javax.inject.Singleton
/**
* Provides repositories in the vault package.
*/
@Module
@InstallIn(SingletonComponent::class)
class VaultRepositoryModule {
@Provides
@Singleton
fun providesVaultRepository(
syncService: SyncService,
vaultSdkSource: VaultSdkSource,
): VaultRepository = VaultRepositoryImpl(
syncService = syncService,
vaultSdkSource = vaultSdkSource,
dispatcher = Dispatchers.IO,
)
}

View file

@ -0,0 +1,243 @@
@file:Suppress("TooManyFunctions")
package com.x8bit.bitwarden.data.vault.repository.util
import com.bitwarden.core.Attachment
import com.bitwarden.core.Card
import com.bitwarden.core.Cipher
import com.bitwarden.core.CipherRepromptType
import com.bitwarden.core.CipherType
import com.bitwarden.core.Field
import com.bitwarden.core.FieldType
import com.bitwarden.core.Identity
import com.bitwarden.core.Login
import com.bitwarden.core.LoginUri
import com.bitwarden.core.PasswordHistory
import com.bitwarden.core.SecureNote
import com.bitwarden.core.SecureNoteType
import com.bitwarden.core.UriMatchType
import com.x8bit.bitwarden.data.vault.datasource.network.model.CipherRepromptTypeJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.CipherTypeJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.FieldTypeJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.SecureNoteTypeJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.UriMatchTypeJson
import java.time.ZoneOffset
/**
* Converts a list of [SyncResponseJson.Cipher] objects to a list of corresponding
* Bitwarden SDK [Cipher] objects.
*/
fun List<SyncResponseJson.Cipher>.toEncryptedSdkCipherList(): List<Cipher> =
map { it.toEncryptedSdkCipher() }
/**
* Converts a of [SyncResponseJson.Cipher] object to a corresponding
* Bitwarden SDK [Cipher] object.
*/
fun SyncResponseJson.Cipher.toEncryptedSdkCipher(): Cipher =
Cipher(
id = id,
organizationId = organizationId,
folderId = folderId,
collectionIds = collectionIds.orEmpty(),
name = name.orEmpty(),
notes = notes,
type = type.toSdkCipherType(),
login = login?.toSdkLogin(),
identity = identity?.toSdkIdentity(),
card = card?.toSdkCard(),
secureNote = secureNote?.toSdkSecureNote(),
favorite = isFavorite,
reprompt = reprompt.toSdkRepromptType(),
organizationUseTotp = shouldOrganizationUseTotp,
edit = shouldEdit,
viewPassword = shouldViewPassword,
localData = null,
attachments = attachments?.toSdkAttachmentList(),
fields = fields?.toSdkFieldList(),
passwordHistory = passwordHistory?.toSdkPasswordHistoryList(),
creationDate = creationDate.toInstant(ZoneOffset.UTC),
deletedDate = deletedDate?.toInstant(ZoneOffset.UTC),
revisionDate = revisionDate.toInstant(ZoneOffset.UTC),
)
/**
* Transforms a [SyncResponseJson.Cipher.Login] into the corresponding Bitwarden SDK [Login].
*/
fun SyncResponseJson.Cipher.Login.toSdkLogin(): Login =
Login(
username = username,
password = password,
passwordRevisionDate = passwordRevisionDate?.toInstant(ZoneOffset.UTC),
uris = uris?.toSdkLoginUriList(),
totp = totp,
autofillOnPageLoad = shouldAutofillOnPageLoad,
)
/**
* Transforms a [SyncResponseJson.Cipher.Identity] into the corresponding Bitwarden SDK [Identity].
*/
fun SyncResponseJson.Cipher.Identity.toSdkIdentity(): Identity =
Identity(
title = title,
middleName = middleName,
firstName = firstName,
lastName = lastName,
address1 = address1,
address2 = address2,
address3 = address3,
city = city,
state = state,
postalCode = postalCode,
country = country,
company = company,
email = email,
phone = phone,
ssn = ssn,
username = username,
passportNumber = passportNumber,
licenseNumber = licenseNumber,
)
/**
* Transforms a [SyncResponseJson.Cipher.Card] into the corresponding Bitwarden SDK [Card].
*/
fun SyncResponseJson.Cipher.Card.toSdkCard(): Card =
Card(
cardholderName = cardholderName,
expMonth = expMonth,
expYear = expirationYear,
code = code,
brand = brand,
number = number,
)
/**
* Transforms a [SyncResponseJson.Cipher.SecureNote] into
* the corresponding Bitwarden SDK [SecureNote].
*/
fun SyncResponseJson.Cipher.SecureNote.toSdkSecureNote(): SecureNote =
SecureNote(
type = when (type) {
SecureNoteTypeJson.GENERIC -> SecureNoteType.GENERIC
},
)
/**
* Transforms a list of [SyncResponseJson.Cipher.Login.Uri] into
* a corresponding list of Bitwarden SDK [LoginUri].
*/
fun List<SyncResponseJson.Cipher.Login.Uri>.toSdkLoginUriList(): List<LoginUri> =
map { it.toSdkLoginUri() }
/**
* Transforms a [SyncResponseJson.Cipher.Login.Uri] into
* a corresponding Bitwarden SDK [LoginUri].
*/
fun SyncResponseJson.Cipher.Login.Uri.toSdkLoginUri(): LoginUri =
LoginUri(
uri = uri,
match = uriMatchType?.toSdkMatchType(),
)
/**
* Transforms a list of [SyncResponseJson.Cipher.Attachment] into
* a corresponding list of Bitwarden SDK [Attachment].
*/
fun List<SyncResponseJson.Cipher.Attachment>.toSdkAttachmentList(): List<Attachment> =
map { it.toSdkAttachment() }
/**
* Transforms a [SyncResponseJson.Cipher.Attachment] into
* a corresponding Bitwarden SDK [Attachment].
*/
fun SyncResponseJson.Cipher.Attachment.toSdkAttachment(): Attachment =
Attachment(
id = id,
url = url,
size = size.toString(),
sizeName = sizeName,
fileName = fileName,
key = key,
)
/**
* Transforms a list of [SyncResponseJson.Cipher.Field] into
* a corresponding list of Bitwarden SDK [Field].
*/
fun List<SyncResponseJson.Cipher.Field>.toSdkFieldList(): List<Field> =
map { it.toSdkField() }
/**
* Transforms a [SyncResponseJson.Cipher.Field] into
* a corresponding Bitwarden SDK [Field].
*/
fun SyncResponseJson.Cipher.Field.toSdkField(): Field =
Field(
name = name,
value = value,
type = type.toSdkFieldType(),
linkedId = linkedIdType?.value,
)
/**
* Transforms a list of [SyncResponseJson.Cipher.PasswordHistory] into
* a corresponding list of Bitwarden SDK [PasswordHistory].
*/
@Suppress("MaxLineLength")
fun List<SyncResponseJson.Cipher.PasswordHistory>.toSdkPasswordHistoryList(): List<PasswordHistory> =
map { it.toSdkPasswordHistory() }
/**
* Transforms a [SyncResponseJson.Cipher.PasswordHistory] into
* a corresponding Bitwarden SDK [PasswordHistory].
*/
fun SyncResponseJson.Cipher.PasswordHistory.toSdkPasswordHistory(): PasswordHistory =
PasswordHistory(
password = password,
lastUsedDate = lastUsedDate.toInstant(ZoneOffset.UTC),
)
/**
* Transforms a [CipherTypeJson] to the corresponding Bitwarden SDK [CipherType].
*/
fun CipherTypeJson.toSdkCipherType(): CipherType =
when (this) {
CipherTypeJson.LOGIN -> CipherType.LOGIN
CipherTypeJson.SECURE_NOTE -> CipherType.SECURE_NOTE
CipherTypeJson.CARD -> CipherType.CARD
CipherTypeJson.IDENTITY -> CipherType.IDENTITY
}
/**
* Transforms a [UriMatchTypeJson] to the corresponding Bitwarden SDK [UriMatchType].
*/
fun UriMatchTypeJson.toSdkMatchType(): UriMatchType =
when (this) {
UriMatchTypeJson.DOMAIN -> UriMatchType.DOMAIN
UriMatchTypeJson.HOST -> UriMatchType.HOST
UriMatchTypeJson.STARTS_WITH -> UriMatchType.STARTS_WITH
UriMatchTypeJson.EXACT -> UriMatchType.EXACT
UriMatchTypeJson.REGULAR_EXPRESSION -> UriMatchType.REGULAR_EXPRESSION
UriMatchTypeJson.NEVER -> UriMatchType.NEVER
}
/**
* Transforms a [CipherRepromptTypeJson] to the corresponding Bitwarden SDK [CipherRepromptType].
*/
fun CipherRepromptTypeJson.toSdkRepromptType(): CipherRepromptType =
when (this) {
CipherRepromptTypeJson.NONE -> CipherRepromptType.NONE
CipherRepromptTypeJson.PASSWORD -> CipherRepromptType.PASSWORD
}
/**
* Transforms a [FieldTypeJson] to the corresponding Bitwarden SDK [FieldType].
*/
fun FieldTypeJson.toSdkFieldType(): FieldType =
when (this) {
FieldTypeJson.TEXT -> FieldType.TEXT
FieldTypeJson.HIDDEN -> FieldType.HIDDEN
FieldTypeJson.BOOLEAN -> FieldType.BOOLEAN
FieldTypeJson.LINKED -> FieldType.LINKED
}

View file

@ -0,0 +1,23 @@
package com.x8bit.bitwarden.data.vault.repository.util
import com.bitwarden.core.Folder
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import java.time.ZoneOffset
/**
* Converts a list of [SyncResponseJson.Folder] objects to a list of corresponding
* Bitwarden SDK [Folder] objects.
*/
fun List<SyncResponseJson.Folder>.toEncryptedSdkFolderList(): List<Folder> =
map { it.toEncryptedSdkFolder() }
/**
* Converts a [SyncResponseJson.Folder] objects to a corresponding
* Bitwarden SDK [Folder] object.
*/
fun SyncResponseJson.Folder.toEncryptedSdkFolder(): Folder =
Folder(
id = id,
name = name.orEmpty(),
revisionDate = revisionDate.toInstant(ZoneOffset.UTC),
)

View file

@ -2,7 +2,7 @@ package com.x8bit.bitwarden.data.platform.base
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import com.x8bit.bitwarden.data.platform.datasource.network.core.ResultCallAdapterFactory
import kotlinx.serialization.json.Json
import com.x8bit.bitwarden.data.platform.datasource.network.di.PlatformNetworkModule
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
@ -13,12 +13,14 @@ import retrofit2.Retrofit
*/
abstract class BaseServiceTest {
private val json = PlatformNetworkModule.providesJson()
protected val server = MockWebServer().apply { start() }
protected val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(server.url("/").toString())
.addCallAdapterFactory(ResultCallAdapterFactory())
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()
@After

View file

@ -0,0 +1,134 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model
import java.time.LocalDateTime
/**
* Create a mock [SyncResponseJson.Cipher] with a given [number].
*/
fun createMockCipher(number: Int): SyncResponseJson.Cipher =
SyncResponseJson.Cipher(
id = "mockId-$number",
organizationId = "mockOrganizationId-$number",
folderId = "mockFolderId-$number",
collectionIds = listOf("mockCollectionId-$number"),
name = "mockName-$number",
notes = "mockNotes-$number",
type = CipherTypeJson.LOGIN,
login = createMockLogin(number = number),
creationDate = LocalDateTime.parse("2023-10-27T12:00:00"),
deletedDate = LocalDateTime.parse("2023-10-27T12:00:00"),
revisionDate = LocalDateTime.parse("2023-10-27T12:00:00"),
attachments = listOf(createMockAttachment(number = number)),
card = createMockCard(number = number),
fields = listOf(createMockField(number = number)),
identity = createMockIdentity(number = number),
isFavorite = false,
passwordHistory = listOf(createMockPasswordHistory(number = number)),
reprompt = CipherRepromptTypeJson.NONE,
secureNote = createMockSecureNote(),
shouldEdit = false,
shouldOrganizationUseTotp = false,
shouldViewPassword = false,
)
/**
* Create a mock [SyncResponseJson.Cipher.Identity] with a given [number].
*/
fun createMockIdentity(number: Int): SyncResponseJson.Cipher.Identity =
SyncResponseJson.Cipher.Identity(
firstName = "mockFirstName-$number",
middleName = "mockMiddleName-$number",
lastName = "mockLastName-$number",
passportNumber = "mockPassportNumber-$number",
country = "mockCountry-$number",
address1 = "mockAddress1-$number",
address2 = "mockAddress2-$number",
address3 = "mockAddress3-$number",
city = "mockCity-$number",
postalCode = "mockPostalCode-$number",
title = "mockTitle-$number",
ssn = "mockSsn-$number",
phone = "mockPhone-$number",
company = "mockCompany-$number",
licenseNumber = "mockLicenseNumber-$number",
state = "mockState-$number",
email = "mockEmail-$number",
username = "mockUsername-$number",
)
/**
* Create a mock [SyncResponseJson.Cipher.Attachment] with a given [number].
*/
fun createMockAttachment(number: Int): SyncResponseJson.Cipher.Attachment =
SyncResponseJson.Cipher.Attachment(
fileName = "mockFileName-$number",
size = 1,
sizeName = "mockSizeName-$number",
id = "mockId-$number",
url = "mockUrl-$number",
key = "mockKey-$number",
)
/**
* Create a mock [SyncResponseJson.Cipher.Card] with a given [number].
*/
fun createMockCard(number: Int): SyncResponseJson.Cipher.Card =
SyncResponseJson.Cipher.Card(
number = "mockNumber-$number",
expMonth = "mockExpMonth-$number",
code = "mockCode-$number",
expirationYear = "mockExpirationYear-$number",
cardholderName = "mockCardholderName-$number",
brand = "mockBrand-$number",
)
/**
* Create a mock [SyncResponseJson.Cipher.PasswordHistory] with a given [number].
*/
fun createMockPasswordHistory(number: Int): SyncResponseJson.Cipher.PasswordHistory =
SyncResponseJson.Cipher.PasswordHistory(
password = "mockPassword-$number",
lastUsedDate = LocalDateTime.parse("2023-10-27T12:00:00"),
)
/**
* Create a mock [SyncResponseJson.Cipher.SecureNote].
*/
fun createMockSecureNote(): SyncResponseJson.Cipher.SecureNote =
SyncResponseJson.Cipher.SecureNote(
type = SecureNoteTypeJson.GENERIC,
)
/**
* Create a mock [SyncResponseJson.Cipher.Field] with a given [number].
*/
fun createMockField(number: Int): SyncResponseJson.Cipher.Field =
SyncResponseJson.Cipher.Field(
linkedIdType = LinkedIdTypeJson.LOGIN_USERNAME,
name = "mockName-$number",
type = FieldTypeJson.HIDDEN,
value = "mockValue-$number",
)
/**
* Create a mock [SyncResponseJson.Cipher.Login] with a given [number].
*/
fun createMockLogin(number: Int): SyncResponseJson.Cipher.Login =
SyncResponseJson.Cipher.Login(
username = "mockUsername-$number",
password = "mockPassword-$number",
passwordRevisionDate = LocalDateTime.parse("2023-10-27T12:00:00"),
shouldAutofillOnPageLoad = false,
uri = "mockUri-$number",
uris = listOf(createMockUri(number = number)),
totp = "mockTotp-$number",
)
/**
* Create a mock [SyncResponseJson.Cipher.Login.Uri] with a given [number].
*/
fun createMockUri(number: Int): SyncResponseJson.Cipher.Login.Uri =
SyncResponseJson.Cipher.Login.Uri(
uri = "mockUri-$number",
uriMatchType = UriMatchTypeJson.HOST,
)

View file

@ -0,0 +1,14 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model
/**
* Create a mock [SyncResponseJson.Collection] with a given [number].
*/
fun createMockCollection(number: Int): SyncResponseJson.Collection =
SyncResponseJson.Collection(
organizationId = "mockOrganizationId-$number",
shouldHidePasswords = false,
name = "mockName-$number",
externalId = "mockExternalId-$number",
isReadOnly = false,
id = "mockId-$number",
)

View file

@ -0,0 +1,22 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model
/**
* Create a mock [SyncResponseJson.Domains] with a given [number].
*/
fun createMockDomains(number: Int): SyncResponseJson.Domains =
SyncResponseJson.Domains(
globalEquivalentDomains = listOf(
SyncResponseJson.Domains.GlobalEquivalentDomain(
isExcluded = false,
domains = listOf(
"mockDomain-$number",
),
type = 1,
),
),
equivalentDomains = listOf(
listOf(
"mockEquivalentDomain-$number",
),
),
)

View file

@ -0,0 +1,13 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model
import java.time.LocalDateTime
/**
* Create a mock [SyncResponseJson.Folder] with a given [number].
*/
fun createMockFolder(number: Int): SyncResponseJson.Folder =
SyncResponseJson.Folder(
id = "mockId-$number",
name = "mockName-$number",
revisionDate = LocalDateTime.parse("2023-10-27T12:00:00"),
)

View file

@ -0,0 +1,12 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model
/**
* Create a mock [SyncResponseJson.Policy] with a given [number].
*/
fun createMockPolicy(number: Int): SyncResponseJson.Policy =
SyncResponseJson.Policy(
organizationId = "mockOrganizationId-$number",
id = "mockId-$number",
type = PolicyTypeJson.MASTER_PASSWORD,
isEnabled = false,
)

View file

@ -0,0 +1,115 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model
import java.time.LocalDateTime
/**
* Create a mock [SyncResponseJson.Profile] with a given [number].
*/
fun createMockProfile(number: Int): SyncResponseJson.Profile =
SyncResponseJson.Profile(
providerOrganizations = listOf(createMockOrganization(number = number)),
isPremiumFromOrganization = false,
shouldForcePasswordReset = false,
avatarColor = "mockAvatarColor-$number",
isEmailVerified = false,
isTwoFactorEnabled = false,
privateKey = "mockPrivateKey-$number",
isPremium = false,
culture = "mockCulture-$number",
name = "mockName-$number",
organizations = listOf(createMockOrganization(number = number)),
shouldUseKeyConnector = false,
id = "mockId-$number",
masterPasswordHint = "mockMasterPasswordHint-$number",
email = "mockEmail-$number",
key = "mockKey-$number",
securityStamp = "mockSecurityStamp-$number",
providers = listOf(createMockProvider(number = number)),
)
/**
* Create a mock [SyncResponseJson.Profile.Organization] with a given [number].
*/
fun createMockOrganization(number: Int): SyncResponseJson.Profile.Organization =
SyncResponseJson.Profile.Organization(
shouldUsePolicies = false,
keyConnectorUrl = "mockKeyConnectorUrl-$number",
type = 1,
seats = 1,
isEnabled = false,
providerType = 1,
isResetPasswordEnrolled = false,
shouldUseSecretsManager = false,
maxCollections = 1,
isSelfHost = false,
shouldUseKeyConnector = false,
permissions = createMockPermissions(),
hasPublicAndPrivateKeys = false,
providerId = "mockProviderId-$number",
id = "mockId-$number",
shouldUseGroups = false,
shouldUseDirectory = false,
key = "mockKey-$number",
providerName = "mockProviderName-$number",
shouldUsersGetPremium = false,
maxStorageGb = 1,
identifier = "mockIdentifier-$number",
shouldUseSso = false,
shouldUseCustomPermissions = false,
isFamilySponsorshipAvailable = false,
shouldUseResetPassword = false,
planProductType = 1,
accessSecretsManager = false,
use2fa = false,
familySponsorshipToDelete = false,
userId = "mockUserId-$number",
shouldUseActivateAutofillPolicy = false,
shouldUseEvents = false,
familySponsorshipFriendlyName = "mockFamilySponsorshipFriendlyName-$number",
isKeyConnectorEnabled = false,
shouldUseTotp = false,
familySponsorshipLastSyncDate = LocalDateTime.parse("2023-10-27T12:00:00"),
shouldUseScim = false,
name = "mockName-$number",
shouldUseApi = false,
isSsoBound = false,
familySponsorshipValidUntil = LocalDateTime.parse("2023-10-27T12:00:00"),
status = 1,
)
/**
* Create a mock [SyncResponseJson.Profile.Permissions].
*/
fun createMockPermissions(): SyncResponseJson.Profile.Permissions =
SyncResponseJson.Profile.Permissions(
shouldManageGroups = false,
shouldManageResetPassword = false,
shouldAccessReports = false,
shouldManagePolicies = false,
shouldDeleteAnyCollection = false,
shouldManageSso = false,
shouldDeleteAssignedCollections = false,
shouldManageUsers = false,
shouldManageScim = false,
shouldAccessImportExport = false,
shouldEditAnyCollection = false,
shouldAccessEventLogs = false,
shouldCreateNewCollections = false,
shouldEditAssignedCollections = false,
)
/**
* Create a mock [SyncResponseJson.Profile.Provider] with a given [number].
*/
fun createMockProvider(number: Int): SyncResponseJson.Profile.Provider =
SyncResponseJson.Profile.Provider(
shouldUseEvents = false,
permissions = createMockPermissions(),
name = "mockName-$number",
id = "mockId-$number",
type = 1,
userId = "mockUserId-$number",
key = "mockKey-$number",
isEnabled = false,
status = 1,
)

View file

@ -0,0 +1,37 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model
import java.time.LocalDateTime
fun createMockSend(number: Int): SyncResponseJson.Send =
SyncResponseJson.Send(
accessCount = 1,
notes = "mockNotes-$number",
revisionDate = LocalDateTime.parse("2023-10-27T12:00:00"),
maxAccessCount = 1,
shouldHideEmail = false,
type = SendTypeJson.FILE,
accessId = "mockAccessId-$number",
password = "mockPassword-$number",
file = createMockFile(number = 1),
deletionDate = LocalDateTime.parse("2023-10-27T12:00:00"),
name = "mockName-$number",
isDisabled = false,
id = "mockId-$number",
text = createMockText(number = number),
key = "mockKey-$number",
expirationDate = LocalDateTime.parse("2023-10-27T12:00:00"),
)
fun createMockFile(number: Int): SyncResponseJson.Send.File =
SyncResponseJson.Send.File(
fileName = "mockFileName-$number",
size = 1,
sizeName = "mockSizeName-$number",
id = "mockId-$number",
)
fun createMockText(number: Int): SyncResponseJson.Send.Text =
SyncResponseJson.Send.Text(
isHidden = false,
text = "mockText-$number",
)

View file

@ -0,0 +1,375 @@
package com.x8bit.bitwarden.data.vault.datasource.network.service
import com.x8bit.bitwarden.data.platform.base.BaseServiceTest
import com.x8bit.bitwarden.data.vault.datasource.network.api.SyncApi
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipher
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCollection
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockDomains
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockFolder
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockPolicy
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockProfile
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSend
import kotlinx.coroutines.test.runTest
import okhttp3.mockwebserver.MockResponse
import org.junit.Test
import org.junit.jupiter.api.Assertions.assertEquals
import retrofit2.create
class SyncServiceTest : BaseServiceTest() {
private val syncApi: SyncApi = retrofit.create()
private val syncService: SyncService = SyncServiceImpl(
syncApi = syncApi,
)
@Test
fun `sync should return the correct response`() = runTest {
server.enqueue(MockResponse().setBody(SYNC_SUCCESS_JSON))
val result = syncService.sync()
assertEquals(SYNC_SUCCESS, result.getOrThrow())
}
}
private const val SYNC_SUCCESS_JSON = """
{
"profile": {
"id": "mockId-1",
"name": "mockName-1",
"email": "mockEmail-1",
"emailVerified": false,
"premium": false,
"premiumFromOrganization": false,
"masterPasswordHint": "mockMasterPasswordHint-1",
"culture": "mockCulture-1",
"twoFactorEnabled": false,
"key": "mockKey-1",
"privateKey": "mockPrivateKey-1",
"securityStamp": "mockSecurityStamp-1",
"forcePasswordReset": false,
"usesKeyConnector": false,
"avatarColor": "mockAvatarColor-1",
"organizations": [
{
"usePolicies": false,
"keyConnectorUrl": "mockKeyConnectorUrl-1",
"type": 1,
"seats": 1,
"enabled": false,
"providerType": 1,
"resetPasswordEnrolled": false,
"useSecretsManager": false,
"maxCollections": 1,
"selfHost": false,
"useKeyConnector": false,
"permissions": {
"manageGroups": false,
"manageResetPassword": false,
"accessReports": false,
"managePolicies": false,
"deleteAnyCollection": false,
"manageSso": false,
"deleteAssignedCollections": false,
"manageUsers": false,
"manageScim": false,
"accessImportExport": false,
"editAnyCollection": false,
"accessEventLogs": false,
"createNewCollections": false,
"editAssignedCollections": false
},
"hasPublicAndPrivateKeys": false,
"providerId": "mockProviderId-1",
"id": "mockId-1",
"useGroups": false,
"useDirectory": false,
"key": "mockKey-1",
"providerName": "mockProviderName-1",
"usersGetPremium": false,
"maxStorageGb": 1,
"identifier": "mockIdentifier-1",
"useSso": false,
"useCustomPermissions": false,
"familySponsorshipAvailable": false,
"useResetPassword": false,
"planProductType": 1,
"accessSecretsManager": false,
"use2fa": false,
"familySponsorshipToDelete": false,
"userId": "mockUserId-1",
"useActivateAutofillPolicy": false,
"useEvents": false,
"familySponsorshipFriendlyName": "mockFamilySponsorshipFriendlyName-1",
"keyConnectorEnabled": false,
"useTotp": false,
"familySponsorshipLastSyncDate": "2023-10-27T12:00:00.00Z",
"useScim": false,
"name": "mockName-1",
"useApi": false,
"ssoBound": false,
"familySponsorshipValidUntil": "2023-10-27T12:00:00.00Z",
"status": 1
}
],
"providers": [
{
"useEvents": false,
"permissions": {
"manageGroups": false,
"manageResetPassword": false,
"accessReports": false,
"managePolicies": false,
"deleteAnyCollection": false,
"manageSso": false,
"deleteAssignedCollections": false,
"manageUsers": false,
"manageScim": false,
"accessImportExport": false,
"editAnyCollection": false,
"accessEventLogs": false,
"createNewCollections": false,
"editAssignedCollections": false
},
"name": "mockName-1",
"id": "mockId-1",
"type": 1,
"userId": "mockUserId-1",
"key": "mockKey-1",
"enabled": false,
"status": 1
}
],
"providerOrganizations": [
{
"usePolicies": false,
"keyConnectorUrl": "mockKeyConnectorUrl-1",
"type": 1,
"seats": 1,
"enabled": false,
"providerType": 1,
"resetPasswordEnrolled": false,
"useSecretsManager": false,
"maxCollections": 1,
"selfHost": false,
"useKeyConnector": false,
"permissions": {
"manageGroups": false,
"manageResetPassword": false,
"accessReports": false,
"managePolicies": false,
"deleteAnyCollection": false,
"manageSso": false,
"deleteAssignedCollections": false,
"manageUsers": false,
"manageScim": false,
"accessImportExport": false,
"editAnyCollection": false,
"accessEventLogs": false,
"createNewCollections": false,
"editAssignedCollections": false
},
"hasPublicAndPrivateKeys": false,
"providerId": "mockProviderId-1",
"id": "mockId-1",
"useGroups": false,
"useDirectory": false,
"key": "mockKey-1",
"providerName": "mockProviderName-1",
"usersGetPremium": false,
"maxStorageGb": 1,
"identifier": "mockIdentifier-1",
"useSso": false,
"useCustomPermissions": false,
"familySponsorshipAvailable": false,
"useResetPassword": false,
"planProductType": 1,
"accessSecretsManager": false,
"use2fa": false,
"familySponsorshipToDelete": false,
"userId": "mockUserId-1",
"useActivateAutofillPolicy": false,
"useEvents": false,
"familySponsorshipFriendlyName": "mockFamilySponsorshipFriendlyName-1",
"keyConnectorEnabled": false,
"useTotp": false,
"familySponsorshipLastSyncDate": "2023-10-27T12:00:00.00Z",
"useScim": false,
"name": "mockName-1",
"useApi": false,
"ssoBound": false,
"familySponsorshipValidUntil": "2023-10-27T12:00:00.00Z",
"status": 1
}
]
},
"folders": [
{
"revisionDate": "2023-10-27T12:00:00.00Z",
"name": "mockName-1",
"id": "mockId-1"
}
],
"collections": [
{
"organizationId": "mockOrganizationId-1",
"hidePasswords": false,
"name": "mockName-1",
"externalId": "mockExternalId-1",
"readOnly": false,
"id": "mockId-1"
}
],
"ciphers": [
{
"notes": "mockNotes-1",
"attachments": [
{
"fileName": "mockFileName-1",
"size": 1,
"sizeName": "mockSizeName-1",
"id": "mockId-1",
"url": "mockUrl-1",
"key": "mockKey-1"
}
],
"organizationUseTotp": false,
"reprompt": 0,
"edit": false,
"passwordHistory": [
{
"password": "mockPassword-1",
"lastUsedDate": "2023-10-27T12:00:00.00Z"
}
],
"revisionDate": "2023-10-27T12:00:00.00Z",
"type": 1,
"login": {
"uris": [
{
"match": 1,
"uri": "mockUri-1"
}
],
"totp": "mockTotp-1",
"password": "mockPassword-1",
"passwordRevisionDate": "2023-10-27T12:00:00.00Z",
"autofillOnPageLoad": false,
"uri": "mockUri-1",
"username": "mockUsername-1"
},
"creationDate": "2023-10-27T12:00:00.00Z",
"secureNote": {
"type": 0
},
"folderId": "mockFolderId-1",
"organizationId": "mockOrganizationId-1",
"deletedDate": "2023-10-27T12:00:00.00Z",
"identity": {
"passportNumber": "mockPassportNumber-1",
"lastName": "mockLastName-1",
"address3": "mockAddress3-1",
"address2": "mockAddress2-1",
"city": "mockCity-1",
"country": "mockCountry-1",
"address1": "mockAddress1-1",
"postalCode": "mockPostalCode-1",
"title": "mockTitle-1",
"ssn": "mockSsn-1",
"firstName": "mockFirstName-1",
"phone": "mockPhone-1",
"middleName": "mockMiddleName-1",
"company": "mockCompany-1",
"licenseNumber": "mockLicenseNumber-1",
"state": "mockState-1",
"email": "mockEmail-1",
"username": "mockUsername-1"
},
"collectionIds": [
"mockCollectionId-1"
],
"name": "mockName-1",
"id": "mockId-1"
"fields": [
{
"linkedId": 100,
"name": "mockName-1",
"type": 1,
"value": "mockValue-1"
}
],
"viewPassword": false,
"favorite": false,
"card": {
"number": "mockNumber-1",
"expMonth": "mockExpMonth-1",
"code": "mockCode-1",
"expYear": "mockExpirationYear-1",
"cardholderName": "mockCardholderName-1",
"brand": "mockBrand-1"
}
}
],
"domains": {
"equivalentDomains": [
[
"mockEquivalentDomain-1"
]
],
"globalEquivalentDomains": [
{
"type": 1,
"domains": [
"mockDomain-1"
],
"excluded": false
}
]
},
"policies": [
{
"organizationId": "mockOrganizationId-1",
"id": "mockId-1",
"type": 1,
"enabled": false
}
],
"sends": [
{
"accessCount": 1,
"notes": "mockNotes-1",
"revisionDate": "2023-10-27T12:00:00.00Z",
"maxAccessCount": 1,
"hideEmail": false,
"type": 1,
"accessId": "mockAccessId-1",
"password": "mockPassword-1",
"file": {
"fileName": "mockFileName-1",
"size": 1,
"sizeName": "mockSizeName-1",
"id": "mockId-1"
},
"deletionDate": "2023-10-27T12:00:00.00Z",
"name": "mockName-1",
"disabled": false,
"id": "mockId-1",
"text": {
"hidden": false,
"text": "mockText-1"
},
"key": "mockKey-1",
"expirationDate": "2023-10-27T12:00:00.00Z"
}
]
}
"""
private val SYNC_SUCCESS = SyncResponseJson(
folders = listOf(createMockFolder(number = 1)),
collections = listOf(createMockCollection(number = 1)),
profile = createMockProfile(number = 1),
ciphers = listOf(createMockCipher(number = 1)),
policies = listOf(createMockPolicy(number = 1)),
domains = createMockDomains(number = 1),
sends = listOf(createMockSend(number = 1)),
)

View file

@ -0,0 +1,159 @@
package com.x8bit.bitwarden.data.vault.datasource.sdk
import com.bitwarden.core.Attachment
import com.bitwarden.core.Card
import com.bitwarden.core.Cipher
import com.bitwarden.core.CipherRepromptType
import com.bitwarden.core.CipherType
import com.bitwarden.core.Field
import com.bitwarden.core.FieldType
import com.bitwarden.core.Identity
import com.bitwarden.core.Login
import com.bitwarden.core.LoginUri
import com.bitwarden.core.PasswordHistory
import com.bitwarden.core.SecureNote
import com.bitwarden.core.SecureNoteType
import com.bitwarden.core.UriMatchType
import java.time.LocalDateTime
import java.time.ZoneOffset
/**
* Create a mock [Cipher] with a given [number].
*/
fun createMockSdkCipher(number: Int): Cipher =
Cipher(
id = "mockId-$number",
organizationId = "mockOrganizationId-$number",
folderId = "mockFolderId-$number",
collectionIds = listOf("mockCollectionId-$number"),
name = "mockName-$number",
notes = "mockNotes-$number",
type = CipherType.LOGIN,
login = createMockSdkLogin(number = number),
creationDate = LocalDateTime
.parse("2023-10-27T12:00:00")
.toInstant(ZoneOffset.UTC),
deletedDate = LocalDateTime
.parse("2023-10-27T12:00:00")
.toInstant(ZoneOffset.UTC),
revisionDate = LocalDateTime
.parse("2023-10-27T12:00:00")
.toInstant(ZoneOffset.UTC),
attachments = listOf(createMockSdkAttachment(number = number)),
card = createMockSdkCard(number = number),
fields = listOf(createMockSdkField(number = number)),
identity = createMockSdkIdentity(number = number),
favorite = false,
passwordHistory = listOf(createMockSdkPasswordHistory(number = number)),
reprompt = CipherRepromptType.NONE,
secureNote = createMockSdkSecureNote(),
edit = false,
organizationUseTotp = false,
viewPassword = false,
localData = null,
)
/**
* Create a mock [SecureNote] with a given [number].
*/
fun createMockSdkSecureNote(): SecureNote =
SecureNote(
type = SecureNoteType.GENERIC,
)
/**
* Create a mock [PasswordHistory] with a given [number].
*/
fun createMockSdkPasswordHistory(number: Int): PasswordHistory =
PasswordHistory(
password = "mockPassword-$number",
lastUsedDate = LocalDateTime
.parse("2023-10-27T12:00:00")
.toInstant(ZoneOffset.UTC),
)
/**
* Create a mock [Identity] with a given [number].
*/
fun createMockSdkIdentity(number: Int): Identity =
Identity(
firstName = "mockFirstName-$number",
middleName = "mockMiddleName-$number",
lastName = "mockLastName-$number",
passportNumber = "mockPassportNumber-$number",
country = "mockCountry-$number",
address1 = "mockAddress1-$number",
address2 = "mockAddress2-$number",
address3 = "mockAddress3-$number",
city = "mockCity-$number",
postalCode = "mockPostalCode-$number",
title = "mockTitle-$number",
ssn = "mockSsn-$number",
phone = "mockPhone-$number",
company = "mockCompany-$number",
licenseNumber = "mockLicenseNumber-$number",
state = "mockState-$number",
email = "mockEmail-$number",
username = "mockUsername-$number",
)
/**
* Create a mock [Field] with a given [number].
*/
fun createMockSdkField(number: Int): Field =
Field(
linkedId = 100U,
name = "mockName-$number",
type = FieldType.HIDDEN,
value = "mockValue-$number",
)
/**
* Create a mock [Card] with a given [number].
*/
fun createMockSdkCard(number: Int): Card =
Card(
number = "mockNumber-$number",
expMonth = "mockExpMonth-$number",
code = "mockCode-$number",
expYear = "mockExpirationYear-$number",
cardholderName = "mockCardholderName-$number",
brand = "mockBrand-$number",
)
/**
* Create a mock [Attachment] with a given [number].
*/
fun createMockSdkAttachment(number: Int): Attachment =
Attachment(
fileName = "mockFileName-$number",
size = "1",
sizeName = "mockSizeName-$number",
id = "mockId-$number",
url = "mockUrl-$number",
key = "mockKey-$number",
)
/**
* Create a mock [Login] with a given [number].
*/
fun createMockSdkLogin(number: Int): Login =
Login(
username = "mockUsername-$number",
password = "mockPassword-$number",
passwordRevisionDate = LocalDateTime
.parse("2023-10-27T12:00:00")
.toInstant(ZoneOffset.UTC),
autofillOnPageLoad = false,
uris = listOf(createMockSdkUri(number = number)),
totp = "mockTotp-$number",
)
/**
* Create a mock [LoginUri] with a given [number].
*/
fun createMockSdkUri(number: Int): LoginUri =
LoginUri(
uri = "mockUri-$number",
match = UriMatchType.HOST,
)

View file

@ -0,0 +1,17 @@
package com.x8bit.bitwarden.data.vault.datasource.sdk
import com.bitwarden.core.Folder
import java.time.LocalDateTime
import java.time.ZoneOffset
/**
* Create a mock [Folder] with a given [number].
*/
fun createMockSdkFolder(number: Int): Folder =
Folder(
id = "mockId-$number",
name = "mockName-$number",
revisionDate = LocalDateTime
.parse("2023-10-27T12:00:00")
.toInstant(ZoneOffset.UTC),
)

View file

@ -0,0 +1,115 @@
package com.x8bit.bitwarden.data.vault.datasource.sdk
import com.bitwarden.core.Cipher
import com.bitwarden.core.CipherListView
import com.bitwarden.core.CipherView
import com.bitwarden.core.Folder
import com.bitwarden.core.FolderView
import com.bitwarden.sdk.ClientVault
import com.x8bit.bitwarden.data.platform.util.asSuccess
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.jupiter.api.Test
class VaultSdkSourceTest {
private val clientVault = mockk<ClientVault>()
private val vaultSdkSource: VaultSdkSource = VaultSdkSourceImpl(
clientVault = clientVault,
)
@Test
fun `Cipher decrypt should call SDK and return a Result with correct data`() = runBlocking {
val mockCipher = mockk<Cipher>()
val expectedResult = mockk<CipherView>()
coEvery {
clientVault.ciphers().decrypt(
cipher = mockCipher,
)
} returns expectedResult
val result = vaultSdkSource.decryptCipher(
cipher = mockCipher,
)
assertEquals(
expectedResult.asSuccess(),
result,
)
coVerify {
clientVault.ciphers().decrypt(
cipher = mockCipher,
)
}
}
@Test
fun `Cipher decryptList should call SDK and return a Result with correct data`() = runBlocking {
val mockCiphers = mockk<List<Cipher>>()
val expectedResult = mockk<List<CipherListView>>()
coEvery {
clientVault.ciphers().decryptList(
ciphers = mockCiphers,
)
} returns expectedResult
val result = vaultSdkSource.decryptCipherList(
cipherList = mockCiphers,
)
assertEquals(
expectedResult.asSuccess(),
result,
)
coVerify {
clientVault.ciphers().decryptList(
ciphers = mockCiphers,
)
}
}
@Test
fun `Folder decrypt should call SDK and return a Result with correct data`() = runBlocking {
val mockFolder = mockk<Folder>()
val expectedResult = mockk<FolderView>()
coEvery {
clientVault.folders().decrypt(
folder = mockFolder,
)
} returns expectedResult
val result = vaultSdkSource.decryptFolder(
folder = mockFolder,
)
assertEquals(
expectedResult.asSuccess(),
result,
)
coVerify {
clientVault.folders().decrypt(
folder = mockFolder,
)
}
}
@Test
fun `Folder decryptList should call SDK and return a Result with correct data`() = runBlocking {
val mockFolders = mockk<List<Folder>>()
val expectedResult = mockk<List<FolderView>>()
coEvery {
clientVault.folders().decryptList(
folders = mockFolders,
)
} returns expectedResult
val result = vaultSdkSource.decryptFolderList(
folderList = mockFolders,
)
assertEquals(
expectedResult.asSuccess(),
result,
)
coVerify {
clientVault.folders().decryptList(
folders = mockFolders,
)
}
}
}

View file

@ -0,0 +1,244 @@
package com.x8bit.bitwarden.data.vault.repository.util
import com.bitwarden.core.CipherRepromptType
import com.bitwarden.core.CipherType
import com.bitwarden.core.FieldType
import com.bitwarden.core.UriMatchType
import com.x8bit.bitwarden.data.vault.datasource.network.model.CipherRepromptTypeJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.CipherTypeJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.FieldTypeJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.UriMatchTypeJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockAttachment
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCard
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipher
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockField
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockIdentity
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockLogin
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockPasswordHistory
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSecureNote
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockUri
import com.x8bit.bitwarden.data.vault.datasource.sdk.createMockSdkAttachment
import com.x8bit.bitwarden.data.vault.datasource.sdk.createMockSdkCard
import com.x8bit.bitwarden.data.vault.datasource.sdk.createMockSdkCipher
import com.x8bit.bitwarden.data.vault.datasource.sdk.createMockSdkField
import com.x8bit.bitwarden.data.vault.datasource.sdk.createMockSdkIdentity
import com.x8bit.bitwarden.data.vault.datasource.sdk.createMockSdkLogin
import com.x8bit.bitwarden.data.vault.datasource.sdk.createMockSdkPasswordHistory
import com.x8bit.bitwarden.data.vault.datasource.sdk.createMockSdkSecureNote
import com.x8bit.bitwarden.data.vault.datasource.sdk.createMockSdkUri
import org.junit.Assert.assertEquals
import org.junit.Test
class VaultSdkCipherExtensionsTest {
@Test
fun `toEncryptedSdkCipherList should convert list of Network Cipher to List of Sdk Cipher`() {
val syncCiphers = listOf(
createMockCipher(number = 1),
createMockCipher(number = 2),
)
val sdkCiphers = syncCiphers.toEncryptedSdkCipherList()
assertEquals(
listOf(
createMockSdkCipher(number = 1),
createMockSdkCipher(number = 2),
),
sdkCiphers,
)
}
@Test
fun `toEncryptedSdkCipher should convert a SyncResponseJson Cipher to a Cipher`() {
val syncCipher = createMockCipher(number = 1)
val sdkCipher = syncCipher.toEncryptedSdkCipher()
assertEquals(
createMockSdkCipher(number = 1),
sdkCipher,
)
}
@Test
fun `toSdkLogin should convert a SyncResponseJson Cipher Login to a Login`() {
val syncLogin = createMockLogin(number = 1)
val sdkLogin = syncLogin.toSdkLogin()
assertEquals(
createMockSdkLogin(number = 1),
sdkLogin,
)
}
@Test
fun `toSdkIdentity should convert a SyncResponseJson Cipher Identity to a Identity`() {
val syncIdentity = createMockIdentity(number = 1)
val sdkIdentity = syncIdentity.toSdkIdentity()
assertEquals(
createMockSdkIdentity(number = 1),
sdkIdentity,
)
}
@Test
fun `toSdkCard should convert a SyncResponseJson Cipher Card to a Card`() {
val syncCard = createMockCard(number = 1)
val sdkCard = syncCard.toSdkCard()
assertEquals(
createMockSdkCard(number = 1),
sdkCard,
)
}
@Test
fun `toSdkSecureNote should convert a SyncResponseJson Cipher SecureNote to a SecureNote`() {
val syncSecureNote = createMockSecureNote()
val sdkSecureNote = syncSecureNote.toSdkSecureNote()
assertEquals(
createMockSdkSecureNote(),
sdkSecureNote,
)
}
@Test
fun `toSdkLoginUriList should convert list of LoginUri to List of Sdk LoginUri`() {
val syncLoginUris = listOf(
createMockUri(number = 1),
createMockUri(number = 2),
)
val sdkLoginUris = syncLoginUris.toSdkLoginUriList()
assertEquals(
listOf(
createMockSdkUri(number = 1),
createMockSdkUri(number = 2),
),
sdkLoginUris,
)
}
@Test
fun `toSdkLoginUri should convert Network Cipher LoginUri to Sdk LoginUri`() {
val syncLoginUri = createMockUri(number = 1)
val sdkLoginUri = syncLoginUri.toSdkLoginUri()
assertEquals(
createMockSdkUri(number = 1),
sdkLoginUri,
)
}
@Test
fun `toSdkAttachmentList should convert list of Attachment to List of Sdk Attachment`() {
val syncAttachments = listOf(
createMockAttachment(number = 1),
createMockAttachment(number = 2),
)
val sdkAttachments = syncAttachments.toSdkAttachmentList()
assertEquals(
listOf(
createMockSdkAttachment(number = 1),
createMockSdkAttachment(number = 2),
),
sdkAttachments,
)
}
@Test
fun `toSdkAttachment should convert Network Cipher Attachment to Sdk Attachment`() {
val syncAttachment = createMockAttachment(number = 1)
val sdkAttachment = syncAttachment.toSdkAttachment()
assertEquals(
createMockSdkAttachment(number = 1),
sdkAttachment,
)
}
@Test
fun `toSdkFieldList should convert list of Network Cipher Field to List of Sdk Field`() {
val syncFields = listOf(
createMockField(number = 1),
createMockField(number = 2),
)
val sdkFields = syncFields.toSdkFieldList()
assertEquals(
listOf(
createMockSdkField(number = 1),
createMockSdkField(number = 2),
),
sdkFields,
)
}
@Test
fun `toSdkField should convert Network Cipher Attachment to Sdk Attachment`() {
val syncField = createMockField(number = 1)
val sdkField = syncField.toSdkField()
assertEquals(
createMockSdkField(number = 1),
sdkField,
)
}
@Test
@Suppress("MaxLineLength")
fun `toSdkPasswordHistoryList should convert PasswordHistory list to Sdk PasswordHistory List`() {
val syncPasswordHistories = listOf(
createMockPasswordHistory(number = 1),
createMockPasswordHistory(number = 2),
)
val sdkPasswordHistories = syncPasswordHistories.toSdkPasswordHistoryList()
assertEquals(
listOf(
createMockSdkPasswordHistory(number = 1),
createMockSdkPasswordHistory(number = 2),
),
sdkPasswordHistories,
)
}
@Test
fun `toSdkPasswordHistory should convert PasswordHistory to Sdk PasswordHistory`() {
val syncPasswordHistory = createMockPasswordHistory(number = 1)
val sdkPasswordHistory = syncPasswordHistory.toSdkPasswordHistory()
assertEquals(
createMockSdkPasswordHistory(number = 1),
sdkPasswordHistory,
)
}
@Test
fun `toSdkCipherType should convert CipherTypeJson to CipherType`() {
val cipherType = CipherTypeJson.IDENTITY
val sdkCipherType = cipherType.toSdkCipherType()
assertEquals(
CipherType.IDENTITY,
sdkCipherType,
)
}
@Test
fun `toSdkMatchType should convert UriMatchTypeJson to UriMatchType`() {
val uriMatchType = UriMatchTypeJson.DOMAIN
val sdkUriMatchType = uriMatchType.toSdkMatchType()
assertEquals(
UriMatchType.DOMAIN,
sdkUriMatchType,
)
}
@Test
fun `toSdkRepromptType should convert CipherRepromptTypeJson to CipherRepromptType`() {
val repromptType = CipherRepromptTypeJson.NONE
val sdkRepromptType = repromptType.toSdkRepromptType()
assertEquals(
CipherRepromptType.NONE,
sdkRepromptType,
)
}
@Test
fun `toSdkFieldType should convert FieldTypeJson to FieldType`() {
val fieldType = FieldTypeJson.HIDDEN
val sdkFieldType = fieldType.toSdkFieldType()
assertEquals(
FieldType.HIDDEN,
sdkFieldType,
)
}
}

View file

@ -0,0 +1,35 @@
package com.x8bit.bitwarden.data.vault.repository.util
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockFolder
import com.x8bit.bitwarden.data.vault.datasource.sdk.createMockSdkFolder
import org.junit.Assert.assertEquals
import org.junit.Test
class VaultSdkFolderExtensionsTest {
@Test
fun `toEncryptedSdkFolderList should convert list of NetworkFolder to List of SdkFolder`() {
val syncFolders = listOf(
createMockFolder(number = 1),
createMockFolder(number = 2),
)
val sdkFolders = syncFolders.toEncryptedSdkFolderList()
assertEquals(
listOf(
createMockSdkFolder(number = 1),
createMockSdkFolder(number = 2),
),
sdkFolders,
)
}
@Test
fun `toEncryptedSdkFolder should convert a NetworkFolder to a SdkFolder`() {
val syncFolder = createMockFolder(number = 1)
val sdkFolder = syncFolder.toEncryptedSdkFolder()
assertEquals(
createMockSdkFolder(number = 1),
sdkFolder,
)
}
}