mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 10:48:47 +03:00
BIT-1559: Implement GET auth-requests (#752)
This commit is contained in:
parent
89fda64baa
commit
d66c3be2a6
16 changed files with 509 additions and 22 deletions
|
@ -0,0 +1,16 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
|
||||
import retrofit2.http.GET
|
||||
|
||||
/**
|
||||
* Defines raw calls under the /auth-requests API.
|
||||
*/
|
||||
interface AuthRequestsApi {
|
||||
|
||||
/**
|
||||
* Gets a list of auth requests for this device.
|
||||
*/
|
||||
@GET("/auth-requests")
|
||||
suspend fun getAuthRequests(): Result<AuthRequestsResponseJson>
|
||||
}
|
|
@ -2,6 +2,8 @@ package com.x8bit.bitwarden.data.auth.datasource.network.di
|
|||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsServiceImpl
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AuthRequestsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AuthRequestsServiceImpl
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesServiceImpl
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedService
|
||||
|
@ -35,6 +37,14 @@ object AuthNetworkModule {
|
|||
json = json,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesAuthRequestsService(
|
||||
retrofits: Retrofits,
|
||||
): AuthRequestsService = AuthRequestsServiceImpl(
|
||||
authRequestsApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesDevicesService(
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
/**
|
||||
* Response body for authentication requests used for Login with device.
|
||||
*
|
||||
* @property authRequests The list of auth requests.
|
||||
*/
|
||||
@Serializable
|
||||
data class AuthRequestsResponseJson(
|
||||
@SerialName("data") val authRequests: List<AuthRequest>,
|
||||
) {
|
||||
/**
|
||||
* Response body for an authentication request.
|
||||
*
|
||||
* @param id The id of this auth request.
|
||||
* @param publicKey The user's public key.
|
||||
* @param platform The platform from which this request was sent.
|
||||
* @param ipAddress The IP address of the device from which this request was sent.
|
||||
* @param key The key of this auth request.
|
||||
* @param masterPasswordHash The hash for this user's master password.
|
||||
* @param creationDate The date & time on which this request was created.
|
||||
* @param responseDate The date & time on which this request was responded to.
|
||||
* @param requestApproved Whether this request was approved.
|
||||
* @param originUrl The origin URL of this auth request.
|
||||
*/
|
||||
@Serializable
|
||||
data class AuthRequest(
|
||||
@SerialName("id")
|
||||
val id: String,
|
||||
|
||||
@SerialName("publicKey")
|
||||
val publicKey: String,
|
||||
|
||||
@SerialName("requestDeviceType")
|
||||
val platform: String,
|
||||
|
||||
@SerialName("requestIpAddress")
|
||||
val ipAddress: String,
|
||||
@SerialName("key")
|
||||
val key: String?,
|
||||
|
||||
@SerialName("masterPasswordHash")
|
||||
val masterPasswordHash: String?,
|
||||
|
||||
@SerialName("creationDate")
|
||||
@Contextual
|
||||
val creationDate: ZonedDateTime,
|
||||
|
||||
@SerialName("responseDate")
|
||||
@Contextual
|
||||
val responseDate: ZonedDateTime?,
|
||||
|
||||
@SerialName("requestApproved")
|
||||
val requestApproved: Boolean?,
|
||||
|
||||
@SerialName("origin")
|
||||
val originUrl: String,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
|
||||
|
||||
/**
|
||||
* Provides an API for interacting with login approval / authentication requests.
|
||||
*/
|
||||
interface AuthRequestsService {
|
||||
/**
|
||||
* Gets the list of auth requests for the current user.
|
||||
*/
|
||||
suspend fun getAuthRequests(): Result<AuthRequestsResponseJson>
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthRequestsApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
|
||||
|
||||
class AuthRequestsServiceImpl(
|
||||
private val authRequestsApi: AuthRequestsApi,
|
||||
) : AuthRequestsService {
|
||||
|
||||
override suspend fun getAuthRequests(): Result<AuthRequestsResponseJson> =
|
||||
authRequestsApi.getAuthRequests()
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
package com.x8bit.bitwarden.data.auth.repository
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequest
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestsResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
|
||||
|
@ -123,6 +125,11 @@ interface AuthRepository : AuthenticatorProvider {
|
|||
*/
|
||||
fun setSsoCallbackResult(result: SsoCallbackResult)
|
||||
|
||||
/**
|
||||
* Get a list of the current user's [AuthRequest]s.
|
||||
*/
|
||||
suspend fun getAuthRequests(): AuthRequestsResult
|
||||
|
||||
/**
|
||||
* Get a [Boolean] indicating whether this is a known device.
|
||||
*/
|
||||
|
|
|
@ -12,12 +12,15 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenRespon
|
|||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AuthRequestsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toKdfTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequest
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestsResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
|
||||
|
@ -65,6 +68,7 @@ import javax.inject.Singleton
|
|||
@Singleton
|
||||
class AuthRepositoryImpl(
|
||||
private val accountsService: AccountsService,
|
||||
private val authRequestsService: AuthRequestsService,
|
||||
private val devicesService: DevicesService,
|
||||
private val haveIBeenPwnedService: HaveIBeenPwnedService,
|
||||
private val identityService: IdentityService,
|
||||
|
@ -426,6 +430,30 @@ class AuthRepositoryImpl(
|
|||
mutableSsoCallbackResultFlow.tryEmit(result)
|
||||
}
|
||||
|
||||
override suspend fun getAuthRequests(): AuthRequestsResult =
|
||||
authRequestsService.getAuthRequests()
|
||||
.fold(
|
||||
onFailure = { AuthRequestsResult.Error },
|
||||
onSuccess = { response ->
|
||||
AuthRequestsResult.Success(
|
||||
authRequests = response.authRequests.map { request ->
|
||||
AuthRequest(
|
||||
id = request.id,
|
||||
publicKey = request.publicKey,
|
||||
platform = request.platform,
|
||||
ipAddress = request.ipAddress,
|
||||
key = request.key,
|
||||
masterPasswordHash = request.masterPasswordHash,
|
||||
creationDate = request.creationDate,
|
||||
responseDate = request.responseDate,
|
||||
requestApproved = request.requestApproved ?: false,
|
||||
originUrl = request.originUrl,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
override suspend fun getIsKnownDevice(emailAddress: String): KnownDeviceResult =
|
||||
devicesService
|
||||
.getIsKnownDevice(
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.repository.di
|
|||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AuthRequestsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
|
||||
|
@ -31,6 +32,7 @@ object AuthRepositoryModule {
|
|||
@Suppress("LongParameterList")
|
||||
fun providesAuthRepository(
|
||||
accountsService: AccountsService,
|
||||
authRequestsService: AuthRequestsService,
|
||||
devicesService: DevicesService,
|
||||
identityService: IdentityService,
|
||||
haveIBeenPwnedService: HaveIBeenPwnedService,
|
||||
|
@ -43,6 +45,7 @@ object AuthRepositoryModule {
|
|||
userLogoutManager: UserLogoutManager,
|
||||
): AuthRepository = AuthRepositoryImpl(
|
||||
accountsService = accountsService,
|
||||
authRequestsService = authRequestsService,
|
||||
devicesService = devicesService,
|
||||
identityService = identityService,
|
||||
authSdkSource = authSdkSource,
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
/**
|
||||
* Represents a Login Approval request.
|
||||
*
|
||||
* @param id The id of this request.
|
||||
* @param publicKey The user's public key.
|
||||
* @param platform The platform from which this request was sent.
|
||||
* @param ipAddress The IP address of the device from which this request was sent.
|
||||
* @param key The key of this request.
|
||||
* @param masterPasswordHash The hash for this user's master password.
|
||||
* @param creationDate The date & time on which this request was created.
|
||||
* @param responseDate The date & time on which this request was responded to.
|
||||
* @param requestApproved Whether this request was approved.
|
||||
* @param originUrl The origin URL of this auth request.
|
||||
*/
|
||||
data class AuthRequest(
|
||||
val id: String,
|
||||
val publicKey: String,
|
||||
val platform: String,
|
||||
val ipAddress: String,
|
||||
val key: String?,
|
||||
val masterPasswordHash: String?,
|
||||
val creationDate: ZonedDateTime,
|
||||
val responseDate: ZonedDateTime?,
|
||||
val requestApproved: Boolean,
|
||||
val originUrl: String,
|
||||
)
|
|
@ -0,0 +1,18 @@
|
|||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
/**
|
||||
* Models result of getting the list of login approval requests for the current user.
|
||||
*/
|
||||
sealed class AuthRequestsResult {
|
||||
/**
|
||||
* Models the result of getting a user's auth requests.
|
||||
*/
|
||||
data class Success(
|
||||
val authRequests: List<AuthRequest>,
|
||||
) : AuthRequestsResult()
|
||||
|
||||
/**
|
||||
* There was an error getting the user's auth requests.
|
||||
*/
|
||||
data object Error : AuthRequestsResult()
|
||||
}
|
|
@ -105,14 +105,14 @@ fun PendingRequestsScreen(
|
|||
.fillMaxSize(),
|
||||
)
|
||||
|
||||
is PendingRequestsState.ViewState.Error -> BitwardenErrorContent(
|
||||
message = viewState.message.toString(resources),
|
||||
PendingRequestsState.ViewState.Error -> BitwardenErrorContent(
|
||||
message = stringResource(R.string.generic_error_message),
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize(),
|
||||
)
|
||||
|
||||
is PendingRequestsState.ViewState.Loading -> BitwardenLoadingContent(
|
||||
PendingRequestsState.ViewState.Loading -> BitwardenLoadingContent(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize(),
|
||||
|
|
|
@ -2,11 +2,18 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.pending
|
|||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestsResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.TimeZone
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val KEY_STATE = "state"
|
||||
|
@ -16,20 +23,36 @@ private const val KEY_STATE = "state"
|
|||
*/
|
||||
@HiltViewModel
|
||||
class PendingRequestsViewModel @Inject constructor(
|
||||
authRepository: AuthRepository,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<PendingRequestsState, PendingRequestsEvent, PendingRequestsAction>(
|
||||
initialState = savedStateHandle[KEY_STATE] ?: PendingRequestsState(
|
||||
viewState = PendingRequestsState.ViewState.Empty,
|
||||
viewState = PendingRequestsState.ViewState.Loading,
|
||||
),
|
||||
) {
|
||||
private val dateTimeFormatter
|
||||
get() = DateTimeFormatter
|
||||
.ofPattern("M/d/yy hh:mm a")
|
||||
.withZone(TimeZone.getDefault().toZoneId())
|
||||
|
||||
init {
|
||||
// TODO BIT-1291: make /auth-requests call
|
||||
viewModelScope.launch {
|
||||
trySendAction(
|
||||
PendingRequestsAction.Internal.AuthRequestsResultReceive(
|
||||
authRequestsResult = authRepository.getAuthRequests(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleAction(action: PendingRequestsAction) {
|
||||
when (action) {
|
||||
PendingRequestsAction.CloseClick -> handleCloseClicked()
|
||||
PendingRequestsAction.DeclineAllRequestsClick -> handleDeclineAllRequestsClicked()
|
||||
|
||||
is PendingRequestsAction.Internal.AuthRequestsResultReceive -> {
|
||||
handleAuthRequestsResultReceived(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,6 +63,36 @@ class PendingRequestsViewModel @Inject constructor(
|
|||
private fun handleDeclineAllRequestsClicked() {
|
||||
sendEvent(PendingRequestsEvent.ShowToast("Not yet implemented.".asText()))
|
||||
}
|
||||
|
||||
private fun handleAuthRequestsResultReceived(
|
||||
action: PendingRequestsAction.Internal.AuthRequestsResultReceive,
|
||||
) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = when (val result = action.authRequestsResult) {
|
||||
is AuthRequestsResult.Success -> {
|
||||
if (result.authRequests.isEmpty()) {
|
||||
PendingRequestsState.ViewState.Empty
|
||||
} else {
|
||||
PendingRequestsState.ViewState.Content(
|
||||
requests = result.authRequests.map { authRequest ->
|
||||
PendingRequestsState.ViewState.Content.PendingLoginRequest(
|
||||
fingerprintPhrase = authRequest.publicKey,
|
||||
platform = authRequest.platform,
|
||||
timestamp = dateTimeFormatter.format(
|
||||
authRequest.creationDate,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AuthRequestsResult.Error -> PendingRequestsState.ViewState.Error
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -81,13 +134,9 @@ data class PendingRequestsState(
|
|||
/**
|
||||
* Represents a state where the [PendingRequestsScreen] is unable to display data due to an
|
||||
* error retrieving it.
|
||||
*
|
||||
* @property message The message to display on the error screen.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Error(
|
||||
val message: Text,
|
||||
) : ViewState()
|
||||
data object Error : ViewState()
|
||||
|
||||
/**
|
||||
* Loading state for the [PendingRequestsScreen], signifying that the content is being
|
||||
|
@ -129,4 +178,16 @@ sealed class PendingRequestsAction {
|
|||
* The user has clicked to deny all login requests.
|
||||
*/
|
||||
data object DeclineAllRequestsClick : PendingRequestsAction()
|
||||
|
||||
/**
|
||||
* Models actions sent by the view model itself.
|
||||
*/
|
||||
sealed class Internal : PendingRequestsAction() {
|
||||
/**
|
||||
* Indicates that a new auth requests result has been received.
|
||||
*/
|
||||
data class AuthRequestsResultReceive(
|
||||
val authRequestsResult: AuthRequestsResult,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthRequestsApi
|
||||
import com.x8bit.bitwarden.data.platform.base.BaseServiceTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import retrofit2.create
|
||||
|
||||
class AuthRequestsServiceTest : BaseServiceTest() {
|
||||
|
||||
private val authRequestsApi: AuthRequestsApi = retrofit.create()
|
||||
private val service = AuthRequestsServiceImpl(
|
||||
authRequestsApi = authRequestsApi,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `getAuthRequests when request response is Failure should return Failure`() = runTest {
|
||||
val response = MockResponse().setResponseCode(400)
|
||||
server.enqueue(response)
|
||||
val actual = service.getAuthRequests()
|
||||
assertTrue(actual.isFailure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getAuthRequests when request response is Success should return Success`() = runTest {
|
||||
val json = """
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "1",
|
||||
"publicKey": "2",
|
||||
"requestDeviceType": "Android",
|
||||
"requestIpAddress": "1.0.0.1",
|
||||
"creationDate": "2024-09-13T01:00:00.00Z",
|
||||
"requestApproved": true,
|
||||
"origin": "www.bitwarden.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
val response = MockResponse().setBody(json).setResponseCode(200)
|
||||
server.enqueue(response)
|
||||
val actual = service.getAuthRequests()
|
||||
assertTrue(actual.isSuccess)
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
|||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
|
||||
|
@ -18,6 +19,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenRespon
|
|||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AuthRequestsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
|
||||
|
@ -28,6 +30,8 @@ import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL
|
|||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_3
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_4
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequest
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestsResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
|
||||
|
@ -58,7 +62,6 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganiz
|
|||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultState
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
|
@ -77,12 +80,14 @@ import org.junit.jupiter.api.Assertions.assertNull
|
|||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class AuthRepositoryTest {
|
||||
|
||||
private val dispatcherManager: DispatcherManager = FakeDispatcherManager()
|
||||
private val accountsService: AccountsService = mockk()
|
||||
private val authRequestsService: AuthRequestsService = mockk()
|
||||
private val devicesService: DevicesService = mockk()
|
||||
private val identityService: IdentityService = mockk()
|
||||
private val haveIBeenPwnedService: HaveIBeenPwnedService = mockk()
|
||||
|
@ -135,6 +140,7 @@ class AuthRepositoryTest {
|
|||
|
||||
private val repository = AuthRepositoryImpl(
|
||||
accountsService = accountsService,
|
||||
authRequestsService = authRequestsService,
|
||||
devicesService = devicesService,
|
||||
identityService = identityService,
|
||||
haveIBeenPwnedService = haveIBeenPwnedService,
|
||||
|
@ -150,7 +156,6 @@ class AuthRepositoryTest {
|
|||
|
||||
@BeforeEach
|
||||
fun beforeEach() {
|
||||
clearMocks(identityService, accountsService, haveIBeenPwnedService)
|
||||
mockkStatic(
|
||||
GetTokenResponseJson.Success::toUserState,
|
||||
RefreshTokenResponseJson::toUserStateJson,
|
||||
|
@ -1235,6 +1240,66 @@ class AuthRepositoryTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getAuthRequests should return failure when service returns failure`() = runTest {
|
||||
coEvery {
|
||||
authRequestsService.getAuthRequests()
|
||||
} returns Throwable("Fail").asFailure()
|
||||
|
||||
val result = repository.getAuthRequests()
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
authRequestsService.getAuthRequests()
|
||||
}
|
||||
assertEquals(AuthRequestsResult.Error, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getAuthRequests should return success when service returns success`() = runTest {
|
||||
val responseJson = AuthRequestsResponseJson(
|
||||
authRequests = listOf(
|
||||
AuthRequestsResponseJson.AuthRequest(
|
||||
id = "1",
|
||||
publicKey = "2",
|
||||
platform = "Android",
|
||||
ipAddress = "192.168.0.1",
|
||||
key = "public",
|
||||
masterPasswordHash = "verySecureHash",
|
||||
creationDate = ZonedDateTime.parse("2024-09-13T00:00Z"),
|
||||
responseDate = null,
|
||||
requestApproved = true,
|
||||
originUrl = "www.bitwarden.com",
|
||||
),
|
||||
),
|
||||
)
|
||||
val expected = AuthRequestsResult.Success(
|
||||
authRequests = listOf(
|
||||
AuthRequest(
|
||||
id = "1",
|
||||
publicKey = "2",
|
||||
platform = "Android",
|
||||
ipAddress = "192.168.0.1",
|
||||
key = "public",
|
||||
masterPasswordHash = "verySecureHash",
|
||||
creationDate = ZonedDateTime.parse("2024-09-13T00:00Z"),
|
||||
responseDate = null,
|
||||
requestApproved = true,
|
||||
originUrl = "www.bitwarden.com",
|
||||
),
|
||||
),
|
||||
)
|
||||
coEvery {
|
||||
authRequestsService.getAuthRequests()
|
||||
} returns responseJson.asSuccess()
|
||||
|
||||
val result = repository.getAuthRequests()
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
authRequestsService.getAuthRequests()
|
||||
}
|
||||
assertEquals(expected, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getIsKnownDevice should return failure when service returns failure`() = runTest {
|
||||
coEvery {
|
||||
|
|
|
@ -69,7 +69,7 @@ class PendingRequestsScreenTest : BaseComposeTest() {
|
|||
|
||||
companion object {
|
||||
val DEFAULT_STATE: PendingRequestsState = PendingRequestsState(
|
||||
viewState = PendingRequestsState.ViewState.Empty,
|
||||
viewState = PendingRequestsState.ViewState.Loading,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,31 +2,137 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.pending
|
|||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequest
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestsResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.TimeZone
|
||||
|
||||
class PendingRequestsViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct when not set`() {
|
||||
val viewModel = createViewModel(state = null)
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
private val authRepository = mockk<AuthRepository>()
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
// Setting the timezone so the tests pass consistently no matter the environment.
|
||||
TimeZone.setDefault(TimeZone.getTimeZone("UTC"))
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
// Clearing the timezone after the test.
|
||||
TimeZone.setDefault(null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct when set`() {
|
||||
val state = DEFAULT_STATE.copy(
|
||||
viewState = PendingRequestsState.ViewState.Loading,
|
||||
fun `initial state should be correct and trigger a getAuthRequests call`() {
|
||||
coEvery {
|
||||
authRepository.getAuthRequests()
|
||||
} returns AuthRequestsResult.Success(
|
||||
authRequests = emptyList(),
|
||||
)
|
||||
val viewModel = createViewModel(state = state)
|
||||
assertEquals(state, viewModel.stateFlow.value)
|
||||
val viewModel = createViewModel(state = null)
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
coVerify {
|
||||
authRepository.getAuthRequests()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPendingResults success with content should update state`() {
|
||||
coEvery {
|
||||
authRepository.getAuthRequests()
|
||||
} returns AuthRequestsResult.Success(
|
||||
authRequests = listOf(
|
||||
AuthRequest(
|
||||
id = "1",
|
||||
publicKey = "pantry-overdue-survive-sleep-jab",
|
||||
platform = "Android",
|
||||
ipAddress = "192.168.0.1",
|
||||
key = "publicKey",
|
||||
masterPasswordHash = "verySecureHash",
|
||||
creationDate = ZonedDateTime.parse("2023-08-24T17:11Z"),
|
||||
responseDate = null,
|
||||
requestApproved = true,
|
||||
originUrl = "www.bitwarden.com",
|
||||
),
|
||||
AuthRequest(
|
||||
id = "2",
|
||||
publicKey = "erupt-anew-matchbook-disk-student",
|
||||
platform = "iOS",
|
||||
ipAddress = "192.168.0.2",
|
||||
key = "publicKey",
|
||||
masterPasswordHash = "verySecureHash",
|
||||
creationDate = ZonedDateTime.parse("2023-08-21T15:43Z"),
|
||||
responseDate = null,
|
||||
requestApproved = false,
|
||||
originUrl = "www.bitwarden.com",
|
||||
),
|
||||
),
|
||||
)
|
||||
val expected = DEFAULT_STATE.copy(
|
||||
viewState = PendingRequestsState.ViewState.Content(
|
||||
requests = listOf(
|
||||
PendingRequestsState.ViewState.Content.PendingLoginRequest(
|
||||
fingerprintPhrase = "pantry-overdue-survive-sleep-jab",
|
||||
platform = "Android",
|
||||
timestamp = "8/24/23 05:11 PM",
|
||||
),
|
||||
PendingRequestsState.ViewState.Content.PendingLoginRequest(
|
||||
fingerprintPhrase = "erupt-anew-matchbook-disk-student",
|
||||
platform = "iOS",
|
||||
timestamp = "8/21/23 03:43 PM",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(expected, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPendingResults success with empty list should update state`() {
|
||||
coEvery {
|
||||
authRepository.getAuthRequests()
|
||||
} returns AuthRequestsResult.Success(
|
||||
authRequests = emptyList(),
|
||||
)
|
||||
val expected = DEFAULT_STATE.copy(
|
||||
viewState = PendingRequestsState.ViewState.Empty,
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(expected, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPendingResults failure with error should update state`() {
|
||||
coEvery {
|
||||
authRepository.getAuthRequests()
|
||||
} returns AuthRequestsResult.Error
|
||||
val expected = DEFAULT_STATE.copy(
|
||||
viewState = PendingRequestsState.ViewState.Error,
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(expected, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on CloseClick should emit NavigateBack`() = runTest {
|
||||
coEvery {
|
||||
authRepository.getAuthRequests()
|
||||
} returns AuthRequestsResult.Success(
|
||||
authRequests = emptyList(),
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(PendingRequestsAction.CloseClick)
|
||||
|
@ -36,6 +142,11 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Test
|
||||
fun `on DeclineAllRequestsClick should send ShowToast event`() = runTest {
|
||||
coEvery {
|
||||
authRepository.getAuthRequests()
|
||||
} returns AuthRequestsResult.Success(
|
||||
authRequests = emptyList(),
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(PendingRequestsAction.DeclineAllRequestsClick)
|
||||
|
@ -50,6 +161,7 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
|
|||
private fun createViewModel(
|
||||
state: PendingRequestsState? = DEFAULT_STATE,
|
||||
): PendingRequestsViewModel = PendingRequestsViewModel(
|
||||
authRepository = authRepository,
|
||||
savedStateHandle = SavedStateHandle().apply { set("state", state) },
|
||||
)
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue