mirror of
https://github.com/bitwarden/android.git
synced 2026-06-02 02:36:58 -05:00
BIT-398: Launch ACTION_VIEW Intent with captcha URL and handle callback (#88)
This commit is contained in:
64
app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt
Normal file
64
app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt
Normal file
@@ -0,0 +1,64 @@
|
||||
package com.x8bit.bitwarden
|
||||
|
||||
import android.content.Intent
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
|
||||
class MainViewModelTest {
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
mockkStatic(LOGIN_RESULT_PATH)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(LOGIN_RESULT_PATH)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ReceiveNewIntent with captcha host should call setCaptchaCallbackToken`() {
|
||||
val authRepository = mockk<AuthRepository> {
|
||||
every {
|
||||
setCaptchaCallbackTokenResult(
|
||||
tokenResult = CaptchaCallbackTokenResult.Success(
|
||||
token = "mockk_token",
|
||||
),
|
||||
)
|
||||
} returns Unit
|
||||
}
|
||||
val mockIntent = mockk<Intent> {
|
||||
every { data?.host } returns "captcha-callback"
|
||||
every { data?.getQueryParameter("token") } returns "mockk_token"
|
||||
every { action } returns Intent.ACTION_VIEW
|
||||
}
|
||||
val viewModel = MainViewModel(
|
||||
authRepository = authRepository,
|
||||
)
|
||||
viewModel.sendAction(
|
||||
MainAction.ReceiveNewIntent(
|
||||
intent = mockIntent,
|
||||
),
|
||||
)
|
||||
verify {
|
||||
authRepository.setCaptchaCallbackTokenResult(
|
||||
tokenResult = CaptchaCallbackTokenResult.Success(
|
||||
token = "mockk_token",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val LOGIN_RESULT_PATH =
|
||||
"com.x8bit.bitwarden.data.auth.datasource.network.util.LoginResultExtensionsKt"
|
||||
}
|
||||
}
|
||||
@@ -24,21 +24,33 @@ class IdentityServiceTest : BaseServiceTest() {
|
||||
@Test
|
||||
fun `getToken when request response is Success should return Success`() = runTest {
|
||||
server.enqueue(MockResponse().setBody(LOGIN_SUCCESS_JSON))
|
||||
val result = identityService.getToken(email = EMAIL, passwordHash = PASSWORD_HASH)
|
||||
val result = identityService.getToken(
|
||||
email = EMAIL,
|
||||
passwordHash = PASSWORD_HASH,
|
||||
captchaToken = null,
|
||||
)
|
||||
assertEquals(Result.success(LOGIN_SUCCESS), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getToken when request is error should return error`() = runTest {
|
||||
server.enqueue(MockResponse().setResponseCode(500))
|
||||
val result = identityService.getToken(email = EMAIL, passwordHash = PASSWORD_HASH)
|
||||
val result = identityService.getToken(
|
||||
email = EMAIL,
|
||||
passwordHash = PASSWORD_HASH,
|
||||
captchaToken = null,
|
||||
)
|
||||
assertTrue(result.isFailure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getToken when response is CaptchaRequired should return CaptchaRequired`() = runTest {
|
||||
server.enqueue(MockResponse().setResponseCode(400).setBody(CAPTCHA_BODY_JSON))
|
||||
val result = identityService.getToken(email = EMAIL, passwordHash = PASSWORD_HASH)
|
||||
val result = identityService.getToken(
|
||||
email = EMAIL,
|
||||
passwordHash = PASSWORD_HASH,
|
||||
captchaToken = null,
|
||||
)
|
||||
assertEquals(Result.success(CAPTCHA_BODY), result)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ 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 io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
@@ -19,4 +21,46 @@ class LoginResultExtensionsTest {
|
||||
assertEquals(expectedIntent.action, intent.action)
|
||||
assertEquals(expectedIntent.data, intent.data)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCaptchaCallbackToken should return null when data is null`() {
|
||||
val intent = mockk<Intent> {
|
||||
every { data } returns null
|
||||
every { action } returns Intent.ACTION_VIEW
|
||||
}
|
||||
val result = intent.getCaptchaCallbackTokenResult()
|
||||
assertEquals(null, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCaptchaCallbackToken should return null when action is not Intent ACTION_VIEW`() {
|
||||
val intent = mockk<Intent> {
|
||||
every { data } returns null
|
||||
every { action } returns Intent.ACTION_ANSWER
|
||||
}
|
||||
val result = intent.getCaptchaCallbackTokenResult()
|
||||
assertEquals(null, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCaptchaCallbackToken should return MissingToken with missing token parameter`() {
|
||||
val intent = mockk<Intent> {
|
||||
every { data?.getQueryParameter("token") } returns null
|
||||
every { action } returns Intent.ACTION_VIEW
|
||||
every { data?.host } returns "captcha-callback"
|
||||
}
|
||||
val result = intent.getCaptchaCallbackTokenResult()
|
||||
assertEquals(CaptchaCallbackTokenResult.MissingToken, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCaptchaCallbackToken should return Success when token query parameter is present`() {
|
||||
val intent = mockk<Intent> {
|
||||
every { data?.getQueryParameter("token") } returns "myToken"
|
||||
every { action } returns Intent.ACTION_VIEW
|
||||
every { data?.host } returns "captcha-callback"
|
||||
}
|
||||
val result = intent.getCaptchaCallbackTokenResult()
|
||||
assertEquals(CaptchaCallbackTokenResult.Success("myToken"), result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.core.Kdf
|
||||
import com.bitwarden.sdk.Client
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthState
|
||||
@@ -8,6 +9,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
@@ -49,49 +51,102 @@ class AuthRepositoryTest {
|
||||
|
||||
@Test
|
||||
fun `login when pre login fails should return Error`() = runTest {
|
||||
coEvery { accountsService.preLogin(EMAIL) } returns (Result.failure(RuntimeException()))
|
||||
val result = repository.login(EMAIL, PASSWORD)
|
||||
coEvery {
|
||||
accountsService.preLogin(email = EMAIL)
|
||||
} returns (Result.failure(RuntimeException()))
|
||||
val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
|
||||
assertEquals(LoginResult.Error, result)
|
||||
assertEquals(AuthState.Unauthenticated, repository.authStateFlow.value)
|
||||
coVerify { accountsService.preLogin(EMAIL) }
|
||||
coVerify { accountsService.preLogin(email = EMAIL) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login get token fails should return Error`() = runTest {
|
||||
coEvery { accountsService.preLogin(EMAIL) } returns Result.success(PRE_LOGIN_SUCCESS)
|
||||
coEvery { identityService.getToken(EMAIL, PASSWORD_HASH) }
|
||||
coEvery {
|
||||
accountsService.preLogin(email = EMAIL)
|
||||
} returns Result.success(PRE_LOGIN_SUCCESS)
|
||||
coEvery {
|
||||
identityService.getToken(
|
||||
email = EMAIL,
|
||||
passwordHash = PASSWORD_HASH,
|
||||
captchaToken = null,
|
||||
)
|
||||
}
|
||||
.returns(Result.failure(RuntimeException()))
|
||||
val result = repository.login(EMAIL, PASSWORD)
|
||||
val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
|
||||
assertEquals(LoginResult.Error, result)
|
||||
assertEquals(AuthState.Unauthenticated, repository.authStateFlow.value)
|
||||
coVerify { accountsService.preLogin(EMAIL) }
|
||||
coVerify { identityService.getToken(EMAIL, PASSWORD_HASH) }
|
||||
coVerify { accountsService.preLogin(email = EMAIL) }
|
||||
coVerify {
|
||||
identityService.getToken(
|
||||
email = EMAIL,
|
||||
passwordHash = PASSWORD_HASH,
|
||||
captchaToken = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login get token succeeds should return Success and update AuthState`() = runTest {
|
||||
coEvery { accountsService.preLogin(EMAIL) } returns Result.success(PRE_LOGIN_SUCCESS)
|
||||
coEvery { identityService.getToken(email = EMAIL, passwordHash = PASSWORD_HASH) }
|
||||
coEvery {
|
||||
accountsService.preLogin(email = EMAIL)
|
||||
} returns Result.success(PRE_LOGIN_SUCCESS)
|
||||
coEvery {
|
||||
identityService.getToken(
|
||||
email = EMAIL,
|
||||
passwordHash = PASSWORD_HASH,
|
||||
captchaToken = null,
|
||||
)
|
||||
}
|
||||
.returns(Result.success(GetTokenResponseJson.Success(accessToken = ACCESS_TOKEN)))
|
||||
every { authInterceptor.authToken = ACCESS_TOKEN } returns Unit
|
||||
val result = repository.login(EMAIL, PASSWORD)
|
||||
val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
|
||||
assertEquals(LoginResult.Success, result)
|
||||
assertEquals(AuthState.Authenticated(ACCESS_TOKEN), repository.authStateFlow.value)
|
||||
verify { authInterceptor.authToken = ACCESS_TOKEN }
|
||||
coVerify { accountsService.preLogin(EMAIL) }
|
||||
coVerify { identityService.getToken(EMAIL, PASSWORD_HASH) }
|
||||
coVerify { accountsService.preLogin(email = EMAIL) }
|
||||
coVerify {
|
||||
identityService.getToken(
|
||||
email = EMAIL,
|
||||
passwordHash = PASSWORD_HASH,
|
||||
captchaToken = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login get token returns captcha request should return CaptchaRequired`() = runTest {
|
||||
coEvery { accountsService.preLogin(EMAIL) } returns Result.success(PRE_LOGIN_SUCCESS)
|
||||
coEvery { identityService.getToken(email = EMAIL, passwordHash = PASSWORD_HASH) }
|
||||
coEvery {
|
||||
identityService.getToken(
|
||||
email = EMAIL,
|
||||
passwordHash = PASSWORD_HASH,
|
||||
captchaToken = null,
|
||||
)
|
||||
}
|
||||
.returns(Result.success(GetTokenResponseJson.CaptchaRequired(CAPTCHA_KEY)))
|
||||
val result = repository.login(EMAIL, PASSWORD)
|
||||
val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
|
||||
assertEquals(LoginResult.CaptchaRequired(CAPTCHA_KEY), result)
|
||||
assertEquals(AuthState.Unauthenticated, repository.authStateFlow.value)
|
||||
coVerify { accountsService.preLogin(EMAIL) }
|
||||
coVerify { identityService.getToken(EMAIL, PASSWORD_HASH) }
|
||||
coVerify { accountsService.preLogin(email = EMAIL) }
|
||||
coVerify {
|
||||
identityService.getToken(
|
||||
email = EMAIL,
|
||||
passwordHash = PASSWORD_HASH,
|
||||
captchaToken = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setCaptchaCallbackToken should change the value of captchaTokenFlow`() = runTest {
|
||||
repository.captchaTokenResultFlow.test {
|
||||
repository.setCaptchaCallbackTokenResult(CaptchaCallbackTokenResult.Success("mockk"))
|
||||
assertEquals(
|
||||
CaptchaCallbackTokenResult.Success("mockk"),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -4,6 +4,7 @@ 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.CaptchaCallbackTokenResult
|
||||
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
|
||||
@@ -13,6 +14,7 @@ import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
@@ -38,7 +40,9 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
@Test
|
||||
fun `initial state should be correct`() = runTest {
|
||||
val viewModel = LoginViewModel(
|
||||
authRepository = mockk(),
|
||||
authRepository = mockk {
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
},
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
viewModel.stateFlow.test {
|
||||
@@ -59,7 +63,9 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
),
|
||||
)
|
||||
val viewModel = LoginViewModel(
|
||||
authRepository = mockk(),
|
||||
authRepository = mockk {
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
},
|
||||
savedStateHandle = handle,
|
||||
)
|
||||
viewModel.stateFlow.test {
|
||||
@@ -71,7 +77,14 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
fun `LoginButtonClick login returns error should do nothing`() = runTest {
|
||||
// TODO: handle and display errors (BIT-320)
|
||||
val authRepository = mockk<AuthRepository> {
|
||||
coEvery { login(email = "test@gmail.com", password = "") } returns LoginResult.Error
|
||||
coEvery {
|
||||
login(
|
||||
email = "test@gmail.com",
|
||||
password = "",
|
||||
captchaToken = null,
|
||||
)
|
||||
} returns LoginResult.Error
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
}
|
||||
val viewModel = LoginViewModel(
|
||||
authRepository = authRepository,
|
||||
@@ -82,14 +95,15 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
}
|
||||
coVerify {
|
||||
authRepository.login(email = "test@gmail.com", password = "")
|
||||
authRepository.login(email = "test@gmail.com", password = "", captchaToken = null)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `LoginButtonClick login returns success should do nothing`() = runTest {
|
||||
val authRepository = mockk<AuthRepository> {
|
||||
coEvery { login("test@gmail.com", "") } returns LoginResult.Success
|
||||
coEvery { login("test@gmail.com", "", captchaToken = null) } returns LoginResult.Success
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
}
|
||||
val viewModel = LoginViewModel(
|
||||
authRepository = authRepository,
|
||||
@@ -100,7 +114,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
}
|
||||
coVerify {
|
||||
authRepository.login(email = "test@gmail.com", password = "")
|
||||
authRepository.login(email = "test@gmail.com", password = "", captchaToken = null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,8 +128,9 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
.generateIntentForCaptcha()
|
||||
} returns mockkIntent
|
||||
val authRepository = mockk<AuthRepository> {
|
||||
coEvery { login("test@gmail.com", "") } returns
|
||||
coEvery { login("test@gmail.com", "", captchaToken = null) } returns
|
||||
LoginResult.CaptchaRequired(captchaId = "mock_captcha_id")
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
}
|
||||
val viewModel = LoginViewModel(
|
||||
authRepository = authRepository,
|
||||
@@ -127,14 +142,16 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
assertEquals(LoginEvent.NavigateToCaptcha(intent = mockkIntent), awaitItem())
|
||||
}
|
||||
coVerify {
|
||||
authRepository.login(email = "test@gmail.com", password = "")
|
||||
authRepository.login(email = "test@gmail.com", password = "", captchaToken = null)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SingleSignOnClick should do nothing`() = runTest {
|
||||
val viewModel = LoginViewModel(
|
||||
authRepository = mockk(),
|
||||
authRepository = mockk {
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
},
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
@@ -146,7 +163,9 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
@Test
|
||||
fun `NotYouButtonClick should emit NavigateToLanding`() = runTest {
|
||||
val viewModel = LoginViewModel(
|
||||
authRepository = mockk(),
|
||||
authRepository = mockk {
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
},
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
@@ -162,7 +181,9 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
fun `PasswordInputChanged should update password input`() = runTest {
|
||||
val input = "input"
|
||||
val viewModel = LoginViewModel(
|
||||
authRepository = mockk(),
|
||||
authRepository = mockk {
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
},
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
@@ -174,6 +195,29 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `captchaTokenFlow success update should trigger a login`() = runTest {
|
||||
val authRepository = mockk<AuthRepository> {
|
||||
every { captchaTokenResultFlow } returns flowOf(
|
||||
CaptchaCallbackTokenResult.Success("token"),
|
||||
)
|
||||
coEvery {
|
||||
login(
|
||||
"test@gmail.com",
|
||||
"",
|
||||
captchaToken = "token",
|
||||
)
|
||||
} returns LoginResult.Success
|
||||
}
|
||||
LoginViewModel(
|
||||
authRepository = authRepository,
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
coVerify {
|
||||
authRepository.login(email = "test@gmail.com", password = "", captchaToken = "token")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_STATE = LoginState(
|
||||
emailAddress = "test@gmail.com",
|
||||
|
||||
Reference in New Issue
Block a user