diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/publicuser/SearchUserRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/publicuser/SearchUserRepository.kt index d4db5e16e1d..4ed5079d957 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/publicuser/SearchUserRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/publicuser/SearchUserRepository.kt @@ -27,11 +27,13 @@ import com.wire.kalium.logic.data.user.SelfUser import com.wire.kalium.logic.data.user.UserDataSource import com.wire.kalium.logic.data.user.UserMapper import com.wire.kalium.logic.data.user.type.DomainUserTypeMapper +import com.wire.kalium.logic.data.user.type.UserEntityTypeMapper import com.wire.kalium.logic.di.MapperProvider import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.flatMap import com.wire.kalium.logic.functional.map import com.wire.kalium.logic.wrapApiRequest +import com.wire.kalium.network.api.base.authenticated.TeamsApi import com.wire.kalium.network.api.base.authenticated.userDetails.ListUserRequest import com.wire.kalium.network.api.base.authenticated.userDetails.UserDetailsApi import com.wire.kalium.network.api.base.authenticated.userDetails.qualifiedIds @@ -41,7 +43,6 @@ import com.wire.kalium.persistence.dao.MetadataDAO import com.wire.kalium.persistence.dao.QualifiedIDEntity import com.wire.kalium.persistence.dao.UserDAO import com.wire.kalium.persistence.dao.UserEntity -import com.wire.kalium.persistence.dao.UserTypeEntity import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterNotNull @@ -93,10 +94,12 @@ internal class SearchUserRepositoryImpl( private val userDAO: UserDAO, private val metadataDAO: MetadataDAO, private val userDetailsApi: UserDetailsApi, + private val teamsApi: TeamsApi, private val userSearchAPiWrapper: UserSearchApiWrapper, private val publicUserMapper: PublicUserMapper = MapperProvider.publicUserMapper(), private val userMapper: UserMapper = MapperProvider.userMapper(), - private val userTypeMapper: DomainUserTypeMapper = MapperProvider.userTypeMapper() + private val userTypeMapper: DomainUserTypeMapper = MapperProvider.userTypeMapper(), + private val userEntityTypeMapper: UserEntityTypeMapper = MapperProvider.userTypeEntityMapper(), ) : SearchUserRepository { override suspend fun searchKnownUsersByNameOrHandleOrEmail( @@ -152,42 +155,56 @@ internal class SearchUserRepositoryImpl( searchUsersOptions ).flatMap { val qualifiedIdList = it.documents.map { it.qualifiedID } - val response = + val usersResponse = if (qualifiedIdList.isEmpty()) Either.Right(listOf()) else wrapApiRequest { userDetailsApi.getMultipleUsers(ListUserRequest.qualifiedIds(qualifiedIdList)) }.map { listUsersDTO -> listUsersDTO.usersFound } - response.map { userProfileDTOList -> - val otherUserList = if (userProfileDTOList.isEmpty()) emptyList() else { - val selfUser = getSelfUser() - val (teamMembers, otherUsers) = userProfileDTOList - .partition { it.isTeamMember(selfUser.teamId?.value, selfUser.id.domain) } - // We need to store all found team members locally and not return them as they will be "known" users from now on. - userDAO.upsertTeamMembers( - teamMembers.map { userProfileDTO -> - userMapper.fromUserProfileDtoToUserEntity( - userProfile = userProfileDTO, - connectionState = ConnectionEntity.State.ACCEPTED, - userTypeEntity = UserTypeEntity.STANDARD - ) - } - ) + usersResponse.flatMap { userProfileDTOList -> + if (userProfileDTOList.isEmpty()) + return Either.Right(UserSearchResult(emptyList())) + + val selfUser = getSelfUser() + val (teamMembers, otherUsers) = userProfileDTOList + .partition { it.isTeamMember(selfUser.teamId?.value, selfUser.id.domain) } + val teamMembersResponse = + if (selfUser.teamId == null || teamMembers.isEmpty()) Either.Right(emptyMap()) + else wrapApiRequest { + teamsApi.getTeamMembersByIds(selfUser.teamId.value, TeamsApi.TeamMemberIdList(teamMembers.map { it.id.value })) + }.map { teamMemberList -> teamMemberList.members.associateBy { it.nonQualifiedUserId } } - otherUsers.map { userProfileDTO -> - publicUserMapper.fromUserProfileDtoToOtherUser( - userDetailResponse = userProfileDTO, - userType = userTypeMapper.fromTeamAndDomain( - otherUserDomain = userProfileDTO.id.domain, - selfUserTeamId = selfUser.teamId?.value, - otherUserTeamId = userProfileDTO.teamId, - selfUserDomain = selfUser.id.domain, - isService = userProfileDTO.service != null, + teamMembersResponse.map { teamMemberMap -> + // We need to store all found team members locally and not return them as they will be "known" users from now on. + teamMembers.map { userProfileDTO -> + userMapper.fromUserProfileDtoToUserEntity( + userProfile = userProfileDTO, + connectionState = ConnectionEntity.State.ACCEPTED, + userTypeEntity = userEntityTypeMapper.teamRoleCodeToUserType( + permissionCode = teamMemberMap[userProfileDTO.id.value]?.permissions?.own, + isService = userProfileDTO.service != null ) ) + }.let { + userDAO.upsertTeamMembers(it) + userDAO.upsertTeamMembersTypes(it) } + + UserSearchResult( + otherUsers.map { userProfileDTO -> + publicUserMapper.fromUserProfileDtoToOtherUser( + userDetailResponse = userProfileDTO, + userType = userTypeMapper.fromTeamAndDomain( + otherUserDomain = userProfileDTO.id.domain, + selfUserTeamId = selfUser.teamId?.value, + otherUserTeamId = userProfileDTO.teamId, + selfUserDomain = selfUser.id.domain, + isService = userProfileDTO.service != null, + ) + ) + } + ) } - UserSearchResult(otherUserList) } } @@ -221,5 +238,4 @@ internal class SearchUserRepositoryImpl( UserSearchResult(it.map(publicUserMapper::fromUserEntityToOtherUser)) } } - } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt index c989ca40438..c1ed4da2348 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt @@ -51,6 +51,7 @@ import com.wire.kalium.logic.functional.mapRight import com.wire.kalium.logic.kaliumLogger import com.wire.kalium.logic.wrapApiRequest import com.wire.kalium.logic.wrapStorageRequest +import com.wire.kalium.network.api.base.authenticated.TeamsApi import com.wire.kalium.network.api.base.authenticated.self.SelfApi import com.wire.kalium.network.api.base.authenticated.userDetails.ListUserRequest import com.wire.kalium.network.api.base.authenticated.userDetails.ListUsersDTO @@ -138,6 +139,7 @@ internal class UserDataSource internal constructor( private val clientDAO: ClientDAO, private val selfApi: SelfApi, private val userDetailsApi: UserDetailsApi, + private val teamsApi: TeamsApi, private val sessionRepository: SessionRepository, private val selfUserId: UserId, private val qualifiedIdMapper: QualifiedIdMapper, @@ -209,7 +211,10 @@ internal class UserDataSource internal constructor( override suspend fun fetchUserInfo(userId: UserId) = wrapApiRequest { userDetailsApi.getUserInfo(userId.toApi()) } - .flatMap { userProfileDTO -> persistUsers(listOf(userProfileDTO)) } + .flatMap { userProfileDTO -> + fetchTeamMembersByIds(listOf(userProfileDTO)) + .flatMap { persistUsers(listOf(userProfileDTO), it) } + } @Suppress("MagicNumber") override suspend fun fetchUsersByIds(qualifiedUserIdList: Set): Either = @@ -237,38 +242,58 @@ internal class UserDataSource internal constructor( kaliumLogger.d("Handling ${listUserProfileDTO.usersFailed.size} failed users") persistIncompleteUsers(listUserProfileDTO.usersFailed) } - persistUsers(listUserProfileDTO.usersFound) + fetchTeamMembersByIds(listUserProfileDTO.usersFound) + .flatMap { persistUsers(listUserProfileDTO.usersFound, it) } } } + private suspend fun fetchTeamMembersByIds(userProfileList: List): Either> { + val selfUserDomain = selfUserId.domain + val selfUserTeamId = selfTeamIdProvider().getOrNull() + val teamMemberIds = userProfileList.filter { it.isTeamMember(selfUserTeamId?.value, selfUserDomain) }.map { it.id.value } + return if (selfUserTeamId == null || teamMemberIds.isEmpty()) Either.Right(emptyList()) + else teamMemberIds + .chunked(BATCH_SIZE) + .foldToEitherWhileRight(emptyList()) { chunk, acc -> + wrapApiRequest { + kaliumLogger.d("Fetching ${chunk.size} team members") + teamsApi.getTeamMembersByIds(selfUserTeamId.value, TeamsApi.TeamMemberIdList(chunk)) + }.map { + kaliumLogger.d("Found ${it.members.size} team members") + (acc + it.members).distinct() + } + } + } + private suspend fun persistIncompleteUsers(usersFailed: List) = wrapStorageRequest { usersFailed.map { userMapper.fromFailedUserToEntity(it) }.forEach { userDAO.insertUser(it) } } - private suspend fun persistUsers(listUserProfileDTO: List) = wrapStorageRequest { - val selfUserDomain = selfUserId.domain + private suspend fun persistUsers( + listUserProfileDTO: List, + listTeamMemberDTO: List + ) = wrapStorageRequest { + val mapTeamMemberDTO = listTeamMemberDTO.associateBy { it.nonQualifiedUserId } val selfUserTeamId = selfTeamIdProvider().getOrNull()?.value val teamMembers = listUserProfileDTO - .filter { userProfileDTO -> userProfileDTO.isTeamMember(selfUserTeamId, selfUserDomain) } - val otherUsers = listUserProfileDTO - .filter { userProfileDTO -> !userProfileDTO.isTeamMember(selfUserTeamId, selfUserDomain) } - userDAO.upsertTeamMembers( - teamMembers.map { userProfileDTO -> + .filter { userProfileDTO -> mapTeamMemberDTO.containsKey(userProfileDTO.id.value) } + .map { userProfileDTO -> userMapper.fromUserProfileDtoToUserEntity( userProfile = userProfileDTO, connectionState = ConnectionEntity.State.ACCEPTED, - userTypeEntity = UserTypeEntity.STANDARD + userTypeEntity = + if (userProfileDTO.service != null) UserTypeEntity.SERVICE + else userTypeEntityMapper.teamRoleCodeToUserType(mapTeamMemberDTO[userProfileDTO.id.value]?.permissions?.own) ) } - ) - - userDAO.upsertUsers( - otherUsers.map { userProfileDTO -> + val otherUsers = listUserProfileDTO + .filter { userProfileDTO -> !mapTeamMemberDTO.containsKey(userProfileDTO.id.value) } + .map { userProfileDTO -> userMapper.fromUserProfileDtoToUserEntity( userProfile = userProfileDTO, - connectionState = ConnectionEntity.State.NOT_CONNECTED, + connectionState = ConnectionEntity.State.NOT_CONNECTED, // this won't be updated, just to avoid a null value userTypeEntity = userTypeEntityMapper.fromTeamAndDomain( otherUserDomain = userProfileDTO.id.domain, selfUserTeamId = selfUserTeamId, @@ -278,7 +303,13 @@ internal class UserDataSource internal constructor( ) ) } - ) + if (teamMembers.isNotEmpty()) { + userDAO.upsertTeamMembers(teamMembers) + userDAO.upsertTeamMembersTypes(teamMembers) + } + if (otherUsers.isNotEmpty()) { + userDAO.upsertUsers(otherUsers) + } } override suspend fun fetchUsersIfUnknownByIds(ids: Set): Either = wrapStorageRequest { diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/type/UserTypeMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/type/UserTypeMapper.kt index 56c60b4728b..9c52663eddb 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/type/UserTypeMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/type/UserTypeMapper.kt @@ -129,13 +129,14 @@ interface UserTypeMapper { private fun selfUserIsTeamMember(selfUserTeamId: String?) = selfUserTeamId != null - fun teamRoleCodeToUserType(permissionCode: Int?): T = when (permissionCode) { - TeamRole.ExternalPartner.value -> external - TeamRole.Member.value -> standard - TeamRole.Admin.value -> admin - TeamRole.Owner.value -> owner - null -> standard - else -> guest - } - + fun teamRoleCodeToUserType(permissionCode: Int?, isService: Boolean = false): T = + if (isService) service + else when (permissionCode) { + TeamRole.ExternalPartner.value -> external + TeamRole.Member.value -> standard + TeamRole.Admin.value -> admin + TeamRole.Owner.value -> owner + null -> standard + else -> guest + } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt index ef8d494eb01..8eb35e1e083 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt @@ -637,6 +637,7 @@ class UserSessionScope internal constructor( userStorage.database.clientDAO, authenticatedNetworkContainer.selfApi, authenticatedNetworkContainer.userDetailsApi, + authenticatedNetworkContainer.teamsApi, globalScope.sessionRepository, userId, qualifiedIdMapper, @@ -694,6 +695,7 @@ class UserSessionScope internal constructor( userStorage.database.userDAO, userStorage.database.metadataDAO, authenticatedNetworkContainer.userDetailsApi, + authenticatedNetworkContainer.teamsApi, userSearchApiWrapper ) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/publicuser/SearchUserRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/publicuser/SearchUserRepositoryTest.kt index 7cb4b7b6e6e..f7e34704b20 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/publicuser/SearchUserRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/publicuser/SearchUserRepositoryTest.kt @@ -20,7 +20,6 @@ package com.wire.kalium.logic.data.publicuser import com.wire.kalium.logic.NetworkFailure import com.wire.kalium.logic.data.id.ConversationId -import com.wire.kalium.logic.data.id.IdMapper import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.id.TeamId import com.wire.kalium.logic.data.publicuser.model.UserSearchResult @@ -33,6 +32,7 @@ import com.wire.kalium.logic.data.user.type.DomainUserTypeMapper import com.wire.kalium.logic.data.user.type.UserType import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.test_util.TestNetworkResponseError +import com.wire.kalium.network.api.base.authenticated.TeamsApi import com.wire.kalium.network.api.base.authenticated.search.ContactDTO import com.wire.kalium.network.api.base.authenticated.search.SearchPolicyDTO import com.wire.kalium.network.api.base.authenticated.search.UserSearchResponse @@ -69,7 +69,6 @@ import kotlin.test.assertIs import kotlin.test.assertTrue import com.wire.kalium.network.api.base.model.UserId as UserIdDTO -// TODO: refactor to arrangement pattern @OptIn(ExperimentalCoroutinesApi::class) class SearchUserRepositoryTest { @@ -337,9 +336,14 @@ class SearchUserRepositoryTest { usersFailed = emptyList(), usersFound = listOf(USER_PROFILE_DTO.copy(id = UserId("teamUser", SELF_USER.id.domain), teamId = SELF_USER.teamId?.value),) ) + val teamMembersResponse = TeamsApi.TeamMemberList( + hasMore = false, + members = listOf(TEAM_MEMBER_DTO.copy(nonQualifiedUserId = "teamUser")) + ) val (arrangement, searchUserRepository) = Arrangement() .withSearchResult(Either.Right(CONTACT_SEARCH_RESPONSE)) .withGetMultipleUsersResult(NetworkResponse.Success(userListResponse, mapOf(), 200)) + .withGetTeamMembersByIdsResult(NetworkResponse.Success(teamMembersResponse, mapOf(), 200)) .withValueByKeyFlowResult(flowOf(JSON_QUALIFIED_ID)) .withGetUserByQualifiedIdResult(flowOf(USER_ENTITY)) .withFromUserEntityToSelfUser(SELF_USER) @@ -360,6 +364,10 @@ class SearchUserRepositoryTest { .suspendFunction(arrangement.userDAO::upsertTeamMembers) .with(eq(listOf(USER_ENTITY))) .wasInvoked() + verify(arrangement.userDAO) + .suspendFunction(arrangement.userDAO::upsertTeamMembersTypes) + .with(eq(listOf(USER_ENTITY))) + .wasInvoked() } internal class Arrangement { @@ -370,6 +378,9 @@ class SearchUserRepositoryTest { @Mock internal val userDetailsApi: UserDetailsApi = mock(classOf()) + @Mock + internal val teamsApi: TeamsApi = mock(classOf()) + @Mock internal val userSearchApiWrapper: UserSearchApiWrapper = mock(classOf()) @@ -390,6 +401,7 @@ class SearchUserRepositoryTest { userDAO, metadataDAO, userDetailsApi, + teamsApi, userSearchApiWrapper, publicUserMapper, userMapper, @@ -420,6 +432,13 @@ class SearchUserRepositoryTest { .thenReturn(result) } + fun withGetTeamMembersByIdsResult(result: NetworkResponse) = apply { + given(teamsApi) + .suspendFunction(teamsApi::getTeamMembersByIds) + .whenInvokedWith(any()) + .thenReturn(result) + } + fun withFromUserProfileDtoToOtherUserResult(result: OtherUser) = apply { given(publicUserMapper) .function(publicUserMapper::fromUserProfileDtoToOtherUser) @@ -570,6 +589,14 @@ class SearchUserRepositoryTest { service = null ) + val TEAM_MEMBER_DTO = TeamsApi.TeamMemberDTO( + nonQualifiedUserId = "value", + createdBy = null, + legalHoldStatus = null, + createdAt = null, + permissions = null, + ) + val USER_RESPONSE = ListUsersDTO(usersFailed = emptyList(), usersFound = listOf(USER_PROFILE_DTO)) const val JSON_QUALIFIED_ID = """{"value":"test" , "domain":"test" }""" diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/user/DomainUserTypeMapperTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/user/DomainUserTypeMapperTest.kt index b812a3cda3c..1fed0cbe4d7 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/user/DomainUserTypeMapperTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/user/DomainUserTypeMapperTest.kt @@ -75,6 +75,14 @@ class DomainUserTypeMapperTest { assertEquals(UserType.INTERNAL, result) } + @Test + fun givenServiceTeamMember_whenMappingToConversationDetails_ThenConversationDetailsUserTypeIsService() { + // when + val result = userTypeMapper.teamRoleCodeToUserType(TeamRole.Member.value, true) + // then + assertEquals(UserType.SERVICE, result) + } + @Test fun givenCommonNotWireDomainAndDifferentTeam_whenMappingToConversationDetails_ThenConversationDetailsUserTypeIsFederated() { // given diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/user/UserEntityTypeMapperTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/user/UserEntityTypeMapperTest.kt index 0cead877981..5843a634fec 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/user/UserEntityTypeMapperTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/user/UserEntityTypeMapperTest.kt @@ -75,6 +75,14 @@ class UserEntityTypeMapperTest { assertEquals(UserTypeEntity.STANDARD, result) } + @Test + fun givenServiceTeamMember_whenMappingToConversationDetails_ThenConversationDetailsUserTypeIsService() { + // when + val result = userTypeMapper.teamRoleCodeToUserType(TeamRole.Member.value, true) + // then + assertEquals(UserTypeEntity.SERVICE, result) + } + @Test fun givenCommonNotTheSameDomainAndDifferentTeam_whenMappingToConversationDetails_ThenConversationDetailsUserTypeIsFederated() { // given diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/user/UserRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/user/UserRepositoryTest.kt index fbb3aaabf9f..5c3fc08aa7e 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/user/UserRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/user/UserRepositoryTest.kt @@ -38,12 +38,14 @@ import com.wire.kalium.logic.functional.getOrNull import com.wire.kalium.logic.sync.receiver.UserEventReceiverTest import com.wire.kalium.logic.util.shouldFail import com.wire.kalium.logic.util.shouldSucceed +import com.wire.kalium.network.api.base.authenticated.TeamsApi import com.wire.kalium.network.api.base.authenticated.self.SelfApi import com.wire.kalium.network.api.base.authenticated.userDetails.ListUserRequest import com.wire.kalium.network.api.base.authenticated.userDetails.ListUsersDTO import com.wire.kalium.network.api.base.authenticated.userDetails.QualifiedUserIdListRequest import com.wire.kalium.network.api.base.authenticated.userDetails.UserDetailsApi import com.wire.kalium.network.api.base.authenticated.userDetails.qualifiedIds +import com.wire.kalium.network.api.base.model.UserProfileDTO import com.wire.kalium.network.utils.NetworkResponse import com.wire.kalium.persistence.dao.MetadataDAO import com.wire.kalium.persistence.dao.QualifiedIDEntity @@ -72,6 +74,7 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertIs import kotlin.test.assertTrue class UserRepositoryTest { @@ -344,7 +347,7 @@ class UserRepositoryTest { verify(arrangement.userDAO) .suspendFunction(arrangement.userDAO::upsertTeamMembers) .with(any()) - .wasInvoked(exactly = once) + .wasNotInvoked() verify(arrangement.userDAO) .suspendFunction(arrangement.userDAO::upsertUsers) .with(any()) @@ -394,7 +397,7 @@ class UserRepositoryTest { verify(arrangement.userDAO) .suspendFunction(arrangement.userDAO::upsertTeamMembers) .with(any()) - .wasInvoked(exactly = once) + .wasNotInvoked() verify(arrangement.userDAO) .suspendFunction(arrangement.userDAO::upsertUsers) .with(any()) @@ -561,6 +564,39 @@ class UserRepositoryTest { .wasInvoked(once) } + @Test + fun givenATeamMemberUser_whenFetchingUserInfo_thenItShouldBeUpsertedAsATeamMember() = runTest { + val (arrangement, userRepository) = Arrangement() + .withUserDaoReturning(TestUser.ENTITY.copy(team = TestTeam.TEAM_ID.value)) + .withSuccessfulGetUsersInfo(TestUser.USER_PROFILE_DTO.copy(teamId = TestTeam.TEAM_ID.value)) + .withSuccessfulFetchTeamMembersByIds(listOf(TestTeam.memberDTO((TestUser.USER_PROFILE_DTO.id.value)))) + .arrange() + + val result = userRepository.fetchUserInfo(TestUser.USER_ID) + + assertIs>(result) + verify(arrangement.userDetailsApi) + .suspendFunction(arrangement.userDetailsApi::getUserInfo) + .with(any()) + .wasInvoked(exactly = once) + verify(arrangement.teamsApi) + .suspendFunction(arrangement.teamsApi::getTeamMembersByIds) + .with(any()) + .wasInvoked(exactly = once) + verify(arrangement.userDAO) + .suspendFunction(arrangement.userDAO::upsertTeamMembers) + .with(any()) + .wasInvoked(exactly = once) + verify(arrangement.userDAO) + .suspendFunction(arrangement.userDAO::upsertTeamMembersTypes) + .with(any()) + .wasInvoked(exactly = once) + verify(arrangement.userDAO) + .suspendFunction(arrangement.userDAO::upsertUsers) + .with(any()) + .wasNotInvoked() + } + private class Arrangement { @Mock val userDAO = configure(mock(classOf())) { stubsUnitByDefault = true } @@ -577,6 +613,9 @@ class UserRepositoryTest { @Mock val userDetailsApi = mock(classOf()) + @Mock + val teamsApi = mock(classOf()) + @Mock val sessionRepository = mock(SessionRepository::class) @@ -595,6 +634,7 @@ class UserRepositoryTest { clientDAO, selfApi, userDetailsApi, + teamsApi, sessionRepository, selfUserId, qualifiedIdMapper, @@ -635,11 +675,18 @@ class UserRepositoryTest { .thenReturn(flowOf(userEntities)) } - fun withSuccessfulGetUsersInfo() = apply { + fun withSuccessfulGetUsersInfo(result: UserProfileDTO = TestUser.USER_PROFILE_DTO) = apply { given(userDetailsApi) .suspendFunction(userDetailsApi::getUserInfo) .whenInvokedWith(any()) - .thenReturn(NetworkResponse.Success(TestUser.USER_PROFILE_DTO, mapOf(), 200)) + .thenReturn(NetworkResponse.Success(result, mapOf(), 200)) + } + + fun withSuccessfulFetchTeamMembersByIds(result: List) = apply { + given(teamsApi) + .suspendFunction(teamsApi::getTeamMembersByIds) + .whenInvokedWith(any(), any()) + .thenReturn(NetworkResponse.Success(TeamsApi.TeamMemberList(false, result), mapOf(), 200)) } fun withSuccessfulGetUsersByQualifiedIdList(knownUserEntities: List) = apply { diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/TeamsApi.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/TeamsApi.kt index 67a07ef05e9..b8348d0bcb4 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/TeamsApi.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/TeamsApi.kt @@ -53,6 +53,11 @@ interface TeamsApi { @SerialName("self") val own: Int ) + @Serializable + data class TeamMemberIdList( + @SerialName("user_ids") val userIds: List + ) + sealed interface GetTeamsOptionsInterface /** @@ -80,6 +85,7 @@ interface TeamsApi { suspend fun deleteConversation(conversationId: NonQualifiedConversationId, teamId: TeamId): NetworkResponse suspend fun getTeamMembers(teamId: TeamId, limitTo: Int?): NetworkResponse + suspend fun getTeamMembersByIds(teamId: TeamId, teamMemberIdList: TeamMemberIdList): NetworkResponse suspend fun getTeamMember(teamId: TeamId, userId: NonQualifiedUserId): NetworkResponse suspend fun getTeamInfo(teamId: TeamId): NetworkResponse suspend fun whiteListedServices(teamId: TeamId, size: Int = DEFAULT_SERVICES_SIZE): NetworkResponse diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v0/authenticated/TeamsApiV0.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v0/authenticated/TeamsApiV0.kt index 969c49c3a13..10b14a2c441 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v0/authenticated/TeamsApiV0.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v0/authenticated/TeamsApiV0.kt @@ -30,6 +30,8 @@ import com.wire.kalium.network.utils.wrapKaliumResponse import io.ktor.client.request.delete import io.ktor.client.request.get import io.ktor.client.request.parameter +import io.ktor.client.request.post +import io.ktor.client.request.setBody internal open class TeamsApiV0 internal constructor( private val authenticatedNetworkClient: AuthenticatedNetworkClient @@ -60,6 +62,15 @@ internal open class TeamsApiV0 internal constructor( } } + override suspend fun getTeamMembersByIds( + teamId: TeamId, + teamMemberIdList: TeamsApi.TeamMemberIdList + ): NetworkResponse = wrapKaliumResponse { + httpClient.post("$PATH_TEAMS/$teamId/$PATH_MEMBERS_BY_IDS") { + setBody(teamMemberIdList) + } + } + override suspend fun getTeamMember(teamId: TeamId, userId: NonQualifiedUserId): NetworkResponse = wrapKaliumResponse { httpClient.get("$PATH_TEAMS/$teamId/$PATH_MEMBERS/$userId") @@ -69,6 +80,7 @@ internal open class TeamsApiV0 internal constructor( const val PATH_TEAMS = "teams" const val PATH_CONVERSATIONS = "conversations" const val PATH_MEMBERS = "members" + const val PATH_MEMBERS_BY_IDS = "get-members-by-ids-using-post" const val PATH_SERVICES = "services" const val PATH_WHITELISTED = "whitelisted" } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAO.kt index 067f8e1fa62..d44e165314b 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAO.kt @@ -148,8 +148,13 @@ interface UserDAO { suspend fun insertOrIgnoreUsers(users: List) /** - * This will update all columns, except [ConnectionEntity.State] or insert a new record with default value - * [ConnectionEntity.State.NOT_CONNECTED] + * This will update all columns, except: + * - [ConnectionEntity.State] + * - [UserEntity.availabilityStatus] + * - [UserEntity.deleted] + * - [UserEntity.defederated] + * and set [UserEntity.hasIncompleteMetadata] to false + * or insert a new record with given values except [UserEntity.defederated] (this will be set to false). * An upsert operation is a one that tries to update a record and if fails (not rows affected by change) inserts instead. * In this case as the transaction can be executed many times, we need to take care for not deleting old data. */