mirror of
https://github.com/bitwarden/android.git
synced 2026-04-27 19:38:42 -05:00
[PM-28470] Implement revoke from organization (#6383)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.RemovePasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RequestOtpResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RevokeFromOrganizationResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
@@ -402,4 +403,11 @@ interface AuthRepository :
|
||||
suspend fun leaveOrganization(
|
||||
organizationId: String,
|
||||
): LeaveOrganizationResult
|
||||
|
||||
/**
|
||||
* Revokes self from the organization that matches the given [organizationId]
|
||||
*/
|
||||
suspend fun revokeFromOrganization(
|
||||
organizationId: String,
|
||||
): RevokeFromOrganizationResult
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.RemovePasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RequestOtpResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RevokeFromOrganizationResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
@@ -1393,6 +1394,14 @@ class AuthRepositoryImpl(
|
||||
onFailure = { LeaveOrganizationResult.Error(error = it) },
|
||||
)
|
||||
|
||||
override suspend fun revokeFromOrganization(
|
||||
organizationId: String,
|
||||
): RevokeFromOrganizationResult =
|
||||
organizationService.revokeFromOrganization(organizationId).fold(
|
||||
onSuccess = { RevokeFromOrganizationResult.Success },
|
||||
onFailure = { RevokeFromOrganizationResult.Error(error = it) },
|
||||
)
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private suspend fun validatePasswordAgainstPolicy(
|
||||
password: String,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
/**
|
||||
* Models result of leaving an organization.
|
||||
*/
|
||||
sealed class RevokeFromOrganizationResult {
|
||||
/**
|
||||
* Revoke from organization succeeded.
|
||||
*/
|
||||
data object Success : RevokeFromOrganizationResult()
|
||||
|
||||
/**
|
||||
* There was an error revoking from the organization.
|
||||
*/
|
||||
data class Error(
|
||||
val error: Throwable?,
|
||||
) : RevokeFromOrganizationResult()
|
||||
}
|
||||
@@ -29,4 +29,10 @@ interface VaultMigrationManager {
|
||||
userId: String,
|
||||
organizationId: String,
|
||||
): MigratePersonalVaultResult
|
||||
|
||||
/**
|
||||
* Clears the migration state, setting it to [VaultMigrationData.NoMigrationRequired].
|
||||
* This should be called when the user declines migration or leaves the organization.
|
||||
*/
|
||||
fun clearMigrationState()
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ class VaultMigrationManagerImpl(
|
||||
.getOrElse { return MigratePersonalVaultResult.Failure(it) }
|
||||
|
||||
if (personalCiphers.isEmpty()) {
|
||||
mutableVaultMigrationDataStateFlow.update { VaultMigrationData.NoMigrationRequired }
|
||||
clearMigrationState()
|
||||
return MigratePersonalVaultResult.Success
|
||||
}
|
||||
|
||||
@@ -212,10 +212,14 @@ class VaultMigrationManagerImpl(
|
||||
collectionIds = listOfNotNull(defaultUserCollection.id),
|
||||
).getOrElse { return MigratePersonalVaultResult.Failure(it) }
|
||||
|
||||
mutableVaultMigrationDataStateFlow.update { VaultMigrationData.NoMigrationRequired }
|
||||
clearMigrationState()
|
||||
return MigratePersonalVaultResult.Success
|
||||
}
|
||||
|
||||
override fun clearMigrationState() {
|
||||
mutableVaultMigrationDataStateFlow.update { VaultMigrationData.NoMigrationRequired }
|
||||
}
|
||||
|
||||
private fun getDefaultUserCollection(
|
||||
vaultData: VaultData,
|
||||
organizationId: String,
|
||||
|
||||
@@ -57,6 +57,8 @@ import com.x8bit.bitwarden.ui.vault.feature.importlogins.navigateToImportLoginsS
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.navigateToVaultItem
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.vaultItemDestination
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.vaultItemListingDestinationAsRoot
|
||||
import com.x8bit.bitwarden.ui.vault.feature.leaveorganization.leaveOrganizationDestination
|
||||
import com.x8bit.bitwarden.ui.vault.feature.leaveorganization.navigateToLeaveOrganization
|
||||
import com.x8bit.bitwarden.ui.vault.feature.manualcodeentry.navigateToManualCodeEntryScreen
|
||||
import com.x8bit.bitwarden.ui.vault.feature.manualcodeentry.vaultManualCodeEntryDestination
|
||||
import com.x8bit.bitwarden.ui.vault.feature.migratetomyitems.migrateToMyItemsDestination
|
||||
@@ -267,7 +269,16 @@ fun NavGraphBuilder.vaultUnlockedGraph(
|
||||
|
||||
migrateToMyItemsDestination(
|
||||
onNavigateToVault = { navController.popBackStack() },
|
||||
onNavigateToLeaveOrganization = { },
|
||||
onNavigateToLeaveOrganization = { organizationId, organizationName ->
|
||||
navController.navigateToLeaveOrganization(
|
||||
organizationId = organizationId,
|
||||
organizationName = organizationName,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
leaveOrganizationDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,12 +51,10 @@ fun SavedStateHandle.toLeaveOrganizationArgs(): LeaveOrganizationArgs {
|
||||
*/
|
||||
fun NavGraphBuilder.leaveOrganizationDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToVault: () -> Unit,
|
||||
) {
|
||||
composableWithPushTransitions<LeaveOrganizationRoute> {
|
||||
LeaveOrganizationScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
onNavigateToVault = onNavigateToVault,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ import com.x8bit.bitwarden.ui.vault.feature.leaveorganization.handlers.rememberL
|
||||
@Composable
|
||||
fun LeaveOrganizationScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToVault: () -> Unit,
|
||||
viewModel: LeaveOrganizationViewModel = hiltViewModel(),
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
) {
|
||||
@@ -60,7 +59,6 @@ fun LeaveOrganizationScreen(
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
LeaveOrganizationEvent.NavigateBack -> onNavigateBack()
|
||||
LeaveOrganizationEvent.NavigateToVault -> onNavigateToVault()
|
||||
is LeaveOrganizationEvent.LaunchUri -> {
|
||||
intentManager.launchUri(event.uri.toUri())
|
||||
}
|
||||
|
||||
@@ -10,9 +10,10 @@ import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LeaveOrganizationResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RevokeFromOrganizationResult
|
||||
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultMigrationManager
|
||||
import com.x8bit.bitwarden.ui.platform.model.SnackbarRelay
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@@ -32,6 +33,7 @@ class LeaveOrganizationViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
private val snackbarRelayManager: SnackbarRelayManager<SnackbarRelay>,
|
||||
private val organizationEventManager: OrganizationEventManager,
|
||||
private val vaultMigrationManager: VaultMigrationManager,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<LeaveOrganizationState, LeaveOrganizationEvent, LeaveOrganizationAction>(
|
||||
initialState = savedStateHandle[KEY_STATE] ?: run {
|
||||
@@ -56,8 +58,8 @@ class LeaveOrganizationViewModel @Inject constructor(
|
||||
LeaveOrganizationAction.LeaveOrganizationClick -> handleLeaveOrganizationClick()
|
||||
LeaveOrganizationAction.HelpLinkClick -> handleHelpLinkClick()
|
||||
LeaveOrganizationAction.DismissDialog -> handleDismissDialog()
|
||||
is LeaveOrganizationAction.Internal.LeaveOrganizationResultReceived -> {
|
||||
handleLeaveOrganizationResultReceived(action)
|
||||
is LeaveOrganizationAction.Internal.RevokeFromOrganizationResultReceived -> {
|
||||
handleRevokeFromOrganizationResultReceived(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,9 +73,9 @@ class LeaveOrganizationViewModel @Inject constructor(
|
||||
it.copy(dialogState = LeaveOrganizationState.DialogState.Loading)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val result = authRepository.leaveOrganization(state.organizationId)
|
||||
val result = authRepository.revokeFromOrganization(state.organizationId)
|
||||
sendAction(
|
||||
LeaveOrganizationAction.Internal.LeaveOrganizationResultReceived(result),
|
||||
LeaveOrganizationAction.Internal.RevokeFromOrganizationResultReceived(result),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -92,27 +94,28 @@ class LeaveOrganizationViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLeaveOrganizationResultReceived(
|
||||
action: LeaveOrganizationAction.Internal.LeaveOrganizationResultReceived,
|
||||
private fun handleRevokeFromOrganizationResultReceived(
|
||||
action: LeaveOrganizationAction.Internal.RevokeFromOrganizationResultReceived,
|
||||
) {
|
||||
when (val result = action.result) {
|
||||
is LeaveOrganizationResult.Success -> {
|
||||
organizationEventManager.trackEvent(
|
||||
event = OrganizationEvent.ItemOrganizationDeclined,
|
||||
)
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialogState = null)
|
||||
}
|
||||
is RevokeFromOrganizationResult.Success -> {
|
||||
snackbarRelayManager.sendSnackbarData(
|
||||
relay = SnackbarRelay.LEFT_ORGANIZATION,
|
||||
data = BitwardenSnackbarData(
|
||||
message = BitwardenString.you_left_the_organization.asText(),
|
||||
),
|
||||
)
|
||||
sendEvent(LeaveOrganizationEvent.NavigateToVault)
|
||||
organizationEventManager.trackEvent(
|
||||
event = OrganizationEvent.ItemOrganizationDeclined,
|
||||
)
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialogState = null)
|
||||
}
|
||||
// Navigation will be handled on RootNavViewModel by migration state change
|
||||
vaultMigrationManager.clearMigrationState()
|
||||
}
|
||||
|
||||
is LeaveOrganizationResult.Error -> {
|
||||
is RevokeFromOrganizationResult.Error -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = LeaveOrganizationState.DialogState.Error(
|
||||
@@ -166,11 +169,6 @@ sealed class LeaveOrganizationEvent {
|
||||
*/
|
||||
data object NavigateBack : LeaveOrganizationEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the Vault screen.
|
||||
*/
|
||||
data object NavigateToVault : LeaveOrganizationEvent()
|
||||
|
||||
/**
|
||||
* Launch external URI.
|
||||
*/
|
||||
@@ -206,10 +204,10 @@ sealed class LeaveOrganizationAction {
|
||||
*/
|
||||
sealed class Internal : LeaveOrganizationAction() {
|
||||
/**
|
||||
* Leave organization result received from repository.
|
||||
* Revoke from organization result received from repository.
|
||||
*/
|
||||
data class LeaveOrganizationResultReceived(
|
||||
val result: LeaveOrganizationResult,
|
||||
data class RevokeFromOrganizationResultReceived(
|
||||
val result: RevokeFromOrganizationResult,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ fun NavController.navigateToMigrateToMyItems(
|
||||
*/
|
||||
fun NavGraphBuilder.migrateToMyItemsDestination(
|
||||
onNavigateToVault: () -> Unit,
|
||||
onNavigateToLeaveOrganization: () -> Unit,
|
||||
onNavigateToLeaveOrganization: (organizationId: String, organizationName: String) -> Unit,
|
||||
) {
|
||||
composableWithSlideTransitions<MigrateToMyItemsRoute> {
|
||||
MigrateToMyItemsScreen(
|
||||
|
||||
@@ -45,7 +45,7 @@ import com.x8bit.bitwarden.ui.vault.feature.migratetomyitems.handler.rememberMig
|
||||
@Composable
|
||||
fun MigrateToMyItemsScreen(
|
||||
onNavigateToVault: () -> Unit,
|
||||
onNavigateToLeaveOrganization: () -> Unit,
|
||||
onNavigateToLeaveOrganization: (organizationId: String, organizationName: String) -> Unit,
|
||||
viewModel: MigrateToMyItemsViewModel = hiltViewModel(),
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
) {
|
||||
@@ -55,7 +55,9 @@ fun MigrateToMyItemsScreen(
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
MigrateToMyItemsEvent.NavigateToVault -> onNavigateToVault()
|
||||
MigrateToMyItemsEvent.NavigateToLeaveOrganization -> onNavigateToLeaveOrganization()
|
||||
is MigrateToMyItemsEvent.NavigateToLeaveOrganization -> {
|
||||
onNavigateToLeaveOrganization(event.organizationId, event.organizationName)
|
||||
}
|
||||
is MigrateToMyItemsEvent.LaunchUri -> intentManager.launchUri(event.uri.toUri())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,12 @@ class MigrateToMyItemsViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleDeclineAndLeaveClicked() {
|
||||
sendEvent(MigrateToMyItemsEvent.NavigateToLeaveOrganization)
|
||||
sendEvent(
|
||||
MigrateToMyItemsEvent.NavigateToLeaveOrganization(
|
||||
organizationId = state.organizationId,
|
||||
organizationName = state.organizationName,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleHelpLinkClicked() {
|
||||
@@ -207,7 +212,10 @@ sealed class MigrateToMyItemsEvent {
|
||||
/**
|
||||
* Navigate to the leave organization flow after declining.
|
||||
*/
|
||||
data object NavigateToLeaveOrganization : MigrateToMyItemsEvent()
|
||||
data class NavigateToLeaveOrganization(
|
||||
val organizationId: String,
|
||||
val organizationName: String,
|
||||
) : MigrateToMyItemsEvent()
|
||||
|
||||
/**
|
||||
* Launch a URI in the browser or appropriate handler.
|
||||
|
||||
@@ -205,6 +205,7 @@ class VaultViewModel @Inject constructor(
|
||||
SnackbarRelay.CIPHER_UPDATED,
|
||||
SnackbarRelay.FOLDER_CREATED,
|
||||
SnackbarRelay.LOGINS_IMPORTED,
|
||||
SnackbarRelay.LEFT_ORGANIZATION,
|
||||
),
|
||||
)
|
||||
.map { VaultAction.Internal.SnackbarDataReceive(it) }
|
||||
|
||||
@@ -1164,6 +1164,26 @@ class VaultMigrationManagerTest {
|
||||
vaultRepository.migrateAttachments(userId, match { it.id == "mockId-2" })
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clearMigrationState should set migration state to NoMigrationRequired`() = runTest {
|
||||
val vaultMigrationManager = createVaultMigrationManager()
|
||||
|
||||
// Initially state should be NoMigrationRequired
|
||||
assertEquals(
|
||||
VaultMigrationData.NoMigrationRequired,
|
||||
vaultMigrationManager.vaultMigrationDataStateFlow.value,
|
||||
)
|
||||
|
||||
// Call clearMigrationState (should remain NoMigrationRequired)
|
||||
vaultMigrationManager.clearMigrationState()
|
||||
|
||||
// Verify state is still NoMigrationRequired
|
||||
assertEquals(
|
||||
VaultMigrationData.NoMigrationRequired,
|
||||
vaultMigrationManager.vaultMigrationDataStateFlow.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createVaultData(
|
||||
|
||||
@@ -24,7 +24,6 @@ import org.junit.Test
|
||||
class LeaveOrganizationScreenTest : BitwardenComposeTest() {
|
||||
|
||||
private var onNavigateBackCalled = false
|
||||
private var onNavigateToVaultCalled = false
|
||||
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<LeaveOrganizationEvent>()
|
||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
@@ -38,7 +37,6 @@ class LeaveOrganizationScreenTest : BitwardenComposeTest() {
|
||||
setContent {
|
||||
LeaveOrganizationScreen(
|
||||
onNavigateBack = { onNavigateBackCalled = true },
|
||||
onNavigateToVault = { onNavigateToVaultCalled = true },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
@@ -50,12 +48,6 @@ class LeaveOrganizationScreenTest : BitwardenComposeTest() {
|
||||
assertTrue(onNavigateBackCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToVault event should call onNavigateToVault`() {
|
||||
mutableEventFlow.tryEmit(LeaveOrganizationEvent.NavigateToVault)
|
||||
assertTrue(onNavigateToVaultCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `back button click should emit NavigateBack event`() {
|
||||
composeTestRule
|
||||
|
||||
@@ -11,13 +11,14 @@ import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LeaveOrganizationResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.Organization
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RevokeFromOrganizationResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultMigrationManager
|
||||
import com.x8bit.bitwarden.ui.platform.model.SnackbarRelay
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
@@ -49,6 +50,10 @@ class LeaveOrganizationViewModelTest : BaseViewModelTest() {
|
||||
every { trackEvent(any()) } just runs
|
||||
}
|
||||
|
||||
private val mockVaultMigrationManager: VaultMigrationManager = mockk {
|
||||
every { clearMigrationState() } just runs
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkStatic(SavedStateHandle::toLeaveOrganizationArgs)
|
||||
@@ -96,9 +101,9 @@ class LeaveOrganizationViewModelTest : BaseViewModelTest() {
|
||||
@Test
|
||||
fun `LeaveOrganizationClick should show loading dialog`() = runTest {
|
||||
coEvery {
|
||||
mockAuthRepository.leaveOrganization(any())
|
||||
mockAuthRepository.revokeFromOrganization(any())
|
||||
} coAnswers {
|
||||
LeaveOrganizationResult.Success
|
||||
RevokeFromOrganizationResult.Success
|
||||
}
|
||||
|
||||
val viewModel = createViewModel()
|
||||
@@ -116,17 +121,14 @@ class LeaveOrganizationViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `LeaveOrganizationClick with Success should track ItemOrganizationDeclined event, send snackbar, and navigate to vault`() =
|
||||
fun `LeaveOrganizationClick with Success should track ItemOrganizationDeclined event, send snackbar, and clear migration state`() =
|
||||
runTest {
|
||||
coEvery {
|
||||
mockAuthRepository.leaveOrganization(ORGANIZATION_ID)
|
||||
} returns LeaveOrganizationResult.Success
|
||||
mockAuthRepository.revokeFromOrganization(ORGANIZATION_ID)
|
||||
} returns RevokeFromOrganizationResult.Success
|
||||
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(LeaveOrganizationAction.LeaveOrganizationClick)
|
||||
assertEquals(LeaveOrganizationEvent.NavigateToVault, awaitItem())
|
||||
}
|
||||
viewModel.trySendAction(LeaveOrganizationAction.LeaveOrganizationClick)
|
||||
|
||||
verify {
|
||||
mockSnackbarRelayManager.sendSnackbarData(
|
||||
@@ -138,15 +140,18 @@ class LeaveOrganizationViewModelTest : BaseViewModelTest() {
|
||||
mockOrganizationEventManager.trackEvent(
|
||||
event = OrganizationEvent.ItemOrganizationDeclined,
|
||||
)
|
||||
mockVaultMigrationManager.clearMigrationState()
|
||||
}
|
||||
|
||||
assertNull(viewModel.stateFlow.value.dialogState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `LeaveOrganizationClick with Error should show error dialog`() = runTest {
|
||||
val error = Throwable("Test error")
|
||||
coEvery {
|
||||
mockAuthRepository.leaveOrganization(ORGANIZATION_ID)
|
||||
} returns LeaveOrganizationResult.Error(error)
|
||||
mockAuthRepository.revokeFromOrganization(ORGANIZATION_ID)
|
||||
} returns RevokeFromOrganizationResult.Error(error)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(LeaveOrganizationAction.LeaveOrganizationClick)
|
||||
@@ -161,8 +166,8 @@ class LeaveOrganizationViewModelTest : BaseViewModelTest() {
|
||||
@Test
|
||||
fun `DismissDialog should clear dialog state`() = runTest {
|
||||
coEvery {
|
||||
mockAuthRepository.leaveOrganization(ORGANIZATION_ID)
|
||||
} returns LeaveOrganizationResult.Error(Throwable("Error"))
|
||||
mockAuthRepository.revokeFromOrganization(ORGANIZATION_ID)
|
||||
} returns RevokeFromOrganizationResult.Error(Throwable("Error"))
|
||||
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(LeaveOrganizationAction.LeaveOrganizationClick)
|
||||
@@ -187,6 +192,7 @@ class LeaveOrganizationViewModelTest : BaseViewModelTest() {
|
||||
authRepository = mockAuthRepository,
|
||||
snackbarRelayManager = mockSnackbarRelayManager,
|
||||
organizationEventManager = mockOrganizationEventManager,
|
||||
vaultMigrationManager = mockVaultMigrationManager,
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
|
||||
@@ -204,6 +210,7 @@ class LeaveOrganizationViewModelTest : BaseViewModelTest() {
|
||||
authRepository = mockAuthRepository,
|
||||
snackbarRelayManager = mockSnackbarRelayManager,
|
||||
organizationEventManager = mockOrganizationEventManager,
|
||||
vaultMigrationManager = mockVaultMigrationManager,
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -48,7 +48,9 @@ class MigrateToMyItemsScreenTest : BitwardenComposeTest() {
|
||||
MigrateToMyItemsScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateToVault = { onNavigateToVaultCalled = true },
|
||||
onNavigateToLeaveOrganization = { onNavigateToLeaveOrganizationCalled = true },
|
||||
onNavigateToLeaveOrganization = { _, _ ->
|
||||
onNavigateToLeaveOrganizationCalled = true
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -107,7 +109,12 @@ class MigrateToMyItemsScreenTest : BitwardenComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `NavigateToLeaveOrganization event should trigger navigation callback`() {
|
||||
mutableEventFlow.tryEmit(MigrateToMyItemsEvent.NavigateToLeaveOrganization)
|
||||
mutableEventFlow.tryEmit(
|
||||
MigrateToMyItemsEvent.NavigateToLeaveOrganization(
|
||||
organizationId = "test-org-id",
|
||||
organizationName = "Test Organization",
|
||||
),
|
||||
)
|
||||
assertTrue(onNavigateToLeaveOrganizationCalled)
|
||||
}
|
||||
|
||||
|
||||
@@ -192,7 +192,13 @@ class MigrateToMyItemsViewModelTest : BaseViewModelTest() {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(MigrateToMyItemsAction.DeclineAndLeaveClicked)
|
||||
assertEquals(MigrateToMyItemsEvent.NavigateToLeaveOrganization, awaitItem())
|
||||
assertEquals(
|
||||
MigrateToMyItemsEvent.NavigateToLeaveOrganization(
|
||||
organizationId = ORGANIZATION_ID,
|
||||
organizationName = ORGANIZATION_NAME,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,4 +47,12 @@ internal interface AuthenticatedOrganizationApi {
|
||||
suspend fun leaveOrganization(
|
||||
@Path("id") organizationId: String,
|
||||
): NetworkResult<Unit>
|
||||
|
||||
/**
|
||||
* Revokes self from organization
|
||||
*/
|
||||
@PUT("/organizations/{orgId}/users/revoke-self")
|
||||
suspend fun revokeFromOrganization(
|
||||
@Path("orgId") organizationId: String,
|
||||
): NetworkResult<Unit>
|
||||
}
|
||||
|
||||
@@ -46,4 +46,11 @@ interface OrganizationService {
|
||||
suspend fun leaveOrganization(
|
||||
organizationId: String,
|
||||
): Result<Unit>
|
||||
|
||||
/**
|
||||
* Make a request to revoke self from the organization
|
||||
*/
|
||||
suspend fun revokeFromOrganization(
|
||||
organizationId: String,
|
||||
): Result<Unit>
|
||||
}
|
||||
|
||||
@@ -62,4 +62,9 @@ internal class OrganizationServiceImpl(
|
||||
authenticatedOrganizationApi
|
||||
.leaveOrganization(organizationId = organizationId)
|
||||
.toResult()
|
||||
|
||||
override suspend fun revokeFromOrganization(organizationId: String): Result<Unit> =
|
||||
authenticatedOrganizationApi
|
||||
.revokeFromOrganization(organizationId = organizationId)
|
||||
.toResult()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user