mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
BIT-1078: Save login items (Networking) (#272)
This commit is contained in:
parent
1f337e94f0
commit
d36601fa3a
11 changed files with 324 additions and 15 deletions
|
@ -16,7 +16,7 @@ class LocalDateTimeSerializer : KSerializer<LocalDateTime> {
|
|||
private val dateTimeFormatterDeserialization = DateTimeFormatter
|
||||
.ofPattern("yyyy-MM-dd'T'HH:mm:ss.[SSSSSSS][SSSSSS][SSSSS][SSSS][SSS][SS][S]'Z'")
|
||||
private val dateTimeFormatterSerialization =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSS'Z'")
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
|
||||
override val descriptor: SerialDescriptor
|
||||
get() = PrimitiveSerialDescriptor(serialName = "LocalDateTime", kind = PrimitiveKind.STRING)
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package com.x8bit.bitwarden.data.vault.datasource.network.api
|
||||
|
||||
import CipherJsonRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
/**
|
||||
* Defines raw calls under the /ciphers API with authentication applied.
|
||||
*/
|
||||
interface CiphersApi {
|
||||
|
||||
/**
|
||||
* Create a cipher.
|
||||
*/
|
||||
@POST("ciphers")
|
||||
suspend fun createCipher(@Body body: CipherJsonRequest): Result<SyncResponseJson.Cipher>
|
||||
}
|
|
@ -3,6 +3,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.service.CiphersService
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersServiceImpl
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
@ -17,6 +19,14 @@ import javax.inject.Singleton
|
|||
@InstallIn(SingletonComponent::class)
|
||||
object VaultNetworkModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCiphersService(
|
||||
retrofits: Retrofits,
|
||||
): CiphersService = CiphersServiceImpl(
|
||||
ciphersApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSyncService(
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
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.SyncResponseJson
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.time.LocalDateTime
|
||||
|
||||
/**
|
||||
* Represents a cipher request.
|
||||
*
|
||||
* @property notes The notes of the cipher (nullable).
|
||||
* @property reprompt The reprompt of the cipher.
|
||||
* @property passwordHistory A list of password history objects
|
||||
* associated with the cipher (nullable).
|
||||
* @property type The type of cipher.
|
||||
* @property login The login 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).
|
||||
* @property identity The identity of the cipher.
|
||||
* @property name The name of the cipher (nullable).
|
||||
* @property fields A list of fields associated with the cipher (nullable).
|
||||
* @property isFavorite If the cipher is a favorite.
|
||||
* @property card The card of the cipher.
|
||||
*/
|
||||
@Serializable
|
||||
data class CipherJsonRequest(
|
||||
@SerialName("notes")
|
||||
val notes: String?,
|
||||
|
||||
@SerialName("reprompt")
|
||||
val reprompt: CipherRepromptTypeJson,
|
||||
|
||||
@SerialName("passwordHistory")
|
||||
val passwordHistory: List<SyncResponseJson.Cipher.PasswordHistory>?,
|
||||
|
||||
@SerialName("lastKnownRevisionDate")
|
||||
@Contextual
|
||||
val lastKnownRevisionDate: LocalDateTime?,
|
||||
|
||||
@SerialName("type")
|
||||
val type: CipherTypeJson,
|
||||
|
||||
@SerialName("login")
|
||||
val login: SyncResponseJson.Cipher.Login?,
|
||||
|
||||
@SerialName("secureNote")
|
||||
val secureNote: SyncResponseJson.Cipher.SecureNote?,
|
||||
|
||||
@SerialName("folderId")
|
||||
val folderId: String?,
|
||||
|
||||
@SerialName("organizationId")
|
||||
val organizationId: String?,
|
||||
|
||||
@SerialName("identity")
|
||||
val identity: SyncResponseJson.Cipher.Identity?,
|
||||
|
||||
@SerialName("name")
|
||||
val name: String?,
|
||||
|
||||
@SerialName("fields")
|
||||
val fields: List<SyncResponseJson.Cipher.Field>?,
|
||||
|
||||
@SerialName("favorite")
|
||||
val isFavorite: Boolean,
|
||||
|
||||
@SerialName("card")
|
||||
val card: SyncResponseJson.Cipher.Card?,
|
||||
)
|
|
@ -0,0 +1,14 @@
|
|||
package com.x8bit.bitwarden.data.vault.datasource.network.service
|
||||
|
||||
import CipherJsonRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
|
||||
/**
|
||||
* Provides an API for querying ciphers endpoints.
|
||||
*/
|
||||
interface CiphersService {
|
||||
/**
|
||||
* Attempt to create a cipher.
|
||||
*/
|
||||
suspend fun createCipher(body: CipherJsonRequest): Result<SyncResponseJson.Cipher>
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package com.x8bit.bitwarden.data.vault.datasource.network.service
|
||||
|
||||
import CipherJsonRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.api.CiphersApi
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
|
||||
class CiphersServiceImpl constructor(
|
||||
private val ciphersApi: CiphersApi,
|
||||
) : CiphersService {
|
||||
override suspend fun createCipher(body: CipherJsonRequest): Result<SyncResponseJson.Cipher> =
|
||||
ciphersApi.createCipher(body = body)
|
||||
}
|
|
@ -10,21 +10,22 @@ import com.x8bit.bitwarden.ui.vault.feature.vault.VaultState
|
|||
* Transforms a [CipherView] into a [VaultState.ViewState.VaultItem].
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
private fun CipherView.toVaultItem(): VaultState.ViewState.VaultItem =
|
||||
when (type) {
|
||||
private fun CipherView.toVaultItemOrNull(): VaultState.ViewState.VaultItem? {
|
||||
val id = this.id ?: return null
|
||||
return when (type) {
|
||||
CipherType.LOGIN -> VaultState.ViewState.VaultItem.Login(
|
||||
id = id.toString(),
|
||||
id = id,
|
||||
name = name.asText(),
|
||||
username = login?.username?.asText(),
|
||||
)
|
||||
|
||||
CipherType.SECURE_NOTE -> VaultState.ViewState.VaultItem.SecureNote(
|
||||
id = id.toString(),
|
||||
id = id,
|
||||
name = name.asText(),
|
||||
)
|
||||
|
||||
CipherType.CARD -> VaultState.ViewState.VaultItem.Card(
|
||||
id = id.toString(),
|
||||
id = id,
|
||||
name = name.asText(),
|
||||
brand = card?.brand?.asText(),
|
||||
lastFourDigits = card?.number
|
||||
|
@ -33,11 +34,12 @@ private fun CipherView.toVaultItem(): VaultState.ViewState.VaultItem =
|
|||
)
|
||||
|
||||
CipherType.IDENTITY -> VaultState.ViewState.VaultItem.Identity(
|
||||
id = id.toString(),
|
||||
id = id,
|
||||
name = name.asText(),
|
||||
firstName = identity?.firstName?.asText(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms [VaultData] into [VaultState.ViewState].
|
||||
|
@ -46,24 +48,28 @@ fun VaultData.toViewState(): VaultState.ViewState =
|
|||
if (cipherViewList.isEmpty() && folderViewList.isEmpty()) {
|
||||
VaultState.ViewState.NoItems
|
||||
} else {
|
||||
// Filter out any items with invalid IDs in the unlikely case they exist
|
||||
val filteredCipherViewList = cipherViewList.filterNot { it.id.isNullOrBlank() }
|
||||
VaultState.ViewState.Content(
|
||||
loginItemsCount = cipherViewList.count { it.type == CipherType.LOGIN },
|
||||
cardItemsCount = cipherViewList.count { it.type == CipherType.CARD },
|
||||
identityItemsCount = cipherViewList.count { it.type == CipherType.IDENTITY },
|
||||
secureNoteItemsCount = cipherViewList.count { it.type == CipherType.SECURE_NOTE },
|
||||
loginItemsCount = filteredCipherViewList.count { it.type == CipherType.LOGIN },
|
||||
cardItemsCount = filteredCipherViewList.count { it.type == CipherType.CARD },
|
||||
identityItemsCount = filteredCipherViewList.count { it.type == CipherType.IDENTITY },
|
||||
secureNoteItemsCount = filteredCipherViewList
|
||||
.count { it.type == CipherType.SECURE_NOTE },
|
||||
favoriteItems = cipherViewList
|
||||
.filter { it.favorite }
|
||||
.map { it.toVaultItem() },
|
||||
.mapNotNull { it.toVaultItemOrNull() },
|
||||
folderItems = folderViewList.map { folderView ->
|
||||
VaultState.ViewState.FolderItem(
|
||||
id = folderView.id,
|
||||
name = folderView.name.asText(),
|
||||
itemCount = cipherViewList.count { folderView.id == it.folderId },
|
||||
itemCount = cipherViewList
|
||||
.count { !it.id.isNullOrBlank() && folderView.id == it.folderId },
|
||||
)
|
||||
},
|
||||
noFolderItems = cipherViewList
|
||||
.filter { it.folderId.isNullOrBlank() }
|
||||
.map { it.toVaultItem() },
|
||||
.mapNotNull { it.toVaultItemOrNull() },
|
||||
// TODO need to populate trash item count in BIT-969
|
||||
trashItemsCount = 0,
|
||||
)
|
||||
|
|
|
@ -69,7 +69,7 @@ class LocalDateTimeSerializerTest {
|
|||
json.parseToJsonElement(
|
||||
"""
|
||||
{
|
||||
"dataAsLocalDateTime": "2023-10-06T17:22:28.4400000Z"
|
||||
"dataAsLocalDateTime": "2023-10-06T17:22:28.440Z"
|
||||
}
|
||||
""",
|
||||
),
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
package com.x8bit.bitwarden.data.vault.datasource.network.model
|
||||
|
||||
import CipherJsonRequest
|
||||
import java.time.LocalDateTime
|
||||
|
||||
/**
|
||||
* Create a mock [CipherJsonRequest] with a given [number].
|
||||
*/
|
||||
fun createMockCipherJsonRequest(number: Int): CipherJsonRequest =
|
||||
CipherJsonRequest(
|
||||
organizationId = "mockOrganizationId-$number",
|
||||
folderId = "mockFolderId-$number",
|
||||
name = "mockName-$number",
|
||||
notes = "mockNotes-$number",
|
||||
type = CipherTypeJson.LOGIN,
|
||||
login = createMockLogin(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(),
|
||||
lastKnownRevisionDate = LocalDateTime.parse("2023-10-27T12:00:00"),
|
||||
)
|
|
@ -0,0 +1,123 @@
|
|||
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.CiphersApi
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipher
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipherJsonRequest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import retrofit2.create
|
||||
|
||||
class CiphersServiceTest : BaseServiceTest() {
|
||||
private val ciphersApi: CiphersApi = retrofit.create()
|
||||
|
||||
private val ciphersService: CiphersService = CiphersServiceImpl(
|
||||
ciphersApi = ciphersApi,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `createCipher should return the correct response`() = runTest {
|
||||
server.enqueue(MockResponse().setBody(CREATE_CIPHER_SUCCESS_JSON))
|
||||
val result = ciphersService.createCipher(
|
||||
body = createMockCipherJsonRequest(number = 1),
|
||||
)
|
||||
assertEquals(
|
||||
createMockCipher(number = 1),
|
||||
result.getOrThrow(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private const val CREATE_CIPHER_SUCCESS_JSON = """
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"key": "mockKey-1"
|
||||
}
|
||||
"""
|
|
@ -54,4 +54,34 @@ class VaultDataExtensionsTest {
|
|||
actual,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toViewState should not transform ciphers with no ID into ViewState items`() {
|
||||
val vaultData = VaultData(
|
||||
cipherViewList = listOf(createMockCipherView(number = 1).copy(id = null)),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
)
|
||||
|
||||
val actual = vaultData.toViewState()
|
||||
|
||||
assertEquals(
|
||||
VaultState.ViewState.Content(
|
||||
loginItemsCount = 0,
|
||||
cardItemsCount = 0,
|
||||
identityItemsCount = 0,
|
||||
secureNoteItemsCount = 0,
|
||||
favoriteItems = emptyList(),
|
||||
folderItems = listOf(
|
||||
VaultState.ViewState.FolderItem(
|
||||
id = "mockId-1",
|
||||
name = "mockName-1".asText(),
|
||||
itemCount = 0,
|
||||
),
|
||||
),
|
||||
noFolderItems = emptyList(),
|
||||
trashItemsCount = 0,
|
||||
),
|
||||
actual,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue