mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
BIT-1719 Log a user out on a notificaiton (#1013)
This commit is contained in:
parent
cb20a6d690
commit
44b65e16b0
8 changed files with 85 additions and 15 deletions
|
@ -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() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue