BIT-1719 Log a user out on a notificaiton (#1013)

This commit is contained in:
Oleg Semenenko 2024-02-15 10:39:06 -06:00 committed by Álison Fernandes
parent cb20a6d690
commit 44b65e16b0
8 changed files with 85 additions and 15 deletions

View file

@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.manager
import android.content.Context import android.content.Context
import android.widget.Toast import android.widget.Toast
import androidx.annotation.StringRes
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
@ -35,9 +36,7 @@ class UserLogoutManagerImpl(
val currentUserState = authDiskSource.userState ?: return val currentUserState = authDiskSource.userState ?: return
if (isExpired) { if (isExpired) {
mainScope.launch { showToast(message = R.string.login_expired)
Toast.makeText(context, R.string.login_expired, Toast.LENGTH_SHORT).show()
}
} }
// Remove the active user from the accounts map // Remove the active user from the accounts map
@ -47,6 +46,10 @@ class UserLogoutManagerImpl(
// Check if there is a new active user // Check if there is a new active user
if (updatedAccounts.isNotEmpty()) { if (updatedAccounts.isNotEmpty()) {
if (userId == currentUserState.activeUserId && !isExpired) {
showToast(message = R.string.account_switched_automatically)
}
// If we logged out a non-active user, we want to leave the active user unchanged. // If we logged out a non-active user, we want to leave the active user unchanged.
// If we logged out the active user, we want to set the active user to the first one // If we logged out the active user, we want to set the active user to the first one
// in the list. // in the list.
@ -118,4 +121,8 @@ class UserLogoutManagerImpl(
vaultDiskSource.deleteVaultData(userId = userId) vaultDiskSource.deleteVaultData(userId = userId)
} }
} }
private fun showToast(@StringRes message: Int) {
mainScope.launch { Toast.makeText(context, message, Toast.LENGTH_SHORT).show() }
}
} }

View file

@ -263,7 +263,7 @@ class AuthRepositoryImpl(
pushManager pushManager
.logoutFlow .logoutFlow
.onEach { logout() } .onEach { logout(userId = it.userId) }
.launchIn(unconfinedScope) .launchIn(unconfinedScope)
// When the policies for the user have been set, complete the login process. // When the policies for the user have been set, complete the login process.

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.platform.manager package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.data.platform.manager.model.NotificationLogoutData
import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherDeleteData import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherDeleteData
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherUpsertData import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherUpsertData
@ -21,7 +22,7 @@ interface PushManager {
/** /**
* Flow that represents requests intended to log a user out. * Flow that represents requests intended to log a user out.
*/ */
val logoutFlow: Flow<Unit> val logoutFlow: Flow<NotificationLogoutData>
/** /**
* Flow that represents requests intended to trigger a passwordless request. * Flow that represents requests intended to trigger a passwordless request.

View file

@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.platform.datasource.network.model.PushTokenReque
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.BitwardenNotification import com.x8bit.bitwarden.data.platform.manager.model.BitwardenNotification
import com.x8bit.bitwarden.data.platform.manager.model.NotificationLogoutData
import com.x8bit.bitwarden.data.platform.manager.model.NotificationPayload import com.x8bit.bitwarden.data.platform.manager.model.NotificationPayload
import com.x8bit.bitwarden.data.platform.manager.model.NotificationType import com.x8bit.bitwarden.data.platform.manager.model.NotificationType
import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData
@ -50,7 +51,7 @@ class PushManagerImpl @Inject constructor(
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined) private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
private val mutableFullSyncSharedFlow = bufferedMutableSharedFlow<Unit>() private val mutableFullSyncSharedFlow = bufferedMutableSharedFlow<Unit>()
private val mutableLogoutSharedFlow = bufferedMutableSharedFlow<Unit>() private val mutableLogoutSharedFlow = bufferedMutableSharedFlow<NotificationLogoutData>()
private val mutablePasswordlessRequestSharedFlow = private val mutablePasswordlessRequestSharedFlow =
bufferedMutableSharedFlow<PasswordlessRequestData>() bufferedMutableSharedFlow<PasswordlessRequestData>()
private val mutableSyncCipherDeleteSharedFlow = private val mutableSyncCipherDeleteSharedFlow =
@ -70,7 +71,7 @@ class PushManagerImpl @Inject constructor(
override val fullSyncFlow: SharedFlow<Unit> override val fullSyncFlow: SharedFlow<Unit>
get() = mutableFullSyncSharedFlow.asSharedFlow() get() = mutableFullSyncSharedFlow.asSharedFlow()
override val logoutFlow: SharedFlow<Unit> override val logoutFlow: SharedFlow<NotificationLogoutData>
get() = mutableLogoutSharedFlow.asSharedFlow() get() = mutableLogoutSharedFlow.asSharedFlow()
override val passwordlessRequestFlow: SharedFlow<PasswordlessRequestData> override val passwordlessRequestFlow: SharedFlow<PasswordlessRequestData>
@ -140,8 +141,11 @@ class PushManagerImpl @Inject constructor(
} }
NotificationType.LOG_OUT -> { NotificationType.LOG_OUT -> {
if (!isLoggedIn) return val payload: NotificationPayload.UserNotification =
mutableLogoutSharedFlow.tryEmit(Unit) json.decodeFromJsonElement(notification.payload)
mutableLogoutSharedFlow.tryEmit(
NotificationLogoutData(payload.userId),
)
} }
NotificationType.SYNC_CIPHER_CREATE, NotificationType.SYNC_CIPHER_CREATE,

View file

@ -0,0 +1,10 @@
package com.x8bit.bitwarden.data.platform.manager.model
/**
* Required data for notification logout operation.
*
* @property userId The ID of the user being logged out.
*/
data class NotificationLogoutData(
val userId: String,
)

View file

@ -1,5 +1,8 @@
package com.x8bit.bitwarden.data.auth.manager package com.x8bit.bitwarden.data.auth.manager
import android.content.Context
import android.widget.Toast
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
@ -11,15 +14,21 @@ import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryDiskSource import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryDiskSource
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import com.x8bit.bitwarden.ui.platform.base.MainDispatcherExtension
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.every import io.mockk.every
import io.mockk.just import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.runs import io.mockk.runs
import io.mockk.unmockkStatic
import io.mockk.verify import io.mockk.verify
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
@ExtendWith(MainDispatcherExtension::class)
class UserLogoutManagerTest { class UserLogoutManagerTest {
private val authDiskSource: AuthDiskSource = mockk { private val authDiskSource: AuthDiskSource = mockk {
every { userState = any() } just runs every { userState = any() } just runs
@ -42,10 +51,11 @@ class UserLogoutManagerTest {
private val vaultDiskSource: VaultDiskSource = mockk { private val vaultDiskSource: VaultDiskSource = mockk {
coEvery { deleteVaultData(any()) } just runs coEvery { deleteVaultData(any()) } just runs
} }
private val context: Context = mockk()
private val userLogoutManager: UserLogoutManager = private val userLogoutManager: UserLogoutManager =
UserLogoutManagerImpl( UserLogoutManagerImpl(
context = mockk(), context = context,
authDiskSource = authDiskSource, authDiskSource = authDiskSource,
generatorDiskSource = generatorDiskSource, generatorDiskSource = generatorDiskSource,
passwordHistoryDiskSource = passwordHistoryDiskSource, passwordHistoryDiskSource = passwordHistoryDiskSource,
@ -55,6 +65,11 @@ class UserLogoutManagerTest {
dispatcherManager = FakeDispatcherManager(), dispatcherManager = FakeDispatcherManager(),
) )
@AfterEach
fun tearDown() {
unmockkStatic(Toast::class)
}
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `logout for single account should clear data associated with the given user and null out the user state`() { fun `logout for single account should clear data associated with the given user and null out the user state`() {
@ -70,6 +85,18 @@ class UserLogoutManagerTest {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `logout for multiple accounts should clear data associated with the given user and change to the new active user`() { fun `logout for multiple accounts should clear data associated with the given user and change to the new active user`() {
mockkStatic(Toast::class)
every {
Toast
.makeText(
context,
R.string.account_switched_automatically,
Toast.LENGTH_SHORT,
)
.show()
} just runs
val userId = USER_ID_1 val userId = USER_ID_1
every { authDiskSource.userState } returns MULTI_USER_STATE every { authDiskSource.userState } returns MULTI_USER_STATE

View file

@ -76,6 +76,7 @@ import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.PushManager import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.NotificationLogoutData
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.Environment import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
@ -201,7 +202,7 @@ class AuthRepositoryTest {
every { logout(any(), any()) } just runs every { logout(any(), any()) } just runs
} }
private val mutableLogoutFlow = bufferedMutableSharedFlow<Unit>() private val mutableLogoutFlow = bufferedMutableSharedFlow<NotificationLogoutData>()
private val mutableSyncOrgKeysFlow = bufferedMutableSharedFlow<Unit>() private val mutableSyncOrgKeysFlow = bufferedMutableSharedFlow<Unit>()
private val mutableActivePolicyFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Policy>>() private val mutableActivePolicyFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Policy>>()
private val pushManager: PushManager = mockk { private val pushManager: PushManager = mockk {
@ -4031,7 +4032,7 @@ class AuthRepositoryTest {
val userId = USER_ID_1 val userId = USER_ID_1
fakeAuthDiskSource.userState = MULTI_USER_STATE fakeAuthDiskSource.userState = MULTI_USER_STATE
mutableLogoutFlow.tryEmit(Unit) mutableLogoutFlow.tryEmit(NotificationLogoutData(userId = userId))
coVerify(exactly = 1) { coVerify(exactly = 1) {
userLogoutManager.logout(userId = userId) userLogoutManager.logout(userId = userId)

View file

@ -15,6 +15,7 @@ import com.x8bit.bitwarden.data.platform.datasource.network.di.PlatformNetworkMo
import com.x8bit.bitwarden.data.platform.datasource.network.model.PushTokenRequest import com.x8bit.bitwarden.data.platform.datasource.network.model.PushTokenRequest
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.NotificationLogoutData
import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherDeleteData import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherDeleteData
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherUpsertData import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherUpsertData
@ -156,6 +157,22 @@ class PushManagerTest {
} }
} }
@Test
fun `onMessageReceived logout should emit to logoutFlow`() = runTest {
val account = mockk<AccountJson> {
every { isLoggedIn } returns true
}
authDiskSource.userState = UserStateJson(userId, mapOf(userId to account))
pushManager.logoutFlow.test {
pushManager.onMessageReceived(LOGOUT_NOTIFICATION_JSON)
assertEquals(
NotificationLogoutData(userId = "078966a2-93c2-4618-ae2a-0a2394c88d37"),
awaitItem(),
)
}
}
@Nested @Nested
inner class LoggedOutUserState { inner class LoggedOutUserState {
@BeforeEach @BeforeEach
@ -168,10 +185,13 @@ class PushManagerTest {
} }
@Test @Test
fun `onMessageReceived logout does nothing`() = runTest { fun `onMessageReceived logout emits to logoutFlow`() = runTest {
pushManager.logoutFlow.test { pushManager.logoutFlow.test {
pushManager.onMessageReceived(LOGOUT_NOTIFICATION_JSON) pushManager.onMessageReceived(LOGOUT_NOTIFICATION_JSON)
expectNoEvents() assertEquals(
NotificationLogoutData(userId = "078966a2-93c2-4618-ae2a-0a2394c88d37"),
awaitItem(),
)
} }
} }
@ -541,7 +561,7 @@ class PushManagerTest {
pushManager.logoutFlow.test { pushManager.logoutFlow.test {
pushManager.onMessageReceived(LOGOUT_NOTIFICATION_JSON) pushManager.onMessageReceived(LOGOUT_NOTIFICATION_JSON)
assertEquals( assertEquals(
Unit, NotificationLogoutData(userId = "078966a2-93c2-4618-ae2a-0a2394c88d37"),
awaitItem(), awaitItem(),
) )
} }