mirror of
https://github.com/bitwarden/android.git
synced 2026-06-03 03:06:21 -05:00
BIT-330: Implement self-hosting/custom environment UI (#184)
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.environment
|
||||
|
||||
import androidx.compose.ui.test.assertTextEquals
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
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 io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class EnvironmentScreenTest : BaseComposeTest() {
|
||||
private var onNavigateBackCalled = false
|
||||
private val mutableEventFlow = MutableSharedFlow<EnvironmentEvent>(
|
||||
extraBufferCapacity = Int.MAX_VALUE,
|
||||
)
|
||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
private val viewModel = mockk<EnvironmentViewModel>(relaxed = true) {
|
||||
every { eventFlow } returns mutableEventFlow
|
||||
every { stateFlow } returns mutableStateFlow
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
composeTestRule.setContent {
|
||||
EnvironmentScreen(
|
||||
onNavigateBack = { onNavigateBackCalled = true },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateBack event should invoke onNavigateBack`() {
|
||||
mutableEventFlow.tryEmit(EnvironmentEvent.NavigateBack)
|
||||
assertTrue(onNavigateBackCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `close click should send CloseClick`() {
|
||||
composeTestRule.onNodeWithContentDescription("Close").performClick()
|
||||
verify {
|
||||
viewModel.trySendAction(EnvironmentAction.CloseClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `save click should send SaveClick`() {
|
||||
composeTestRule.onNodeWithText("Save").performClick()
|
||||
verify {
|
||||
viewModel.trySendAction(EnvironmentAction.SaveClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `server URL should change according to the state`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Server URL")
|
||||
// Click to focus to see placeholder
|
||||
.performClick()
|
||||
.assertTextEquals("Server URL", "ex. https://bitwarden.company.com", "")
|
||||
|
||||
mutableStateFlow.update { it.copy(serverUrl = "server-url") }
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Server URL")
|
||||
.assertTextEquals("Server URL", "server-url")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `server URL change should send ServerUrlChange`() {
|
||||
composeTestRule.onNodeWithText("Server URL").performTextInput("updated-server-url")
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
EnvironmentAction.ServerUrlChange(serverUrl = "updated-server-url"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `web vault URL should change according to the state`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Web vault server URL")
|
||||
.assertTextEquals("Web vault server URL", "")
|
||||
|
||||
mutableStateFlow.update { it.copy(webVaultServerUrl = "web-vault-url") }
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Web vault server URL")
|
||||
.assertTextEquals("Web vault server URL", "web-vault-url")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `web vault server URL change should send WebVaultServerUrlChange`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Web vault server URL")
|
||||
.performTextInput("updated-web-vault-url")
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
EnvironmentAction.WebVaultServerUrlChange(
|
||||
webVaultServerUrl = "updated-web-vault-url",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `API server URL should change according to the state`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("API server URL")
|
||||
.assertTextEquals("API server URL", "")
|
||||
|
||||
mutableStateFlow.update { it.copy(apiServerUrl = "api-url") }
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("API server URL")
|
||||
.assertTextEquals("API server URL", "api-url")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `API server URL change should send ApiServerUrlChange`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("API server URL")
|
||||
.performTextInput("updated-api-url")
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
EnvironmentAction.ApiServerUrlChange(apiServerUrl = "updated-api-url"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `identity server URL should change according to the state`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Identity server URL")
|
||||
.assertTextEquals("Identity server URL", "")
|
||||
|
||||
mutableStateFlow.update { it.copy(identityServerUrl = "identity-url") }
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Identity server URL")
|
||||
.assertTextEquals("Identity server URL", "identity-url")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `identity server URL change should send IdentityServerUrlChange`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Identity server URL")
|
||||
.performTextInput("updated-identity-url")
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
EnvironmentAction.IdentityServerUrlChange(
|
||||
identityServerUrl = "updated-identity-url",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `icons server URL should change according to the state`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Icons server URL")
|
||||
.assertTextEquals("Icons server URL", "")
|
||||
|
||||
mutableStateFlow.update { it.copy(iconsServerUrl = "icons-url") }
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Icons server URL")
|
||||
.assertTextEquals("Icons server URL", "icons-url")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `icons server URL change should send IconsServerUrlChange`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Icons server URL")
|
||||
.performTextInput("updated-icons-url")
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
EnvironmentAction.IconsServerUrlChange(iconsServerUrl = "updated-icons-url"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val DEFAULT_STATE = EnvironmentState(
|
||||
serverUrl = "",
|
||||
webVaultServerUrl = "",
|
||||
apiServerUrl = "",
|
||||
identityServerUrl = "",
|
||||
iconsServerUrl = "",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.environment
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class EnvironmentViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct when there is no saved state`() {
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(
|
||||
DEFAULT_STATE,
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct when restoring from the save state handle`() {
|
||||
val savedState = EnvironmentState(
|
||||
serverUrl = "saved-server",
|
||||
webVaultServerUrl = "saved-web-vault",
|
||||
apiServerUrl = "saved-api",
|
||||
identityServerUrl = "saved-identity",
|
||||
iconsServerUrl = "saved-icons",
|
||||
)
|
||||
val viewModel = createViewModel(
|
||||
savedStateHandle = SavedStateHandle(
|
||||
initialState = mapOf(
|
||||
"state" to savedState,
|
||||
),
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
EnvironmentState(
|
||||
serverUrl = "saved-server",
|
||||
webVaultServerUrl = "saved-web-vault",
|
||||
apiServerUrl = "saved-api",
|
||||
identityServerUrl = "saved-identity",
|
||||
iconsServerUrl = "saved-icons",
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CloseClick should emit NavigateBack`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(EnvironmentAction.CloseClick)
|
||||
assertEquals(EnvironmentEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SaveClick should emit ShowTest`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(EnvironmentAction.SaveClick)
|
||||
assertEquals(
|
||||
EnvironmentEvent.ShowToast("Not yet implemented.".asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ServerUrlChange should update the server URL`() {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.actionChannel.trySend(
|
||||
EnvironmentAction.ServerUrlChange(serverUrl = "updated-server-url"),
|
||||
)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(serverUrl = "updated-server-url"),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WebVaultServerUrlChange should update the web vault server URL`() {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.actionChannel.trySend(
|
||||
EnvironmentAction.WebVaultServerUrlChange(webVaultServerUrl = "updated-web-vault-url"),
|
||||
)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(webVaultServerUrl = "updated-web-vault-url"),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ApiServerUrlChange should update the API server URL`() {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.actionChannel.trySend(
|
||||
EnvironmentAction.ApiServerUrlChange(apiServerUrl = "updated-api-url"),
|
||||
)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(apiServerUrl = "updated-api-url"),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `IdentityServerUrlChange should update the identity server URL`() {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.actionChannel.trySend(
|
||||
EnvironmentAction.IdentityServerUrlChange(identityServerUrl = "updated-identity-url"),
|
||||
)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(identityServerUrl = "updated-identity-url"),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `IconsServerUrlChange should update the icons server URL`() {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.actionChannel.trySend(
|
||||
EnvironmentAction.IconsServerUrlChange(iconsServerUrl = "updated-icons-url"),
|
||||
)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(iconsServerUrl = "updated-icons-url"),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
//region Helper methods
|
||||
|
||||
private fun createViewModel(
|
||||
savedStateHandle: SavedStateHandle = SavedStateHandle(),
|
||||
): EnvironmentViewModel =
|
||||
EnvironmentViewModel(
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
|
||||
//endregion Helper methods
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_STATE = EnvironmentState(
|
||||
serverUrl = "",
|
||||
webVaultServerUrl = "",
|
||||
apiServerUrl = "",
|
||||
identityServerUrl = "",
|
||||
iconsServerUrl = "",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,7 @@ class LandingScreenTest : BaseComposeTest() {
|
||||
LandingScreen(
|
||||
onNavigateToCreateAccount = {},
|
||||
onNavigateToLogin = { _ -> },
|
||||
onNavigateToEnvironment = {},
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
@@ -67,6 +68,7 @@ class LandingScreenTest : BaseComposeTest() {
|
||||
LandingScreen(
|
||||
onNavigateToCreateAccount = {},
|
||||
onNavigateToLogin = { _ -> },
|
||||
onNavigateToEnvironment = {},
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
@@ -87,6 +89,7 @@ class LandingScreenTest : BaseComposeTest() {
|
||||
LandingScreen(
|
||||
onNavigateToCreateAccount = {},
|
||||
onNavigateToLogin = { _ -> },
|
||||
onNavigateToEnvironment = {},
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
@@ -107,6 +110,7 @@ class LandingScreenTest : BaseComposeTest() {
|
||||
LandingScreen(
|
||||
onNavigateToCreateAccount = {},
|
||||
onNavigateToLogin = { _ -> },
|
||||
onNavigateToEnvironment = {},
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
@@ -128,6 +132,7 @@ class LandingScreenTest : BaseComposeTest() {
|
||||
LandingScreen(
|
||||
onNavigateToCreateAccount = {},
|
||||
onNavigateToLogin = { _ -> },
|
||||
onNavigateToEnvironment = {},
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
@@ -148,6 +153,7 @@ class LandingScreenTest : BaseComposeTest() {
|
||||
LandingScreen(
|
||||
onNavigateToCreateAccount = {},
|
||||
onNavigateToLogin = { _ -> },
|
||||
onNavigateToEnvironment = {},
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
@@ -174,6 +180,7 @@ class LandingScreenTest : BaseComposeTest() {
|
||||
LandingScreen(
|
||||
onNavigateToCreateAccount = {},
|
||||
onNavigateToLogin = { _ -> },
|
||||
onNavigateToEnvironment = {},
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
@@ -194,6 +201,7 @@ class LandingScreenTest : BaseComposeTest() {
|
||||
LandingScreen(
|
||||
onNavigateToCreateAccount = { onNavigateToCreateAccountCalled = true },
|
||||
onNavigateToLogin = { _ -> },
|
||||
onNavigateToEnvironment = {},
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
@@ -217,6 +225,7 @@ class LandingScreenTest : BaseComposeTest() {
|
||||
onNavigateToLogin = { email ->
|
||||
capturedEmail = email
|
||||
},
|
||||
onNavigateToEnvironment = {},
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
@@ -224,6 +233,24 @@ class LandingScreenTest : BaseComposeTest() {
|
||||
assertEquals(testEmail, capturedEmail)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToEnvironment event should call onNavigateToEvent`() {
|
||||
var onNavigateToEnvironmentCalled = false
|
||||
val viewModel = mockk<LandingViewModel>(relaxed = true) {
|
||||
every { eventFlow } returns flowOf(LandingEvent.NavigateToEnvironment)
|
||||
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
LandingScreen(
|
||||
onNavigateToCreateAccount = { },
|
||||
onNavigateToLogin = { _ -> },
|
||||
onNavigateToEnvironment = { onNavigateToEnvironmentCalled = true },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
assertTrue(onNavigateToEnvironmentCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `selecting environment should send EnvironmentOptionSelect action`() {
|
||||
val selectedEnvironment = Environment.Eu
|
||||
@@ -236,6 +263,7 @@ class LandingScreenTest : BaseComposeTest() {
|
||||
LandingScreen(
|
||||
onNavigateToCreateAccount = {},
|
||||
onNavigateToLogin = { _ -> },
|
||||
onNavigateToEnvironment = {},
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
@@ -272,6 +300,7 @@ class LandingScreenTest : BaseComposeTest() {
|
||||
LandingScreen(
|
||||
onNavigateToCreateAccount = {},
|
||||
onNavigateToLogin = { _ -> },
|
||||
onNavigateToEnvironment = {},
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
@@ -321,6 +350,7 @@ class LandingScreenTest : BaseComposeTest() {
|
||||
LandingScreen(
|
||||
onNavigateToCreateAccount = {},
|
||||
onNavigateToLogin = { _ -> },
|
||||
onNavigateToEnvironment = {},
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user