BIT-330: Implement self-hosting/custom environment UI (#184)

This commit is contained in:
Brian Yencho
2023-10-31 11:12:01 -05:00
committed by GitHub
parent 9dca3fb38f
commit d14fe9b647
11 changed files with 825 additions and 4 deletions

View File

@@ -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 = "",
)
}
}

View File

@@ -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 = "",
)
}
}

View File

@@ -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,
)
}