diff options
author | 2024-01-23 12:21:43 -0500 | |
---|---|---|
committer | 2024-01-25 19:56:19 -0500 | |
commit | f30cb97a784ba508a82863ef74ea0135355aad0c (patch) | |
tree | fee5089bc3b3bf2b33e5caa9851303e7b0c52b07 | |
parent | c5d725eec15d2cff2aab08437948d6d0f2d01a63 (diff) |
UserInteractor and Profile
A domain component which applies business logic to User data.
This component maps Users to Profiles, the set of which is
defined by Profile.Type
Bug: 309960444
Test: atest IntentResolver-tests-unit:UserInteractorTest \
FakeUserRepositoryTest
Change-Id: I9832836ae019ba1b0ae45366f9fc0e26bc9b23ce
11 files changed, 539 insertions, 40 deletions
diff --git a/java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt b/java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt index fc82efee..a0b2d1ef 100644 --- a/java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt +++ b/java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt @@ -1,8 +1,8 @@ package com.android.intentresolver.v2.data.repository import android.content.pm.UserInfo -import com.android.intentresolver.v2.data.model.User -import com.android.intentresolver.v2.data.model.User.Role +import com.android.intentresolver.v2.shared.model.User +import com.android.intentresolver.v2.shared.model.User.Role /** Maps the UserInfo to one of the defined [Roles][User.Role], if possible. */ fun UserInfo.getSupportedUserRole(): Role? = diff --git a/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt b/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt index d2011aed..91ad6409 100644 --- a/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt +++ b/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt @@ -20,8 +20,8 @@ import com.android.intentresolver.inject.Background import com.android.intentresolver.inject.Main import com.android.intentresolver.inject.ProfileParent import com.android.intentresolver.v2.data.broadcastFlow -import com.android.intentresolver.v2.data.model.User import com.android.intentresolver.v2.data.repository.UserRepositoryImpl.UserEvent +import com.android.intentresolver.v2.shared.model.User import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -49,7 +49,7 @@ interface UserRepository { * * Availability is currently defined as not being in [quietMode][UserInfo.isQuietModeEnabled]. */ - fun isAvailable(user: User): Flow<Boolean> + val availability: Flow<Map<User, Boolean>> /** * Request that availability be updated to the requested state. This currently includes toggling @@ -145,30 +145,16 @@ constructor( override val users: Flow<List<User>> = usersWithState.map { userStateMap -> userStateMap.map { it.user } }.distinctUntilChanged() - private val availability: Flow<Map<UserHandle, Boolean>> = + override val availability: Flow<Map<User, Boolean>> = usersWithState - .map { list -> list.associateBy { it.user.handle }.mapValues { it.value.available } } + .map { list -> list.associate { it.user to it.available } } .distinctUntilChanged() - override fun isAvailable(user: User): Flow<Boolean> { - return isAvailable(user.handle) - } - - @VisibleForTesting - fun isAvailable(handle: UserHandle): Flow<Boolean> { - return availability.map { it[handle] ?: false } - } - override suspend fun requestState(user: User, available: Boolean) { require(user.type == User.Type.PROFILE) { "Only profile users are supported" } - return requestState(user.handle, available) - } - - @VisibleForTesting - suspend fun requestState(user: UserHandle, available: Boolean) { return withContext(backgroundDispatcher) { Log.i(TAG, "requestQuietModeEnabled: ${!available} for user $user") - userManager.requestQuietModeEnabled(/* enableQuietMode = */ !available, user) + userManager.requestQuietModeEnabled(/* enableQuietMode = */ !available, user.handle) } } diff --git a/java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt b/java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt index 94f985e7..a84342f4 100644 --- a/java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt +++ b/java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt @@ -25,8 +25,11 @@ interface UserRepositoryModule { @Provides @Singleton @ProfileParent - fun profileParent(@ApplicationUser user: UserHandle, userManager: UserManager): UserHandle { - return userManager.getProfileParent(user) ?: user + fun profileParent( + @ApplicationContext context: Context, + userManager: UserManager + ): UserHandle { + return userManager.getProfileParent(context.user) ?: context.user } } diff --git a/java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt b/java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt index 7ee78d91..3553744a 100644 --- a/java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt +++ b/java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt @@ -2,7 +2,7 @@ package com.android.intentresolver.v2.data.repository import android.content.Context import androidx.core.content.getSystemService -import com.android.intentresolver.v2.data.model.User +import com.android.intentresolver.v2.shared.model.User /** * Provides cached instances of a [system service][Context.getSystemService] created with diff --git a/java/src/com/android/intentresolver/v2/domain/interactor/UserInteractor.kt b/java/src/com/android/intentresolver/v2/domain/interactor/UserInteractor.kt new file mode 100644 index 00000000..e1b3fb36 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/domain/interactor/UserInteractor.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.domain.interactor + +import android.os.UserHandle +import com.android.intentresolver.inject.ApplicationUser +import com.android.intentresolver.v2.data.repository.UserRepository +import com.android.intentresolver.v2.domain.model.Profile +import com.android.intentresolver.v2.domain.model.Profile.Type +import com.android.intentresolver.v2.shared.model.User +import com.android.intentresolver.v2.shared.model.User.Role +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull + +/** The high level User interface. */ +class UserInteractor +@Inject +constructor( + private val userRepository: UserRepository, + /** The specific [User] of the application which started this one. */ + @ApplicationUser val launchedAs: UserHandle, +) { + /** The profile group associated with the launching app user. */ + val profiles: Flow<List<Profile>> = + userRepository.users.map { users -> + users.mapNotNull { user -> + when (user.role) { + // PERSONAL includes CLONE + Role.PERSONAL -> { + Profile(Type.PERSONAL, user, users.firstOrNull { it.role == Role.CLONE }) + } + Role.CLONE -> { + /* ignore, included above */ + null + } + // others map 1:1 + else -> Profile(profileFromRole(user.role), user) + } + } + } + + /** The [Profile] of the application which started this one. */ + val launchedAsProfile: Flow<Profile> = + profiles.map { profiles -> + // The launching user profile is the one with a primary id or clone id + // matching the application user id. By definition there must always be exactly + // one matching profile for the current user. + profiles.single { + it.primary.id == launchedAs.identifier || it.clone?.id == launchedAs.identifier + } + } + + /** + * Provides a flow to report on the availability of the profile. An unavailable profile may be + * hidden or appear disabled within the app. + */ + fun isAvailable(type: Type): Flow<Boolean> { + val profileFlow = profiles.map { list -> list.firstOrNull { it.type == type } } + return combine(profileFlow, userRepository.availability) { profile, availability -> + when (profile) { + null -> false + else -> availability.getOrDefault(profile.primary, false) + } + } + } + + private fun profileFromRole(role: Role): Type = + when (role) { + Role.PERSONAL -> Type.PERSONAL + Role.CLONE -> Type.PERSONAL /* CLONE maps to PERSONAL */ + Role.PRIVATE -> Type.PRIVATE + Role.WORK -> Type.WORK + } +} diff --git a/java/src/com/android/intentresolver/v2/domain/model/Profile.kt b/java/src/com/android/intentresolver/v2/domain/model/Profile.kt new file mode 100644 index 00000000..46015c7a --- /dev/null +++ b/java/src/com/android/intentresolver/v2/domain/model/Profile.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.domain.model + +import com.android.intentresolver.v2.domain.model.Profile.Type +import com.android.intentresolver.v2.shared.model.User + +/** + * A domain layer model which associates [users][User] into a [Type] instance. + * + * This is a simple abstraction which combines a primary [user][User] with an optional + * [cloned apps][User.Role.CLONE] user. This encapsulates the cloned app user id, while still being + * available where needed. + */ +data class Profile( + val type: Type, + val primary: User, + /** + * An optional [User] of which contains second instances of some applications installed for the + * personal user. This value may only be supplied when creating the PERSONAL profile. + */ + val clone: User? = null +) { + + init { + clone?.apply { + require(primary.role == User.Role.PERSONAL) { + "clone is not supported for profile=${this@Profile.type} / primary=$primary" + } + require(role == User.Role.CLONE) { "clone is not a clone user ($this)" } + } + } + + enum class Type { + PERSONAL, + WORK, + PRIVATE + } +} diff --git a/java/src/com/android/intentresolver/v2/data/model/User.kt b/java/src/com/android/intentresolver/v2/shared/model/User.kt index 504b04c8..97db3280 100644 --- a/java/src/com/android/intentresolver/v2/data/model/User.kt +++ b/java/src/com/android/intentresolver/v2/shared/model/User.kt @@ -1,10 +1,25 @@ -package com.android.intentresolver.v2.data.model +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.shared.model import android.annotation.UserIdInt import android.os.UserHandle -import com.android.intentresolver.v2.data.model.User.Type -import com.android.intentresolver.v2.data.model.User.Type.FULL -import com.android.intentresolver.v2.data.model.User.Type.PROFILE +import com.android.intentresolver.v2.shared.model.User.Type.FULL +import com.android.intentresolver.v2.shared.model.User.Type.PROFILE /** * A User represents the owner of a distinct set of content. diff --git a/tests/shared/src/com/android/intentresolver/v2/data/repository/FakeUserRepository.kt b/tests/shared/src/com/android/intentresolver/v2/data/repository/FakeUserRepository.kt new file mode 100644 index 00000000..5ed6f506 --- /dev/null +++ b/tests/shared/src/com/android/intentresolver/v2/data/repository/FakeUserRepository.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.data.repository + +import com.android.intentresolver.v2.shared.model.User +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update + +/** A simple repository which can be initialized from a list and updated. */ +class FakeUserRepository(vararg userList: User) : UserRepository { + internal data class UserState(val user: User, val available: Boolean) + + private val userState = MutableStateFlow(userList.map { UserState(it, available = true) }) + + // Expose a List<User> from List<UserState> + override val users = userState.map { userList -> userList.map { it.user } } + + fun addUser(user: User, available: Boolean) { + require(userState.value.none { it.user.id == user.id }) { + "A User with ${user.id} already exists!" + } + userState.update { it + UserState(user, available) } + } + + fun removeUser(user: User) { + require(userState.value.any { it.user.id == user.id }) { + "A User with ${user.id} does not exist!" + } + userState.update { it.filterNot { state -> state.user.id == user.id } } + } + + override val availability = + userState.map { userStateList -> userStateList.associate { it.user to it.available } } + + override suspend fun requestState(user: User, available: Boolean) { + userState.update { userStateList -> + userStateList.map { userState -> + if (userState.user.id == user.id) { + UserState(user, available) + } else { + userState + } + } + } + } +} diff --git a/tests/unit/src/com/android/intentresolver/v2/data/repository/FakeUserRepositoryTest.kt b/tests/unit/src/com/android/intentresolver/v2/data/repository/FakeUserRepositoryTest.kt new file mode 100644 index 00000000..334f31ad --- /dev/null +++ b/tests/unit/src/com/android/intentresolver/v2/data/repository/FakeUserRepositoryTest.kt @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.data.repository + +import com.android.intentresolver.v2.coroutines.collectLastValue +import com.android.intentresolver.v2.shared.model.User +import com.google.common.truth.Truth.assertThat +import kotlin.random.Random +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class FakeUserRepositoryTest { + private val baseId = Random.nextInt(1000, 2000) + + private val personalUser = User(id = baseId, role = User.Role.PERSONAL) + private val cloneUser = User(id = baseId + 1, role = User.Role.CLONE) + private val workUser = User(id = baseId + 2, role = User.Role.WORK) + private val privateUser = User(id = baseId + 3, role = User.Role.PRIVATE) + + @Test + fun init() = runTest { + val repo = FakeUserRepository(personalUser, workUser, privateUser) + + val users by collectLastValue(repo.users) + assertThat(users).containsExactly(personalUser, workUser, privateUser) + } + + @Test + fun addUser() = runTest { + val repo = FakeUserRepository() + + val users by collectLastValue(repo.users) + assertThat(users).isEmpty() + + repo.addUser(personalUser, true) + assertThat(users).containsExactly(personalUser) + + repo.addUser(workUser, false) + assertThat(users).containsExactly(personalUser, workUser) + } + + @Test + fun removeUser() = runTest { + val repo = FakeUserRepository(personalUser, workUser) + + val users by collectLastValue(repo.users) + repo.removeUser(workUser) + assertThat(users).containsExactly(personalUser) + + repo.removeUser(personalUser) + assertThat(users).isEmpty() + } + + @Test + fun isAvailable_defaultValue() = runTest { + val repo = FakeUserRepository(personalUser, workUser) + + val available by collectLastValue(repo.availability) + + repo.requestState(workUser, false) + assertThat(available!![workUser]).isFalse() + + repo.requestState(workUser, true) + assertThat(available!![workUser]).isTrue() + } + + @Test + fun isAvailable() = runTest { + val repo = FakeUserRepository(personalUser, workUser) + + val available by collectLastValue(repo.availability) + assertThat(available!![workUser]).isTrue() + + repo.requestState(workUser, false) + assertThat(available!![workUser]).isFalse() + + repo.requestState(workUser, true) + assertThat(available!![workUser]).isTrue() + } + + @Test + fun isAvailable_addRemove() = runTest { + val repo = FakeUserRepository(personalUser, workUser) + + val available by collectLastValue(repo.availability) + assertThat(available!![workUser]).isTrue() + + repo.removeUser(workUser) + assertThat(available!![workUser]).isNull() + + repo.addUser(workUser, true) + assertThat(available!![workUser]).isTrue() + } +} diff --git a/tests/unit/src/com/android/intentresolver/v2/data/repository/UserRepositoryImplTest.kt b/tests/unit/src/com/android/intentresolver/v2/data/repository/UserRepositoryImplTest.kt index 77f47285..6c61dfd6 100644 --- a/tests/unit/src/com/android/intentresolver/v2/data/repository/UserRepositoryImplTest.kt +++ b/tests/unit/src/com/android/intentresolver/v2/data/repository/UserRepositoryImplTest.kt @@ -8,10 +8,10 @@ import android.os.UserHandle.USER_SYSTEM import android.os.UserManager import com.android.intentresolver.mock import com.android.intentresolver.v2.coroutines.collectLastValue -import com.android.intentresolver.v2.data.model.User -import com.android.intentresolver.v2.data.model.User.Role import com.android.intentresolver.v2.platform.FakeUserManager import com.android.intentresolver.v2.platform.FakeUserManager.ProfileType +import com.android.intentresolver.v2.shared.model.User +import com.android.intentresolver.v2.shared.model.User.Role import com.android.intentresolver.whenever import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage @@ -66,30 +66,32 @@ internal class UserRepositoryImplTest { fun isAvailable() = runTest { val repo = createUserRepository(userManager) val work = userState.createProfile(ProfileType.WORK) + val workUser = User(work.identifier, Role.WORK) - val available by collectLastValue(repo.isAvailable(work)) - assertThat(available).isTrue() + val available by collectLastValue(repo.availability) + assertThat(available?.get(workUser)).isTrue() userState.setQuietMode(work, true) - assertThat(available).isFalse() + assertThat(available?.get(workUser)).isFalse() userState.setQuietMode(work, false) - assertThat(available).isTrue() + assertThat(available?.get(workUser)).isTrue() } @Test fun requestState() = runTest { val repo = createUserRepository(userManager) val work = userState.createProfile(ProfileType.WORK) + val workUser = User(work.identifier, Role.WORK) - val available by collectLastValue(repo.isAvailable(work)) - assertThat(available).isTrue() + val available by collectLastValue(repo.availability) + assertThat(available?.get(workUser)).isTrue() - repo.requestState(work, false) - assertThat(available).isFalse() + repo.requestState(workUser, false) + assertThat(available?.get(workUser)).isFalse() - repo.requestState(work, true) - assertThat(available).isTrue() + repo.requestState(workUser, true) + assertThat(available?.get(workUser)).isTrue() } @Test(expected = IllegalArgumentException::class) diff --git a/tests/unit/src/com/android/intentresolver/v2/domain/interactor/UserInteractorTest.kt b/tests/unit/src/com/android/intentresolver/v2/domain/interactor/UserInteractorTest.kt new file mode 100644 index 00000000..6fa055ef --- /dev/null +++ b/tests/unit/src/com/android/intentresolver/v2/domain/interactor/UserInteractorTest.kt @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.domain.interactor + +import com.android.intentresolver.v2.coroutines.collectLastValue +import com.android.intentresolver.v2.data.repository.FakeUserRepository +import com.android.intentresolver.v2.domain.model.Profile +import com.android.intentresolver.v2.domain.model.Profile.Type.PERSONAL +import com.android.intentresolver.v2.domain.model.Profile.Type.PRIVATE +import com.android.intentresolver.v2.domain.model.Profile.Type.WORK +import com.android.intentresolver.v2.shared.model.User +import com.android.intentresolver.v2.shared.model.User.Role +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import kotlin.random.Random +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class UserInteractorTest { + private val baseId = Random.nextInt(1000, 2000) + + private val personalUser = User(id = baseId, role = Role.PERSONAL) + private val cloneUser = User(id = baseId + 1, role = Role.CLONE) + private val workUser = User(id = baseId + 2, role = Role.WORK) + private val privateUser = User(id = baseId + 3, role = Role.PRIVATE) + + @Test + fun launchedByProfile(): Unit = runTest { + val profileInteractor = + UserInteractor( + userRepository = FakeUserRepository(personalUser, cloneUser), + launchedAs = personalUser.handle + ) + + val launchedAsProfile by collectLastValue(profileInteractor.launchedAsProfile) + + assertThat(launchedAsProfile).isEqualTo(Profile(PERSONAL, personalUser, cloneUser)) + } + + @Test + fun launchedByProfile_asClone(): Unit = runTest { + val profileInteractor = + UserInteractor( + userRepository = FakeUserRepository(personalUser, cloneUser), + launchedAs = cloneUser.handle + ) + val profiles by collectLastValue(profileInteractor.launchedAsProfile) + + assertThat(profiles).isEqualTo(Profile(PERSONAL, personalUser, cloneUser)) + } + + @Test + fun profiles_withPersonal(): Unit = runTest { + val profileInteractor = + UserInteractor( + userRepository = FakeUserRepository(personalUser), + launchedAs = personalUser.handle + ) + + val profiles by collectLastValue(profileInteractor.profiles) + + assertThat(profiles).containsExactly(Profile(PERSONAL, personalUser)) + } + + @Test + fun profiles_addClone(): Unit = runTest { + val fakeUserRepo = FakeUserRepository(personalUser) + val profileInteractor = + UserInteractor(userRepository = fakeUserRepo, launchedAs = personalUser.handle) + + val profiles by collectLastValue(profileInteractor.profiles) + assertThat(profiles).containsExactly(Profile(PERSONAL, personalUser)) + + fakeUserRepo.addUser(cloneUser, available = true) + assertThat(profiles).containsExactly(Profile(PERSONAL, personalUser, cloneUser)) + } + + @Test + fun profiles_withPersonalAndClone(): Unit = runTest { + val profileInteractor = + UserInteractor( + userRepository = FakeUserRepository(personalUser, cloneUser), + launchedAs = personalUser.handle + ) + val profiles by collectLastValue(profileInteractor.profiles) + + assertThat(profiles).containsExactly(Profile(PERSONAL, personalUser, cloneUser)) + } + + @Test + fun profiles_withAllSupportedTypes(): Unit = runTest { + val profileInteractor = + UserInteractor( + userRepository = FakeUserRepository(personalUser, cloneUser, workUser, privateUser), + launchedAs = personalUser.handle + ) + val profiles by collectLastValue(profileInteractor.profiles) + + assertThat(profiles) + .containsExactly( + Profile(PERSONAL, personalUser, cloneUser), + Profile(WORK, workUser), + Profile(PRIVATE, privateUser) + ) + } + + @Test + fun profiles_preservesIterationOrder(): Unit = runTest { + val profileInteractor = + UserInteractor( + userRepository = FakeUserRepository(workUser, cloneUser, privateUser, personalUser), + launchedAs = personalUser.handle + ) + + val profiles by collectLastValue(profileInteractor.profiles) + + assertThat(profiles) + .containsExactly( + Profile(WORK, workUser), + Profile(PRIVATE, privateUser), + Profile(PERSONAL, personalUser, cloneUser), + ) + } + + @Test + fun isAvailable_defaultValue() = runTest { + val userRepo = FakeUserRepository(personalUser) + userRepo.addUser(workUser, false) + + val profileInteractor = + UserInteractor(userRepository = userRepo, launchedAs = personalUser.handle) + val personalAvailable by collectLastValue(profileInteractor.isAvailable(PERSONAL)) + val workAvailable by collectLastValue(profileInteractor.isAvailable(WORK)) + + assertWithMessage("personalAvailable").that(personalAvailable!!).isTrue() + + assertWithMessage("workAvailable").that(workAvailable!!).isFalse() + } + + @Test + fun isAvailable() = runTest { + val userRepo = FakeUserRepository(workUser, personalUser) + val profileInteractor = + UserInteractor(userRepository = userRepo, launchedAs = personalUser.handle) + val workAvailable by collectLastValue(profileInteractor.isAvailable(WORK)) + + // Default state is enabled in FakeUserManager + assertWithMessage("workAvailable").that(workAvailable).isTrue() + + // Making user unavailable makes profile unavailable + userRepo.requestState(workUser, false) + assertWithMessage("workAvailable").that(workAvailable).isFalse() + + // Making user available makes profile available again + userRepo.requestState(workUser, true) + assertWithMessage("workAvailable").that(workAvailable).isTrue() + + // When a user is removed availability should update to false + userRepo.removeUser(workUser) + assertWithMessage("workAvailable").that(workAvailable).isFalse() + } +} |