mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 18:58:59 +03:00
Update LoginWithDeviceScreen to support Admin Approval type (#1175)
This commit is contained in:
parent
1150f01666
commit
b2005f01c1
22 changed files with 675 additions and 144 deletions
|
@ -1,9 +1,12 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestUpdateRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.Path
|
||||
|
||||
|
@ -12,6 +15,15 @@ import retrofit2.http.Path
|
|||
*/
|
||||
interface AuthenticatedAuthRequestsApi {
|
||||
|
||||
/**
|
||||
* Notifies the server of a new admin authentication request.
|
||||
*/
|
||||
@POST("/auth-requests/admin-request")
|
||||
suspend fun createAdminAuthRequest(
|
||||
@Header("Device-Identifier") deviceIdentifier: String,
|
||||
@Body body: AuthRequestRequestJson,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest>
|
||||
|
||||
/**
|
||||
* Updates an authentication request.
|
||||
*/
|
||||
|
|
|
@ -85,6 +85,7 @@ object AuthNetworkModule {
|
|||
fun providesNewAuthRequestService(
|
||||
retrofits: Retrofits,
|
||||
): NewAuthRequestService = NewAuthRequestServiceImpl(
|
||||
authenticatedAuthRequestsApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
unauthenticatedAuthRequestsApi = retrofits.unauthenticatedApiRetrofit.create(),
|
||||
)
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
|
||||
|
||||
/**
|
||||
|
@ -9,12 +10,14 @@ interface NewAuthRequestService {
|
|||
/**
|
||||
* Informs the server of a new auth request in order to notify approving devices.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
suspend fun createAuthRequest(
|
||||
email: String,
|
||||
publicKey: String,
|
||||
deviceId: String,
|
||||
accessCode: String,
|
||||
fingerprint: String,
|
||||
authRequestType: AuthRequestTypeJson,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest>
|
||||
|
||||
/**
|
||||
|
@ -23,5 +26,6 @@ interface NewAuthRequestService {
|
|||
suspend fun getAuthRequestUpdate(
|
||||
requestId: String,
|
||||
accessCode: String,
|
||||
isSso: Boolean,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest>
|
||||
}
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthenticatedAuthRequestsApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedAuthRequestsApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
|
||||
/**
|
||||
* The default implementation of the [NewAuthRequestService].
|
||||
*/
|
||||
class NewAuthRequestServiceImpl(
|
||||
private val authenticatedAuthRequestsApi: AuthenticatedAuthRequestsApi,
|
||||
private val unauthenticatedAuthRequestsApi: UnauthenticatedAuthRequestsApi,
|
||||
) : NewAuthRequestService {
|
||||
override suspend fun createAuthRequest(
|
||||
|
@ -17,25 +20,54 @@ class NewAuthRequestServiceImpl(
|
|||
deviceId: String,
|
||||
accessCode: String,
|
||||
fingerprint: String,
|
||||
authRequestType: AuthRequestTypeJson,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest> =
|
||||
unauthenticatedAuthRequestsApi.createAuthRequest(
|
||||
deviceIdentifier = deviceId,
|
||||
body = AuthRequestRequestJson(
|
||||
email = email,
|
||||
publicKey = publicKey,
|
||||
deviceId = deviceId,
|
||||
accessCode = accessCode,
|
||||
fingerprint = fingerprint,
|
||||
type = AuthRequestTypeJson.LOGIN_WITH_DEVICE,
|
||||
),
|
||||
)
|
||||
when (authRequestType) {
|
||||
AuthRequestTypeJson.LOGIN_WITH_DEVICE -> {
|
||||
unauthenticatedAuthRequestsApi.createAuthRequest(
|
||||
deviceIdentifier = deviceId,
|
||||
body = AuthRequestRequestJson(
|
||||
email = email,
|
||||
publicKey = publicKey,
|
||||
deviceId = deviceId,
|
||||
accessCode = accessCode,
|
||||
fingerprint = fingerprint,
|
||||
type = authRequestType,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
AuthRequestTypeJson.UNLOCK -> {
|
||||
UnsupportedOperationException("Unlock AuthRequestType is currently unsupported")
|
||||
.asFailure()
|
||||
}
|
||||
|
||||
AuthRequestTypeJson.ADMIN_APPROVAL -> {
|
||||
authenticatedAuthRequestsApi.createAdminAuthRequest(
|
||||
deviceIdentifier = deviceId,
|
||||
body = AuthRequestRequestJson(
|
||||
email = email,
|
||||
publicKey = publicKey,
|
||||
deviceId = deviceId,
|
||||
accessCode = accessCode,
|
||||
fingerprint = fingerprint,
|
||||
type = authRequestType,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAuthRequestUpdate(
|
||||
requestId: String,
|
||||
accessCode: String,
|
||||
isSso: Boolean,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest> =
|
||||
unauthenticatedAuthRequestsApi.getAuthRequestUpdate(
|
||||
requestId = requestId,
|
||||
accessCode = accessCode,
|
||||
)
|
||||
if (isSso) {
|
||||
authenticatedAuthRequestsApi.getAuthRequest(requestId)
|
||||
} else {
|
||||
unauthenticatedAuthRequestsApi.getAuthRequestUpdate(
|
||||
requestId = requestId,
|
||||
accessCode = accessCode,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.manager
|
|||
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequest
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestResult
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestType
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestUpdatesResult
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestsResult
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestsUpdatesResult
|
||||
|
@ -15,7 +16,10 @@ interface AuthRequestManager {
|
|||
/**
|
||||
* Creates a new authentication request and then continues to emit updates over time.
|
||||
*/
|
||||
fun createAuthRequestWithUpdates(email: String): Flow<CreateAuthRequestResult>
|
||||
fun createAuthRequestWithUpdates(
|
||||
email: String,
|
||||
authRequestType: AuthRequestType,
|
||||
): Flow<CreateAuthRequestResult>
|
||||
|
||||
/**
|
||||
* Get an auth request by its [fingerprint] and emits updates for that request.
|
||||
|
|
|
@ -2,15 +2,19 @@ package com.x8bit.bitwarden.data.auth.manager
|
|||
|
||||
import com.bitwarden.core.AuthRequestResponse
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AuthRequestsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.NewAuthRequestService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequest
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestResult
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestType
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestUpdatesResult
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestsResult
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestsUpdatesResult
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.CreateAuthRequestResult
|
||||
import com.x8bit.bitwarden.data.auth.manager.util.isSso
|
||||
import com.x8bit.bitwarden.data.auth.manager.util.toAuthRequestTypeJson
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
|
@ -57,11 +61,17 @@ class AuthRequestManagerImpl(
|
|||
@Suppress("LongMethod")
|
||||
override fun createAuthRequestWithUpdates(
|
||||
email: String,
|
||||
authRequestType: AuthRequestType,
|
||||
): Flow<CreateAuthRequestResult> = flow {
|
||||
val initialResult = createNewAuthRequest(email).getOrNull() ?: run {
|
||||
emit(CreateAuthRequestResult.Error)
|
||||
return@flow
|
||||
}
|
||||
val initialResult = createNewAuthRequest(
|
||||
email = email,
|
||||
authRequestType = authRequestType.toAuthRequestTypeJson(),
|
||||
)
|
||||
.getOrNull()
|
||||
?: run {
|
||||
emit(CreateAuthRequestResult.Error)
|
||||
return@flow
|
||||
}
|
||||
val authRequestResponse = initialResult.authRequestResponse
|
||||
var authRequest = initialResult.authRequest
|
||||
emit(CreateAuthRequestResult.Update(authRequest))
|
||||
|
@ -73,6 +83,7 @@ class AuthRequestManagerImpl(
|
|||
.getAuthRequestUpdate(
|
||||
requestId = authRequest.id,
|
||||
accessCode = authRequestResponse.accessCode,
|
||||
isSso = authRequestType.isSso,
|
||||
)
|
||||
.map { request ->
|
||||
AuthRequest(
|
||||
|
@ -310,6 +321,7 @@ class AuthRequestManagerImpl(
|
|||
*/
|
||||
private suspend fun createNewAuthRequest(
|
||||
email: String,
|
||||
authRequestType: AuthRequestTypeJson,
|
||||
): Result<NewAuthRequestData> =
|
||||
authSdkSource
|
||||
.getNewAuthRequest(email)
|
||||
|
@ -321,6 +333,7 @@ class AuthRequestManagerImpl(
|
|||
deviceId = authDiskSource.uniqueAppId,
|
||||
accessCode = authRequestResponse.accessCode,
|
||||
fingerprint = authRequestResponse.fingerprint,
|
||||
authRequestType = authRequestType,
|
||||
)
|
||||
.map { request ->
|
||||
AuthRequest(
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
package com.x8bit.bitwarden.data.auth.manager.model
|
||||
|
||||
/**
|
||||
* Represents the type of request to be made when making auth requests.
|
||||
*/
|
||||
enum class AuthRequestType {
|
||||
OTHER_DEVICE,
|
||||
SSO_OTHER_DEVICE,
|
||||
SSO_ADMIN_APPROVAL,
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package com.x8bit.bitwarden.data.auth.manager.util
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestType
|
||||
|
||||
/**
|
||||
* Indicates if the given [AuthRequestType] uses SSO authentication.
|
||||
*/
|
||||
val AuthRequestType.isSso: Boolean
|
||||
get() = when (this) {
|
||||
AuthRequestType.OTHER_DEVICE -> false
|
||||
AuthRequestType.SSO_OTHER_DEVICE,
|
||||
AuthRequestType.SSO_ADMIN_APPROVAL,
|
||||
-> true
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the [AuthRequestType] to the appropriate [AuthRequestTypeJson].
|
||||
*/
|
||||
fun AuthRequestType.toAuthRequestTypeJson(): AuthRequestTypeJson =
|
||||
when (this) {
|
||||
AuthRequestType.OTHER_DEVICE,
|
||||
AuthRequestType.SSO_OTHER_DEVICE,
|
||||
-> AuthRequestTypeJson.LOGIN_WITH_DEVICE
|
||||
|
||||
AuthRequestType.SSO_ADMIN_APPROVAL -> AuthRequestTypeJson.ADMIN_APPROVAL
|
||||
}
|
|
@ -17,6 +17,7 @@ import com.x8bit.bitwarden.ui.auth.feature.landing.landingDestination
|
|||
import com.x8bit.bitwarden.ui.auth.feature.login.loginDestination
|
||||
import com.x8bit.bitwarden.ui.auth.feature.login.navigateToLogin
|
||||
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.loginWithDeviceDestination
|
||||
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.model.LoginWithDeviceType
|
||||
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.navigateToLoginWithDevice
|
||||
import com.x8bit.bitwarden.ui.auth.feature.masterpasswordhint.masterPasswordHintDestination
|
||||
import com.x8bit.bitwarden.ui.auth.feature.masterpasswordhint.navigateToMasterPasswordHint
|
||||
|
@ -87,6 +88,7 @@ fun NavGraphBuilder.authGraph(navController: NavHostController) {
|
|||
onNavigateToLoginWithDevice = { emailAddress ->
|
||||
navController.navigateToLoginWithDevice(
|
||||
emailAddress = emailAddress,
|
||||
loginType = LoginWithDeviceType.OTHER_DEVICE,
|
||||
)
|
||||
},
|
||||
onNavigateToTwoFactorLogin = { emailAddress, password ->
|
||||
|
|
|
@ -4,20 +4,29 @@ import androidx.lifecycle.SavedStateHandle
|
|||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.navArgument
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.model.LoginWithDeviceType
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||
|
||||
private const val EMAIL_ADDRESS: String = "email_address"
|
||||
private const val LOGIN_WITH_DEVICE_PREFIX = "login_with_device"
|
||||
private const val LOGIN_WITH_DEVICE_ROUTE = "$LOGIN_WITH_DEVICE_PREFIX/{$EMAIL_ADDRESS}"
|
||||
private const val LOGIN_TYPE: String = "login_type"
|
||||
private const val LOGIN_WITH_DEVICE_ROUTE =
|
||||
"$LOGIN_WITH_DEVICE_PREFIX/{$EMAIL_ADDRESS}/{$LOGIN_TYPE}"
|
||||
|
||||
/**
|
||||
* Class to retrieve login with device arguments from the [SavedStateHandle].
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
data class LoginWithDeviceArgs(val emailAddress: String) {
|
||||
data class LoginWithDeviceArgs(
|
||||
val emailAddress: String,
|
||||
val loginType: LoginWithDeviceType,
|
||||
) {
|
||||
constructor(savedStateHandle: SavedStateHandle) : this(
|
||||
checkNotNull(savedStateHandle[EMAIL_ADDRESS]) as String,
|
||||
emailAddress = checkNotNull(savedStateHandle.get<String>(EMAIL_ADDRESS)),
|
||||
loginType = checkNotNull(savedStateHandle.get<LoginWithDeviceType>(LOGIN_TYPE)),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -26,9 +35,13 @@ data class LoginWithDeviceArgs(val emailAddress: String) {
|
|||
*/
|
||||
fun NavController.navigateToLoginWithDevice(
|
||||
emailAddress: String,
|
||||
loginType: LoginWithDeviceType,
|
||||
navOptions: NavOptions? = null,
|
||||
) {
|
||||
this.navigate("$LOGIN_WITH_DEVICE_PREFIX/$emailAddress", navOptions)
|
||||
this.navigate(
|
||||
route = "$LOGIN_WITH_DEVICE_PREFIX/$emailAddress/$loginType",
|
||||
navOptions = navOptions,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -40,6 +53,10 @@ fun NavGraphBuilder.loginWithDeviceDestination(
|
|||
) {
|
||||
composableWithSlideTransitions(
|
||||
route = LOGIN_WITH_DEVICE_ROUTE,
|
||||
arguments = listOf(
|
||||
navArgument(EMAIL_ADDRESS) { type = NavType.StringType },
|
||||
navArgument(LOGIN_TYPE) { type = NavType.EnumType(LoginWithDeviceType::class.java) },
|
||||
),
|
||||
) {
|
||||
LoginWithDeviceScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
|
|
|
@ -96,7 +96,7 @@ fun LoginWithDeviceScreen(
|
|||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = R.string.log_in_with_device),
|
||||
title = state.toolbarTitle(),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = painterResource(id = R.drawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
|
@ -144,7 +144,7 @@ private fun LoginWithDeviceScreenContent(
|
|||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.log_in_initiated),
|
||||
text = state.title(),
|
||||
textAlign = TextAlign.Start,
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
|
@ -156,7 +156,7 @@ private fun LoginWithDeviceScreenContent(
|
|||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.a_notification_has_been_sent_to_your_device),
|
||||
text = state.subtitle(),
|
||||
textAlign = TextAlign.Start,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
|
@ -167,9 +167,8 @@ private fun LoginWithDeviceScreenContent(
|
|||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
Text(
|
||||
text = stringResource(id = R.string.please_make_sure_your_vault_is_unlocked_and_the_fingerprint_phrase_matches_on_the_other_device),
|
||||
text = state.description(),
|
||||
textAlign = TextAlign.Start,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
|
@ -204,33 +203,35 @@ private fun LoginWithDeviceScreenContent(
|
|||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minHeight = 40.dp)
|
||||
.align(Alignment.Start),
|
||||
) {
|
||||
if (state.isResendNotificationLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 64.dp)
|
||||
.size(size = 16.dp),
|
||||
)
|
||||
} else {
|
||||
BitwardenClickableText(
|
||||
modifier = Modifier.semantics { testTag = "ResendNotificationButton" },
|
||||
label = stringResource(id = R.string.resend_notification),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
innerPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp),
|
||||
onClick = onResendNotificationClick,
|
||||
)
|
||||
if (state.allowsResend) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minHeight = 40.dp)
|
||||
.align(Alignment.Start),
|
||||
) {
|
||||
if (state.isResendNotificationLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 64.dp)
|
||||
.size(size = 16.dp),
|
||||
)
|
||||
} else {
|
||||
BitwardenClickableText(
|
||||
modifier = Modifier.semantics { testTag = "ResendNotificationButton" },
|
||||
label = stringResource(id = R.string.resend_notification),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
innerPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp),
|
||||
onClick = onResendNotificationClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(28.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.need_another_option),
|
||||
text = state.otherOptions(),
|
||||
textAlign = TextAlign.Start,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
|
|
|
@ -10,6 +10,8 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
|
||||
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.model.LoginWithDeviceType
|
||||
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.util.toAuthRequestType
|
||||
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
|
||||
|
@ -35,12 +37,16 @@ class LoginWithDeviceViewModel @Inject constructor(
|
|||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<LoginWithDeviceState, LoginWithDeviceEvent, LoginWithDeviceAction>(
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: LoginWithDeviceState(
|
||||
emailAddress = LoginWithDeviceArgs(savedStateHandle).emailAddress,
|
||||
viewState = LoginWithDeviceState.ViewState.Loading,
|
||||
dialogState = null,
|
||||
loginData = null,
|
||||
),
|
||||
?: run {
|
||||
val args = LoginWithDeviceArgs(savedStateHandle)
|
||||
LoginWithDeviceState(
|
||||
loginWithDeviceType = args.loginType,
|
||||
emailAddress = args.emailAddress,
|
||||
viewState = LoginWithDeviceState.ViewState.Loading,
|
||||
dialogState = null,
|
||||
loginData = null,
|
||||
)
|
||||
},
|
||||
) {
|
||||
private var authJob: Job = Job().apply { complete() }
|
||||
|
||||
|
@ -104,6 +110,7 @@ class LoginWithDeviceViewModel @Inject constructor(
|
|||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = LoginWithDeviceState.ViewState.Content(
|
||||
loginWithDeviceType = it.loginWithDeviceType,
|
||||
fingerprintPhrase = "",
|
||||
isResendNotificationLoading = false,
|
||||
),
|
||||
|
@ -125,6 +132,7 @@ class LoginWithDeviceViewModel @Inject constructor(
|
|||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = LoginWithDeviceState.ViewState.Content(
|
||||
loginWithDeviceType = it.loginWithDeviceType,
|
||||
fingerprintPhrase = result.authRequest.fingerprint,
|
||||
isResendNotificationLoading = false,
|
||||
),
|
||||
|
@ -137,6 +145,7 @@ class LoginWithDeviceViewModel @Inject constructor(
|
|||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = LoginWithDeviceState.ViewState.Content(
|
||||
loginWithDeviceType = it.loginWithDeviceType,
|
||||
fingerprintPhrase = "",
|
||||
isResendNotificationLoading = false,
|
||||
),
|
||||
|
@ -152,6 +161,7 @@ class LoginWithDeviceViewModel @Inject constructor(
|
|||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = LoginWithDeviceState.ViewState.Content(
|
||||
loginWithDeviceType = it.loginWithDeviceType,
|
||||
fingerprintPhrase = "",
|
||||
isResendNotificationLoading = false,
|
||||
),
|
||||
|
@ -167,6 +177,7 @@ class LoginWithDeviceViewModel @Inject constructor(
|
|||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = LoginWithDeviceState.ViewState.Content(
|
||||
loginWithDeviceType = it.loginWithDeviceType,
|
||||
fingerprintPhrase = "",
|
||||
isResendNotificationLoading = false,
|
||||
),
|
||||
|
@ -256,16 +267,26 @@ class LoginWithDeviceViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val result = authRepository.login(
|
||||
email = state.emailAddress,
|
||||
requestId = loginData.requestId,
|
||||
accessCode = loginData.accessCode,
|
||||
asymmetricalKey = loginData.asymmetricalKey,
|
||||
requestPrivateKey = loginData.privateKey,
|
||||
masterPasswordHash = loginData.masterPasswordHash,
|
||||
captchaToken = loginData.captchaToken,
|
||||
)
|
||||
sendAction(LoginWithDeviceAction.Internal.ReceiveLoginResult(result))
|
||||
when (state.loginWithDeviceType) {
|
||||
LoginWithDeviceType.OTHER_DEVICE -> {
|
||||
val result = authRepository.login(
|
||||
email = state.emailAddress,
|
||||
requestId = loginData.requestId,
|
||||
accessCode = loginData.accessCode,
|
||||
asymmetricalKey = loginData.asymmetricalKey,
|
||||
requestPrivateKey = loginData.privateKey,
|
||||
masterPasswordHash = loginData.masterPasswordHash,
|
||||
captchaToken = loginData.captchaToken,
|
||||
)
|
||||
sendAction(LoginWithDeviceAction.Internal.ReceiveLoginResult(result))
|
||||
}
|
||||
|
||||
LoginWithDeviceType.SSO_ADMIN_APPROVAL,
|
||||
LoginWithDeviceType.SSO_OTHER_DEVICE,
|
||||
-> {
|
||||
sendEvent(LoginWithDeviceEvent.ShowToast("Not yet implemented!"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -273,7 +294,10 @@ class LoginWithDeviceViewModel @Inject constructor(
|
|||
setIsResendNotificationLoading(isResend)
|
||||
authJob.cancel()
|
||||
authJob = authRepository
|
||||
.createAuthRequestWithUpdates(email = state.emailAddress)
|
||||
.createAuthRequestWithUpdates(
|
||||
email = state.emailAddress,
|
||||
authRequestType = state.loginWithDeviceType.toAuthRequestType(),
|
||||
)
|
||||
.map { LoginWithDeviceAction.Internal.NewAuthRequestResultReceive(it) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
@ -301,11 +325,25 @@ class LoginWithDeviceViewModel @Inject constructor(
|
|||
*/
|
||||
@Parcelize
|
||||
data class LoginWithDeviceState(
|
||||
val loginWithDeviceType: LoginWithDeviceType,
|
||||
val emailAddress: String,
|
||||
val viewState: ViewState,
|
||||
val dialogState: DialogState?,
|
||||
val loginData: LoginData?,
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
* The toolbar text for the UI.
|
||||
*/
|
||||
val toolbarTitle: Text
|
||||
get() = when (loginWithDeviceType) {
|
||||
LoginWithDeviceType.OTHER_DEVICE,
|
||||
LoginWithDeviceType.SSO_OTHER_DEVICE,
|
||||
-> R.string.log_in_with_device.asText()
|
||||
|
||||
LoginWithDeviceType.SSO_ADMIN_APPROVAL -> R.string.log_in_initiated.asText()
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the specific view states for the [LoginWithDeviceScreen].
|
||||
*/
|
||||
|
@ -322,12 +360,74 @@ data class LoginWithDeviceState(
|
|||
* Content state for the [LoginWithDeviceScreen] showing the actual content or items.
|
||||
*
|
||||
* @property fingerprintPhrase The fingerprint phrase to present to the user.
|
||||
* @property isResendNotificationLoading Indicates if the resend loading spinner should be
|
||||
* displayed.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Content(
|
||||
val fingerprintPhrase: String,
|
||||
val isResendNotificationLoading: Boolean,
|
||||
) : ViewState()
|
||||
private val loginWithDeviceType: LoginWithDeviceType,
|
||||
) : ViewState() {
|
||||
/**
|
||||
* The title text for the UI.
|
||||
*/
|
||||
val title: Text
|
||||
get() = when (loginWithDeviceType) {
|
||||
LoginWithDeviceType.OTHER_DEVICE,
|
||||
LoginWithDeviceType.SSO_OTHER_DEVICE,
|
||||
-> R.string.log_in_initiated.asText()
|
||||
|
||||
LoginWithDeviceType.SSO_ADMIN_APPROVAL,
|
||||
-> R.string.admin_approval_requested.asText()
|
||||
}
|
||||
|
||||
/**
|
||||
* The subtitle text for the UI.
|
||||
*/
|
||||
val subtitle: Text
|
||||
get() = when (loginWithDeviceType) {
|
||||
LoginWithDeviceType.OTHER_DEVICE,
|
||||
LoginWithDeviceType.SSO_OTHER_DEVICE,
|
||||
-> R.string.a_notification_has_been_sent_to_your_device.asText()
|
||||
|
||||
LoginWithDeviceType.SSO_ADMIN_APPROVAL,
|
||||
-> R.string.your_request_has_been_sent_to_your_admin.asText()
|
||||
}
|
||||
|
||||
/**
|
||||
* The description text for the UI.
|
||||
*/
|
||||
@Suppress("MaxLineLength")
|
||||
val description: Text
|
||||
get() = when (loginWithDeviceType) {
|
||||
LoginWithDeviceType.OTHER_DEVICE,
|
||||
LoginWithDeviceType.SSO_OTHER_DEVICE,
|
||||
-> R.string.please_make_sure_your_vault_is_unlocked_and_the_fingerprint_phrase_matches_on_the_other_device.asText()
|
||||
|
||||
LoginWithDeviceType.SSO_ADMIN_APPROVAL,
|
||||
-> R.string.you_will_be_notified_once_approved.asText()
|
||||
}
|
||||
|
||||
/**
|
||||
* The text to display indicating that there are other option for logging in.
|
||||
*/
|
||||
@Suppress("MaxLineLength")
|
||||
val otherOptions: Text
|
||||
get() = when (loginWithDeviceType) {
|
||||
LoginWithDeviceType.OTHER_DEVICE,
|
||||
LoginWithDeviceType.SSO_OTHER_DEVICE,
|
||||
-> R.string.log_in_with_device_must_be_set_up_in_the_settings_of_the_bitwarden_app_need_another_option.asText()
|
||||
|
||||
LoginWithDeviceType.SSO_ADMIN_APPROVAL -> R.string.trouble_logging_in.asText()
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the resend button should be available.
|
||||
*/
|
||||
val allowsResend: Boolean
|
||||
get() = loginWithDeviceType != LoginWithDeviceType.SSO_ADMIN_APPROVAL
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.model
|
||||
|
||||
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.LoginWithDeviceScreen
|
||||
|
||||
/**
|
||||
* Represents the different ways you may want to display the [LoginWithDeviceScreen].
|
||||
*/
|
||||
enum class LoginWithDeviceType {
|
||||
OTHER_DEVICE,
|
||||
SSO_ADMIN_APPROVAL,
|
||||
SSO_OTHER_DEVICE,
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.util
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestType
|
||||
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.model.LoginWithDeviceType
|
||||
|
||||
/**
|
||||
* Converts the [LoginWithDeviceType] to an appropriate [AuthRequestType].
|
||||
*/
|
||||
fun LoginWithDeviceType.toAuthRequestType(): AuthRequestType =
|
||||
when (this) {
|
||||
LoginWithDeviceType.OTHER_DEVICE -> AuthRequestType.OTHER_DEVICE
|
||||
LoginWithDeviceType.SSO_ADMIN_APPROVAL -> AuthRequestType.SSO_ADMIN_APPROVAL
|
||||
LoginWithDeviceType.SSO_OTHER_DEVICE -> AuthRequestType.SSO_OTHER_DEVICE
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthenticatedAuthRequestsApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedAuthRequestsApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.base.BaseServiceTest
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
|
@ -14,15 +16,97 @@ import java.time.ZonedDateTime
|
|||
|
||||
class NewAuthRequestServiceTest : BaseServiceTest() {
|
||||
|
||||
private val authRequestsApi: UnauthenticatedAuthRequestsApi = retrofit.create()
|
||||
private val authenticatedAuthRequestsApi: AuthenticatedAuthRequestsApi = retrofit.create()
|
||||
private val unauthenticatedAuthRequestsApi: UnauthenticatedAuthRequestsApi = retrofit.create()
|
||||
private val service = NewAuthRequestServiceImpl(
|
||||
unauthenticatedAuthRequestsApi = authRequestsApi,
|
||||
authenticatedAuthRequestsApi = authenticatedAuthRequestsApi,
|
||||
unauthenticatedAuthRequestsApi = unauthenticatedAuthRequestsApi,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `createAuthRequest when request response is Failure should return Failure`() = runTest {
|
||||
val response = MockResponse().setResponseCode(400)
|
||||
server.enqueue(response)
|
||||
fun `createAuthRequest when LOGIN_WITH_DEVICE and request is Failure should return Failure`() =
|
||||
runTest {
|
||||
val response = MockResponse().setResponseCode(400)
|
||||
server.enqueue(response)
|
||||
val deviceIdentifier = "4321"
|
||||
val actual = service.createAuthRequest(
|
||||
email = "test@gmail.com",
|
||||
publicKey = "1234",
|
||||
deviceId = deviceIdentifier,
|
||||
accessCode = "accessCode",
|
||||
fingerprint = "fingerprint",
|
||||
authRequestType = AuthRequestTypeJson.LOGIN_WITH_DEVICE,
|
||||
)
|
||||
|
||||
val request = server.takeRequest()
|
||||
assertEquals(deviceIdentifier, request.getHeader("Device-Identifier"))
|
||||
assertEquals("$urlPrefix/auth-requests", request.requestUrl.toString())
|
||||
assertTrue(actual.isFailure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createAuthRequest when LOGIN_WITH_DEVICE and request is Success should return Success`() =
|
||||
runTest {
|
||||
val response = MockResponse().setBody(AUTH_REQUEST_RESPONSE_JSON).setResponseCode(200)
|
||||
server.enqueue(response)
|
||||
val deviceIdentifier = "4321"
|
||||
val actual = service.createAuthRequest(
|
||||
email = "test@gmail.com",
|
||||
publicKey = "1234",
|
||||
deviceId = deviceIdentifier,
|
||||
accessCode = "accessCode",
|
||||
fingerprint = "fingerprint",
|
||||
authRequestType = AuthRequestTypeJson.LOGIN_WITH_DEVICE,
|
||||
)
|
||||
val request = server.takeRequest()
|
||||
assertEquals(deviceIdentifier, request.getHeader("Device-Identifier"))
|
||||
assertEquals("$urlPrefix/auth-requests", request.requestUrl.toString())
|
||||
assertEquals(AUTH_REQUEST_RESPONSE.asSuccess(), actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createAuthRequest when ADMIN_APPROVAL and request is Failure should return Failure`() =
|
||||
runTest {
|
||||
val response = MockResponse().setResponseCode(400)
|
||||
server.enqueue(response)
|
||||
val deviceIdentifier = "4321"
|
||||
val actual = service.createAuthRequest(
|
||||
email = "test@gmail.com",
|
||||
publicKey = "1234",
|
||||
deviceId = deviceIdentifier,
|
||||
accessCode = "accessCode",
|
||||
fingerprint = "fingerprint",
|
||||
authRequestType = AuthRequestTypeJson.ADMIN_APPROVAL,
|
||||
)
|
||||
|
||||
val request = server.takeRequest()
|
||||
assertEquals(deviceIdentifier, request.getHeader("Device-Identifier"))
|
||||
assertEquals("$urlPrefix/auth-requests/admin-request", request.requestUrl.toString())
|
||||
assertTrue(actual.isFailure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createAuthRequest when ADMIN_APPROVAL and request is Success should return Success`() =
|
||||
runTest {
|
||||
val response = MockResponse().setBody(AUTH_REQUEST_RESPONSE_JSON).setResponseCode(200)
|
||||
server.enqueue(response)
|
||||
val deviceIdentifier = "4321"
|
||||
val actual = service.createAuthRequest(
|
||||
email = "test@gmail.com",
|
||||
publicKey = "1234",
|
||||
deviceId = deviceIdentifier,
|
||||
accessCode = "accessCode",
|
||||
fingerprint = "fingerprint",
|
||||
authRequestType = AuthRequestTypeJson.ADMIN_APPROVAL,
|
||||
)
|
||||
val request = server.takeRequest()
|
||||
assertEquals(deviceIdentifier, request.getHeader("Device-Identifier"))
|
||||
assertEquals("$urlPrefix/auth-requests/admin-request", request.requestUrl.toString())
|
||||
assertEquals(AUTH_REQUEST_RESPONSE.asSuccess(), actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createAuthRequest when UNLOCK should return Failure`() = runTest {
|
||||
val deviceIdentifier = "4321"
|
||||
val actual = service.createAuthRequest(
|
||||
email = "test@gmail.com",
|
||||
|
@ -30,48 +114,82 @@ class NewAuthRequestServiceTest : BaseServiceTest() {
|
|||
deviceId = deviceIdentifier,
|
||||
accessCode = "accessCode",
|
||||
fingerprint = "fingerprint",
|
||||
)
|
||||
assertEquals(deviceIdentifier, server.takeRequest().getHeader("Device-Identifier"))
|
||||
assertTrue(actual.isFailure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createAuthRequest when request response is Success should return Success`() = runTest {
|
||||
val response = MockResponse().setBody(AUTH_REQUEST_RESPONSE_JSON).setResponseCode(200)
|
||||
server.enqueue(response)
|
||||
val deviceIdentifier = "4321"
|
||||
val actual = service.createAuthRequest(
|
||||
email = "test@gmail.com",
|
||||
publicKey = "1234",
|
||||
deviceId = deviceIdentifier,
|
||||
accessCode = "accessCode",
|
||||
fingerprint = "fingerprint",
|
||||
)
|
||||
assertEquals(deviceIdentifier, server.takeRequest().getHeader("Device-Identifier"))
|
||||
assertEquals(AUTH_REQUEST_RESPONSE.asSuccess(), actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getAuthRequestUpdate when request response is Failure should return Failure`() = runTest {
|
||||
val response = MockResponse().setResponseCode(400)
|
||||
server.enqueue(response)
|
||||
val actual = service.getAuthRequestUpdate(
|
||||
requestId = "1",
|
||||
accessCode = "accessCode",
|
||||
authRequestType = AuthRequestTypeJson.UNLOCK,
|
||||
)
|
||||
assertTrue(actual.isFailure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getAuthRequestUpdate when request response is Success should return Success`() = runTest {
|
||||
val response = MockResponse().setBody(AUTH_REQUEST_RESPONSE_JSON).setResponseCode(200)
|
||||
server.enqueue(response)
|
||||
val actual = service.getAuthRequestUpdate(
|
||||
requestId = "1",
|
||||
accessCode = "accessCode",
|
||||
)
|
||||
assertEquals(AUTH_REQUEST_RESPONSE.asSuccess(), actual)
|
||||
}
|
||||
fun `getAuthRequestUpdate when not SSO and response is Failure should return Failure`() =
|
||||
runTest {
|
||||
val response = MockResponse().setResponseCode(400)
|
||||
server.enqueue(response)
|
||||
val requestId = "1"
|
||||
val accessCode = "accessCode"
|
||||
val actual = service.getAuthRequestUpdate(
|
||||
requestId = requestId,
|
||||
accessCode = accessCode,
|
||||
isSso = false,
|
||||
)
|
||||
val request = server.takeRequest()
|
||||
assertEquals(
|
||||
"$urlPrefix/auth-requests/$requestId/response?code=$accessCode",
|
||||
request.requestUrl.toString(),
|
||||
)
|
||||
assertTrue(actual.isFailure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getAuthRequestUpdate when not SSO and response is Success should return Success`() =
|
||||
runTest {
|
||||
val response = MockResponse().setBody(AUTH_REQUEST_RESPONSE_JSON).setResponseCode(200)
|
||||
server.enqueue(response)
|
||||
val requestId = "1"
|
||||
val accessCode = "accessCode"
|
||||
val actual = service.getAuthRequestUpdate(
|
||||
requestId = requestId,
|
||||
accessCode = accessCode,
|
||||
isSso = false,
|
||||
)
|
||||
val request = server.takeRequest()
|
||||
assertEquals(
|
||||
"$urlPrefix/auth-requests/$requestId/response?code=$accessCode",
|
||||
request.requestUrl.toString(),
|
||||
)
|
||||
assertEquals(AUTH_REQUEST_RESPONSE.asSuccess(), actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getAuthRequestUpdate when SSO and response is Failure should return Failure`() =
|
||||
runTest {
|
||||
val response = MockResponse().setResponseCode(400)
|
||||
server.enqueue(response)
|
||||
val requestId = "1"
|
||||
val actual = service.getAuthRequestUpdate(
|
||||
requestId = requestId,
|
||||
accessCode = "accessCode",
|
||||
isSso = true,
|
||||
)
|
||||
val request = server.takeRequest()
|
||||
assertEquals("$urlPrefix/auth-requests/$requestId", request.requestUrl.toString())
|
||||
assertTrue(actual.isFailure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getAuthRequestUpdate when SSO and response is Success should return Success`() =
|
||||
runTest {
|
||||
val response = MockResponse().setBody(AUTH_REQUEST_RESPONSE_JSON).setResponseCode(200)
|
||||
server.enqueue(response)
|
||||
val requestId = "1"
|
||||
val actual = service.getAuthRequestUpdate(
|
||||
requestId = requestId,
|
||||
accessCode = "accessCode",
|
||||
isSso = true,
|
||||
)
|
||||
val request = server.takeRequest()
|
||||
assertEquals("$urlPrefix/auth-requests/$requestId", request.requestUrl.toString())
|
||||
assertEquals(AUTH_REQUEST_RESPONSE.asSuccess(), actual)
|
||||
}
|
||||
}
|
||||
|
||||
private const val AUTH_REQUEST_RESPONSE_JSON = """
|
||||
|
|
|
@ -6,6 +6,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
|||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AuthRequestsService
|
||||
|
@ -13,6 +14,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.service.NewAuthRequestSe
|
|||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequest
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestResult
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestType
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestUpdatesResult
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestsResult
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestsUpdatesResult
|
||||
|
@ -74,13 +76,19 @@ class AuthRequestManagerTest {
|
|||
deviceId = fakeAuthDiskSource.uniqueAppId,
|
||||
accessCode = authRequestResponse.accessCode,
|
||||
fingerprint = authRequestResponse.fingerprint,
|
||||
authRequestType = AuthRequestTypeJson.LOGIN_WITH_DEVICE,
|
||||
)
|
||||
} returns Throwable("Fail").asFailure()
|
||||
|
||||
repository.createAuthRequestWithUpdates(email = email).test {
|
||||
assertEquals(CreateAuthRequestResult.Error, awaitItem())
|
||||
awaitComplete()
|
||||
}
|
||||
repository
|
||||
.createAuthRequestWithUpdates(
|
||||
email = email,
|
||||
authRequestType = AuthRequestType.OTHER_DEVICE,
|
||||
)
|
||||
.test {
|
||||
assertEquals(CreateAuthRequestResult.Error, awaitItem())
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
|
@ -127,30 +135,37 @@ class AuthRequestManagerTest {
|
|||
deviceId = fakeAuthDiskSource.uniqueAppId,
|
||||
accessCode = authRequestResponse.accessCode,
|
||||
fingerprint = authRequestResponse.fingerprint,
|
||||
authRequestType = AuthRequestTypeJson.LOGIN_WITH_DEVICE,
|
||||
)
|
||||
} returns authRequestResponseJson.asSuccess()
|
||||
coEvery {
|
||||
newAuthRequestService.getAuthRequestUpdate(
|
||||
requestId = authRequest.id,
|
||||
accessCode = authRequestResponse.accessCode,
|
||||
isSso = false,
|
||||
)
|
||||
} returnsMany listOf(
|
||||
authRequestResponseJson.asSuccess(),
|
||||
updatedAuthRequestResponseJson.asSuccess(),
|
||||
)
|
||||
|
||||
repository.createAuthRequestWithUpdates(email = email).test {
|
||||
assertEquals(CreateAuthRequestResult.Update(authRequest), awaitItem())
|
||||
assertEquals(CreateAuthRequestResult.Update(authRequest), awaitItem())
|
||||
assertEquals(
|
||||
CreateAuthRequestResult.Success(
|
||||
authRequest = authRequest.copy(requestApproved = true),
|
||||
authRequestResponse = authRequestResponse,
|
||||
),
|
||||
awaitItem(),
|
||||
repository
|
||||
.createAuthRequestWithUpdates(
|
||||
email = email,
|
||||
authRequestType = AuthRequestType.OTHER_DEVICE,
|
||||
)
|
||||
awaitComplete()
|
||||
}
|
||||
.test {
|
||||
assertEquals(CreateAuthRequestResult.Update(authRequest), awaitItem())
|
||||
assertEquals(CreateAuthRequestResult.Update(authRequest), awaitItem())
|
||||
assertEquals(
|
||||
CreateAuthRequestResult.Success(
|
||||
authRequest = authRequest.copy(requestApproved = true),
|
||||
authRequestResponse = authRequestResponse,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
|
@ -197,20 +212,27 @@ class AuthRequestManagerTest {
|
|||
deviceId = fakeAuthDiskSource.uniqueAppId,
|
||||
accessCode = authRequestResponse.accessCode,
|
||||
fingerprint = authRequestResponse.fingerprint,
|
||||
authRequestType = AuthRequestTypeJson.LOGIN_WITH_DEVICE,
|
||||
)
|
||||
} returns authRequestResponseJson.asSuccess()
|
||||
coEvery {
|
||||
newAuthRequestService.getAuthRequestUpdate(
|
||||
requestId = authRequest.id,
|
||||
accessCode = authRequestResponse.accessCode,
|
||||
isSso = false,
|
||||
)
|
||||
} returns updatedAuthRequestResponseJson.asSuccess()
|
||||
|
||||
repository.createAuthRequestWithUpdates(email = email).test {
|
||||
assertEquals(CreateAuthRequestResult.Update(authRequest), awaitItem())
|
||||
assertEquals(CreateAuthRequestResult.Declined, awaitItem())
|
||||
awaitComplete()
|
||||
}
|
||||
repository
|
||||
.createAuthRequestWithUpdates(
|
||||
email = email,
|
||||
authRequestType = AuthRequestType.OTHER_DEVICE,
|
||||
)
|
||||
.test {
|
||||
assertEquals(CreateAuthRequestResult.Update(authRequest), awaitItem())
|
||||
assertEquals(CreateAuthRequestResult.Declined, awaitItem())
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
|
@ -257,20 +279,27 @@ class AuthRequestManagerTest {
|
|||
deviceId = fakeAuthDiskSource.uniqueAppId,
|
||||
accessCode = authRequestResponse.accessCode,
|
||||
fingerprint = authRequestResponse.fingerprint,
|
||||
authRequestType = AuthRequestTypeJson.ADMIN_APPROVAL,
|
||||
)
|
||||
} returns authRequestResponseJson.asSuccess()
|
||||
coEvery {
|
||||
newAuthRequestService.getAuthRequestUpdate(
|
||||
requestId = authRequest.id,
|
||||
accessCode = authRequestResponse.accessCode,
|
||||
isSso = true,
|
||||
)
|
||||
} returns updatedAuthRequestResponseJson.asSuccess()
|
||||
|
||||
repository.createAuthRequestWithUpdates(email = email).test {
|
||||
assertEquals(CreateAuthRequestResult.Update(authRequest), awaitItem())
|
||||
assertEquals(CreateAuthRequestResult.Expired, awaitItem())
|
||||
awaitComplete()
|
||||
}
|
||||
repository
|
||||
.createAuthRequestWithUpdates(
|
||||
email = email,
|
||||
authRequestType = AuthRequestType.SSO_ADMIN_APPROVAL,
|
||||
)
|
||||
.test {
|
||||
assertEquals(CreateAuthRequestResult.Update(authRequest), awaitItem())
|
||||
assertEquals(CreateAuthRequestResult.Expired, awaitItem())
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
|
@ -282,10 +311,15 @@ class AuthRequestManagerTest {
|
|||
authSdkSource.getNewAuthRequest(email = email)
|
||||
} returns Throwable("Fail").asFailure()
|
||||
|
||||
repository.createAuthRequestWithUpdates(email = email).test {
|
||||
assertEquals(CreateAuthRequestResult.Error, awaitItem())
|
||||
awaitComplete()
|
||||
}
|
||||
repository
|
||||
.createAuthRequestWithUpdates(
|
||||
email = email,
|
||||
authRequestType = AuthRequestType.OTHER_DEVICE,
|
||||
)
|
||||
.test {
|
||||
assertEquals(CreateAuthRequestResult.Error, awaitItem())
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package com.x8bit.bitwarden.data.auth.manager.util
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestType
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AuthRequestTypeExtensionsTest {
|
||||
@Test
|
||||
fun `isSso should return the correct value for each type`() {
|
||||
mapOf(
|
||||
AuthRequestType.OTHER_DEVICE to false,
|
||||
AuthRequestType.SSO_OTHER_DEVICE to true,
|
||||
AuthRequestType.SSO_ADMIN_APPROVAL to true,
|
||||
)
|
||||
.forEach { (type, expected) ->
|
||||
assertEquals(expected, type.isSso)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toAuthRequestTypeJson should return the correct value for each type`() {
|
||||
mapOf(
|
||||
AuthRequestType.OTHER_DEVICE to AuthRequestTypeJson.LOGIN_WITH_DEVICE,
|
||||
AuthRequestType.SSO_OTHER_DEVICE to AuthRequestTypeJson.LOGIN_WITH_DEVICE,
|
||||
AuthRequestType.SSO_ADMIN_APPROVAL to AuthRequestTypeJson.ADMIN_APPROVAL,
|
||||
)
|
||||
.forEach { (type, expected) ->
|
||||
assertEquals(expected, type.toAuthRequestTypeJson())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,6 +17,8 @@ abstract class BaseServiceTest {
|
|||
|
||||
protected val server = MockWebServer().apply { start() }
|
||||
|
||||
protected val urlPrefix: String get() = "http://${server.hostName}:${server.port}"
|
||||
|
||||
protected val retrofit: Retrofit = Retrofit.Builder()
|
||||
.baseUrl(server.url("/").toString())
|
||||
.addCallAdapterFactory(ResultCallAdapterFactory())
|
||||
|
|
|
@ -11,6 +11,7 @@ import androidx.compose.ui.test.performClick
|
|||
import androidx.compose.ui.test.performScrollTo
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.model.LoginWithDeviceType
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
|
@ -176,7 +177,9 @@ private val DEFAULT_STATE = LoginWithDeviceState(
|
|||
viewState = LoginWithDeviceState.ViewState.Content(
|
||||
fingerprintPhrase = "alabster-drinkable-mystified-rapping-irrigate",
|
||||
isResendNotificationLoading = false,
|
||||
loginWithDeviceType = LoginWithDeviceType.OTHER_DEVICE,
|
||||
),
|
||||
dialogState = null,
|
||||
loginData = null,
|
||||
loginWithDeviceType = LoginWithDeviceType.OTHER_DEVICE,
|
||||
)
|
||||
|
|
|
@ -5,11 +5,13 @@ import app.cash.turbine.test
|
|||
import com.bitwarden.core.AuthRequestResponse
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequest
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestType
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.CreateAuthRequestResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.model.LoginWithDeviceType
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import io.mockk.awaits
|
||||
|
@ -31,7 +33,7 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
|||
bufferedMutableSharedFlow<CaptchaCallbackTokenResult>()
|
||||
private val authRepository = mockk<AuthRepository> {
|
||||
coEvery {
|
||||
createAuthRequestWithUpdates(EMAIL)
|
||||
createAuthRequestWithUpdates(email = EMAIL, authRequestType = any())
|
||||
} returns mutableCreateAuthRequestWithUpdatesFlow
|
||||
coEvery { captchaTokenResultFlow } returns mutableCaptchaTokenResultFlow
|
||||
}
|
||||
|
@ -44,7 +46,10 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
|||
viewModel.stateFlow.value,
|
||||
)
|
||||
coVerify(exactly = 1) {
|
||||
authRepository.createAuthRequestWithUpdates(EMAIL)
|
||||
authRepository.createAuthRequestWithUpdates(
|
||||
email = EMAIL,
|
||||
authRequestType = AuthRequestType.OTHER_DEVICE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -53,12 +58,18 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
|||
val newEmail = "newEmail@gmail.com"
|
||||
val state = DEFAULT_STATE.copy(emailAddress = newEmail)
|
||||
coEvery {
|
||||
authRepository.createAuthRequestWithUpdates(newEmail)
|
||||
authRepository.createAuthRequestWithUpdates(
|
||||
email = newEmail,
|
||||
authRequestType = AuthRequestType.OTHER_DEVICE,
|
||||
)
|
||||
} returns mutableCreateAuthRequestWithUpdatesFlow
|
||||
val viewModel = createViewModel(state = state)
|
||||
assertEquals(state, viewModel.stateFlow.value)
|
||||
coVerify(exactly = 1) {
|
||||
authRepository.createAuthRequestWithUpdates(newEmail)
|
||||
authRepository.createAuthRequestWithUpdates(
|
||||
email = newEmail,
|
||||
authRequestType = AuthRequestType.OTHER_DEVICE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,7 +111,10 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
|||
viewModel.stateFlow.value,
|
||||
)
|
||||
verify(exactly = 2) {
|
||||
authRepository.createAuthRequestWithUpdates(EMAIL)
|
||||
authRepository.createAuthRequestWithUpdates(
|
||||
email = EMAIL,
|
||||
authRequestType = AuthRequestType.OTHER_DEVICE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -191,6 +205,43 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on createAuthRequestWithUpdates Success with SSO_ADMIN_APPROVAL should emit toast`() =
|
||||
runTest {
|
||||
val initialViewState = DEFAULT_CONTENT_VIEW_STATE.copy(
|
||||
loginWithDeviceType = LoginWithDeviceType.SSO_ADMIN_APPROVAL,
|
||||
)
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
viewState = initialViewState,
|
||||
loginWithDeviceType = LoginWithDeviceType.SSO_ADMIN_APPROVAL,
|
||||
)
|
||||
val viewModel = createViewModel(initialState)
|
||||
|
||||
viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->
|
||||
assertEquals(initialState, stateFlow.awaitItem())
|
||||
mutableCreateAuthRequestWithUpdatesFlow.tryEmit(
|
||||
CreateAuthRequestResult.Success(AUTH_REQUEST, AUTH_REQUEST_RESPONSE),
|
||||
)
|
||||
assertEquals(
|
||||
initialState.copy(
|
||||
viewState = initialViewState.copy(
|
||||
fingerprintPhrase = "",
|
||||
),
|
||||
dialogState = LoginWithDeviceState.DialogState.Loading(
|
||||
message = R.string.logging_in.asText(),
|
||||
),
|
||||
loginData = DEFAULT_LOGIN_DATA,
|
||||
),
|
||||
stateFlow.awaitItem(),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
LoginWithDeviceEvent.ShowToast("Not yet implemented!"),
|
||||
eventFlow.awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on createAuthRequestWithUpdates Success and login two factor required should emit NavigateToTwoFactorLogin`() =
|
||||
|
@ -419,6 +470,7 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
|||
savedStateHandle = SavedStateHandle().apply {
|
||||
set("state", state)
|
||||
set("email_address", state?.emailAddress ?: EMAIL)
|
||||
set("login_type", state?.loginWithDeviceType ?: LoginWithDeviceType.OTHER_DEVICE)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -429,6 +481,7 @@ private const val FINGERPRINT = "fingerprint"
|
|||
private val DEFAULT_CONTENT_VIEW_STATE = LoginWithDeviceState.ViewState.Content(
|
||||
fingerprintPhrase = FINGERPRINT,
|
||||
isResendNotificationLoading = false,
|
||||
loginWithDeviceType = LoginWithDeviceType.OTHER_DEVICE,
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE = LoginWithDeviceState(
|
||||
|
@ -436,6 +489,7 @@ private val DEFAULT_STATE = LoginWithDeviceState(
|
|||
viewState = DEFAULT_CONTENT_VIEW_STATE,
|
||||
dialogState = null,
|
||||
loginData = null,
|
||||
loginWithDeviceType = LoginWithDeviceType.OTHER_DEVICE,
|
||||
)
|
||||
|
||||
private val AUTH_REQUEST = AuthRequest(
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.util
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestType
|
||||
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.model.LoginWithDeviceType
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class LoginWithDeviceTypeExtensionsTest {
|
||||
@Test
|
||||
fun `toAuthRequestTypeJson should return the correct value for each type`() {
|
||||
mapOf(
|
||||
LoginWithDeviceType.OTHER_DEVICE to AuthRequestType.OTHER_DEVICE,
|
||||
LoginWithDeviceType.SSO_OTHER_DEVICE to AuthRequestType.SSO_OTHER_DEVICE,
|
||||
LoginWithDeviceType.SSO_ADMIN_APPROVAL to AuthRequestType.SSO_ADMIN_APPROVAL,
|
||||
)
|
||||
.forEach { (type, expected) ->
|
||||
assertEquals(expected, type.toAuthRequestType())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,28 @@
|
|||
package com.x8bit.bitwarden.ui.platform.base
|
||||
|
||||
import app.cash.turbine.ReceiveTurbine
|
||||
import app.cash.turbine.TurbineContext
|
||||
import app.cash.turbine.turbineScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.junit.jupiter.api.extension.RegisterExtension
|
||||
|
||||
abstract class BaseViewModelTest {
|
||||
@Suppress("unused")
|
||||
@RegisterExtension
|
||||
protected open val mainDispatcherExtension = MainDispatcherExtension()
|
||||
|
||||
protected suspend fun <S : Any, E : Any, T : BaseViewModel<S, E, *>> T.stateEventFlow(
|
||||
backgroundScope: CoroutineScope,
|
||||
validate: suspend TurbineContext.(
|
||||
stateFlow: ReceiveTurbine<S>,
|
||||
eventFlow: ReceiveTurbine<E>,
|
||||
) -> Unit,
|
||||
) {
|
||||
turbineScope {
|
||||
this.validate(
|
||||
this@stateEventFlow.stateFlow.testIn(backgroundScope),
|
||||
this@stateEventFlow.eventFlow.testIn(backgroundScope),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue