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 08438762687..d4db5e16e1d 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 @@ -35,11 +35,13 @@ import com.wire.kalium.logic.wrapApiRequest 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 +import com.wire.kalium.network.api.base.model.isTeamMember import com.wire.kalium.persistence.dao.ConnectionEntity 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 @@ -158,7 +160,21 @@ internal class SearchUserRepositoryImpl( response.map { userProfileDTOList -> val otherUserList = if (userProfileDTOList.isEmpty()) emptyList() else { val selfUser = getSelfUser() - userProfileDTOList.map { userProfileDTO -> + 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 + ) + } + ) + + otherUsers.map { userProfileDTO -> publicUserMapper.fromUserProfileDtoToOtherUser( userDetailResponse = userProfileDTO, userType = userTypeMapper.fromTeamAndDomain( 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 79ff9fa0d87..c989ca40438 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 @@ -58,6 +58,7 @@ 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.SelfUserDTO import com.wire.kalium.network.api.base.model.UserProfileDTO +import com.wire.kalium.network.api.base.model.isTeamMember import com.wire.kalium.persistence.dao.ConnectionEntity import com.wire.kalium.persistence.dao.MetadataDAO import com.wire.kalium.persistence.dao.QualifiedIDEntity @@ -250,9 +251,9 @@ internal class UserDataSource internal constructor( val selfUserDomain = selfUserId.domain val selfUserTeamId = selfTeamIdProvider().getOrNull()?.value val teamMembers = listUserProfileDTO - .filter { userProfileDTO -> isTeamMember(selfUserTeamId, userProfileDTO, selfUserDomain) } + .filter { userProfileDTO -> userProfileDTO.isTeamMember(selfUserTeamId, selfUserDomain) } val otherUsers = listUserProfileDTO - .filter { userProfileDTO -> !isTeamMember(selfUserTeamId, userProfileDTO, selfUserDomain) } + .filter { userProfileDTO -> !userProfileDTO.isTeamMember(selfUserTeamId, selfUserDomain) } userDAO.upsertTeamMembers( teamMembers.map { userProfileDTO -> userMapper.fromUserProfileDtoToUserEntity( @@ -280,14 +281,6 @@ internal class UserDataSource internal constructor( ) } - private fun isTeamMember( - selfUserTeamId: String?, - userProfileDTO: UserProfileDTO, - selfUserDomain: String? - ) = (selfUserTeamId != null && - userProfileDTO.teamId == selfUserTeamId && - userProfileDTO.id.domain == selfUserDomain) - override suspend fun fetchUsersIfUnknownByIds(ids: Set): Either = wrapStorageRequest { val qualifiedIDList = ids.map { it.toDao() } val knownUsers = userDAO.getUsersByQualifiedIDList(ids.map { it.toDao() }) 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 e4b4acd4666..7cb4b7b6e6e 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 @@ -39,6 +39,7 @@ import com.wire.kalium.network.api.base.authenticated.search.UserSearchResponse import com.wire.kalium.network.api.base.authenticated.userDetails.ListUsersDTO import com.wire.kalium.network.api.base.authenticated.userDetails.UserDetailsApi import com.wire.kalium.network.api.base.model.LegalHoldStatusResponse +import com.wire.kalium.network.api.base.model.UserId import com.wire.kalium.network.api.base.model.UserProfileDTO import com.wire.kalium.network.utils.NetworkResponse import com.wire.kalium.persistence.dao.ConnectionEntity @@ -53,14 +54,15 @@ import io.mockative.Times import io.mockative.any import io.mockative.anything import io.mockative.classOf +import io.mockative.eq import io.mockative.given import io.mockative.mock import io.mockative.once import io.mockative.verify import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest -import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertIs @@ -71,60 +73,12 @@ import com.wire.kalium.network.api.base.model.UserId as UserIdDTO @OptIn(ExperimentalCoroutinesApi::class) class SearchUserRepositoryTest { - @Mock - private val metadataDAO: MetadataDAO = mock(classOf()) - - @Mock - private val userDetailsApi: UserDetailsApi = mock(classOf()) - - @Mock - private val userSearchApiWrapper: UserSearchApiWrapper = mock(classOf()) - - @Mock - private val publicUserMapper: PublicUserMapper = mock(classOf()) - - @Mock - private val userMapper: UserMapper = mock(classOf()) - - @Mock - private val idMapper: IdMapper = mock(classOf()) - - @Mock - private val domainUserTypeMapper: DomainUserTypeMapper = mock(classOf()) - - @Mock - private val userDAO: UserDAO = mock(classOf()) - - private lateinit var searchUserRepository: SearchUserRepository - - @BeforeTest - fun setup() { - searchUserRepository = SearchUserRepositoryImpl( - userDAO, - metadataDAO, - userDetailsApi, - userSearchApiWrapper, - publicUserMapper, - userMapper, - domainUserTypeMapper - ) - - given(domainUserTypeMapper).invocation { federated }.then { UserType.FEDERATED } - - given(domainUserTypeMapper).invocation { guest }.then { UserType.GUEST } - - given(domainUserTypeMapper).invocation { standard }.then { UserType.INTERNAL } - - given(domainUserTypeMapper).invocation { external }.then { UserType.EXTERNAL } - } - @Test fun givenContactSearchApiFailure_whenSearchPublicContact_resultIsFailure() = runTest { // given - given(userSearchApiWrapper) - .suspendFunction(userSearchApiWrapper::search) - .whenInvokedWith(anything(), anything(), anything(), anything()) - .thenReturn(Either.Left(TestNetworkResponseError.noNetworkConnection())) + val (_, searchUserRepository) = Arrangement() + .withSearchResult(Either.Left(TestNetworkResponseError.noNetworkConnection())) + .arrange() // when val actual = searchUserRepository.searchUserDirectory(TEST_QUERY, TEST_DOMAIN) @@ -136,17 +90,16 @@ class SearchUserRepositoryTest { @Test fun givenContactSearchApiFailure_whenSearchPublicContact_thenOnlyContactSearchApiISInvoked() = runTest { // given - given(userSearchApiWrapper) - .suspendFunction(userSearchApiWrapper::search) - .whenInvokedWith(anything(), anything(), anything(), anything()) - .thenReturn(Either.Left(TestNetworkResponseError.noNetworkConnection())) + val (arrangement, searchUserRepository) = Arrangement() + .withSearchResult(Either.Left(TestNetworkResponseError.noNetworkConnection())) + .arrange() // when searchUserRepository.searchUserDirectory(TEST_QUERY, TEST_DOMAIN) // then - verify(userSearchApiWrapper) - .suspendFunction(userSearchApiWrapper::search) + verify(arrangement.userSearchApiWrapper) + .suspendFunction(arrangement.userSearchApiWrapper::search) .with(anything(), anything(), anything(), anything()) .wasInvoked(exactly = once) } @@ -154,21 +107,20 @@ class SearchUserRepositoryTest { @Test fun givenContactSearchApiFailure_whenSearchPublicContact_thenUserDetailsApiAndPublicUserMapperIsNotInvoked() = runTest { // given - given(userSearchApiWrapper) - .suspendFunction(userSearchApiWrapper::search) - .whenInvokedWith(anything(), anything(), anything(), anything()) - .thenReturn(Either.Left(TestNetworkResponseError.noNetworkConnection())) + val (arrangement, searchUserRepository) = Arrangement() + .withSearchResult(Either.Left(TestNetworkResponseError.noNetworkConnection())) + .arrange() // when searchUserRepository.searchUserDirectory(TEST_QUERY, TEST_DOMAIN) // then - verify(userDetailsApi) - .suspendFunction(userDetailsApi::getMultipleUsers) + verify(arrangement.userDetailsApi) + .suspendFunction(arrangement.userDetailsApi::getMultipleUsers) .with(any()) .wasNotInvoked() - verify(publicUserMapper) - .function(publicUserMapper::fromUserProfileDtoToOtherUser) + verify(arrangement.publicUserMapper) + .function(arrangement.publicUserMapper::fromUserProfileDtoToOtherUser) .with(any(), any()) .wasNotInvoked() } @@ -176,15 +128,10 @@ class SearchUserRepositoryTest { @Test fun givenContactSearchApiSuccessButuserDetailsApiFailure_whenSearchPublicContact_resultIsFailure() = runTest { // given - given(userSearchApiWrapper) - .suspendFunction(userSearchApiWrapper::search) - .whenInvokedWith(anything(), anything(), anything(), anything()) - .thenReturn(Either.Right(CONTACT_SEARCH_RESPONSE)) - - given(userDetailsApi) - .suspendFunction(userDetailsApi::getMultipleUsers) - .whenInvokedWith(any()) - .thenReturn(TestNetworkResponseError.genericResponseError()) + val (_, searchUserRepository) = Arrangement() + .withSearchResult(Either.Right(CONTACT_SEARCH_RESPONSE)) + .withGetMultipleUsersResult(TestNetworkResponseError.genericResponseError()) + .arrange() // when val actual = searchUserRepository.searchUserDirectory(TEST_QUERY, TEST_DOMAIN) @@ -196,22 +143,17 @@ class SearchUserRepositoryTest { @Test fun givenContactSearchApiSuccessButuserDetailsApiFailure_whenSearchPublicContact_ThenPublicUserMapperIsNotInvoked() = runTest { // given - given(userSearchApiWrapper) - .suspendFunction(userSearchApiWrapper::search) - .whenInvokedWith(anything(), anything(), anything(), anything()) - .thenReturn(Either.Right(CONTACT_SEARCH_RESPONSE)) - - given(userDetailsApi) - .suspendFunction(userDetailsApi::getMultipleUsers) - .whenInvokedWith(any()) - .then { TestNetworkResponseError.genericResponseError() } + val (arrangement, searchUserRepository) = Arrangement() + .withSearchResult(Either.Right(CONTACT_SEARCH_RESPONSE)) + .withGetMultipleUsersResult(TestNetworkResponseError.genericResponseError()) + .arrange() // when searchUserRepository.searchUserDirectory(TEST_QUERY, TEST_DOMAIN) // then - verify(publicUserMapper) - .function(publicUserMapper::fromUserProfileDtoToOtherUser) + verify(arrangement.publicUserMapper) + .function(arrangement.publicUserMapper::fromUserProfileDtoToOtherUser) .with(any(), any()) .wasNotInvoked() } @@ -220,26 +162,22 @@ class SearchUserRepositoryTest { fun givenContactSearchApiSuccessButUserDetailsApiFailure_whenSearchPublicContact_ThenContactSearchApiAndUserDetailsApiIsInvoked() = runTest { // given - given(userSearchApiWrapper) - .suspendFunction(userSearchApiWrapper::search) - .whenInvokedWith(anything(), anything(), anything(), anything()) - .thenReturn(Either.Right(CONTACT_SEARCH_RESPONSE)) + val (arrangement, searchUserRepository) = Arrangement() + .withSearchResult(Either.Right(CONTACT_SEARCH_RESPONSE)) + .withGetMultipleUsersResult(TestNetworkResponseError.genericResponseError()) + .arrange() - given(userDetailsApi) - .suspendFunction(userDetailsApi::getMultipleUsers) - .whenInvokedWith(any()) - .then { TestNetworkResponseError.genericResponseError() } // when searchUserRepository.searchUserDirectory(TEST_QUERY, TEST_DOMAIN) // then - verify(userSearchApiWrapper) - .suspendFunction(userSearchApiWrapper::search) + verify(arrangement.userSearchApiWrapper) + .suspendFunction(arrangement.userSearchApiWrapper::search) .with(anything(), anything(), anything(), anything()) .wasInvoked(exactly = once) - verify(userDetailsApi) - .suspendFunction(userDetailsApi::getMultipleUsers) + verify(arrangement.userDetailsApi) + .suspendFunction(arrangement.userDetailsApi::getMultipleUsers) .with(any()) .wasInvoked(exactly = once) } @@ -247,45 +185,15 @@ class SearchUserRepositoryTest { @Test fun givenContactSearchApiAndUserDetailsApiAndPublicUserApiReturnSuccess_WhenSearchPublicContact_ThenResultIsSuccess() = runTest { // given - given(userSearchApiWrapper) - .suspendFunction(userSearchApiWrapper::search) - .whenInvokedWith(anything(), anything(), anything(), anything()) - .thenReturn(Either.Right(CONTACT_SEARCH_RESPONSE)) - - given(userDetailsApi) - .suspendFunction(userDetailsApi::getMultipleUsers) - .whenInvokedWith(any()) - .then { NetworkResponse.Success(USER_RESPONSE, mapOf(), 200) } - - given(publicUserMapper) - .function(publicUserMapper::fromUserProfileDtoToOtherUser) - .whenInvokedWith(any(), any()) - .then { _, _ -> PUBLIC_USER } - - given(metadataDAO) - .suspendFunction(metadataDAO::valueByKeyFlow) - .whenInvokedWith(any()) - .then { flowOf(JSON_QUALIFIED_ID) } - - given(userDAO).suspendFunction(userDAO::getUserByQualifiedID) - .whenInvokedWith(any()) - .then { flowOf(USER_ENTITY) } - - given(userMapper) - .function(userMapper::fromUserEntityToSelfUser) - .whenInvokedWith(any()) - .then { SELF_USER } - - given(domainUserTypeMapper) - .invocation { - domainUserTypeMapper.fromTeamAndDomain( - "domain", - null, - "team", - "someId", - false - ) - }.then { UserType.FEDERATED } + val (_, searchUserRepository) = Arrangement() + .withSearchResult(Either.Right(CONTACT_SEARCH_RESPONSE)) + .withGetMultipleUsersResult(NetworkResponse.Success(USER_RESPONSE, mapOf(), 200)) + .withFromUserProfileDtoToOtherUserResult(PUBLIC_USER) + .withValueByKeyFlowResult(flowOf(JSON_QUALIFIED_ID)) + .withGetUserByQualifiedIdResult(flowOf(USER_ENTITY)) + .withFromUserEntityToSelfUser(SELF_USER) + .withFromTeamAndDomain(UserType.FEDERATED) + .arrange() // when val actual = searchUserRepository.searchUserDirectory(TEST_QUERY, TEST_DOMAIN) @@ -298,45 +206,15 @@ class SearchUserRepositoryTest { fun givenContactSearchApiAndUserDetailsApiAndPublicUserApiReturnSuccess_WhenSearchPublicContact_ThenResultIsEqualToExpectedValue() = runTest { // given - given(userSearchApiWrapper) - .suspendFunction(userSearchApiWrapper::search) - .whenInvokedWith(anything(), anything(), anything(), anything()) - .thenReturn(Either.Right(CONTACT_SEARCH_RESPONSE)) - - given(userDetailsApi) - .suspendFunction(userDetailsApi::getMultipleUsers) - .whenInvokedWith(any()) - .then { NetworkResponse.Success(USER_RESPONSE, mapOf(), 200) } - - given(publicUserMapper) - .function(publicUserMapper::fromUserProfileDtoToOtherUser) - .whenInvokedWith(any(), any()) - .then { _, _ -> PUBLIC_USER } - - given(metadataDAO) - .suspendFunction(metadataDAO::valueByKeyFlow) - .whenInvokedWith(any()) - .then { flowOf(JSON_QUALIFIED_ID) } - - given(userDAO).suspendFunction(userDAO::getUserByQualifiedID) - .whenInvokedWith(any()) - .then { flowOf(USER_ENTITY) } - - given(userMapper) - .function(userMapper::fromUserEntityToSelfUser) - .whenInvokedWith(any()) - .then { SELF_USER } - - given(domainUserTypeMapper) - .invocation { - domainUserTypeMapper.fromTeamAndDomain( - "domain", - null, - "team", - "someId", - false - ) - }.then { UserType.FEDERATED } + val (_, searchUserRepository) = Arrangement() + .withSearchResult(Either.Right(CONTACT_SEARCH_RESPONSE)) + .withGetMultipleUsersResult(NetworkResponse.Success(USER_RESPONSE, mapOf(), 200)) + .withFromUserProfileDtoToOtherUserResult(PUBLIC_USER) + .withValueByKeyFlowResult(flowOf(JSON_QUALIFIED_ID)) + .withGetUserByQualifiedIdResult(flowOf(USER_ENTITY)) + .withFromUserEntityToSelfUser(SELF_USER) + .withFromTeamAndDomain(UserType.FEDERATED) + .arrange() val expectedResult = UserSearchResult( result = listOf(PUBLIC_USER) @@ -352,29 +230,13 @@ class SearchUserRepositoryTest { fun givenAValidUserSearchWithEmptyResults_WhenSearchingSomeText_ThenResultIsAnEmptyList() = runTest { // given - given(userSearchApiWrapper) - .suspendFunction(userSearchApiWrapper::search) - .whenInvokedWith(anything(), anything(), anything(), anything()) - .thenReturn(Either.Right(CONTACT_SEARCH_RESPONSE)) - - given(userDetailsApi) - .suspendFunction(userDetailsApi::getMultipleUsers) - .whenInvokedWith(any()) - .then { NetworkResponse.Success(USER_RESPONSE.copy(usersFound = emptyList()), mapOf(), 200) } - - given(metadataDAO) - .suspendFunction(metadataDAO::valueByKeyFlow) - .whenInvokedWith(any()) - .then { flowOf(JSON_QUALIFIED_ID) } - - given(userDAO).suspendFunction(userDAO::getUserByQualifiedID) - .whenInvokedWith(any()) - .then { flowOf(USER_ENTITY) } - - given(userMapper) - .function(userMapper::fromUserEntityToSelfUser) - .whenInvokedWith(any()) - .then { SELF_USER } + val (_, searchUserRepository) = Arrangement() + .withSearchResult(Either.Right(CONTACT_SEARCH_RESPONSE)) + .withGetMultipleUsersResult(NetworkResponse.Success(USER_RESPONSE.copy(usersFound = emptyList()), mapOf(), 200)) + .withValueByKeyFlowResult(flowOf(JSON_QUALIFIED_ID)) + .withGetUserByQualifiedIdResult(flowOf(USER_ENTITY)) + .withFromUserEntityToSelfUser(SELF_USER) + .arrange() val expectedResult = UserSearchResult( result = emptyList() @@ -390,15 +252,10 @@ class SearchUserRepositoryTest { fun givenASearchWithConversationExcludedOption_WhenSearchingUsersByNameOrHandleOrEmail_ThenSearchForUsersNotInTheConversation() = runTest { // given - given(userDAO) - .suspendFunction(userDAO::getUsersNotInConversationByNameOrHandleOrEmail) - .whenInvokedWith(anything(), anything()) - .then { _, _ -> flowOf(listOf()) } - - given(userDAO) - .suspendFunction(userDAO::getUserByNameOrHandleOrEmailAndConnectionStates) - .whenInvokedWith(anything(), anything()) - .then { _, _ -> flowOf(listOf()) } + val (arrangement, searchUserRepository) = Arrangement() + .withGetUsersNotInConversationByNameOrHandleOrEmailResult(flowOf(listOf())) + .withGetUserByNameOrHandleOrEmailAndConnectionStatesResult(flowOf(listOf())) + .arrange() // when searchUserRepository.searchKnownUsersByNameOrHandleOrEmail( @@ -411,13 +268,13 @@ class SearchUserRepositoryTest { ) ) - verify(userDAO) - .suspendFunction(userDAO::getUserByNameOrHandleOrEmailAndConnectionStates) + verify(arrangement.userDAO) + .suspendFunction(arrangement.userDAO::getUserByNameOrHandleOrEmailAndConnectionStates) .with(anything(), anything()) .wasNotInvoked() - verify(userDAO) - .suspendFunction(userDAO::getUsersNotInConversationByNameOrHandleOrEmail) + verify(arrangement.userDAO) + .suspendFunction(arrangement.userDAO::getUsersNotInConversationByNameOrHandleOrEmail) .with(anything(), anything()) .wasInvoked(Times(1)) } @@ -425,15 +282,10 @@ class SearchUserRepositoryTest { @Test fun givenASearchWithConversationExcludedOption_WhenSearchingUsersByHandle_ThenSearchForUsersNotInTheConversation() = runTest { // given - given(userDAO) - .suspendFunction(userDAO::getUserByHandleAndConnectionStates) - .whenInvokedWith(anything(), anything()) - .then { _, _ -> flowOf(listOf()) } - - given(userDAO) - .suspendFunction(userDAO::getUsersNotInConversationByHandle) - .whenInvokedWith(anything(), anything()) - .then { _, _ -> flowOf(listOf()) } + val (arrangement, searchUserRepository) = Arrangement() + .withGetUserByHandleAndConnectionStatesResult(flowOf(listOf())) + .withGetUsersNotInConversationByHandleResult(flowOf(listOf())) + .arrange() // when searchUserRepository.searchKnownUsersByHandle( @@ -447,13 +299,13 @@ class SearchUserRepositoryTest { ) // then - verify(userDAO) - .suspendFunction(userDAO::getUserByHandleAndConnectionStates) + verify(arrangement.userDAO) + .suspendFunction(arrangement.userDAO::getUserByHandleAndConnectionStates) .with(anything(), anything()) .wasNotInvoked() - verify(userDAO) - .suspendFunction(userDAO::getUsersNotInConversationByHandle) + verify(arrangement.userDAO) + .suspendFunction(arrangement.userDAO::getUsersNotInConversationByHandle) .with(anything(), anything()) .wasInvoked(exactly = once) } @@ -461,10 +313,9 @@ class SearchUserRepositoryTest { @Test fun givenContactSearchApiSuccessButListIsEmpty_whenSearchPublicContact_thenReturnEmptyListWithoutCallingUserDetailsApi() = runTest { // given - given(userSearchApiWrapper) - .suspendFunction(userSearchApiWrapper::search) - .whenInvokedWith(anything(), anything(), anything(), anything()) - .thenReturn(Either.Right(CONTACT_SEARCH_RESPONSE_EMPTY)) + val (arrangement, searchUserRepository) = Arrangement() + .withSearchResult(Either.Right(CONTACT_SEARCH_RESPONSE_EMPTY)) + .arrange() // when val actual = searchUserRepository.searchUserDirectory(TEST_QUERY, TEST_DOMAIN) @@ -473,12 +324,180 @@ class SearchUserRepositoryTest { assertIs>(actual) assertTrue { actual.value.result.isEmpty() } - verify(userDetailsApi) - .suspendFunction(userDetailsApi::getMultipleUsers) + verify(arrangement.userDetailsApi) + .suspendFunction(arrangement.userDetailsApi::getMultipleUsers) .with(any()) .wasNotInvoked() } + @Test + fun givenContactSearchApiReturnsTeamMembers_whenSearchPublicContact_thenStoreThemLocallyAndExcludeFromResult() = runTest { + // given + val userListResponse = ListUsersDTO( + usersFailed = emptyList(), + usersFound = listOf(USER_PROFILE_DTO.copy(id = UserId("teamUser", SELF_USER.id.domain), teamId = SELF_USER.teamId?.value),) + ) + val (arrangement, searchUserRepository) = Arrangement() + .withSearchResult(Either.Right(CONTACT_SEARCH_RESPONSE)) + .withGetMultipleUsersResult(NetworkResponse.Success(userListResponse, mapOf(), 200)) + .withValueByKeyFlowResult(flowOf(JSON_QUALIFIED_ID)) + .withGetUserByQualifiedIdResult(flowOf(USER_ENTITY)) + .withFromUserEntityToSelfUser(SELF_USER) + .withUpsertTeamMembersSuccess() + .withFromUserProfileDtoToOtherUserResult(PUBLIC_USER) + .withFromUserProfileDtoToUserEntityResult(USER_ENTITY) + .withFromTeamAndDomain(UserType.FEDERATED) + .arrange() + + // when + val actual = searchUserRepository.searchUserDirectory(TEST_QUERY, TEST_DOMAIN) + + // then + assertIs>(actual) + assertTrue { actual.value.result.isEmpty() } + + verify(arrangement.userDAO) + .suspendFunction(arrangement.userDAO::upsertTeamMembers) + .with(eq(listOf(USER_ENTITY))) + .wasInvoked() + } + + internal class Arrangement { + + @Mock + internal val metadataDAO: MetadataDAO = mock(classOf()) + + @Mock + internal val userDetailsApi: UserDetailsApi = mock(classOf()) + + @Mock + internal val userSearchApiWrapper: UserSearchApiWrapper = mock(classOf()) + + @Mock + internal val publicUserMapper: PublicUserMapper = mock(classOf()) + + @Mock + internal val userMapper: UserMapper = mock(classOf()) + + @Mock + internal val domainUserTypeMapper: DomainUserTypeMapper = mock(classOf()) + + @Mock + internal val userDAO: UserDAO = mock(classOf()) + + private val searchUserRepository: SearchUserRepository by lazy { + SearchUserRepositoryImpl( + userDAO, + metadataDAO, + userDetailsApi, + userSearchApiWrapper, + publicUserMapper, + userMapper, + domainUserTypeMapper + ) + } + + fun arrange() = this to searchUserRepository + + init { + given(domainUserTypeMapper).invocation { federated }.then { UserType.FEDERATED } + given(domainUserTypeMapper).invocation { guest }.then { UserType.GUEST } + given(domainUserTypeMapper).invocation { standard }.then { UserType.INTERNAL } + given(domainUserTypeMapper).invocation { external }.then { UserType.EXTERNAL } + } + + fun withSearchResult(result: Either) = apply { + given(userSearchApiWrapper) + .suspendFunction(userSearchApiWrapper::search) + .whenInvokedWith(anything(), anything(), anything(), anything()) + .thenReturn(result) + } + + fun withGetMultipleUsersResult(result: NetworkResponse) = apply { + given(userDetailsApi) + .suspendFunction(userDetailsApi::getMultipleUsers) + .whenInvokedWith(any()) + .thenReturn(result) + } + + fun withFromUserProfileDtoToOtherUserResult(result: OtherUser) = apply { + given(publicUserMapper) + .function(publicUserMapper::fromUserProfileDtoToOtherUser) + .whenInvokedWith(any(), any()) + .thenReturn(result) + } + + fun withValueByKeyFlowResult(result: Flow) = apply { + given(metadataDAO) + .suspendFunction(metadataDAO::valueByKeyFlow) + .whenInvokedWith(any()) + .thenReturn(result) + } + + fun withGetUserByQualifiedIdResult(result: Flow) = apply { + given(userDAO) + .suspendFunction(userDAO::getUserByQualifiedID) + .whenInvokedWith(any()) + .thenReturn(result) + } + + fun withFromUserEntityToSelfUser(result: SelfUser) = apply { + given(userMapper) + .function(userMapper::fromUserEntityToSelfUser) + .whenInvokedWith(any()) + .thenReturn(result) + } + + fun withFromTeamAndDomain(result: UserType) = apply { + given(domainUserTypeMapper) + .function(domainUserTypeMapper::fromTeamAndDomain) + .whenInvokedWith(any(), any(), any(), any(), any()) + .thenReturn(result) + } + + fun withGetUsersNotInConversationByNameOrHandleOrEmailResult(result: Flow>) = apply { + given(userDAO) + .suspendFunction(userDAO::getUsersNotInConversationByNameOrHandleOrEmail) + .whenInvokedWith(anything(), anything()) + .thenReturn(result) + } + + fun withGetUserByNameOrHandleOrEmailAndConnectionStatesResult(result: Flow>) = apply { + given(userDAO) + .suspendFunction(userDAO::getUserByNameOrHandleOrEmailAndConnectionStates) + .whenInvokedWith(anything(), anything()) + .thenReturn(result) + } + + fun withGetUserByHandleAndConnectionStatesResult(result: Flow>) = apply { + given(userDAO) + .suspendFunction(userDAO::getUserByHandleAndConnectionStates) + .whenInvokedWith(anything(), anything()) + .thenReturn(result) + } + + fun withGetUsersNotInConversationByHandleResult(result: Flow>) = apply { + given(userDAO) + .suspendFunction(userDAO::getUsersNotInConversationByHandle) + .whenInvokedWith(anything(), anything()) + .thenReturn(result) + } + + fun withFromUserProfileDtoToUserEntityResult(result: UserEntity) = apply { + given(userMapper) + .function(userMapper::fromUserProfileDtoToUserEntity) + .whenInvokedWith(any(), any(), any()) + .thenReturn(result) + } + + fun withUpsertTeamMembersSuccess() = apply { + given(userDAO) + .suspendFunction(userDAO::upsertTeamMembers) + .whenInvokedWith(anything()) + .thenReturn(Unit) + } + } + private companion object { const val TEST_QUERY = "testQuery" const val TEST_DOMAIN = "testDomain" @@ -536,26 +555,23 @@ class SearchUserRepositoryTest { took = 0, ) - val USER_RESPONSE = ListUsersDTO( - usersFailed = emptyList(), - usersFound = listOf( - UserProfileDTO( - accentId = 1, - handle = "handle", - id = UserIdDTO(value = "value", domain = "domain"), - name = "name", - legalHoldStatus = LegalHoldStatusResponse.ENABLED, - teamId = "team", - assets = emptyList(), - deleted = null, - email = null, - expiresAt = null, - nonQualifiedId = "value", - service = null - ) - ) + val USER_PROFILE_DTO = UserProfileDTO( + accentId = 1, + handle = "handle", + id = UserIdDTO(value = "value", domain = "domain"), + name = "name", + legalHoldStatus = LegalHoldStatusResponse.ENABLED, + teamId = "team", + assets = emptyList(), + deleted = null, + email = null, + expiresAt = null, + nonQualifiedId = "value", + service = null ) + val USER_RESPONSE = ListUsersDTO(usersFailed = emptyList(), usersFound = listOf(USER_PROFILE_DTO)) + const val JSON_QUALIFIED_ID = """{"value":"test" , "domain":"test" }""" val USER_ENTITY = UserEntity( @@ -584,7 +600,7 @@ class SearchUserRepositoryTest { email = null, phone = null, accentId = 0, - teamId = null, + teamId = TeamId("someTeamId"), connectionStatus = ConnectionState.NOT_CONNECTED, previewPicture = null, completePicture = null, diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/model/UserDTO.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/model/UserDTO.kt index 8375a6381af..0220c4d358b 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/model/UserDTO.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/model/UserDTO.kt @@ -54,6 +54,9 @@ data class UserProfileDTO( @SerialName("legalhold_status") val legalHoldStatus: LegalHoldStatusResponse, ) : UserDTO() +fun UserProfileDTO.isTeamMember(selfUserTeamId: String?, selfUserDomain: String?) = + (selfUserTeamId != null && this.teamId == selfUserTeamId && this.id.domain == selfUserDomain) + @Serializable data class SelfUserDTO( @SerialName("qualified_id") override val id: UserId,