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.widget.Toast
import androidx.annotation.StringRes
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
@ -35,9 +36,7 @@ class UserLogoutManagerImpl(
val currentUserState = authDiskSource.userState ?: return
if (isExpired) {
mainScope.launch {
Toast.makeText(context, R.string.login_expired, Toast.LENGTH_SHORT).show()
}
showToast(message = R.string.login_expired)
}
// Remove the active user from the accounts map
@ -47,6 +46,10 @@ class UserLogoutManagerImpl(
// Check if there is a new active user
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 the active user, we want to set the active user to the first one
// in the list.
@ -118,4 +121,8 @@ class UserLogoutManagerImpl(
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
.logoutFlow
.onEach { logout() }
.onEach { logout(userId = it.userId) }
.launchIn(unconfinedScope)
// 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
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.SyncCipherDeleteData
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.
*/
val logoutFlow: Flow<Unit>
val logoutFlow: Flow<NotificationLogoutData>
/**
* 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.manager.dispatcher.DispatcherManager
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.NotificationType
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 mutableFullSyncSharedFlow = bufferedMutableSharedFlow<Unit>()
private val mutableLogoutSharedFlow = bufferedMutableSharedFlow<Unit>()
private val mutableLogoutSharedFlow = bufferedMutableSharedFlow<NotificationLogoutData>()
private val mutablePasswordlessRequestSharedFlow =
bufferedMutableSharedFlow<PasswordlessRequestData>()
private val mutableSyncCipherDeleteSharedFlow =
@ -70,7 +71,7 @@ class PushManagerImpl @Inject constructor(
override val fullSyncFlow: SharedFlow<Unit>
get() = mutableFullSyncSharedFlow.asSharedFlow()
override val logoutFlow: SharedFlow<Unit>
override val logoutFlow: SharedFlow<NotificationLogoutData>
get() = mutableLogoutSharedFlow.asSharedFlow()
override val passwordlessRequestFlow: SharedFlow<PasswordlessRequestData>
@ -140,8 +141,11 @@ class PushManagerImpl @Inject constructor(
}
NotificationType.LOG_OUT -> {
if (!isLoggedIn) return
mutableLogoutSharedFlow.tryEmit(Unit)
val payload: NotificationPayload.UserNotification =
json.decodeFromJsonElement(notification.payload)
mutableLogoutSharedFlow.tryEmit(
NotificationLogoutData(payload.userId),
)
}
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
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.model.AccountJson
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.PasswordHistoryDiskSource
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import com.x8bit.bitwarden.ui.platform.base.MainDispatcherExtension
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.unmockkStatic
import io.mockk.verify
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
@ExtendWith(MainDispatcherExtension::class)
class UserLogoutManagerTest {
private val authDiskSource: AuthDiskSource = mockk {
every { userState = any() } just runs
@ -42,10 +51,11 @@ class UserLogoutManagerTest {
private val vaultDiskSource: VaultDiskSource = mockk {
coEvery { deleteVaultData(any()) } just runs
}
private val context: Context = mockk()
private val userLogoutManager: UserLogoutManager =
UserLogoutManagerImpl(
context = mockk(),
context = context,
authDiskSource = authDiskSource,
generatorDiskSource = generatorDiskSource,
passwordHistoryDiskSource = passwordHistoryDiskSource,
@ -55,6 +65,11 @@ class UserLogoutManagerTest {
dispatcherManager = FakeDispatcherManager(),
)
@AfterEach
fun tearDown() {
unmockkStatic(Toast::class)
}
@Suppress("MaxLineLength")
@Test
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")
@Test
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
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.PushManager
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.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
@ -201,7 +202,7 @@ class AuthRepositoryTest {
every { logout(any(), any()) } just runs
}
private val mutableLogoutFlow = bufferedMutableSharedFlow<Unit>()
private val mutableLogoutFlow = bufferedMutableSharedFlow<NotificationLogoutData>()
private val mutableSyncOrgKeysFlow = bufferedMutableSharedFlow<Unit>()
private val mutableActivePolicyFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Policy>>()
private val pushManager: PushManager = mockk {
@ -4031,7 +4032,7 @@ class AuthRepositoryTest {
val userId = USER_ID_1
fakeAuthDiskSource.userState = MULTI_USER_STATE
mutableLogoutFlow.tryEmit(Unit)
mutableLogoutFlow.tryEmit(NotificationLogoutData(userId = userId))
coVerify(exactly = 1) {
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.service.PushService
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.SyncCipherDeleteData
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
inner class LoggedOutUserState {
@BeforeEach
@ -168,10 +185,13 @@ class PushManagerTest {
}
@Test
fun `onMessageReceived logout does nothing`() = runTest {
fun `onMessageReceived logout emits to logoutFlow`() = runTest {
pushManager.logoutFlow.test {
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.onMessageReceived(LOGOUT_NOTIFICATION_JSON)
assertEquals(
Unit,
NotificationLogoutData(userId = "078966a2-93c2-4618-ae2a-0a2394c88d37"),
awaitItem(),
)
}