mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
BIT-817, BIT-991: Add self-hosted/custom environment functionality (#209)
This commit is contained in:
parent
c67fa04d1c
commit
3755b0ed07
9 changed files with 458 additions and 41 deletions
|
@ -35,6 +35,9 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
|
||||
|
@ -61,6 +64,20 @@ fun EnvironmentScreen(
|
|||
}
|
||||
}
|
||||
|
||||
BitwardenBasicDialog(
|
||||
visibilityState = if (state.shouldShowErrorDialog) {
|
||||
BasicDialogState.Shown(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.environment_page_urls_error.asText(),
|
||||
)
|
||||
} else {
|
||||
BasicDialogState.Hidden
|
||||
},
|
||||
onDismissRequest = remember(viewModel) {
|
||||
{ viewModel.trySendAction(EnvironmentAction.ErrorDialogDismiss) }
|
||||
},
|
||||
)
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
|
|
|
@ -3,9 +3,15 @@ package com.x8bit.bitwarden.ui.auth.feature.environment
|
|||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.util.orNullIfBlank
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.isValidUri
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
@ -20,17 +26,26 @@ private const val KEY_STATE = "state"
|
|||
*/
|
||||
@HiltViewModel
|
||||
class EnvironmentViewModel @Inject constructor(
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<EnvironmentState, EnvironmentEvent, EnvironmentAction>(
|
||||
// TODO: Pull non-saved state from EnvironmentRepository (BIT-817)
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: EnvironmentState(
|
||||
serverUrl = "",
|
||||
webVaultServerUrl = "",
|
||||
apiServerUrl = "",
|
||||
identityServerUrl = "",
|
||||
iconsServerUrl = "",
|
||||
),
|
||||
initialState = savedStateHandle[KEY_STATE] ?: run {
|
||||
val environmentUrlData = when (val environment = environmentRepository.environment) {
|
||||
Environment.Us,
|
||||
Environment.Eu,
|
||||
-> EnvironmentUrlDataJson(base = "")
|
||||
|
||||
is Environment.SelfHosted -> environment.environmentUrlData
|
||||
}
|
||||
EnvironmentState(
|
||||
serverUrl = environmentUrlData.base,
|
||||
webVaultServerUrl = environmentUrlData.webVault.orEmpty(),
|
||||
apiServerUrl = environmentUrlData.api.orEmpty(),
|
||||
identityServerUrl = environmentUrlData.identity.orEmpty(),
|
||||
iconsServerUrl = environmentUrlData.icon.orEmpty(),
|
||||
shouldShowErrorDialog = false,
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
||||
init {
|
||||
|
@ -44,6 +59,7 @@ class EnvironmentViewModel @Inject constructor(
|
|||
override fun handleAction(action: EnvironmentAction): Unit = when (action) {
|
||||
is EnvironmentAction.CloseClick -> handleCloseClickAction()
|
||||
is EnvironmentAction.SaveClick -> handleSaveClickAction()
|
||||
is EnvironmentAction.ErrorDialogDismiss -> handleErrorDialogDismiss()
|
||||
is EnvironmentAction.ServerUrlChange -> handleServerUrlChangeAction(action)
|
||||
is EnvironmentAction.WebVaultServerUrlChange -> handleWebVaultServerUrlChangeAction(action)
|
||||
is EnvironmentAction.ApiServerUrlChange -> handleApiServerUrlChangeAction(action)
|
||||
|
@ -56,8 +72,48 @@ class EnvironmentViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleSaveClickAction() {
|
||||
// TODO: Save custom value (BIT-817)
|
||||
sendEvent(EnvironmentEvent.ShowToast("Not yet implemented.".asText()))
|
||||
val state = mutableStateFlow.value
|
||||
|
||||
val urlsAreAllNullOrValid = listOf(
|
||||
state.serverUrl,
|
||||
state.webVaultServerUrl,
|
||||
state.apiServerUrl,
|
||||
state.identityServerUrl,
|
||||
state.iconsServerUrl,
|
||||
)
|
||||
.map { it.orNullIfBlank() }
|
||||
.all { url ->
|
||||
url == null || url.isValidUri()
|
||||
}
|
||||
|
||||
if (!urlsAreAllNullOrValid) {
|
||||
mutableStateFlow.update { it.copy(shouldShowErrorDialog = true) }
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure all non-null/non-empty values have "http(s)://" prefixed.
|
||||
val updatedServerUrl = state.serverUrl.prefixHttpsIfNecessaryOrNull() ?: ""
|
||||
val updatedWebVaultServerUrl = state.webVaultServerUrl.prefixHttpsIfNecessaryOrNull()
|
||||
val updatedApiServerUrl = state.apiServerUrl.prefixHttpsIfNecessaryOrNull()
|
||||
val updatedIdentityServerUrl = state.identityServerUrl.prefixHttpsIfNecessaryOrNull()
|
||||
val updatedIconsServerUrl = state.iconsServerUrl.prefixHttpsIfNecessaryOrNull()
|
||||
|
||||
environmentRepository.environment = Environment.SelfHosted(
|
||||
environmentUrlData = EnvironmentUrlDataJson(
|
||||
base = updatedServerUrl,
|
||||
api = updatedApiServerUrl,
|
||||
identity = updatedIdentityServerUrl,
|
||||
icon = updatedIconsServerUrl,
|
||||
webVault = updatedWebVaultServerUrl,
|
||||
),
|
||||
)
|
||||
|
||||
sendEvent(EnvironmentEvent.ShowToast(message = R.string.environment_saved.asText()))
|
||||
sendEvent(EnvironmentEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleErrorDialogDismiss() {
|
||||
mutableStateFlow.update { it.copy(shouldShowErrorDialog = false) }
|
||||
}
|
||||
|
||||
private fun handleServerUrlChangeAction(
|
||||
|
@ -111,6 +167,7 @@ data class EnvironmentState(
|
|||
val apiServerUrl: String,
|
||||
val identityServerUrl: String,
|
||||
val iconsServerUrl: String,
|
||||
val shouldShowErrorDialog: Boolean,
|
||||
) : Parcelable
|
||||
|
||||
/**
|
||||
|
@ -144,6 +201,11 @@ sealed class EnvironmentAction {
|
|||
*/
|
||||
data object SaveClick : EnvironmentAction()
|
||||
|
||||
/**
|
||||
* User dismissed an error dialog.
|
||||
*/
|
||||
data object ErrorDialogDismiss : EnvironmentAction()
|
||||
|
||||
/**
|
||||
* Indicates that the overall server URL has changed.
|
||||
*/
|
||||
|
@ -179,3 +241,14 @@ sealed class EnvironmentAction {
|
|||
val iconsServerUrl: String,
|
||||
) : EnvironmentAction()
|
||||
}
|
||||
|
||||
/**
|
||||
* If the given [String] is a valid URI, "https://" will be appended if it is not already present.
|
||||
* Otherwise `null` will be returned.
|
||||
*/
|
||||
private fun String.prefixHttpsIfNecessaryOrNull(): String? =
|
||||
when {
|
||||
this.isBlank() || !this.isValidUri() -> null
|
||||
"http://" in this || "https://" in this -> this
|
||||
else -> "https://$this"
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
|||
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.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
|
@ -23,6 +24,7 @@ import kotlinx.coroutines.flow.onEach
|
|||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.net.URI
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val KEY_STATE = "state"
|
||||
|
@ -41,7 +43,7 @@ class LoginViewModel @Inject constructor(
|
|||
emailAddress = LoginArgs(savedStateHandle).emailAddress,
|
||||
isLoginButtonEnabled = true,
|
||||
passwordInput = "",
|
||||
environmentLabel = environmentRepository.environment.label,
|
||||
environmentLabel = environmentRepository.environment.labelOrBaseUrlHost,
|
||||
loadingDialogState = LoadingDialogState.Hidden,
|
||||
errorDialogState = BasicDialogState.Hidden,
|
||||
captchaToken = LoginArgs(savedStateHandle).captchaToken,
|
||||
|
@ -280,3 +282,23 @@ sealed class LoginAction {
|
|||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the [Environment.label] for non-custom values. Otherwise returns the host of the
|
||||
* custom base URL.
|
||||
*/
|
||||
private val Environment.labelOrBaseUrlHost: Text
|
||||
get() = when (this) {
|
||||
is Environment.Us -> this.label
|
||||
is Environment.Eu -> this.label
|
||||
is Environment.SelfHosted -> {
|
||||
// Grab the domain
|
||||
// Ex:
|
||||
// - "https://www.abc.com/path-1/path-1" -> "www.abc.com"
|
||||
URI
|
||||
.create(this.environmentUrlData.base)
|
||||
.host
|
||||
.orEmpty()
|
||||
.asText()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,23 @@
|
|||
package com.x8bit.bitwarden.ui.platform.base.util
|
||||
|
||||
import java.net.URI
|
||||
|
||||
/**
|
||||
* Whether or not string is a valid email address.
|
||||
*
|
||||
* This just checks if the string contains the "@" symbol.
|
||||
*/
|
||||
fun String.isValidEmail(): Boolean = contains("@")
|
||||
|
||||
/**
|
||||
* Returns `true` if the given [String] is a non-blank, valid URI and `false` otherwise.
|
||||
*
|
||||
* Note that this does not require the URI to contain a URL scheme like `https://`.
|
||||
*/
|
||||
fun String.isValidUri(): Boolean =
|
||||
try {
|
||||
URI.create(this)
|
||||
this.isNotBlank()
|
||||
} catch (_: IllegalArgumentException) {
|
||||
false
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue