mirror of
https://github.com/bitwarden/android.git
synced 2026-06-01 10:16:47 -05:00
BIT-817, BIT-991: Add self-hosted/custom environment functionality (#209)
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.environment
|
||||
|
||||
import androidx.compose.ui.test.assert
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertTextEquals
|
||||
import androidx.compose.ui.test.filterToOne
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.isDialog
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
@@ -59,6 +65,49 @@ class EnvironmentScreenTest : BaseComposeTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `error dialog should be shown or hidden according to the state`() {
|
||||
composeTestRule.onNode(isDialog()).assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
shouldShowErrorDialog = true,
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.onNode(isDialog()).assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("An error has occurred.")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onNodeWithText(
|
||||
"One or more of the URLs entered are invalid. " +
|
||||
"Please revise it and try to save again.",
|
||||
)
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onNodeWithText("Ok")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `error dialog OK click should send ErrorDialogDismiss action`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
shouldShowErrorDialog = true,
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Ok")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(EnvironmentAction.ErrorDialogDismiss) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `server URL should change according to the state`() {
|
||||
composeTestRule
|
||||
@@ -195,6 +244,7 @@ class EnvironmentScreenTest : BaseComposeTest() {
|
||||
apiServerUrl = "",
|
||||
identityServerUrl = "",
|
||||
iconsServerUrl = "",
|
||||
shouldShowErrorDialog = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@ package com.x8bit.bitwarden.ui.auth.feature.environment
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
|
||||
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 kotlinx.coroutines.test.runTest
|
||||
@@ -10,8 +14,11 @@ import org.junit.jupiter.api.Test
|
||||
|
||||
class EnvironmentViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val fakeEnvironmentRepository = FakeEnvironmentRepository()
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `initial state should be correct when there is no saved state`() {
|
||||
fun `initial state should be correct when there is no saved state and the current environment is not self-hosted`() {
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(
|
||||
DEFAULT_STATE,
|
||||
@@ -19,9 +26,35 @@ class EnvironmentViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `initial state should be correct when there is no saved state and the current environment is self-hosted`() {
|
||||
val selfHostedEnvironmentUrlData = EnvironmentUrlDataJson(
|
||||
base = "self-hosted-base",
|
||||
api = "self-hosted-api",
|
||||
identity = "self-hosted-identity",
|
||||
icon = "self-hosted-icons",
|
||||
webVault = "self-hosted-web-vault",
|
||||
)
|
||||
fakeEnvironmentRepository.environment = Environment.SelfHosted(
|
||||
environmentUrlData = selfHostedEnvironmentUrlData,
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
serverUrl = "self-hosted-base",
|
||||
webVaultServerUrl = "self-hosted-web-vault",
|
||||
apiServerUrl = "self-hosted-api",
|
||||
identityServerUrl = "self-hosted-identity",
|
||||
iconsServerUrl = "self-hosted-icons",
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct when restoring from the save state handle`() {
|
||||
val savedState = EnvironmentState(
|
||||
val savedState = DEFAULT_STATE.copy(
|
||||
serverUrl = "saved-server",
|
||||
webVaultServerUrl = "saved-web-vault",
|
||||
apiServerUrl = "saved-api",
|
||||
@@ -36,7 +69,7 @@ class EnvironmentViewModelTest : BaseViewModelTest() {
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
EnvironmentState(
|
||||
DEFAULT_STATE.copy(
|
||||
serverUrl = "saved-server",
|
||||
webVaultServerUrl = "saved-web-vault",
|
||||
apiServerUrl = "saved-api",
|
||||
@@ -57,17 +90,149 @@ class EnvironmentViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SaveClick should emit ShowTest`() = runTest {
|
||||
fun `SaveClick should show the error dialog when any URLs are invalid`() = runTest {
|
||||
assertEquals(
|
||||
Environment.Us,
|
||||
fakeEnvironmentRepository.environment,
|
||||
)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(EnvironmentAction.SaveClick)
|
||||
assertEquals(
|
||||
EnvironmentEvent.ShowToast("Not yet implemented.".asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
// Update to valid absolute URL
|
||||
listOf(
|
||||
EnvironmentAction.WebVaultServerUrlChange(
|
||||
webVaultServerUrl = "web vault",
|
||||
),
|
||||
)
|
||||
.forEach { viewModel.trySendAction(it) }
|
||||
|
||||
val initialState = DEFAULT_STATE.copy(webVaultServerUrl = "web vault")
|
||||
assertEquals(
|
||||
initialState,
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
viewModel.actionChannel.trySend(EnvironmentAction.SaveClick)
|
||||
|
||||
assertEquals(
|
||||
initialState.copy(
|
||||
shouldShowErrorDialog = true,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
// The Environment has not been updated
|
||||
assertEquals(
|
||||
Environment.Us,
|
||||
fakeEnvironmentRepository.environment,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `SaveClick should emit NavigateBack and ShowToast and update the environment when all URLs are valid`() =
|
||||
runTest {
|
||||
assertEquals(
|
||||
Environment.Us,
|
||||
fakeEnvironmentRepository.environment,
|
||||
)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
// Update to valid absolute or relative URLs
|
||||
listOf(
|
||||
EnvironmentAction.ServerUrlChange(
|
||||
serverUrl = "https://server-url",
|
||||
),
|
||||
EnvironmentAction.WebVaultServerUrlChange(
|
||||
webVaultServerUrl = "http://web-vault-url",
|
||||
),
|
||||
EnvironmentAction.ApiServerUrlChange(
|
||||
apiServerUrl = "api-url",
|
||||
),
|
||||
EnvironmentAction.IdentityServerUrlChange(
|
||||
identityServerUrl = "identity-url",
|
||||
),
|
||||
EnvironmentAction.IconsServerUrlChange(
|
||||
iconsServerUrl = "icons-url",
|
||||
),
|
||||
)
|
||||
.forEach { viewModel.trySendAction(it) }
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(EnvironmentAction.SaveClick)
|
||||
|
||||
assertEquals(
|
||||
EnvironmentEvent.ShowToast(R.string.environment_saved.asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
assertEquals(
|
||||
EnvironmentEvent.NavigateBack,
|
||||
awaitItem(),
|
||||
)
|
||||
// All the updated URLs should be prefixed with "https://" or "http://"
|
||||
assertEquals(
|
||||
Environment.SelfHosted(
|
||||
environmentUrlData = EnvironmentUrlDataJson(
|
||||
base = "https://server-url",
|
||||
api = "https://api-url",
|
||||
identity = "https://identity-url",
|
||||
icon = "https://icons-url",
|
||||
notifications = null,
|
||||
webVault = "http://web-vault-url",
|
||||
events = null,
|
||||
),
|
||||
),
|
||||
fakeEnvironmentRepository.environment,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `SaveClick should emit NavigateBack and ShowToast and update the environment when some URLs are valid and others are null`() =
|
||||
runTest {
|
||||
assertEquals(
|
||||
Environment.Us,
|
||||
fakeEnvironmentRepository.environment,
|
||||
)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
// Update to valid absolute URL
|
||||
listOf(
|
||||
EnvironmentAction.WebVaultServerUrlChange(
|
||||
webVaultServerUrl = "http://web-vault-url",
|
||||
),
|
||||
)
|
||||
.forEach { viewModel.trySendAction(it) }
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(EnvironmentAction.SaveClick)
|
||||
|
||||
assertEquals(
|
||||
EnvironmentEvent.ShowToast(R.string.environment_saved.asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
assertEquals(
|
||||
EnvironmentEvent.NavigateBack,
|
||||
awaitItem(),
|
||||
)
|
||||
// All the updated URLs should be prefixed with "https://" or "http://"
|
||||
assertEquals(
|
||||
Environment.SelfHosted(
|
||||
environmentUrlData = EnvironmentUrlDataJson(
|
||||
base = "",
|
||||
api = null,
|
||||
identity = null,
|
||||
icon = null,
|
||||
notifications = null,
|
||||
webVault = "http://web-vault-url",
|
||||
events = null,
|
||||
),
|
||||
),
|
||||
fakeEnvironmentRepository.environment,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ServerUrlChange should update the server URL`() {
|
||||
val viewModel = createViewModel()
|
||||
@@ -134,6 +299,7 @@ class EnvironmentViewModelTest : BaseViewModelTest() {
|
||||
savedStateHandle: SavedStateHandle = SavedStateHandle(),
|
||||
): EnvironmentViewModel =
|
||||
EnvironmentViewModel(
|
||||
environmentRepository = fakeEnvironmentRepository,
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
|
||||
@@ -146,6 +312,7 @@ class EnvironmentViewModelTest : BaseViewModelTest() {
|
||||
apiServerUrl = "",
|
||||
identityServerUrl = "",
|
||||
iconsServerUrl = "",
|
||||
shouldShowErrorDialog = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,23 +149,6 @@ class LandingViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `external environment updates should update the selected environment type`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
|
||||
fakeEnvironmentRepository.environment = Environment.Eu
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
selectedEnvironmentType = Environment.Type.EU,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `EnvironmentTypeSelect should update value of selected region for US or EU`() = runTest {
|
||||
val inputEnvironmentType = Environment.Type.EU
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.net.Uri
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
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.util.CaptchaCallbackTokenResult
|
||||
@@ -44,7 +45,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct`() = runTest {
|
||||
fun `initial state should be correct for non-custom Environments`() = runTest {
|
||||
val viewModel = LoginViewModel(
|
||||
authRepository = mockk {
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
@@ -59,6 +60,57 @@ 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,
|
||||
)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
environmentLabel = "".asText(),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@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,
|
||||
)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
environmentLabel = "abc.com".asText(),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should pull from handle when present`() = runTest {
|
||||
val expectedState = DEFAULT_STATE.copy(
|
||||
|
||||
@@ -29,4 +29,42 @@ class StringExtensionTest {
|
||||
assertTrue(it.isValidEmail())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isValidUri should return true for an absolute URL`() {
|
||||
assertTrue("https://abc.com".isValidUri())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isValidUri should return true for an absolute non-URL path`() {
|
||||
assertTrue("file:///abc/com".isValidUri())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isValidUri should return true for a relative URI`() {
|
||||
assertTrue("abc.com".isValidUri())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isValidUri should return false for a blank or empty String`() {
|
||||
listOf(
|
||||
"",
|
||||
" ",
|
||||
)
|
||||
.forEach { badUri ->
|
||||
assertFalse(badUri.isValidUri())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isValidUri should return false when there are invalid characters present`() {
|
||||
listOf(
|
||||
"abc com",
|
||||
"abc<>com",
|
||||
"abc[]com",
|
||||
)
|
||||
.forEach { badUri ->
|
||||
assertFalse(badUri.isValidUri())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user