Add logout manager (#604)

This commit is contained in:
Brian Yencho 2024-01-14 09:58:14 -06:00 committed by Álison Fernandes
parent f1b9ded3e3
commit 3def25366b
10 changed files with 494 additions and 279 deletions

View file

@ -0,0 +1,17 @@
package com.x8bit.bitwarden.data.auth.manager
/**
* Manages the logging out of users and clearing of their data.
*/
interface UserLogoutManager {
/**
* Completely logs out the given [userId], removing all data.
*/
fun logout(userId: String)
/**
* Partially logs out the given [userId]. All data for the given [userId] will be removed with
* the exception of basic account data.
*/
fun softLogout(userId: String)
}

View file

@ -0,0 +1,110 @@
package com.x8bit.bitwarden.data.auth.manager
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
/**
* Primary implementation of [UserLogoutManager].
*/
@Suppress("LongParameterList")
class UserLogoutManagerImpl(
private val authDiskSource: AuthDiskSource,
private val generatorDiskSource: GeneratorDiskSource,
private val passwordHistoryDiskSource: PasswordHistoryDiskSource,
private val pushDiskSource: PushDiskSource,
private val settingsDiskSource: SettingsDiskSource,
private val vaultDiskSource: VaultDiskSource,
private val dispatcherManager: DispatcherManager,
) : UserLogoutManager {
private val scope = CoroutineScope(dispatcherManager.unconfined)
override fun logout(userId: String) {
val currentUserState = authDiskSource.userState ?: return
// Remove the active user from the accounts map
val updatedAccounts = currentUserState
.accounts
.filterKeys { it != userId }
// Check if there is a new active user
if (updatedAccounts.isNotEmpty()) {
// 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.
val updatedActiveUserId = currentUserState
.activeUserId
.takeUnless { it == userId }
?: updatedAccounts.entries.first().key
// Update the user information and emit an updated token
authDiskSource.userState = currentUserState.copy(
activeUserId = updatedActiveUserId,
accounts = updatedAccounts,
)
} else {
// Update the user information and log out
authDiskSource.userState = null
}
clearData(userId = userId)
}
override fun softLogout(userId: String) {
val userState = authDiskSource.userState ?: return
val updatedAccount = userState
.accounts[userId]
// Clear the tokens for the current user if present
?.copy(
tokens = AccountJson.Tokens(
accessToken = null,
refreshToken = null,
),
)
authDiskSource.userState = userState
.copy(
accounts = userState
.accounts
.toMutableMap()
.apply {
updatedAccount?.let { set(userId, updatedAccount) }
},
)
// Save any data that will still need to be retained after otherwise clearing all dat
val vaultTimeoutInMinutes = settingsDiskSource.getVaultTimeoutInMinutes(userId = userId)
val vaultTimeoutAction = settingsDiskSource.getVaultTimeoutAction(userId = userId)
clearData(userId = userId)
// Restore data that is still required
settingsDiskSource.apply {
storeVaultTimeoutInMinutes(
userId = userId,
vaultTimeoutInMinutes = vaultTimeoutInMinutes,
)
storeVaultTimeoutAction(
userId = userId,
vaultTimeoutAction = vaultTimeoutAction,
)
}
}
private fun clearData(userId: String) {
authDiskSource.clearData(userId = userId)
generatorDiskSource.clearData(userId = userId)
pushDiskSource.clearData(userId = userId)
settingsDiskSource.clearData(userId = userId)
scope.launch {
passwordHistoryDiskSource.clearPasswordHistories(userId = userId)
vaultDiskSource.deleteVaultData(userId = userId)
}
}
}

View file

@ -0,0 +1,45 @@
package com.x8bit.bitwarden.data.auth.manager.di
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManagerImpl
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
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 dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
/**
* Provides managers in the auth package.
*/
@Module
@InstallIn(SingletonComponent::class)
object AuthManagerModule {
@Provides
@Singleton
fun provideUserLogoutManager(
authDiskSource: AuthDiskSource,
generatorDiskSource: GeneratorDiskSource,
passwordHistoryDiskSource: PasswordHistoryDiskSource,
pushDiskSource: PushDiskSource,
settingsDiskSource: SettingsDiskSource,
vaultDiskSource: VaultDiskSource,
dispatcherManager: DispatcherManager,
): UserLogoutManager =
UserLogoutManagerImpl(
authDiskSource = authDiskSource,
generatorDiskSource = generatorDiskSource,
passwordHistoryDiskSource = passwordHistoryDiskSource,
pushDiskSource = pushDiskSource,
settingsDiskSource = settingsDiskSource,
vaultDiskSource = vaultDiskSource,
dispatcherManager = dispatcherManager,
)
}

View file

@ -15,6 +15,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toKdfTypeJson
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
@ -65,6 +66,7 @@ class AuthRepositoryImpl constructor(
private val environmentRepository: EnvironmentRepository,
private val settingsRepository: SettingsRepository,
private val vaultRepository: VaultRepository,
private val userLogoutManager: UserLogoutManager,
dispatcherManager: DispatcherManager,
) : AuthRepository {
private val mutableSpecialCircumstanceStateFlow =
@ -254,49 +256,9 @@ class AuthRepositoryImpl constructor(
}
override fun logout(userId: String) {
val currentUserState = authDiskSource.userState ?: return
val wasActiveUser = userId == activeUserId
// Remove the active user from the accounts map
val updatedAccounts = currentUserState
.accounts
.filterKeys { it != userId }
authDiskSource.apply {
storeUserKey(userId = userId, userKey = null)
storePrivateKey(userId = userId, privateKey = null)
storeUserAutoUnlockKey(userId = userId, userAutoUnlockKey = null)
storeOrganizationKeys(userId = userId, organizationKeys = null)
storeOrganizations(userId = userId, organizations = null)
}
// Check if there is a new active user
if (updatedAccounts.isNotEmpty()) {
// 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.
val updatedActiveUserId = currentUserState
.activeUserId
.takeUnless { it == userId }
?: updatedAccounts.entries.first().key
// Update the user information and emit an updated token
authDiskSource.userState = currentUserState.copy(
activeUserId = updatedActiveUserId,
accounts = updatedAccounts,
)
} else {
// Update the user information and log out
authDiskSource.userState = null
}
// Clear settings
settingsRepository.clearData(userId)
// Delete all the vault data
vaultRepository.deleteVaultData(userId)
// Lock the vault for the logged out user
vaultRepository.lockVaultIfNecessary(userId)
userLogoutManager.logout(userId = userId)
// Clear the current vault data if the logged out user was the active one.
if (wasActiveUser) vaultRepository.clearUnlockedData()

View file

@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedService
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.AuthRepositoryImpl
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
@ -36,6 +37,7 @@ object AuthRepositoryModule {
environmentRepository: EnvironmentRepository,
settingsRepository: SettingsRepository,
vaultRepository: VaultRepository,
userLogoutManager: UserLogoutManager,
): AuthRepository = AuthRepositoryImpl(
accountsService = accountsService,
identityService = identityService,
@ -46,5 +48,6 @@ object AuthRepositoryModule {
environmentRepository = environmentRepository,
settingsRepository = settingsRepository,
vaultRepository = vaultRepository,
userLogoutManager = userLogoutManager,
)
}

View file

@ -12,6 +12,11 @@ interface PushDiskSource {
*/
var registeredPushToken: String?
/**
* Clears all the data for the given user.
*/
fun clearData(userId: String)
/**
* Retrieves the last stored token for a user.
*/

View file

@ -26,6 +26,11 @@ class PushDiskSourceImpl(
)
}
override fun clearData(userId: String) {
storeCurrentPushToken(userId = userId, pushToken = null)
storeLastPushTokenRegistrationDate(userId = userId, registrationDate = null)
}
override fun getCurrentPushToken(userId: String): String? {
return getString("${CURRENT_PUSH_TOKEN_KEY}_$userId")
}

View file

@ -0,0 +1,227 @@
package com.x8bit.bitwarden.data.auth.manager
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
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
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 io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import org.junit.jupiter.api.Test
class UserLogoutManagerTest {
private val authDiskSource: AuthDiskSource = mockk {
every { userState = any() } just runs
every { clearData(any()) } just runs
}
private val generatorDiskSource: GeneratorDiskSource = mockk {
every { clearData(any()) } just runs
}
private val settingsDiskSource: SettingsDiskSource = mockk {
every { clearData(any()) } just runs
every { storeVaultTimeoutInMinutes(any(), any()) } just runs
every { storeVaultTimeoutAction(any(), any()) } just runs
}
private val pushDiskSource: PushDiskSource = mockk {
coEvery { clearData(any()) } just runs
}
private val passwordHistoryDiskSource: PasswordHistoryDiskSource = mockk {
coEvery { clearPasswordHistories(any()) } just runs
}
private val vaultDiskSource: VaultDiskSource = mockk {
coEvery { deleteVaultData(any()) } just runs
}
private val userLogoutManager: UserLogoutManager =
UserLogoutManagerImpl(
authDiskSource = authDiskSource,
generatorDiskSource = generatorDiskSource,
passwordHistoryDiskSource = passwordHistoryDiskSource,
pushDiskSource = pushDiskSource,
settingsDiskSource = settingsDiskSource,
vaultDiskSource = vaultDiskSource,
dispatcherManager = FakeDispatcherManager(),
)
@Suppress("MaxLineLength")
@Test
fun `logout for single account should clear data associated with the given user and null out the user state`() {
val userId = USER_ID_1
every { authDiskSource.userState } returns SINGLE_USER_STATE_1
userLogoutManager.logout(userId = USER_ID_1)
verify { authDiskSource.userState = null }
assertDataCleared(userId = userId)
}
@Suppress("MaxLineLength")
@Test
fun `logout for multiple accounts should clear data associated with the given user and change to the new active user`() {
val userId = USER_ID_1
every { authDiskSource.userState } returns MULTI_USER_STATE
userLogoutManager.logout(userId = USER_ID_1)
verify { authDiskSource.userState = SINGLE_USER_STATE_2 }
assertDataCleared(userId = userId)
}
@Suppress("MaxLineLength")
@Test
fun `logout for non-active accounts should clear data associated with the given user and leave the active user unchanged`() {
val userId = USER_ID_2
every { authDiskSource.userState } returns MULTI_USER_STATE
userLogoutManager.logout(userId = USER_ID_2)
verify { authDiskSource.userState = SINGLE_USER_STATE_1 }
assertDataCleared(userId = userId)
}
@Suppress("MaxLineLength")
@Test
fun `softLogout should clear most data associated with the given user and remove token data from the account in the user state`() {
val userId = USER_ID_1
val vaultTimeoutInMinutes = 360
val vaultTimeoutAction = VaultTimeoutAction.LOCK
every { authDiskSource.userState } returns SINGLE_USER_STATE_1
every {
settingsDiskSource.getVaultTimeoutInMinutes(userId = userId)
} returns vaultTimeoutInMinutes
every {
settingsDiskSource.getVaultTimeoutAction(userId = userId)
} returns vaultTimeoutAction
userLogoutManager.softLogout(userId = userId)
val updatedAccount = ACCOUNT_1
.copy(
tokens = AccountJson.Tokens(
accessToken = null,
refreshToken = null,
),
)
val updatedUserState = SINGLE_USER_STATE_1
.copy(
accounts = SINGLE_USER_STATE_1
.accounts
.toMutableMap().apply {
set(userId, updatedAccount)
},
)
verify { authDiskSource.userState = updatedUserState }
assertDataCleared(userId = userId)
verify {
settingsDiskSource.storeVaultTimeoutInMinutes(
userId = userId,
vaultTimeoutInMinutes = vaultTimeoutInMinutes,
)
}
verify {
settingsDiskSource.storeVaultTimeoutAction(
userId = userId,
vaultTimeoutAction = vaultTimeoutAction,
)
}
}
private fun assertDataCleared(userId: String) {
verify { authDiskSource.clearData(userId = userId) }
verify { generatorDiskSource.clearData(userId = userId) }
verify { pushDiskSource.clearData(userId = userId) }
verify { settingsDiskSource.clearData(userId = userId) }
coVerify { passwordHistoryDiskSource.clearPasswordHistories(userId = userId) }
coVerify {
vaultDiskSource.deleteVaultData(userId = userId)
}
}
}
private const val EMAIL_2 = "test2@bitwarden.com"
private const val ACCESS_TOKEN = "accessToken"
private const val ACCESS_TOKEN_2 = "accessToken2"
private const val REFRESH_TOKEN = "refreshToken"
private const val USER_ID_1 = "2a135b23-e1fb-42c9-bec3-573857bc8181"
private const val USER_ID_2 = "b9d32ec0-6497-4582-9798-b350f53bfa02"
private val ACCOUNT_1 = AccountJson(
profile = AccountJson.Profile(
userId = USER_ID_1,
email = "test@bitwarden.com",
isEmailVerified = true,
name = "Bitwarden Tester",
hasPremium = false,
stamp = null,
organizationId = null,
avatarColorHex = null,
forcePasswordResetReason = null,
kdfType = KdfTypeJson.ARGON2_ID,
kdfIterations = 600000,
kdfMemory = 16,
kdfParallelism = 4,
userDecryptionOptions = null,
),
tokens = AccountJson.Tokens(
accessToken = ACCESS_TOKEN,
refreshToken = REFRESH_TOKEN,
),
settings = AccountJson.Settings(
environmentUrlData = null,
),
)
private val ACCOUNT_2 = AccountJson(
profile = AccountJson.Profile(
userId = USER_ID_2,
email = EMAIL_2,
isEmailVerified = true,
name = "Bitwarden Tester 2",
hasPremium = false,
stamp = null,
organizationId = null,
avatarColorHex = null,
forcePasswordResetReason = null,
kdfType = KdfTypeJson.PBKDF2_SHA256,
kdfIterations = 400000,
kdfMemory = null,
kdfParallelism = null,
userDecryptionOptions = null,
),
tokens = AccountJson.Tokens(
accessToken = ACCESS_TOKEN_2,
refreshToken = "refreshToken",
),
settings = AccountJson.Settings(
environmentUrlData = null,
),
)
private val SINGLE_USER_STATE_1 = UserStateJson(
activeUserId = USER_ID_1,
accounts = mapOf(
USER_ID_1 to ACCOUNT_1,
),
)
private val SINGLE_USER_STATE_2 = UserStateJson(
activeUserId = USER_ID_2,
accounts = mapOf(
USER_ID_2 to ACCOUNT_2,
),
)
private val MULTI_USER_STATE = UserStateJson(
activeUserId = USER_ID_1,
accounts = mapOf(
USER_ID_1 to ACCOUNT_1,
USER_ID_2 to ACCOUNT_2,
),
)

View file

@ -25,6 +25,7 @@ import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_2
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_3
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_4
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
@ -120,6 +121,9 @@ class AuthRepositoryTest {
),
)
}
private val userLogoutManager: UserLogoutManager = mockk {
every { logout(any()) } just runs
}
private val repository = AuthRepositoryImpl(
accountsService = accountsService,
@ -130,6 +134,7 @@ class AuthRepositoryTest {
environmentRepository = fakeEnvironmentRepository,
settingsRepository = settingsRepository,
vaultRepository = vaultRepository,
userLogoutManager = userLogoutManager,
dispatcherManager = dispatcherManager,
)
@ -150,6 +155,49 @@ class AuthRepositoryTest {
)
}
@Test
fun `authStateFlow should react to user state changes`() {
assertEquals(
AuthState.Unauthenticated,
repository.authStateFlow.value,
)
// Update the active user updates the state
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
assertEquals(
AuthState.Authenticated(ACCESS_TOKEN),
repository.authStateFlow.value,
)
// Updating the non-active user does not update the state
fakeAuthDiskSource.userState = MULTI_USER_STATE
assertEquals(
AuthState.Authenticated(ACCESS_TOKEN),
repository.authStateFlow.value,
)
// Clearing the tokens of the active state results in the Unauthenticated state
val updatedAccount = ACCOUNT_1.copy(
tokens = AccountJson.Tokens(
accessToken = null,
refreshToken = null,
),
)
val updatedState = MULTI_USER_STATE.copy(
accounts = MULTI_USER_STATE
.accounts
.toMutableMap()
.apply {
set(USER_ID_1, updatedAccount)
},
)
fakeAuthDiskSource.userState = updatedState
assertEquals(
AuthState.Unauthenticated,
repository.authStateFlow.value,
)
}
@Test
fun `userStateFlow should update according to changes in its underyling data sources`() {
fakeAuthDiskSource.userState = null
@ -1043,251 +1091,26 @@ class AuthRepositoryTest {
@Suppress("MaxLineLength")
@Test
fun `logout for single account should clear the access token and stored data`() = runTest {
// First login:
val successResponse = GET_TOKEN_RESPONSE_SUCCESS
coEvery {
accountsService.preLogin(email = EMAIL)
} returns Result.success(PRE_LOGIN_SUCCESS)
coEvery {
identityService.getToken(
email = EMAIL,
passwordHash = PASSWORD_HASH,
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
} returns Result.success(successResponse)
coEvery {
vaultRepository.unlockVault(
userId = USER_ID_1,
email = EMAIL,
kdf = ACCOUNT_1.profile.toSdkParams(),
userKey = successResponse.key,
privateKey = successResponse.privateKey,
organizationKeys = ORGANIZATION_KEYS,
masterPassword = PASSWORD,
)
} returns VaultUnlockResult.Success
coEvery { vaultRepository.sync() } just runs
every {
GET_TOKEN_RESPONSE_SUCCESS.toUserState(
previousUserState = null,
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US,
)
} returns SINGLE_USER_STATE_1
fakeAuthDiskSource.apply {
storeUserKey(
userId = USER_ID_1,
userKey = PUBLIC_KEY,
)
storePrivateKey(
userId = USER_ID_1,
privateKey = PRIVATE_KEY,
)
storeUserAutoUnlockKey(
userId = USER_ID_1,
userAutoUnlockKey = USER_AUTO_UNLOCK_KEY,
)
storeOrganizationKeys(
userId = USER_ID_1,
organizationKeys = ORGANIZATION_KEYS,
)
storeOrganizations(
userId = USER_ID_1,
organizations = ORGANIZATIONS,
)
}
fun `logout for the active account should call logout on the UserLogoutManager and clear the user's in memory vault data`() {
val userId = USER_ID_1
fakeAuthDiskSource.userState = MULTI_USER_STATE
repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
repository.logout(userId = userId)
assertEquals(AuthState.Authenticated(ACCESS_TOKEN), repository.authStateFlow.value)
assertEquals(SINGLE_USER_STATE_1, fakeAuthDiskSource.userState)
// Then call logout:
repository.authStateFlow.test {
assertEquals(AuthState.Authenticated(ACCESS_TOKEN), awaitItem())
repository.logout()
assertEquals(AuthState.Unauthenticated, awaitItem())
assertNull(fakeAuthDiskSource.userState)
fakeAuthDiskSource.assertPrivateKey(
userId = USER_ID_1,
privateKey = null,
)
fakeAuthDiskSource.assertUserKey(
userId = USER_ID_1,
userKey = null,
)
fakeAuthDiskSource.assertUserAutoUnlockKey(
userId = USER_ID_1,
userAutoUnlockKey = null,
)
fakeAuthDiskSource.assertOrganizationKeys(
userId = USER_ID_1,
organizationKeys = null,
)
fakeAuthDiskSource.assertOrganizations(
userId = USER_ID_1,
organizations = null,
)
verify { settingsRepository.clearData(userId = USER_ID_1) }
verify { vaultRepository.deleteVaultData(userId = USER_ID_1) }
verify { vaultRepository.clearUnlockedData() }
verify { vaultRepository.lockVaultIfNecessary(userId = USER_ID_1) }
}
verify { userLogoutManager.logout(userId = userId) }
verify { vaultRepository.clearUnlockedData() }
}
@Suppress("MaxLineLength")
@Test
fun `logout for multiple accounts should update current access token and stored keys`() =
runTest {
// First populate multiple user accounts
fakeAuthDiskSource.userState = SINGLE_USER_STATE_2
fun `logout for an inactive account should call logout on the UserLogoutManager`() {
val userId = USER_ID_2
fakeAuthDiskSource.userState = MULTI_USER_STATE
// Then login:
val successResponse = GET_TOKEN_RESPONSE_SUCCESS
coEvery {
accountsService.preLogin(email = EMAIL)
} returns Result.success(PRE_LOGIN_SUCCESS)
coEvery {
identityService.getToken(
email = EMAIL,
passwordHash = PASSWORD_HASH,
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
} returns Result.success(successResponse)
coEvery {
vaultRepository.unlockVault(
userId = USER_ID_1,
email = EMAIL,
kdf = ACCOUNT_1.profile.toSdkParams(),
userKey = successResponse.key,
privateKey = successResponse.privateKey,
organizationKeys = null,
masterPassword = PASSWORD,
)
} returns VaultUnlockResult.Success
coEvery { vaultRepository.sync() } just runs
every {
GET_TOKEN_RESPONSE_SUCCESS.toUserState(
previousUserState = SINGLE_USER_STATE_2,
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US,
)
} returns MULTI_USER_STATE
fakeAuthDiskSource.apply {
storeUserKey(
userId = USER_ID_2,
userKey = PUBLIC_KEY,
)
storePrivateKey(
userId = USER_ID_2,
privateKey = PRIVATE_KEY,
)
storeUserAutoUnlockKey(
userId = USER_ID_2,
userAutoUnlockKey = USER_AUTO_UNLOCK_KEY,
)
storeOrganizationKeys(
userId = USER_ID_2,
organizationKeys = ORGANIZATION_KEYS,
)
}
repository.logout(userId = userId)
repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
assertEquals(AuthState.Authenticated(ACCESS_TOKEN), repository.authStateFlow.value)
assertEquals(MULTI_USER_STATE, fakeAuthDiskSource.userState)
// Then call logout:
repository.authStateFlow.test {
assertEquals(AuthState.Authenticated(ACCESS_TOKEN), awaitItem())
repository.logout()
assertEquals(AuthState.Authenticated(ACCESS_TOKEN_2), awaitItem())
assertEquals(SINGLE_USER_STATE_2, fakeAuthDiskSource.userState)
fakeAuthDiskSource.assertPrivateKey(
userId = USER_ID_1,
privateKey = null,
)
fakeAuthDiskSource.assertUserKey(
userId = USER_ID_1,
userKey = null,
)
fakeAuthDiskSource.assertUserAutoUnlockKey(
userId = USER_ID_1,
userAutoUnlockKey = null,
)
fakeAuthDiskSource.assertOrganizationKeys(
userId = USER_ID_1,
organizationKeys = null,
)
verify { settingsRepository.clearData(userId = USER_ID_1) }
verify { vaultRepository.deleteVaultData(userId = USER_ID_1) }
verify { vaultRepository.clearUnlockedData() }
verify { vaultRepository.lockVaultIfNecessary(userId = USER_ID_1) }
}
}
@Test
fun `logout for non-active accounts should leave the active user unchanged`() = runTest {
// First populate multiple user accounts and active user is #3
val initialUserState = MULTI_USER_STATE_2
val finalUserState = initialUserState.copy(
accounts = initialUserState.accounts.filter { it.key != USER_ID_2 },
)
fakeAuthDiskSource.userState = initialUserState
fakeAuthDiskSource.apply {
storeUserKey(
userId = USER_ID_2,
userKey = PUBLIC_KEY,
)
storePrivateKey(
userId = USER_ID_2,
privateKey = PRIVATE_KEY,
)
storeUserAutoUnlockKey(
userId = USER_ID_2,
userAutoUnlockKey = USER_AUTO_UNLOCK_KEY,
)
storeOrganizationKeys(
userId = USER_ID_2,
organizationKeys = ORGANIZATION_KEYS,
)
}
assertEquals(initialUserState, fakeAuthDiskSource.userState)
repository.authStateFlow.test {
assertEquals(AuthState.Authenticated(ACCESS_TOKEN_3), awaitItem())
repository.logout(USER_ID_2)
// The auth state does not actually change
expectNoEvents()
assertEquals(finalUserState, fakeAuthDiskSource.userState)
fakeAuthDiskSource.assertPrivateKey(
userId = USER_ID_2,
privateKey = null,
)
fakeAuthDiskSource.assertUserKey(
userId = USER_ID_2,
userKey = null,
)
fakeAuthDiskSource.assertUserAutoUnlockKey(
userId = USER_ID_2,
userAutoUnlockKey = null,
)
fakeAuthDiskSource.assertOrganizationKeys(
userId = USER_ID_2,
organizationKeys = null,
)
verify { settingsRepository.clearData(userId = USER_ID_2) }
verify { vaultRepository.deleteVaultData(userId = USER_ID_2) }
verify(exactly = 0) { vaultRepository.clearUnlockedData() }
verify { vaultRepository.lockVaultIfNecessary(userId = USER_ID_2) }
}
verify { userLogoutManager.logout(userId = userId) }
verify(exactly = 0) { vaultRepository.clearUnlockedData() }
}
@Test

View file

@ -38,6 +38,24 @@ class PushDiskSourceTest {
assertNull(pushDiskSource.registeredPushToken)
}
@Test
fun `clearData should clear all necessary data for the given user`() {
val userId = "userId"
pushDiskSource.storeCurrentPushToken(
userId = userId,
pushToken = "pushToken",
)
pushDiskSource.storeLastPushTokenRegistrationDate(
userId = userId,
registrationDate = ZonedDateTime.now(),
)
pushDiskSource.clearData(userId = userId)
assertNull(pushDiskSource.getCurrentPushToken(userId = userId))
assertNull(pushDiskSource.getLastPushTokenRegistrationDate(userId = userId))
}
@Test
fun `getCurrentPushToken should pull from SharedPreferences`() {
val currentPushTokenBaseKey = "bwPreferencesStorage:pushCurrentToken"