mirror of
https://github.com/bitwarden/android.git
synced 2025-02-03 13:41:00 +03:00
BIT-332: Launch web view for hcaptchca (#73)
This commit is contained in:
parent
5fc78afa0a
commit
02a1445f39
7 changed files with 164 additions and 2 deletions
|
@ -0,0 +1,32 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.util
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import java.util.Base64
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Generates an [Intent] to display a CAPTCHA challenge for Bitwarden authentication.
|
||||
*/
|
||||
fun LoginResult.CaptchaRequired.generateIntentForCaptcha(): Intent {
|
||||
val json = buildJsonObject {
|
||||
put(key = "siteKey", value = captchaId)
|
||||
put(key = "locale", value = Locale.getDefault().toString())
|
||||
put(key = "callbackUri", value = "bitwarden://captcha-callback")
|
||||
put(key = "captchaRequiredText", value = "Captcha required")
|
||||
}
|
||||
val base64Data = Base64
|
||||
.getEncoder()
|
||||
.encodeToString(
|
||||
json
|
||||
.toString()
|
||||
.toByteArray(Charsets.UTF_8),
|
||||
)
|
||||
val parentParam = "bitwarden%3A%2F%2Fcaptcha-callback"
|
||||
val url = "https://vault.bitwarden.com/captcha-mobile-connector.html" +
|
||||
"?data=$base64Data&parent=$parentParam&v=1"
|
||||
return Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
}
|
|
@ -14,6 +14,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
@ -21,6 +22,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
|
||||
|
||||
/**
|
||||
|
@ -31,11 +33,13 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
|
|||
fun LoginScreen(
|
||||
onNavigateToLanding: () -> Unit,
|
||||
viewModel: LoginViewModel = hiltViewModel(),
|
||||
intentHandler: IntentHandler = IntentHandler(context = LocalContext.current),
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
LoginEvent.NavigateToLanding -> onNavigateToLanding()
|
||||
is LoginEvent.NavigateToCaptcha -> intentHandler.startActivity(intent = event.intent)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.login
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.util.generateIntentForCaptcha
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
@ -60,8 +62,13 @@ class LoginViewModel @Inject constructor(
|
|||
LoginResult.Error -> Unit
|
||||
// No action required on success, root nav will navigate to logged in state
|
||||
LoginResult.Success -> Unit
|
||||
// TODO: launch intent with captcha URL BIT-399
|
||||
is LoginResult.CaptchaRequired -> Unit
|
||||
is LoginResult.CaptchaRequired -> {
|
||||
sendEvent(
|
||||
event = LoginEvent.NavigateToCaptcha(
|
||||
intent = result.generateIntentForCaptcha(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -97,6 +104,11 @@ sealed class LoginEvent {
|
|||
* Navigates to the Landing screen.
|
||||
*/
|
||||
data object NavigateToLanding : LoginEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the captcha verification screen.
|
||||
*/
|
||||
data class NavigateToCaptcha(val intent: Intent) : LoginEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
package com.x8bit.bitwarden.ui.platform.base.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
||||
/**
|
||||
* A utility class for simplifying the handling of Android Intents within a given context.
|
||||
*/
|
||||
class IntentHandler(private val context: Context) {
|
||||
|
||||
/**
|
||||
* Start an activity using the provided [Intent].
|
||||
*/
|
||||
fun startActivity(intent: Intent) {
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.util
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class LoginResultExtensionsTest {
|
||||
|
||||
@Test
|
||||
fun `generateIntentForCaptcha should return valid Intent`() {
|
||||
val captchaRequired = LoginResult.CaptchaRequired("testCaptchaId")
|
||||
val intent = captchaRequired.generateIntentForCaptcha()
|
||||
val expectedUrl = "https://vault.bitwarden.com/captcha-mobile-connector.html" +
|
||||
"?data=eyJzaXRlS2V5IjoidGVzdENhcHRjaGkxZGQiLCJsb2NhbGUiOiJlbl9VUyJ9" +
|
||||
"&parent=bitwarden%3A%2F%2Fcaptcha-callback&v=1"
|
||||
val expectedIntent = Intent(Intent.ACTION_VIEW, Uri.parse(expectedUrl))
|
||||
assertEquals(expectedIntent.action, intent.action)
|
||||
assertEquals(expectedIntent.data, intent.data)
|
||||
}
|
||||
}
|
|
@ -1,9 +1,11 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.login
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
|
@ -85,4 +87,30 @@ class LoginScreenTest : BaseComposeTest() {
|
|||
}
|
||||
assertTrue(onNavigateToLandingCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToCaptcha should call intentHandler startActivity`() {
|
||||
val intentHandler = mockk<IntentHandler>(relaxed = true) {
|
||||
every { startActivity(any()) } returns Unit
|
||||
}
|
||||
val mockIntent = mockk<Intent>()
|
||||
val viewModel = mockk<LoginViewModel>(relaxed = true) {
|
||||
every { eventFlow } returns flowOf(LoginEvent.NavigateToCaptcha(mockIntent))
|
||||
every { stateFlow } returns MutableStateFlow(
|
||||
LoginState(
|
||||
emailAddress = "",
|
||||
isLoginButtonEnabled = false,
|
||||
passwordInput = "",
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
LoginScreen(
|
||||
onNavigateToLanding = {},
|
||||
intentHandler = intentHandler,
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
verify { intentHandler.startActivity(mockIntent) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.login
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.util.generateIntentForCaptcha
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
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
|
||||
|
||||
class LoginViewModelTest : BaseViewModelTest() {
|
||||
|
@ -18,6 +25,16 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
it["email_address"] = "test@gmail.com"
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
mockkStatic(LOGIN_RESULT_PATH)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(LOGIN_RESULT_PATH)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct`() = runTest {
|
||||
val viewModel = LoginViewModel(
|
||||
|
@ -87,6 +104,33 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `LoginButtonClick login returns CaptchaRequired should emit NavigateToCaptcha`() =
|
||||
runTest {
|
||||
val mockkIntent = mockk<Intent>()
|
||||
every {
|
||||
LoginResult
|
||||
.CaptchaRequired(captchaId = "mock_captcha_id")
|
||||
.generateIntentForCaptcha()
|
||||
} returns mockkIntent
|
||||
val authRepository = mockk<AuthRepository> {
|
||||
coEvery { login("test@gmail.com", "") } returns
|
||||
LoginResult.CaptchaRequired(captchaId = "mock_captcha_id")
|
||||
}
|
||||
val viewModel = LoginViewModel(
|
||||
authRepository = authRepository,
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(LoginAction.LoginButtonClick)
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
assertEquals(LoginEvent.NavigateToCaptcha(intent = mockkIntent), awaitItem())
|
||||
}
|
||||
coVerify {
|
||||
authRepository.login(email = "test@gmail.com", password = "")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SingleSignOnClick should do nothing`() = runTest {
|
||||
val viewModel = LoginViewModel(
|
||||
|
@ -136,5 +180,8 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
passwordInput = "",
|
||||
isLoginButtonEnabled = true,
|
||||
)
|
||||
|
||||
private const val LOGIN_RESULT_PATH =
|
||||
"com.x8bit.bitwarden.data.auth.datasource.network.util.LoginResultExtensionsKt"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue