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 a3886937ecd..b847fe9a577 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 @@ -26,7 +26,6 @@ import com.wire.kalium.logic.data.publicuser.model.UserSearchResult 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.UserEntityTypeMapper import com.wire.kalium.logic.di.MapperProvider import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.flatMap @@ -35,11 +34,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.UserDetailsEntity +import com.wire.kalium.persistence.dao.UserTypeEntity import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterNotNull @@ -93,7 +94,6 @@ internal class SearchUserRepositoryImpl( private val userDetailsApi: UserDetailsApi, private val userSearchAPiWrapper: UserSearchApiWrapper, private val userMapper: UserMapper = MapperProvider.userMapper(), - private val userTypeEntityMapper: UserEntityTypeMapper = MapperProvider.userTypeEntityMapper() ) : SearchUserRepository { override suspend fun searchKnownUsersByNameOrHandleOrEmail( @@ -157,7 +157,22 @@ 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.upsertUsers( + teamMembers.map { userProfileDTO -> + userMapper.fromUserProfileDtoToUserEntity( + userProfile = userProfileDTO, + connectionState = ConnectionEntity.State.ACCEPTED, + userTypeEntity = userDAO.observeUserDetailsByQualifiedID(userProfileDTO.id.toDao()) + .firstOrNull()?.userType ?: UserTypeEntity.STANDARD + ) + } + ) + + otherUsers.map { userProfileDTO -> userMapper.fromUserProfileDtoToOtherUser(userProfileDTO, selfUser) } } 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 a7df43d8086..3373932fdca 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 @@ -57,6 +57,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 @@ -259,9 +260,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.upsertUsers( teamMembers.map { userProfileDTO -> @@ -291,14 +292,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.getUsersDetailsByQualifiedIDList(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 34cecb4a572..5c413695f1b 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,12 +20,12 @@ 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.publicuser.model.UserSearchResult +import com.wire.kalium.logic.data.user.OtherUser +import com.wire.kalium.logic.data.user.SelfUser 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.UserType import com.wire.kalium.logic.framework.TestUser +import com.wire.kalium.logic.framework.TestUser.USER_PROFILE_DTO import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.test_util.TestNetworkResponseError import com.wire.kalium.network.api.base.authenticated.search.ContactDTO @@ -33,83 +33,40 @@ import com.wire.kalium.network.api.base.authenticated.search.SearchPolicyDTO 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.UserProfileDTO +import com.wire.kalium.network.api.base.model.UserId import com.wire.kalium.network.utils.NetworkResponse import com.wire.kalium.persistence.dao.MetadataDAO import com.wire.kalium.persistence.dao.UserDAO +import com.wire.kalium.persistence.dao.UserDetailsEntity +import com.wire.kalium.persistence.dao.UserEntity import io.mockative.Mock 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 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 { - @Mock - private val metadataDAO: MetadataDAO = mock(classOf()) - - @Mock - private val userDetailsApi: UserDetailsApi = mock(classOf()) - - @Mock - private val userSearchApiWrapper: UserSearchApiWrapper = 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, - userMapper - ) - - 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) @@ -121,17 +78,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) } @@ -139,21 +95,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(userMapper) - .function(userMapper::fromUserProfileDtoToOtherUser) + verify(arrangement.userMapper) + .function(arrangement.userMapper::fromUserProfileDtoToOtherUser) .with(any(), any()) .wasNotInvoked() } @@ -161,15 +116,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) @@ -181,22 +131,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(userMapper) - .function(userMapper::fromUserProfileDtoToOtherUser) + verify(arrangement.userMapper) + .function(arrangement.userMapper::fromUserProfileDtoToOtherUser) .with(any(), any()) .wasNotInvoked() } @@ -205,26 +150,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) } @@ -232,45 +173,14 @@ 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(userMapper) - .function(userMapper::fromUserProfileDtoToOtherUser) - .whenInvokedWith(any(), any()) - .then { _, _ -> TestUser.OTHER } - - given(metadataDAO) - .suspendFunction(metadataDAO::valueByKeyFlow) - .whenInvokedWith(any()) - .then { flowOf(JSON_QUALIFIED_ID) } - - given(userDAO).suspendFunction(userDAO::observeUserDetailsByQualifiedID) - .whenInvokedWith(any()) - .then { flowOf(TestUser.DETAILS_ENTITY) } - - given(userMapper) - .function(userMapper::fromUserDetailsEntityToSelfUser) - .whenInvokedWith(any()) - .then { TestUser.SELF.copy(teamId = null) } - - given(domainUserTypeMapper) - .invocation { - domainUserTypeMapper.fromTeamAndDomain( - "domain", - null, - "team", - "domain", - false - ) - }.then { UserType.FEDERATED } + val (_, searchUserRepository) = Arrangement() + .withSearchResult(Either.Right(CONTACT_SEARCH_RESPONSE)) + .withGetMultipleUsersResult(NetworkResponse.Success(USER_RESPONSE, mapOf(), 200)) + .withFromUserProfileDtoToOtherUserResult(TestUser.OTHER) + .withValueByKeyFlowResult(flowOf(JSON_QUALIFIED_ID)) + .withObserveUserDetailsByQualifiedIdResult(flowOf(TestUser.DETAILS_ENTITY)) + .withFromUserDetailsEntityToSelfUser(TestUser.SELF.copy(teamId = null)) + .arrange() // when val actual = searchUserRepository.searchUserDirectory(TEST_QUERY, TEST_DOMAIN) @@ -283,45 +193,14 @@ 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(userMapper) - .function(userMapper::fromUserProfileDtoToOtherUser) - .whenInvokedWith(any(), any()) - .then { _, _ -> TestUser.OTHER } - - given(metadataDAO) - .suspendFunction(metadataDAO::valueByKeyFlow) - .whenInvokedWith(any()) - .then { flowOf(JSON_QUALIFIED_ID) } - - given(userDAO).suspendFunction(userDAO::observeUserDetailsByQualifiedID) - .whenInvokedWith(any()) - .then { flowOf(TestUser.DETAILS_ENTITY) } - - given(userMapper) - .function(userMapper::fromUserDetailsEntityToSelfUser) - .whenInvokedWith(any()) - .then { TestUser.SELF.copy(teamId = null) } - - given(domainUserTypeMapper) - .invocation { - domainUserTypeMapper.fromTeamAndDomain( - "domain", - null, - "team", - "domain", - false - ) - }.then { UserType.FEDERATED } + val (_, searchUserRepository) = Arrangement() + .withSearchResult(Either.Right(CONTACT_SEARCH_RESPONSE)) + .withGetMultipleUsersResult(NetworkResponse.Success(USER_RESPONSE, mapOf(), 200)) + .withFromUserProfileDtoToOtherUserResult(TestUser.OTHER) + .withValueByKeyFlowResult(flowOf(JSON_QUALIFIED_ID)) + .withObserveUserDetailsByQualifiedIdResult(flowOf(TestUser.DETAILS_ENTITY)) + .withFromUserDetailsEntityToSelfUser(TestUser.SELF.copy(teamId = null)) + .arrange() val expectedResult = UserSearchResult( result = listOf(TestUser.OTHER) @@ -337,29 +216,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::observeUserDetailsByQualifiedID) - .whenInvokedWith(any()) - .then { flowOf(TestUser.DETAILS_ENTITY) } - - given(userMapper) - .function(userMapper::fromUserDetailsEntityToSelfUser) - .whenInvokedWith(any()) - .then { TestUser.SELF } + val (_, searchUserRepository) = Arrangement() + .withSearchResult(Either.Right(CONTACT_SEARCH_RESPONSE)) + .withGetMultipleUsersResult(NetworkResponse.Success(USER_RESPONSE.copy(usersFound = emptyList()), mapOf(), 200)) + .withValueByKeyFlowResult(flowOf(JSON_QUALIFIED_ID)) + .withObserveUserDetailsByQualifiedIdResult(flowOf(TestUser.DETAILS_ENTITY)) + .withFromUserDetailsEntityToSelfUser(TestUser.SELF) + .arrange() val expectedResult = UserSearchResult( result = emptyList() @@ -375,15 +238,10 @@ class SearchUserRepositoryTest { fun givenASearchWithConversationExcludedOption_WhenSearchingUsersByNameOrHandleOrEmail_ThenSearchForUsersNotInTheConversation() = runTest { // given - given(userDAO) - .suspendFunction(userDAO::getUsersDetailsNotInConversationByNameOrHandleOrEmail) - .whenInvokedWith(anything(), anything()) - .then { _, _ -> flowOf(listOf()) } - - given(userDAO) - .suspendFunction(userDAO::getUserDetailsByNameOrHandleOrEmailAndConnectionStates) - .whenInvokedWith(anything(), anything()) - .then { _, _ -> flowOf(listOf()) } + val (arrangement, searchUserRepository) = Arrangement() + .withGetUsersDetailsNotInConversationByNameOrHandleOrEmailResult(flowOf(listOf())) + .withGetUserDetailsByNameOrHandleOrEmailAndConnectionStatesResult(flowOf(listOf())) + .arrange() // when searchUserRepository.searchKnownUsersByNameOrHandleOrEmail( @@ -396,13 +254,14 @@ class SearchUserRepositoryTest { ) ) - verify(userDAO) - .suspendFunction(userDAO::getUserDetailsByNameOrHandleOrEmailAndConnectionStates) + // then + verify(arrangement.userDAO) + .suspendFunction(arrangement.userDAO::getUserDetailsByNameOrHandleOrEmailAndConnectionStates) .with(anything(), anything()) .wasNotInvoked() - verify(userDAO) - .suspendFunction(userDAO::getUsersDetailsNotInConversationByNameOrHandleOrEmail) + verify(arrangement.userDAO) + .suspendFunction(arrangement.userDAO::getUsersDetailsNotInConversationByNameOrHandleOrEmail) .with(anything(), anything()) .wasInvoked(Times(1)) } @@ -410,15 +269,10 @@ class SearchUserRepositoryTest { @Test fun givenASearchWithConversationExcludedOption_WhenSearchingUsersByHandle_ThenSearchForUsersNotInTheConversation() = runTest { // given - given(userDAO) - .suspendFunction(userDAO::getUserDetailsByHandleAndConnectionStates) - .whenInvokedWith(anything(), anything()) - .then { _, _ -> flowOf(listOf()) } - - given(userDAO) - .suspendFunction(userDAO::getUsersDetailsNotInConversationByHandle) - .whenInvokedWith(anything(), anything()) - .then { _, _ -> flowOf(listOf()) } + val (arrangement, searchUserRepository) = Arrangement() + .withGetUserDetailsByHandleAndConnectionStatesResult(flowOf(listOf())) + .withGetUsersDetailsNotInConversationByHandleResult(flowOf(listOf())) + .arrange() // when searchUserRepository.searchKnownUsersByHandle( @@ -432,13 +286,13 @@ class SearchUserRepositoryTest { ) // then - verify(userDAO) - .suspendFunction(userDAO::getUserDetailsByHandleAndConnectionStates) + verify(arrangement.userDAO) + .suspendFunction(arrangement.userDAO::getUserDetailsByHandleAndConnectionStates) .with(anything(), anything()) .wasNotInvoked() - verify(userDAO) - .suspendFunction(userDAO::getUsersDetailsNotInConversationByHandle) + verify(arrangement.userDAO) + .suspendFunction(arrangement.userDAO::getUsersDetailsNotInConversationByHandle) .with(anything(), anything()) .wasInvoked(exactly = once) } @@ -446,10 +300,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) @@ -458,12 +311,159 @@ 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", TestUser.SELF.id.domain), teamId = TestUser.SELF.teamId?.value), + ) + ) + val (arrangement, searchUserRepository) = Arrangement() + .withSearchResult(Either.Right(CONTACT_SEARCH_RESPONSE)) + .withGetMultipleUsersResult(NetworkResponse.Success(userListResponse, mapOf(), 200)) + .withValueByKeyFlowResult(flowOf(JSON_QUALIFIED_ID)) + .withObserveUserDetailsByQualifiedIdResult(flowOf(TestUser.DETAILS_ENTITY)) + .withFromUserDetailsEntityToSelfUser(TestUser.SELF) + .withUpsertUsersSuccess() + .withFromUserProfileDtoToOtherUserResult(TestUser.OTHER) + .withFromUserProfileDtoToUserEntityResult(TestUser.ENTITY) + .arrange() + + // when + val actual = searchUserRepository.searchUserDirectory(TEST_QUERY, TEST_DOMAIN) + + // then + assertIs>(actual) + assertTrue { actual.value.result.isEmpty() } + + verify(arrangement.userDAO) + .suspendFunction(arrangement.userDAO::upsertUsers) + .with(eq(listOf(TestUser.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 userMapper: UserMapper = mock(classOf()) + + @Mock + internal val userDAO: UserDAO = mock(classOf()) + + private val searchUserRepository: SearchUserRepository by lazy { + SearchUserRepositoryImpl( + userDAO, + metadataDAO, + userDetailsApi, + userSearchApiWrapper, + userMapper, + ) + } + + fun arrange() = this to searchUserRepository + + 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(userMapper) + .function(userMapper::fromUserProfileDtoToOtherUser) + .whenInvokedWith(any(), any()) + .thenReturn(result) + } + + fun withValueByKeyFlowResult(result: Flow) = apply { + given(metadataDAO) + .suspendFunction(metadataDAO::valueByKeyFlow) + .whenInvokedWith(any()) + .thenReturn(result) + } + + fun withObserveUserDetailsByQualifiedIdResult(result: Flow) = apply { + given(userDAO) + .suspendFunction(userDAO::observeUserDetailsByQualifiedID) + .whenInvokedWith(any()) + .thenReturn(result) + } + + fun withFromUserDetailsEntityToSelfUser(result: SelfUser) = apply { + given(userMapper) + .function(userMapper::fromUserDetailsEntityToSelfUser) + .whenInvokedWith(any()) + .thenReturn(result) + } + + fun withGetUsersDetailsNotInConversationByNameOrHandleOrEmailResult(result: Flow>) = apply { + given(userDAO) + .suspendFunction(userDAO::getUsersDetailsNotInConversationByNameOrHandleOrEmail) + .whenInvokedWith(anything(), anything()) + .thenReturn(result) + } + + fun withGetUserDetailsByNameOrHandleOrEmailAndConnectionStatesResult(result: Flow>) = apply { + given(userDAO) + .suspendFunction(userDAO::getUserDetailsByNameOrHandleOrEmailAndConnectionStates) + .whenInvokedWith(anything(), anything()) + .thenReturn(result) + } + + fun withGetUserDetailsByHandleAndConnectionStatesResult(result: Flow>) = apply { + given(userDAO) + .suspendFunction(userDAO::getUserDetailsByHandleAndConnectionStates) + .whenInvokedWith(anything(), anything()) + .thenReturn(result) + } + + fun withGetUsersDetailsNotInConversationByHandleResult(result: Flow>) = apply { + given(userDAO) + .suspendFunction(userDAO::getUsersDetailsNotInConversationByHandle) + .whenInvokedWith(anything(), anything()) + .thenReturn(result) + } + + fun withFromUserProfileDtoToUserEntityResult(result: UserEntity) = apply { + given(userMapper) + .function(userMapper::fromUserProfileDtoToUserEntity) + .whenInvokedWith(any(), any(), any()) + .thenReturn(result) + } + + fun withUpsertUsersSuccess() = apply { + given(userDAO) + .suspendFunction(userDAO::upsertUsers) + .whenInvokedWith(anything()) + .thenReturn(Unit) + } + } + private companion object { const val TEST_QUERY = "testQuery" const val TEST_DOMAIN = "testDomain" @@ -502,26 +502,7 @@ 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, - supportedProtocols = null - ) - ) - ) + val USER_RESPONSE = ListUsersDTO(usersFailed = emptyList(), usersFound = listOf(USER_PROFILE_DTO)) const val JSON_QUALIFIED_ID = """{"value":"test" , "domain":"test" }""" } 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 cb8023fb6d0..ee0b9469b8d 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 @@ -56,6 +56,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,