From b61c796f7bbd3aeba9b0484b69799593756bf087 Mon Sep 17 00:00:00 2001 From: Brian Yencho Date: Wed, 6 Dec 2023 09:23:09 -0600 Subject: [PATCH] Simplify LoginScreen / LoginViewModel tests (#330) --- .../ui/auth/feature/login/LoginScreenTest.kt | 185 +++----------- .../auth/feature/login/LoginViewModelTest.kt | 227 ++++++------------ 2 files changed, 108 insertions(+), 304 deletions(-) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreenTest.kt index 690b5d3ae..2ede57645 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreenTest.kt @@ -21,35 +21,38 @@ import io.mockk.every import io.mockk.mockk import io.mockk.verify import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.flowOf +import org.junit.Before import org.junit.Test class LoginScreenTest : BaseComposeTest() { + private val intentHandler = mockk(relaxed = true) { + every { startCustomTabsActivity(any()) } returns Unit + } + private var onNavigateBackCalled = false + private val mutableEventFlow = MutableSharedFlow( + extraBufferCapacity = Int.MAX_VALUE, + ) + private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE) + private val viewModel = mockk(relaxed = true) { + every { eventFlow } returns mutableEventFlow + every { stateFlow } returns mutableStateFlow + } + + @Before + fun setUp() { + composeTestRule.setContent { + LoginScreen( + onNavigateBack = { onNavigateBackCalled = true }, + viewModel = viewModel, + intentHandler = intentHandler, + ) + } + } @Test fun `close button click should send CloseButtonClick action`() { - val viewModel = mockk(relaxed = true) { - every { eventFlow } returns emptyFlow() - every { stateFlow } returns MutableStateFlow( - LoginState( - emailAddress = "", - captchaToken = null, - isLoginButtonEnabled = false, - passwordInput = "", - environmentLabel = "".asText(), - loadingDialogState = LoadingDialogState.Hidden, - errorDialogState = BasicDialogState.Hidden, - ), - ) - } - composeTestRule.setContent { - LoginScreen( - onNavigateBack = {}, - viewModel = viewModel, - ) - } composeTestRule.onNodeWithContentDescription("Close").performClick() verify { viewModel.trySendAction(LoginAction.CloseButtonClick) @@ -58,26 +61,6 @@ class LoginScreenTest : BaseComposeTest() { @Test fun `Not you text click should send NotYouButtonClick action`() { - val viewModel = mockk(relaxed = true) { - every { eventFlow } returns emptyFlow() - every { stateFlow } returns MutableStateFlow( - LoginState( - emailAddress = "", - captchaToken = null, - isLoginButtonEnabled = false, - passwordInput = "", - environmentLabel = "".asText(), - loadingDialogState = LoadingDialogState.Hidden, - errorDialogState = BasicDialogState.Hidden, - ), - ) - } - composeTestRule.setContent { - LoginScreen( - onNavigateBack = {}, - viewModel = viewModel, - ) - } composeTestRule.onNodeWithText("Not you?").performScrollTo().performClick() verify { viewModel.trySendAction(LoginAction.NotYouButtonClick) @@ -86,26 +69,6 @@ class LoginScreenTest : BaseComposeTest() { @Test fun `master password hint text click should send MasterPasswordHintClick action`() { - val viewModel = mockk(relaxed = true) { - every { eventFlow } returns emptyFlow() - every { stateFlow } returns MutableStateFlow( - LoginState( - emailAddress = "", - captchaToken = null, - isLoginButtonEnabled = false, - passwordInput = "", - environmentLabel = "".asText(), - loadingDialogState = LoadingDialogState.Hidden, - errorDialogState = BasicDialogState.Hidden, - ), - ) - } - composeTestRule.setContent { - LoginScreen( - onNavigateBack = {}, - viewModel = viewModel, - ) - } composeTestRule.onNodeWithText("Get your master password hint").performClick() verify { viewModel.trySendAction(LoginAction.MasterPasswordHintClick) @@ -114,26 +77,6 @@ class LoginScreenTest : BaseComposeTest() { @Test fun `master password hint option menu click should send MasterPasswordHintClick action`() { - val viewModel = mockk(relaxed = true) { - every { eventFlow } returns emptyFlow() - every { stateFlow } returns MutableStateFlow( - LoginState( - emailAddress = "", - captchaToken = null, - isLoginButtonEnabled = false, - passwordInput = "", - environmentLabel = "".asText(), - loadingDialogState = LoadingDialogState.Hidden, - errorDialogState = BasicDialogState.Hidden, - ), - ) - } - composeTestRule.setContent { - LoginScreen( - onNavigateBack = {}, - viewModel = viewModel, - ) - } // Confirm dropdown version of item is absent composeTestRule .onAllNodesWithText("Get your master password hint") @@ -154,26 +97,6 @@ class LoginScreenTest : BaseComposeTest() { @Test fun `password input change should send PasswordInputChanged action`() { val input = "input" - val viewModel = mockk(relaxed = true) { - every { eventFlow } returns emptyFlow() - every { stateFlow } returns MutableStateFlow( - LoginState( - emailAddress = "", - captchaToken = null, - isLoginButtonEnabled = false, - passwordInput = "", - environmentLabel = "".asText(), - loadingDialogState = LoadingDialogState.Hidden, - errorDialogState = BasicDialogState.Hidden, - ), - ) - } - composeTestRule.setContent { - LoginScreen( - onNavigateBack = {}, - viewModel = viewModel, - ) - } composeTestRule.onNodeWithText("Master password").performTextInput(input) verify { viewModel.trySendAction(LoginAction.PasswordInputChanged(input)) @@ -182,57 +105,25 @@ class LoginScreenTest : BaseComposeTest() { @Test fun `NavigateBack should call onNavigateBack`() { - var onNavigateBackCalled = false - val viewModel = mockk(relaxed = true) { - every { eventFlow } returns flowOf(LoginEvent.NavigateBack) - every { stateFlow } returns MutableStateFlow( - LoginState( - emailAddress = "", - captchaToken = null, - isLoginButtonEnabled = false, - passwordInput = "", - environmentLabel = "".asText(), - loadingDialogState = LoadingDialogState.Hidden, - errorDialogState = BasicDialogState.Hidden, - ), - ) - } - composeTestRule.setContent { - LoginScreen( - onNavigateBack = { onNavigateBackCalled = true }, - viewModel = viewModel, - ) - } + mutableEventFlow.tryEmit(LoginEvent.NavigateBack) assertTrue(onNavigateBackCalled) } @Test fun `NavigateToCaptcha should call intentHandler startCustomTabsActivity`() { - val intentHandler = mockk(relaxed = true) { - every { startCustomTabsActivity(any()) } returns Unit - } val mockUri = mockk() - val viewModel = mockk(relaxed = true) { - every { eventFlow } returns flowOf(LoginEvent.NavigateToCaptcha(mockUri)) - every { stateFlow } returns MutableStateFlow( - LoginState( - emailAddress = "", - captchaToken = null, - isLoginButtonEnabled = false, - passwordInput = "", - environmentLabel = "".asText(), - loadingDialogState = LoadingDialogState.Hidden, - errorDialogState = BasicDialogState.Hidden, - ), - ) - } - composeTestRule.setContent { - LoginScreen( - onNavigateBack = {}, - intentHandler = intentHandler, - viewModel = viewModel, - ) - } + mutableEventFlow.tryEmit(LoginEvent.NavigateToCaptcha(mockUri)) verify { intentHandler.startCustomTabsActivity(mockUri) } } } + +private val DEFAULT_STATE = + LoginState( + emailAddress = "", + captchaToken = null, + isLoginButtonEnabled = false, + passwordInput = "", + environmentLabel = "".asText(), + loadingDialogState = LoadingDialogState.Hidden, + errorDialogState = BasicDialogState.Hidden, + ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt index f73b4ff35..2d981f47a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt @@ -7,10 +7,11 @@ import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.LoginResult +import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha -import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.model.Environment +import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.components.BasicDialogState @@ -21,7 +22,8 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.unmockkStatic -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals @@ -33,6 +35,15 @@ class LoginViewModelTest : BaseViewModelTest() { private val savedStateHandle = SavedStateHandle().also { it["email_address"] = "test@gmail.com" } + private val mutableCaptchaTokenResultFlow = MutableSharedFlow( + extraBufferCapacity = Int.MAX_VALUE, + ) + private val mutableStateFlow = MutableStateFlow(null) + private val authRepository: AuthRepository = mockk(relaxed = true) { + every { captchaTokenResultFlow } returns mutableCaptchaTokenResultFlow + every { userStateFlow } returns mutableStateFlow + } + private val fakeEnvironmentRepository = FakeEnvironmentRepository() @BeforeEach fun setUp() { @@ -46,15 +57,7 @@ class LoginViewModelTest : BaseViewModelTest() { @Test fun `initial state should be correct for non-custom Environments`() = runTest { - val viewModel = LoginViewModel( - authRepository = mockk { - every { captchaTokenResultFlow } returns flowOf() - }, - environmentRepository = mockk { - every { environment } returns Environment.Us - }, - savedStateHandle = savedStateHandle, - ) + val viewModel = createViewModel() viewModel.stateFlow.test { assertEquals(DEFAULT_STATE, awaitItem()) } @@ -62,19 +65,12 @@ class LoginViewModelTest : BaseViewModelTest() { @Test fun `initial state should be correct for custom Environments with empty base URLs`() = runTest { - val viewModel = LoginViewModel( - authRepository = mockk { - every { captchaTokenResultFlow } returns flowOf() - }, - environmentRepository = mockk { - every { environment } returns Environment.SelfHosted( - environmentUrlData = EnvironmentUrlDataJson( - base = "", - ), - ) - }, - savedStateHandle = savedStateHandle, + fakeEnvironmentRepository.environment = Environment.SelfHosted( + environmentUrlData = EnvironmentUrlDataJson( + base = "", + ), ) + val viewModel = createViewModel() viewModel.stateFlow.test { assertEquals( DEFAULT_STATE.copy( @@ -88,19 +84,12 @@ class LoginViewModelTest : BaseViewModelTest() { @Test fun `initial state should be correct for custom Environments with non-empty base URLs`() = runTest { - val viewModel = LoginViewModel( - authRepository = mockk { - every { captchaTokenResultFlow } returns flowOf() - }, - environmentRepository = mockk { - every { environment } returns Environment.SelfHosted( - environmentUrlData = EnvironmentUrlDataJson( - base = "https://abc.com/path-1/path-2", - ), - ) - }, - savedStateHandle = savedStateHandle, + fakeEnvironmentRepository.environment = Environment.SelfHosted( + environmentUrlData = EnvironmentUrlDataJson( + base = "https://abc.com/path-1/path-2", + ), ) + val viewModel = createViewModel() viewModel.stateFlow.test { assertEquals( DEFAULT_STATE.copy( @@ -117,21 +106,8 @@ class LoginViewModelTest : BaseViewModelTest() { passwordInput = "input", isLoginButtonEnabled = true, ) - val handle = SavedStateHandle( - mapOf( - "email_address" to "test@gmail.com", - "state" to expectedState, - ), - ) - val viewModel = LoginViewModel( - authRepository = mockk { - every { captchaTokenResultFlow } returns flowOf() - }, - environmentRepository = mockk { - every { environment } returns Environment.Us - }, - savedStateHandle = handle, - ) + savedStateHandle["state"] = expectedState + val viewModel = createViewModel() viewModel.stateFlow.test { assertEquals(expectedState, awaitItem()) } @@ -139,15 +115,7 @@ class LoginViewModelTest : BaseViewModelTest() { @Test fun `CloseButtonClick should emit NavigateBack`() = runTest { - val viewModel = LoginViewModel( - authRepository = mockk { - every { captchaTokenResultFlow } returns flowOf() - }, - environmentRepository = mockk { - every { environment } returns Environment.Us - }, - savedStateHandle = savedStateHandle, - ) + val viewModel = createViewModel() viewModel.eventFlow.test { viewModel.actionChannel.trySend(LoginAction.CloseButtonClick) assertEquals( @@ -159,24 +127,14 @@ class LoginViewModelTest : BaseViewModelTest() { @Test fun `LoginButtonClick login returns error should update errorDialogState`() = runTest { - val authRepository = mockk { - coEvery { - login( - email = "test@gmail.com", - password = "", - captchaToken = null, - ) - } returns LoginResult.Error(errorMessage = "mock_error") - every { captchaTokenResultFlow } returns flowOf() - } - val environmentRepository = mockk { - every { environment } returns Environment.Us - } - val viewModel = LoginViewModel( - authRepository = authRepository, - environmentRepository = environmentRepository, - savedStateHandle = savedStateHandle, - ) + coEvery { + authRepository.login( + email = "test@gmail.com", + password = "", + captchaToken = null, + ) + } returns LoginResult.Error(errorMessage = "mock_error") + val viewModel = createViewModel() viewModel.stateFlow.test { assertEquals(DEFAULT_STATE, awaitItem()) viewModel.trySendAction(LoginAction.LoginButtonClick) @@ -206,19 +164,14 @@ class LoginViewModelTest : BaseViewModelTest() { @Test fun `LoginButtonClick login returns success should update loadingDialogState`() = runTest { - val authRepository = mockk { - coEvery { - login("test@gmail.com", "", captchaToken = null) - } returns LoginResult.Success - every { captchaTokenResultFlow } returns flowOf() - } - val viewModel = LoginViewModel( - authRepository = authRepository, - environmentRepository = mockk { - every { environment } returns Environment.Us - }, - savedStateHandle = savedStateHandle, - ) + coEvery { + authRepository.login( + email = "test@gmail.com", + password = "", + captchaToken = null, + ) + } returns LoginResult.Success + val viewModel = createViewModel() viewModel.stateFlow.test { assertEquals(DEFAULT_STATE, awaitItem()) viewModel.trySendAction(LoginAction.LoginButtonClick) @@ -247,18 +200,14 @@ class LoginViewModelTest : BaseViewModelTest() { every { generateUriForCaptcha(captchaId = "mock_captcha_id") } returns mockkUri - val authRepository = mockk { - coEvery { login("test@gmail.com", "", captchaToken = null) } returns - LoginResult.CaptchaRequired(captchaId = "mock_captcha_id") - every { captchaTokenResultFlow } returns flowOf() - } - val viewModel = LoginViewModel( - authRepository = authRepository, - environmentRepository = mockk { - every { environment } returns Environment.Us - }, - savedStateHandle = savedStateHandle, - ) + coEvery { + authRepository.login( + email = "test@gmail.com", + password = "", + captchaToken = null, + ) + } returns LoginResult.CaptchaRequired(captchaId = "mock_captcha_id") + val viewModel = createViewModel() viewModel.eventFlow.test { viewModel.actionChannel.trySend(LoginAction.LoginButtonClick) assertEquals(DEFAULT_STATE, viewModel.stateFlow.value) @@ -271,15 +220,7 @@ class LoginViewModelTest : BaseViewModelTest() { @Test fun `MasterPasswordHintClick should emit ShowToast`() = runTest { - val viewModel = LoginViewModel( - authRepository = mockk { - every { captchaTokenResultFlow } returns flowOf() - }, - environmentRepository = mockk { - every { environment } returns Environment.Us - }, - savedStateHandle = savedStateHandle, - ) + val viewModel = createViewModel() viewModel.eventFlow.test { viewModel.actionChannel.trySend(LoginAction.MasterPasswordHintClick) assertEquals(DEFAULT_STATE, viewModel.stateFlow.value) @@ -292,15 +233,7 @@ class LoginViewModelTest : BaseViewModelTest() { @Test fun `SingleSignOnClick should emit ShowToast`() = runTest { - val viewModel = LoginViewModel( - authRepository = mockk { - every { captchaTokenResultFlow } returns flowOf() - }, - environmentRepository = mockk { - every { environment } returns Environment.Us - }, - savedStateHandle = savedStateHandle, - ) + val viewModel = createViewModel() viewModel.eventFlow.test { viewModel.actionChannel.trySend(LoginAction.SingleSignOnClick) assertEquals(DEFAULT_STATE, viewModel.stateFlow.value) @@ -313,15 +246,7 @@ class LoginViewModelTest : BaseViewModelTest() { @Test fun `NotYouButtonClick should emit NavigateBack`() = runTest { - val viewModel = LoginViewModel( - authRepository = mockk { - every { captchaTokenResultFlow } returns flowOf() - }, - environmentRepository = mockk { - every { environment } returns Environment.Us - }, - savedStateHandle = savedStateHandle, - ) + val viewModel = createViewModel() viewModel.eventFlow.test { viewModel.actionChannel.trySend(LoginAction.NotYouButtonClick) assertEquals( @@ -334,15 +259,7 @@ class LoginViewModelTest : BaseViewModelTest() { @Test fun `PasswordInputChanged should update password input`() = runTest { val input = "input" - val viewModel = LoginViewModel( - authRepository = mockk { - every { captchaTokenResultFlow } returns flowOf() - }, - environmentRepository = mockk { - every { environment } returns Environment.Us - }, - savedStateHandle = savedStateHandle, - ) + val viewModel = createViewModel() viewModel.eventFlow.test { viewModel.actionChannel.trySend(LoginAction.PasswordInputChanged(input)) assertEquals( @@ -354,31 +271,27 @@ class LoginViewModelTest : BaseViewModelTest() { @Test fun `captchaTokenFlow success update should trigger a login`() = runTest { - val authRepository = mockk { - every { captchaTokenResultFlow } returns flowOf( - CaptchaCallbackTokenResult.Success("token"), + coEvery { + authRepository.login( + email = "test@gmail.com", + password = "", + captchaToken = "token", ) - coEvery { - login( - "test@gmail.com", - "", - captchaToken = "token", - ) - } returns LoginResult.Success - } - val environmentRepository = mockk { - every { environment } returns Environment.Us - } - LoginViewModel( - authRepository = authRepository, - environmentRepository = environmentRepository, - savedStateHandle = savedStateHandle, - ) + } returns LoginResult.Success + createViewModel() + mutableCaptchaTokenResultFlow.tryEmit(CaptchaCallbackTokenResult.Success("token")) coVerify { authRepository.login(email = "test@gmail.com", password = "", captchaToken = "token") } } + private fun createViewModel(): LoginViewModel = + LoginViewModel( + authRepository = authRepository, + environmentRepository = fakeEnvironmentRepository, + savedStateHandle = savedStateHandle, + ) + companion object { private val DEFAULT_STATE = LoginState( emailAddress = "test@gmail.com",