Allow for null access tokens for soft logout states (#596)

This commit is contained in:
Brian Yencho 2024-01-12 15:57:43 -06:00 committed by Álison Fernandes
parent 5288a697e5
commit 8f22231c4a
25 changed files with 281 additions and 19 deletions

View file

@ -23,6 +23,10 @@ data class AccountJson(
@SerialName("settings")
val settings: Settings,
) {
/**
* Whether or not the account should be considered logged in.
*/
val isLoggedIn: Boolean get() = tokens.accessToken != null
/**
* Represents a user's personal profile.
@ -96,10 +100,10 @@ data class AccountJson(
@Serializable
data class Tokens(
@SerialName("accessToken")
val accessToken: String,
val accessToken: String?,
@SerialName("refreshToken")
val refreshToken: String,
val refreshToken: String?,
)
/**

View file

@ -83,13 +83,11 @@ class AuthRepositoryImpl constructor(
.userStateFlow
.map { userState ->
userState
?.activeAccount
?.tokens
?.accessToken
?.let {
AuthState.Authenticated(
userState
.activeAccount
.tokens
.accessToken,
)
AuthState.Authenticated(accessToken = it)
}
?: AuthState.Unauthenticated
}
@ -191,6 +189,13 @@ class AuthRepositoryImpl constructor(
.environment
.environmentUrlData,
)
// Check for existing organization keys for a soft-logout account.
// We can separately unlock the vault for organization data after receiving
// the sync response if this data is currently absent.
val organizationKeys =
authDiskSource.getOrganizationKeys(
userId = userStateJson.activeUserId,
)
vaultRepository.clearUnlockedData()
vaultRepository.unlockVault(
userId = userStateJson.activeUserId,
@ -199,9 +204,7 @@ class AuthRepositoryImpl constructor(
userKey = loginResponse.key,
privateKey = loginResponse.privateKey,
masterPassword = password,
// We can separately unlock the vault for organization data after
// receiving the sync response.
organizationKeys = null,
organizationKeys = organizationKeys,
)
authDiskSource.userState = userStateJson
authDiskSource.storeUserKey(
@ -228,10 +231,15 @@ class AuthRepositoryImpl constructor(
)
override fun refreshAccessTokenSynchronously(userId: String): Result<RefreshTokenResponseJson> {
val refreshAccount = authDiskSource.userState?.accounts?.get(userId)
val refreshToken = authDiskSource
.userState
?.accounts
?.get(userId)
?.tokens
?.refreshToken
?: return IllegalStateException("Must be logged in.").asFailure()
return identityService
.refreshTokenSynchronously(refreshAccount.tokens.refreshToken)
.refreshTokenSynchronously(refreshToken)
.onSuccess {
// Update the existing UserState with updated token information
authDiskSource.userState = it.toUserStateJson(

View file

@ -42,6 +42,8 @@ data class UserState(
* @property avatarColorHex Hex color value for a user's avatar in the "#AARRGGBB" format.
* @property environment The [Environment] associated with the user's account.
* @property isPremium `true` if the account has a premium membership.
* @property isLoggedIn `true` if the account is logged in, or `false` if it requires additional
* authentication to view their vault.
* @property isVaultUnlocked Whether or not the user's vault is currently unlocked.
* @property organizations List of [Organization]s the user is associated with, if any.
*/
@ -52,6 +54,7 @@ data class UserState(
val avatarColorHex: String,
val environment: Environment,
val isPremium: Boolean,
val isLoggedIn: Boolean,
val isVaultUnlocked: Boolean,
val organizations: List<Organization>,
)

View file

@ -66,6 +66,7 @@ fun UserStateJson.toUserState(
.environmentUrlData
.toEnvironmentUrlsOrDefault(),
isPremium = accountJson.profile.hasPremium == true,
isLoggedIn = accountJson.isLoggedIn,
isVaultUnlocked = userId in vaultState.unlockedVaultUserIds,
organizations = userOrganizationsList
.find { it.userId == userId }

View file

@ -53,7 +53,9 @@ class LandingViewModel @Inject constructor(
get() {
val currentEmail = state.emailInput
val accountSummaries = state.accountSummaries
return accountSummaries.find { it.email == currentEmail }
return accountSummaries
.find { it.email == currentEmail }
?.takeUnless { !it.isLoggedIn }
}
init {

View file

@ -25,6 +25,7 @@ data class AccountSummary(
val avatarColorHex: String,
val environmentLabel: String,
val isActive: Boolean,
val isLoggedIn: Boolean,
val isVaultUnlocked: Boolean,
) : Parcelable {
@ -40,6 +41,7 @@ data class AccountSummary(
val status: Status
get() = when {
isActive -> Status.ACTIVE
!isLoggedIn -> Status.LOGGED_OUT
isVaultUnlocked -> Status.UNLOCKED
else -> Status.LOCKED
}
@ -58,6 +60,11 @@ data class AccountSummary(
*/
LOCKED,
/**
* The account is currently logged out.
*/
LOGGED_OUT,
/**
* The account is currently unlocked.
*/

View file

@ -41,7 +41,10 @@ class RootNavViewModel @Inject constructor(
) {
val userState = action.userState
val updatedRootNavState = when {
userState == null || userState.hasPendingAccountAddition -> RootNavState.Auth
userState == null ||
!userState.activeAccount.isLoggedIn ||
userState.hasPendingAccountAddition -> RootNavState.Auth
userState.activeAccount.isVaultUnlocked -> {
RootNavState.VaultUnlocked(
activeUserId = userState.activeAccount.userId,

View file

@ -37,6 +37,7 @@ val AccountSummary.iconRes: Int
get() = when (this.status) {
AccountSummary.Status.ACTIVE -> R.drawable.ic_check_mark
AccountSummary.Status.LOCKED -> R.drawable.ic_locked
AccountSummary.Status.LOGGED_OUT -> R.drawable.ic_locked
AccountSummary.Status.UNLOCKED -> R.drawable.ic_unlocked
}
@ -48,5 +49,6 @@ val AccountSummary.supportingTextResOrNull: Int?
get() = when (this.status) {
AccountSummary.Status.ACTIVE -> null
AccountSummary.Status.LOCKED -> R.string.account_locked
AccountSummary.Status.LOGGED_OUT -> R.string.account_logged_out
AccountSummary.Status.UNLOCKED -> R.string.account_unlocked
}

View file

@ -39,6 +39,7 @@ fun UserState.Account.toAccountSummary(
avatarColorHex = this.avatarColorHex,
environmentLabel = this.environment.label,
isActive = isActive,
isLoggedIn = this.isLoggedIn,
isVaultUnlocked = this.isVaultUnlocked,
)

View file

@ -594,6 +594,89 @@ class AuthRepositoryTest {
verify { vaultRepository.clearUnlockedData() }
}
@Suppress("MaxLineLength")
@Test
fun `login get token succeeds when the current user is in a soft-logout state should use existing organization keys when unlocking the vault`() =
runTest {
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
// Users in a soft-logout state have some existing data stored to disk from previous
// sync requests.
fakeAuthDiskSource.storeOrganizationKeys(
userId = USER_ID_1,
organizationKeys = ORGANIZATION_KEYS,
)
val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
assertEquals(LoginResult.Success, result)
assertEquals(AuthState.Authenticated(ACCESS_TOKEN), repository.authStateFlow.value)
coVerify { accountsService.preLogin(email = EMAIL) }
fakeAuthDiskSource.assertPrivateKey(
userId = USER_ID_1,
privateKey = "privateKey",
)
fakeAuthDiskSource.assertUserKey(
userId = USER_ID_1,
userKey = "key",
)
coVerify {
identityService.getToken(
email = EMAIL,
passwordHash = PASSWORD_HASH,
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
vaultRepository.unlockVault(
userId = USER_ID_1,
email = EMAIL,
kdf = ACCOUNT_1.profile.toSdkParams(),
userKey = successResponse.key,
privateKey = successResponse.privateKey,
organizationKeys = ORGANIZATION_KEYS,
masterPassword = PASSWORD,
)
vaultRepository.sync()
}
assertEquals(
SINGLE_USER_STATE_1,
fakeAuthDiskSource.userState,
)
assertNull(repository.specialCircumstance)
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
verify(exactly = 0) { vaultRepository.lockVaultIfNecessary(any()) }
verify { vaultRepository.clearUnlockedData() }
}
@Test
fun `login get token returns captcha request should return CaptchaRequired`() = runTest {
coEvery { accountsService.preLogin(EMAIL) } returns Result.success(PRE_LOGIN_SUCCESS)
@ -981,7 +1064,7 @@ class AuthRepositoryTest {
kdf = ACCOUNT_1.profile.toSdkParams(),
userKey = successResponse.key,
privateKey = successResponse.privateKey,
organizationKeys = null,
organizationKeys = ORGANIZATION_KEYS,
masterPassword = PASSWORD,
)
} returns VaultUnlockResult.Success

View file

@ -104,6 +104,7 @@ class UserStateJsonExtensionsTest {
avatarColorHex = "activeAvatarColorHex",
environment = Environment.Eu,
isPremium = false,
isLoggedIn = true,
isVaultUnlocked = true,
organizations = listOf(
Organization(
@ -125,7 +126,10 @@ class UserStateJsonExtensionsTest {
every { avatarColorHex } returns "activeAvatarColorHex"
every { hasPremium } returns null
},
tokens = mockk(),
tokens = AccountJson.Tokens(
accessToken = "accessToken",
refreshToken = "refreshToken",
),
settings = AccountJson.Settings(
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_EU,
),
@ -167,6 +171,7 @@ class UserStateJsonExtensionsTest {
avatarColorHex = "#ffecbc49",
environment = Environment.Eu,
isPremium = true,
isLoggedIn = false,
isVaultUnlocked = false,
organizations = listOf(
Organization(
@ -189,7 +194,10 @@ class UserStateJsonExtensionsTest {
every { avatarColorHex } returns null
every { hasPremium } returns true
},
tokens = mockk(),
tokens = AccountJson.Tokens(
accessToken = null,
refreshToken = null,
),
settings = AccountJson.Settings(
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_EU,
),

View file

@ -432,6 +432,7 @@ private val ACTIVE_ACCOUNT_SUMMARY = AccountSummary(
avatarColorHex = "#aa00aa",
environmentLabel = "bitwarden.com",
isActive = true,
isLoggedIn = true,
isVaultUnlocked = true,
)

View file

@ -69,6 +69,7 @@ class LandingViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = true,
organizations = emptyList(),
),
@ -189,7 +190,7 @@ class LandingViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `ContinueButtonClick with an email input matching an existing account should show the account already added dialog`() {
fun `ContinueButtonClick with an email input matching an existing account that is logged in should show the account already added dialog`() {
val rememberedEmail = "active@bitwarden.com"
val activeAccount = UserState.Account(
userId = "activeUserId",
@ -198,6 +199,7 @@ class LandingViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = true,
organizations = emptyList(),
)
@ -234,6 +236,55 @@ class LandingViewModelTest : BaseViewModelTest() {
)
}
@Suppress("MaxLineLength")
@Test
fun `ContinueButtonClick with an email input matching an existing account that is logged out should emit NavigateToLogin`() =
runTest {
val rememberedEmail = "active@bitwarden.com"
val activeAccount = UserState.Account(
userId = "activeUserId",
name = "name",
email = rememberedEmail,
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isLoggedIn = false,
isVaultUnlocked = true,
organizations = emptyList(),
)
val userState = UserState(
activeUserId = "activeUserId",
accounts = listOf(activeAccount),
)
val viewModel = createViewModel(
rememberedEmail = rememberedEmail,
userState = userState,
)
val accountSummaries = userState.toAccountSummaries()
val initialState = DEFAULT_STATE.copy(
emailInput = rememberedEmail,
isContinueButtonEnabled = true,
isRememberMeEnabled = true,
accountSummaries = accountSummaries,
)
assertEquals(
initialState,
viewModel.stateFlow.value,
)
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(LandingAction.ContinueButtonClick)
assertEquals(
LandingEvent.NavigateToLogin(rememberedEmail),
awaitItem(),
)
assertEquals(
initialState,
viewModel.stateFlow.value,
)
}
}
@Test
fun `CreateAccountClick should emit NavigateToCreateAccount`() = runTest {
val viewModel = createViewModel()

View file

@ -288,6 +288,7 @@ private val ACTIVE_ACCOUNT_SUMMARY = AccountSummary(
avatarColorHex = "#aa00aa",
environmentLabel = "bitwarden.com",
isActive = true,
isLoggedIn = true,
isVaultUnlocked = true,
)

View file

@ -121,6 +121,7 @@ class LoginViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = true,
organizations = emptyList(),
),

View file

@ -273,6 +273,7 @@ private val ACTIVE_ACCOUNT_SUMMARY = AccountSummary(
avatarColorHex = "#aa00aa",
environmentLabel = "bitwarden.com",
isActive = true,
isLoggedIn = true,
isVaultUnlocked = true,
)
@ -283,6 +284,7 @@ private val LOCKED_ACCOUNT_SUMMARY = AccountSummary(
avatarColorHex = "#00aaaa",
environmentLabel = "bitwarden.com",
isActive = false,
isLoggedIn = true,
isVaultUnlocked = false,
)

View file

@ -106,6 +106,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
avatarColorHex = "#00aaaa",
environment = Environment.Us,
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = true,
organizations = emptyList(),
),
@ -137,6 +138,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
avatarColorHex = "#00aaaa",
environment = Environment.Us,
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = false,
organizations = emptyList(),
),
@ -156,6 +158,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
avatarColorHex = "#00aaaa",
environmentLabel = "bitwarden.com",
isActive = true,
isLoggedIn = true,
isVaultUnlocked = false,
),
),
@ -352,6 +355,7 @@ private val DEFAULT_STATE: VaultUnlockState = VaultUnlockState(
avatarColorHex = "#aa00aa",
environmentLabel = "bitwarden.com",
isActive = true,
isLoggedIn = true,
isVaultUnlocked = true,
),
),
@ -373,6 +377,7 @@ private val DEFAULT_USER_STATE = UserState(
environment = Environment.Us,
avatarColorHex = "#aa00aa",
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = true,
organizations = emptyList(),
),

View file

@ -36,6 +36,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = true,
organizations = emptyList(),
),
@ -62,6 +63,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = false,
organizations = emptyList(),
),

View file

@ -1737,6 +1737,7 @@ private val DEFAULT_USER_STATE = UserState(
environment = Environment.Us,
avatarColorHex = "#aa00aa",
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = true,
organizations = emptyList(),
),

View file

@ -823,6 +823,7 @@ class AddSendViewModelTest : BaseViewModelTest() {
avatarColorHex = "#ff00ff",
environment = Environment.Us,
isPremium = false,
isLoggedIn = true,
isVaultUnlocked = true,
organizations = emptyList(),
)

View file

@ -795,6 +795,7 @@ class VaultItemViewModelTest : BaseViewModelTest() {
avatarColorHex = "#ff00ff",
environment = Environment.Us,
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = true,
organizations = emptyList(),
),

View file

@ -1031,6 +1031,7 @@ private val ACTIVE_ACCOUNT_SUMMARY = AccountSummary(
avatarColorHex = "#aa00aa",
environmentLabel = "bitwarden.com",
isActive = true,
isLoggedIn = true,
isVaultUnlocked = true,
)
@ -1041,6 +1042,7 @@ private val LOCKED_ACCOUNT_SUMMARY = AccountSummary(
avatarColorHex = "#00aaaa",
environmentLabel = "bitwarden.com",
isActive = false,
isLoggedIn = true,
isVaultUnlocked = false,
)

View file

@ -130,6 +130,7 @@ class VaultViewModelTest : BaseViewModelTest() {
avatarColorHex = "#00aaaa",
environment = Environment.Us,
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = true,
organizations = listOf(
Organization(
@ -154,6 +155,7 @@ class VaultViewModelTest : BaseViewModelTest() {
avatarColorHex = "#00aaaa",
environmentLabel = "bitwarden.com",
isActive = true,
isLoggedIn = true,
isVaultUnlocked = true,
),
),
@ -1030,6 +1032,7 @@ private val DEFAULT_USER_STATE = UserState(
avatarColorHex = "#aa00aa",
environment = Environment.Us,
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = true,
organizations = emptyList(),
),
@ -1040,6 +1043,7 @@ private val DEFAULT_USER_STATE = UserState(
avatarColorHex = "#00aaaa",
environment = Environment.Us,
isPremium = false,
isLoggedIn = true,
isVaultUnlocked = false,
organizations = emptyList(),
),
@ -1062,6 +1066,7 @@ private fun createMockVaultState(
avatarColorHex = "#aa00aa",
environmentLabel = "bitwarden.com",
isActive = true,
isLoggedIn = true,
isVaultUnlocked = true,
),
AccountSummary(
@ -1071,6 +1076,7 @@ private fun createMockVaultState(
avatarColorHex = "#00aaaa",
environmentLabel = "bitwarden.com",
isActive = false,
isLoggedIn = true,
isVaultUnlocked = false,
),
),

View file

@ -68,6 +68,17 @@ class AccountSummaryExtensionsTest {
)
}
@Test
fun `iconRes returns a locked lock for logged out accounts`() {
assertEquals(
R.drawable.ic_locked,
mockk<AccountSummary>() {
every { status } returns AccountSummary.Status.LOGGED_OUT
}
.iconRes,
)
}
@Test
fun `iconRes returns an unlocked lock for unlocked accounts`() {
assertEquals(
@ -100,6 +111,17 @@ class AccountSummaryExtensionsTest {
)
}
@Test
fun `supportingTextResOrNull returns Logged Out for logged out accounts`() {
assertEquals(
R.string.account_logged_out,
mockk<AccountSummary>() {
every { status } returns AccountSummary.Status.LOGGED_OUT
}
.supportingTextResOrNull,
)
}
@Test
fun `supportingTextResOrNull returns Unlocked for unlocked accounts`() {
assertEquals(

View file

@ -23,6 +23,7 @@ class UserStateExtensionsTest {
avatarColorHex = "activeAvatarColorHex",
environmentLabel = "bitwarden.com",
isActive = true,
isLoggedIn = true,
isVaultUnlocked = true,
),
AccountSummary(
@ -32,6 +33,7 @@ class UserStateExtensionsTest {
avatarColorHex = "lockedAvatarColorHex",
environmentLabel = "bitwarden.eu",
isActive = false,
isLoggedIn = true,
isVaultUnlocked = false,
),
AccountSummary(
@ -41,8 +43,19 @@ class UserStateExtensionsTest {
avatarColorHex = "unlockedAvatarColorHex",
environmentLabel = "vault.qa.bitwarden.pw",
isActive = false,
isLoggedIn = true,
isVaultUnlocked = true,
),
AccountSummary(
userId = "loggedOutUserId",
name = "loggedOutName",
email = "loggedOutEmail",
avatarColorHex = "loggedOutAvatarColorHex",
environmentLabel = "vault.qa.bitwarden.pw",
isActive = false,
isLoggedIn = false,
isVaultUnlocked = false,
),
),
UserState(
activeUserId = "activeUserId",
@ -54,6 +67,7 @@ class UserStateExtensionsTest {
avatarColorHex = "activeAvatarColorHex",
environment = Environment.Us,
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = true,
organizations = listOf(
Organization(
@ -69,6 +83,7 @@ class UserStateExtensionsTest {
avatarColorHex = "lockedAvatarColorHex",
environment = Environment.Eu,
isPremium = false,
isLoggedIn = true,
isVaultUnlocked = false,
organizations = listOf(
Organization(
@ -88,6 +103,7 @@ class UserStateExtensionsTest {
),
),
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = true,
organizations = listOf(
Organization(
@ -96,6 +112,26 @@ class UserStateExtensionsTest {
),
),
),
UserState.Account(
userId = "loggedOutUserId",
name = "loggedOutName",
email = "loggedOutEmail",
avatarColorHex = "loggedOutAvatarColorHex",
environment = Environment.SelfHosted(
environmentUrlData = EnvironmentUrlDataJson(
base = "https://vault.qa.bitwarden.pw",
),
),
isPremium = true,
isLoggedIn = false,
isVaultUnlocked = false,
organizations = listOf(
Organization(
id = "organizationId",
name = "organizationName",
),
),
),
),
)
.toAccountSummaries(),
@ -112,6 +148,7 @@ class UserStateExtensionsTest {
avatarColorHex = "avatarColorHex",
environmentLabel = "bitwarden.com",
isActive = true,
isLoggedIn = true,
isVaultUnlocked = true,
),
UserState.Account(
@ -121,6 +158,7 @@ class UserStateExtensionsTest {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = true,
organizations = listOf(
Organization(
@ -143,6 +181,7 @@ class UserStateExtensionsTest {
avatarColorHex = "avatarColorHex",
environmentLabel = "bitwarden.com",
isActive = false,
isLoggedIn = true,
isVaultUnlocked = false,
),
UserState.Account(
@ -152,6 +191,7 @@ class UserStateExtensionsTest {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = false,
isLoggedIn = true,
isVaultUnlocked = false,
organizations = listOf(
Organization(
@ -175,6 +215,7 @@ class UserStateExtensionsTest {
avatarColorHex = "avatarColorHex",
environmentLabel = "bitwarden.com",
isActive = true,
isLoggedIn = true,
isVaultUnlocked = true,
),
UserState(
@ -187,6 +228,7 @@ class UserStateExtensionsTest {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = true,
organizations = listOf(
Organization(
@ -211,6 +253,7 @@ class UserStateExtensionsTest {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = true,
organizations = emptyList(),
)
@ -244,6 +287,7 @@ class UserStateExtensionsTest {
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = true,
organizations = listOf(
Organization(