diff --git a/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatBlocksDaoTest.kt b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatBlocksDaoTest.kt index 8ba43eb91d..ac0e215217 100644 --- a/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatBlocksDaoTest.kt +++ b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatBlocksDaoTest.kt @@ -58,7 +58,7 @@ class ChatBlocksDaoTest { runTest { val user = createUserEntity("account1", "Account 1") usersDao.saveUser(user) - val account1 = usersDao.getUserWithUserId("account1").blockingGet() + val account1 = usersDao.getUserWithUserId("account1")!! conversationsDao.upsertConversations( accountId = user.id, @@ -126,7 +126,7 @@ class ChatBlocksDaoTest { runTest { val user = createUserEntity("account1", "Account 1") usersDao.saveUser(user) - val account1 = usersDao.getUserWithUserId("account1").blockingGet() + val account1 = usersDao.getUserWithUserId("account1")!! conversationsDao.upsertConversations( account1.id, @@ -252,7 +252,7 @@ class ChatBlocksDaoTest { runTest { val user = createUserEntity("account1", "Account 1") usersDao.saveUser(user) - val account1 = usersDao.getUserWithUserId("account1").blockingGet() + val account1 = usersDao.getUserWithUserId("account1")!! conversationsDao.upsertConversations( account1.id, @@ -331,7 +331,7 @@ class ChatBlocksDaoTest { runTest { val user = createUserEntity("account1", "Account 1") usersDao.saveUser(user) - val account1 = usersDao.getUserWithUserId("account1").blockingGet() + val account1 = usersDao.getUserWithUserId("account1")!! conversationsDao.upsertConversations( account1.id, @@ -421,7 +421,7 @@ class ChatBlocksDaoTest { runTest { val user = createUserEntity("account1", "Account 1") usersDao.saveUser(user) - val account1 = usersDao.getUserWithUserId("account1").blockingGet() + val account1 = usersDao.getUserWithUserId("account1")!! conversationsDao.upsertConversations( account1.id, @@ -484,7 +484,7 @@ class ChatBlocksDaoTest { runTest { val user = createUserEntity("account1", "Account 1") usersDao.saveUser(user) - val account1 = usersDao.getUserWithUserId("account1").blockingGet() + val account1 = usersDao.getUserWithUserId("account1")!! conversationsDao.upsertConversations( account1.id, @@ -532,7 +532,7 @@ class ChatBlocksDaoTest { runTest { val user = createUserEntity("account1", "Account 1") usersDao.saveUser(user) - val account1 = usersDao.getUserWithUserId("account1").blockingGet() + val account1 = usersDao.getUserWithUserId("account1")!! conversationsDao.upsertConversations( account1.id, @@ -575,7 +575,7 @@ class ChatBlocksDaoTest { runTest { val user = createUserEntity("account1", "Account 1") usersDao.saveUser(user) - val account1 = usersDao.getUserWithUserId("account1").blockingGet() + val account1 = usersDao.getUserWithUserId("account1")!! conversationsDao.upsertConversations( account1.id, diff --git a/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt index e81c6da923..2815679aa5 100644 --- a/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt +++ b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt @@ -60,8 +60,8 @@ class ChatMessagesDaoTest { usersDao.saveUser(createUserEntity("account1", "Account 1")) usersDao.saveUser(createUserEntity("account2", "Account 2")) - val account1 = usersDao.getUserWithUserId("account1").blockingGet() - val account2 = usersDao.getUserWithUserId("account2").blockingGet() + val account1 = usersDao.getUserWithUserId("account1")!! + val account2 = usersDao.getUserWithUserId("account2")!! // Problem: lets say we want to update the conv list -> We don#t know the primary keys! // with account@token that would be easier! diff --git a/app/src/androidTest/java/com/nextcloud/talk/data/user/UsersDaoTest.kt b/app/src/androidTest/java/com/nextcloud/talk/data/user/UsersDaoTest.kt index 89be281f9c..8ba7a9e54c 100644 --- a/app/src/androidTest/java/com/nextcloud/talk/data/user/UsersDaoTest.kt +++ b/app/src/androidTest/java/com/nextcloud/talk/data/user/UsersDaoTest.kt @@ -13,6 +13,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.nextcloud.talk.data.source.local.TalkDatabase import com.nextcloud.talk.data.user.model.UserEntity import com.nextcloud.talk.models.json.push.PushConfigurationState +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull @@ -43,154 +45,168 @@ class UsersDaoTest { } @Test - fun saveAndGetUser() { - val user = createUserEntity("user1", "Account 1", "https://server1.com") - val id = usersDao.saveUser(user) + fun saveAndGetUser() = + runTest { + val user = createUserEntity("user1", "Account 1", "https://server1.com") + val id = usersDao.saveUser(user) - val retrieved = usersDao.getUserWithId(id).blockingGet() - assertNotNull(retrieved) - assertEquals("user1", retrieved.userId) - } + val retrieved = usersDao.getUserWithId(id) + assertNotNull(retrieved) + assertEquals("user1", retrieved?.userId) + } @Test - fun saveUsersAndGetAll() { - val user1 = createUserEntity("user1", "Account 1", "https://server1.com") - val user2 = createUserEntity("user2", "Account 2", "https://server1.com") - usersDao.saveUsers(user1, user2) + fun saveUsersAndGetAll() = + runTest { + val user1 = createUserEntity("user1", "Account 1", "https://server1.com") + val user2 = createUserEntity("user2", "Account 2", "https://server1.com") + usersDao.saveUsers(user1, user2) - val users = usersDao.getUsers().blockingGet() - assertEquals(2, users.size) - } + val users = usersDao.getUsers() + assertEquals(2, users.size) + } @Test - fun getActiveUser() { - val user1 = createUserEntity("user1", "Account 1", "https://server1.com").apply { current = true } - val user2 = createUserEntity("user2", "Account 2", "https://server1.com").apply { current = false } - usersDao.saveUsers(user1, user2) + fun getActiveUser() = + runTest { + val user1 = createUserEntity("user1", "Account 1", "https://server1.com").apply { current = true } + val user2 = createUserEntity("user2", "Account 2", "https://server1.com").apply { current = false } + usersDao.saveUsers(user1, user2) - val active = usersDao.getActiveUser().blockingGet() - assertNotNull(active) - assertEquals("user1", active.userId) + val active = usersDao.getActiveUser() + assertNotNull(active) + assertEquals("user1", active?.userId) - assertEquals("user1", usersDao.getActiveUserSynchronously()?.userId) + assertEquals("user1", usersDao.getActiveUserSynchronously()?.userId) - val activeObs = usersDao.getActiveUserObservable().blockingFirst() - assertEquals("user1", activeObs.userId) - } + val activeFlow = usersDao.getActiveUserFlow().first() + assertEquals("user1", activeFlow?.userId) + } @Test - fun setUserAsActiveWithId() { - val user1 = createUserEntity("user1", "Account 1", "https://server1.com").apply { current = true } - val user2 = createUserEntity("user2", "Account 2", "https://server1.com").apply { current = false } - val id1 = usersDao.saveUser(user1) - val id2 = usersDao.saveUser(user2) - - usersDao.setUserAsActiveWithId(id2) - - val retrieved1 = usersDao.getUserWithId(id1).blockingGet() - val retrieved2 = usersDao.getUserWithId(id2).blockingGet() - assertEquals(false, retrieved1.current) - assertEquals(true, retrieved2.current) - } + fun setUserAsActiveWithId() = + runTest { + val user1 = createUserEntity("user1", "Account 1", "https://server1.com").apply { current = true } + val user2 = createUserEntity("user2", "Account 2", "https://server1.com").apply { current = false } + val id1 = usersDao.saveUser(user1) + val id2 = usersDao.saveUser(user2) + + usersDao.setUserAsActiveWithId(id2) + + val retrieved1 = usersDao.getUserWithId(id1) + val retrieved2 = usersDao.getUserWithId(id2) + assertEquals(false, retrieved1?.current) + assertEquals(true, retrieved2?.current) + } @Test - fun deleteUser() { - val user = createUserEntity("user1", "Account 1", "https://server1.com") - val id = usersDao.saveUser(user) - val savedUser = usersDao.getUserWithId(id).blockingGet() + fun deleteUser() = + runTest { + val user = createUserEntity("user1", "Account 1", "https://server1.com") + val id = usersDao.saveUser(user) + val savedUser = usersDao.getUserWithId(id) - usersDao.deleteUser(savedUser) + usersDao.deleteUser(savedUser!!) - val users = usersDao.getUsers().blockingGet() - assertTrue(users.isEmpty()) - } + val users = usersDao.getUsers() + assertTrue(users.isEmpty()) + } @Test - fun updateUser() { - val user = createUserEntity("user1", "Account 1", "https://server1.com") - val id = usersDao.saveUser(user) - val savedUser = usersDao.getUserWithId(id).blockingGet() + fun updateUser() = + runTest { + val user = createUserEntity("user1", "Account 1", "https://server1.com") + val id = usersDao.saveUser(user) + val savedUser = usersDao.getUserWithId(id)!! - savedUser.displayName = "New Display Name" - usersDao.updateUser(savedUser) + savedUser.displayName = "New Display Name" + usersDao.updateUser(savedUser) - val retrieved = usersDao.getUserWithId(id).blockingGet() - assertEquals("New Display Name", retrieved.displayName) - } + val retrieved = usersDao.getUserWithId(id) + assertEquals("New Display Name", retrieved?.displayName) + } @Test - fun getScheduledForDeletion() { - val user1 = createUserEntity("user1", "Account 1", "https://server1.com").apply { scheduledForDeletion = true } - val user2 = createUserEntity("user2", "Account 2", "https://server1.com").apply { scheduledForDeletion = false } - usersDao.saveUsers(user1, user2) - - val scheduled = usersDao.getUsersScheduledForDeletion().blockingGet() - assertEquals(1, scheduled.size) - assertEquals("user1", scheduled[0].userId) - - val notScheduled = usersDao.getUsersNotScheduledForDeletion().blockingGet() - assertEquals(1, notScheduled.size) - assertEquals("user2", notScheduled[0].userId) - - val all = usersDao.getUsers().blockingGet() - assertEquals(1, all.size) - assertEquals("user2", all[0].userId) - } + fun getScheduledForDeletion() = + runTest { + val user1 = createUserEntity("user1", "Account 1", "https://server1.com") + .apply { scheduledForDeletion = true } + val user2 = createUserEntity("user2", "Account 2", "https://server1.com") + .apply { scheduledForDeletion = false } + usersDao.saveUsers(user1, user2) + + val scheduled = usersDao.getUsersScheduledForDeletion() + assertEquals(1, scheduled.size) + assertEquals("user1", scheduled[0].userId) + + val notScheduled = usersDao.getUsersNotScheduledForDeletion() + assertEquals(1, notScheduled.size) + assertEquals("user2", notScheduled[0].userId) + + val all = usersDao.getUsers() + assertEquals(1, all.size) + assertEquals("user2", all[0].userId) + } @Test - fun getUserWithUserId() { - val user = createUserEntity("user1", "Account 1", "https://server1.com") - usersDao.saveUser(user) + fun getUserWithUserId() = + runTest { + val user = createUserEntity("user1", "Account 1", "https://server1.com") + usersDao.saveUser(user) - val retrieved = usersDao.getUserWithUserId("user1").blockingGet() - assertNotNull(retrieved) - assertEquals("Account 1", retrieved.username) + val retrieved = usersDao.getUserWithUserId("user1") + assertNotNull(retrieved) + assertEquals("Account 1", retrieved?.username) - val nonexistent = usersDao.getUserWithUserId("nonexistent").blockingGet() - assertNull(nonexistent) - } + val nonexistent = usersDao.getUserWithUserId("nonexistent") + assertNull(nonexistent) + } @Test - fun getUserWithUsernameAndServer() { - val user = createUserEntity("user1", "Account 1", "https://server1.com") - usersDao.saveUser(user) + fun getUserWithUsernameAndServer() = + runTest { + val user = createUserEntity("user1", "Account 1", "https://server1.com") + usersDao.saveUser(user) - val retrieved = usersDao.getUserWithUsernameAndServer("Account 1", "https://server1.com").blockingGet() - assertNotNull(retrieved) - assertEquals("user1", retrieved.userId) - } + val retrieved = usersDao.getUserWithUsernameAndServer("Account 1", "https://server1.com") + assertNotNull(retrieved) + assertEquals("user1", retrieved?.userId) + } @Test - fun updatePushState() { - val user = createUserEntity("user1", "Account 1", "https://server1.com") - val id = usersDao.saveUser(user) + fun updatePushState() = + runTest { + val user = createUserEntity("user1", "Account 1", "https://server1.com") + val id = usersDao.saveUser(user) - val newState = PushConfigurationState("token", "id", "sig", "key", true) - val updatedCount = usersDao.updatePushState(id, newState).blockingGet() - assertEquals(1, updatedCount) + val newState = PushConfigurationState("token", "id", "sig", "key", true) + val updatedCount = usersDao.updatePushState(id, newState) + assertEquals(1, updatedCount) - val retrieved = usersDao.getUserWithId(id).blockingGet() - assertEquals(newState, retrieved.pushConfigurationState) - } + val retrieved = usersDao.getUserWithId(id) + assertEquals(newState, retrieved?.pushConfigurationState) + } @Test - fun setActiveNonExistentUser() { - val count = usersDao.setUserAsActiveWithId(9999) - assertEquals(0, count) - } + fun setActiveNonExistentUser() = + runTest { + val count = usersDao.setUserAsActiveWithId(9999) + assertEquals(0, count) + } @Test - fun deleteActiveUser() { - val user = createUserEntity("user1", "Account 1", "https://server1.com").apply { current = true } - val id = usersDao.saveUser(user) - val savedUser = usersDao.getUserWithId(id).blockingGet() - - usersDao.deleteUser(savedUser) - - val active = usersDao.getActiveUser().blockingGet() - assertNull(active) - assertNull(usersDao.getActiveUserSynchronously()) - } + fun deleteActiveUser() = + runTest { + val user = createUserEntity("user1", "Account 1", "https://server1.com").apply { current = true } + val id = usersDao.saveUser(user) + val savedUser = usersDao.getUserWithId(id)!! + + usersDao.deleteUser(savedUser) + + val active = usersDao.getActiveUser() + assertNull(active) + assertNull(usersDao.getActiveUserSynchronously()) + } private fun createUserEntity(userId: String, userName: String, server: String) = UserEntity( diff --git a/app/src/main/java/com/nextcloud/talk/data/user/UsersDao.kt b/app/src/main/java/com/nextcloud/talk/data/user/UsersDao.kt index 8b83c20e61..27f7019a1c 100644 --- a/app/src/main/java/com/nextcloud/talk/data/user/UsersDao.kt +++ b/app/src/main/java/com/nextcloud/talk/data/user/UsersDao.kt @@ -15,59 +15,57 @@ import androidx.room.Query import androidx.room.Update import com.nextcloud.talk.data.user.model.UserEntity import com.nextcloud.talk.models.json.push.PushConfigurationState -import io.reactivex.Maybe -import io.reactivex.Observable -import io.reactivex.Single +import kotlinx.coroutines.flow.Flow @Dao @Suppress("TooManyFunctions") -abstract class UsersDao { +interface UsersDao { // get active user. ORDER BY/LIMIT make this deterministic if more than one row is ever // marked current=1 (e.g. a duplicate-account row left over from a past bug), instead of // relying on whatever order an unordered full-table scan happens to return. @Query("SELECT * FROM User where current = 1 ORDER BY id DESC LIMIT 1") - abstract fun getActiveUser(): Maybe + suspend fun getActiveUser(): UserEntity? // get active user @Query("SELECT * FROM User where current = 1 ORDER BY id DESC LIMIT 1") - abstract fun getActiveUserObservable(): Observable + fun getActiveUserFlow(): Flow @Query("SELECT * FROM User where current = 1 ORDER BY id DESC LIMIT 1") - abstract fun getActiveUserSynchronously(): UserEntity? + fun getActiveUserSynchronously(): UserEntity? @Delete - abstract fun deleteUser(user: UserEntity): Int + suspend fun deleteUser(user: UserEntity): Int @Update - abstract fun updateUser(user: UserEntity): Int + suspend fun updateUser(user: UserEntity): Int @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun saveUser(user: UserEntity): Long + suspend fun saveUser(user: UserEntity): Long @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun saveUsers(vararg users: UserEntity): List + suspend fun saveUsers(vararg users: UserEntity): List // get all users not scheduled for deletion @Query("SELECT * FROM User where scheduledForDeletion != 1") - abstract fun getUsers(): Single> + suspend fun getUsers(): List @Query("SELECT * FROM User where id = :id") - abstract fun getUserWithId(id: Long): Maybe + suspend fun getUserWithId(id: Long): UserEntity? @Query("SELECT * FROM User where id = :id AND scheduledForDeletion != 1") - abstract fun getUserWithIdNotScheduledForDeletion(id: Long): Maybe + suspend fun getUserWithIdNotScheduledForDeletion(id: Long): UserEntity? @Query("SELECT * FROM User where userId = :userId") - abstract fun getUserWithUserId(userId: String): Maybe + suspend fun getUserWithUserId(userId: String): UserEntity? @Query("SELECT * FROM User where scheduledForDeletion = 1") - abstract fun getUsersScheduledForDeletion(): Single> + suspend fun getUsersScheduledForDeletion(): List @Query("SELECT * FROM User where scheduledForDeletion = 0") - abstract fun getUsersNotScheduledForDeletion(): Single> + suspend fun getUsersNotScheduledForDeletion(): List @Query("SELECT * FROM User WHERE username = :username AND baseUrl = :server") - abstract fun getUserWithUsernameAndServer(username: String, server: String): Maybe + suspend fun getUserWithUsernameAndServer(username: String, server: String): UserEntity? @Query( "UPDATE User SET current = CASE " + @@ -75,10 +73,10 @@ abstract class UsersDao { "WHEN id != :id THEN 0 " + "END" ) - abstract fun setUserAsActiveWithId(id: Long): Int + suspend fun setUserAsActiveWithId(id: Long): Int @Query("Update User SET pushConfigurationState = :state WHERE id == :id") - abstract fun updatePushState(id: Long, state: PushConfigurationState): Single + suspend fun updatePushState(id: Long, state: PushConfigurationState): Int companion object { const val TAG = "UsersDao" diff --git a/app/src/main/java/com/nextcloud/talk/data/user/UsersRepository.kt b/app/src/main/java/com/nextcloud/talk/data/user/UsersRepository.kt index 4889b5cb74..ce15a57077 100644 --- a/app/src/main/java/com/nextcloud/talk/data/user/UsersRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/data/user/UsersRepository.kt @@ -9,24 +9,22 @@ package com.nextcloud.talk.data.user import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.json.push.PushConfigurationState -import io.reactivex.Maybe -import io.reactivex.Observable -import io.reactivex.Single +import kotlinx.coroutines.flow.Flow @Suppress("TooManyFunctions") interface UsersRepository { - fun getActiveUser(): Maybe - fun getActiveUserObservable(): Observable - fun getUsers(): Single> - fun getUserWithId(id: Long): Maybe - fun getUserWithIdNotScheduledForDeletion(id: Long): Maybe - fun getUserWithUserId(userId: String): Maybe - fun getUsersScheduledForDeletion(): Single> - fun getUsersNotScheduledForDeletion(): Single> - fun getUserWithUsernameAndServer(username: String, server: String): Maybe - fun updateUser(user: User): Int - fun insertUser(user: User): Long - fun setUserAsActiveWithId(id: Long): Single - fun deleteUser(user: User): Int - fun updatePushState(id: Long, state: PushConfigurationState): Single + suspend fun getActiveUser(): User? + fun getActiveUserFlow(): Flow + suspend fun getUsers(): List + suspend fun getUserWithId(id: Long): User? + suspend fun getUserWithIdNotScheduledForDeletion(id: Long): User? + suspend fun getUserWithUserId(userId: String): User? + suspend fun getUsersScheduledForDeletion(): List + suspend fun getUsersNotScheduledForDeletion(): List + suspend fun getUserWithUsernameAndServer(username: String, server: String): User? + suspend fun updateUser(user: User): Int + suspend fun insertUser(user: User): Long + suspend fun setUserAsActiveWithId(id: Long): Boolean + suspend fun deleteUser(user: User): Int + suspend fun updatePushState(id: Long, state: PushConfigurationState): Int } diff --git a/app/src/main/java/com/nextcloud/talk/data/user/UsersRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/data/user/UsersRepositoryImpl.kt index e3a835f0a3..5a53c3bbf8 100644 --- a/app/src/main/java/com/nextcloud/talk/data/user/UsersRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/data/user/UsersRepositoryImpl.kt @@ -10,73 +10,55 @@ package com.nextcloud.talk.data.user import android.util.Log import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.json.push.PushConfigurationState -import io.reactivex.Maybe -import io.reactivex.Observable -import io.reactivex.Single +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map @Suppress("TooManyFunctions") class UsersRepositoryImpl(private val usersDao: UsersDao) : UsersRepository { - override fun getActiveUser(): Maybe { - val user = usersDao.getActiveUser() - .map { - setUserAsActiveWithId(it.id) - UserMapper.toModel(it)!! - } - return user + override suspend fun getActiveUser(): User? { + val entity = usersDao.getActiveUser() ?: return null + setUserAsActiveWithId(entity.id) + return UserMapper.toModel(entity) } - override fun getActiveUserObservable(): Observable = - usersDao.getActiveUserObservable().map { + override fun getActiveUserFlow(): Flow = + usersDao.getActiveUserFlow().map { UserMapper.toModel(it) } - override fun getUsers(): Single> = usersDao.getUsers().map { UserMapper.toModel(it) } + override suspend fun getUsers(): List = UserMapper.toModel(usersDao.getUsers()) - override fun getUserWithId(id: Long): Maybe = usersDao.getUserWithId(id).map { UserMapper.toModel(it) } + override suspend fun getUserWithId(id: Long): User? = UserMapper.toModel(usersDao.getUserWithId(id)) - override fun getUserWithIdNotScheduledForDeletion(id: Long): Maybe = - usersDao.getUserWithIdNotScheduledForDeletion(id).map { - UserMapper.toModel(it) - } + override suspend fun getUserWithIdNotScheduledForDeletion(id: Long): User? = + UserMapper.toModel(usersDao.getUserWithIdNotScheduledForDeletion(id)) - override fun getUserWithUserId(userId: String): Maybe = - usersDao.getUserWithUserId(userId).map { - UserMapper.toModel(it) - } + override suspend fun getUserWithUserId(userId: String): User? = + UserMapper.toModel(usersDao.getUserWithUserId(userId)) - override fun getUsersScheduledForDeletion(): Single> = - usersDao.getUsersScheduledForDeletion().map { - UserMapper.toModel(it) - } + override suspend fun getUsersScheduledForDeletion(): List = + UserMapper.toModel(usersDao.getUsersScheduledForDeletion()) - override fun getUsersNotScheduledForDeletion(): Single> = - usersDao.getUsersNotScheduledForDeletion().map { - UserMapper.toModel(it) - } + override suspend fun getUsersNotScheduledForDeletion(): List = + UserMapper.toModel(usersDao.getUsersNotScheduledForDeletion()) - override fun getUserWithUsernameAndServer(username: String, server: String): Maybe = - usersDao.getUserWithUsernameAndServer(username, server).map { - UserMapper.toModel(it) - } + override suspend fun getUserWithUsernameAndServer(username: String, server: String): User? = + UserMapper.toModel(usersDao.getUserWithUsernameAndServer(username, server)) - override fun updateUser(user: User): Int = usersDao.updateUser(UserMapper.toEntity(user)) + override suspend fun updateUser(user: User): Int = usersDao.updateUser(UserMapper.toEntity(user)) - override fun insertUser(user: User): Long = usersDao.saveUser(UserMapper.toEntity(user)) + override suspend fun insertUser(user: User): Long = usersDao.saveUser(UserMapper.toEntity(user)) - override fun setUserAsActiveWithId(id: Long): Single { + override suspend fun setUserAsActiveWithId(id: Long): Boolean { val amountUpdated = usersDao.setUserAsActiveWithId(id) Log.d(TAG, "setUserAsActiveWithId. amountUpdated: $amountUpdated") - return if (amountUpdated > 0) { - Single.just(true) - } else { - Single.just(false) - } + return amountUpdated > 0 } - override fun deleteUser(user: User): Int = usersDao.deleteUser(UserMapper.toEntity(user)) + override suspend fun deleteUser(user: User): Int = usersDao.deleteUser(UserMapper.toEntity(user)) - override fun updatePushState(id: Long, state: PushConfigurationState): Single = + override suspend fun updatePushState(id: Long, state: PushConfigurationState): Int = usersDao.updatePushState(id, state) companion object { diff --git a/app/src/main/java/com/nextcloud/talk/users/UserManager.kt b/app/src/main/java/com/nextcloud/talk/users/UserManager.kt index 8a785130a1..8549616ab5 100644 --- a/app/src/main/java/com/nextcloud/talk/users/UserManager.kt +++ b/app/src/main/java/com/nextcloud/talk/users/UserManager.kt @@ -21,32 +21,53 @@ import io.reactivex.Observable import io.reactivex.Single import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.Subject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.rx2.rxMaybe +import kotlinx.coroutines.rx2.rxSingle @Suppress("TooManyFunctions") class UserManager internal constructor(private val userRepository: UsersRepository) { + + private val managerScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + @Deprecated("Use suspend fun getUsers() instead") val users: Single> - get() = userRepository.getUsers() + get() = rxSingle { getUsers() } + suspend fun getUsers(): List = userRepository.getUsers() + + @Deprecated("Use suspend fun getUsersScheduledForDeletion() instead") val usersScheduledForDeletion: Single> - get() = userRepository.getUsersScheduledForDeletion() + get() = rxSingle { getUsersScheduledForDeletion() } + + suspend fun getUsersScheduledForDeletion(): List = userRepository.getUsersScheduledForDeletion() + /** + * @deprecated coroutine-native code should use [com.nextcloud.talk.utils.database.user.CurrentUserProvider] + * instead. + */ + @Deprecated("Use CurrentUserProvider.getCurrentUser() instead") val currentUser: Maybe - get() { - return userRepository.getActiveUser() - .switchIfEmpty(Maybe.defer { getAnyUserAndSetAsActive() }) - } + get() = rxMaybe { getCurrentUserOrAny() } + + private suspend fun getCurrentUserOrAny(): User? = userRepository.getActiveUser() ?: getAnyUserAndSetAsActive() /** - * Backed by [activeUserSubject] rather than [UsersRepository.getActiveUserObservable] directly, so that + * Backed by [activeUserSubject] rather than [UsersRepository.getActiveUserFlow] directly, so that * [setUserAsActive] can push the newly-active user out synchronously the moment it succeeds, instead of * consumers having to wait for Room's invalidation-tracker round trip to notice the DB write and re-query. * That round trip is asynchronous and was racing against code (e.g. AccountVerificationActivity. * proceedWithLogin()) that both changes the active user and immediately acts as if every observer already * knows about it - e.g. launching a screen for the new user before its avatar/data had actually updated. - * Room's own observable is still relied on underneath to seed this and to catch any change to the `current` - * flag that doesn't go through [setUserAsActive]. + * Room's own [UsersRepository.getActiveUserFlow] is still relied on underneath to seed this and to catch + * any change to the `current` flag that doesn't go through [setUserAsActive]. * * RxJava-based for CurrentUserProviderOld, the still-used but deprecated consumer. Coroutine-based code * should prefer [currentUserFlow] instead, which is updated at the exact same point and needs no RxJava @@ -64,50 +85,67 @@ class UserManager internal constructor(private val userRepository: UsersReposito private val activeUserSubject: Subject by lazy { val subject = BehaviorSubject.create().toSerialized() - userRepository.getActiveUserObservable().subscribe(subject::onNext) { } + managerScope.launch { + userRepository.getActiveUserFlow().filterNotNull().collect(subject::onNext) + } subject } private val activeUserStateFlow: MutableStateFlow by lazy { val flow = MutableStateFlow(null) - userRepository.getActiveUserObservable().subscribe({ flow.value = it }) { } + managerScope.launch { + userRepository.getActiveUserFlow().collect { flow.value = it } + } flow } - fun deleteUser(internalId: Long): Int = - userRepository.deleteUser(userRepository.getUserWithId(internalId).blockingGet()) + @Deprecated("Use suspend fun deleteUser(internalId: Long) instead") + fun deleteUser(internalId: Long): Int = runBlocking { deleteUserSuspend(internalId) } + + suspend fun deleteUserSuspend(internalId: Long): Int { + val user = userRepository.getUserWithId(internalId) ?: return 0 + return userRepository.deleteUser(user) + } + + @Deprecated("Use suspend fun getUserWithId(id: Long) instead") + fun getUserWithId(id: Long): Maybe = rxMaybe { getUserWithIdSuspend(id) } - fun getUserWithId(id: Long): Maybe = userRepository.getUserWithId(id) + suspend fun getUserWithIdSuspend(id: Long): User? = userRepository.getUserWithId(id) + @Deprecated("Use suspend fun checkIfUserIsScheduledForDeletion(username, server) instead") fun checkIfUserIsScheduledForDeletion(username: String, server: String): Single = - userRepository - .getUserWithUsernameAndServer(username, server) - .map { it.scheduledForDeletion } - .switchIfEmpty(Single.just(false)) + rxSingle { checkIfUserIsScheduledForDeletionSuspend(username, server) } - fun getUserWithInternalId(id: Long): Maybe = userRepository.getUserWithIdNotScheduledForDeletion(id) + suspend fun checkIfUserIsScheduledForDeletionSuspend(username: String, server: String): Boolean = + userRepository.getUserWithUsernameAndServer(username, server)?.scheduledForDeletion ?: false + @Deprecated("Use suspend fun getUserWithInternalId(id: Long) instead") + fun getUserWithInternalId(id: Long): Maybe = rxMaybe { getUserWithInternalIdSuspend(id) } + + suspend fun getUserWithInternalIdSuspend(id: Long): User? = userRepository.getUserWithIdNotScheduledForDeletion(id) + + @Deprecated("Use suspend fun checkIfUserExists(username, server) instead") fun checkIfUserExists(username: String, server: String): Single = - userRepository - .getUserWithUsernameAndServer(username, server) - .map { true } - .switchIfEmpty(Single.just(false)) + rxSingle { checkIfUserExistsSuspend(username, server) } + + suspend fun checkIfUserExistsSuspend(username: String, server: String): Boolean = + userRepository.getUserWithUsernameAndServer(username, server) != null /** * Don't ask * * @return `true` if the user was updated **AND** there is another user to set as active, `false` otherwise */ - fun scheduleUserForDeletionWithId(id: Long): Single = - userRepository.getUserWithId(id) - .map { user -> - user.scheduledForDeletion = true - user.current = false - userRepository.updateUser(user) - } - .flatMap { getAnyUserAndSetAsActive() } - .map { true } - .switchIfEmpty(Single.just(false)) + @Deprecated("Use suspend fun scheduleUserForDeletionWithId(id: Long) instead") + fun scheduleUserForDeletionWithId(id: Long): Single = rxSingle { scheduleUserForDeletionWithIdSuspend(id) } + + suspend fun scheduleUserForDeletionWithIdSuspend(id: Long): Boolean { + val user = userRepository.getUserWithId(id) ?: return false + user.scheduledForDeletion = true + user.current = false + userRepository.updateUser(user) + return getAnyUserAndSetAsActive() != null + } /** * If there is more than one local User row for the same username+baseUrl (e.g. reusing the @@ -124,120 +162,114 @@ class UserManager internal constructor(private val userRepository: UsersReposito * * @return the number of duplicate rows scheduled for deletion */ - fun scheduleDuplicateAccountsForDeletion(): Single = - Single.zip( - users, - userRepository.getActiveUser().map { it.id }.toSingle(NO_ACTIVE_USER_ID) - ) { allUsers, activeUserId -> - val duplicateGroups = allUsers - .filter { !it.username.isNullOrEmpty() && !it.baseUrl.isNullOrEmpty() } - .groupBy { it.username to it.baseUrl } - .values - .filter { it.size > 1 } - - var scheduledCount = 0 - duplicateGroups.forEach { duplicates -> - val userToKeep = duplicates.firstOrNull { it.id == activeUserId } - ?: duplicates.firstOrNull { it.current } - ?: duplicates.minByOrNull { it.id ?: Long.MAX_VALUE } - duplicates - .filter { it.id != userToKeep?.id } - .forEach { duplicate -> - duplicate.scheduledForDeletion = true - userRepository.updateUser(duplicate) - scheduledCount++ - } - } - scheduledCount - } + @Deprecated("Use suspend fun scheduleDuplicateAccountsForDeletion() instead") + fun scheduleDuplicateAccountsForDeletion(): Single = rxSingle { scheduleDuplicateAccountsForDeletionSuspend() } - private fun getAnyUserAndSetAsActive(): Maybe { - val results = userRepository.getUsersNotScheduledForDeletion() + suspend fun scheduleDuplicateAccountsForDeletionSuspend(): Int { + val allUsers = getUsers() + val activeUserId = userRepository.getActiveUser()?.id ?: NO_ACTIVE_USER_ID - return results - .flatMapMaybe { - if (it.isNotEmpty()) { - val user = it.first() - if (setUserAsActive(user).blockingGet()) { - userRepository.getActiveUser() - } else { - Maybe.empty() - } - } else { - Maybe.empty() + val duplicateGroups = allUsers + .filter { !it.username.isNullOrEmpty() && !it.baseUrl.isNullOrEmpty() } + .groupBy { it.username to it.baseUrl } + .values + .filter { it.size > 1 } + + var scheduledCount = 0 + duplicateGroups.forEach { duplicates -> + val userToKeep = duplicates.firstOrNull { it.id == activeUserId } + ?: duplicates.firstOrNull { it.current } + ?: duplicates.minByOrNull { it.id ?: Long.MAX_VALUE } + duplicates + .filter { it.id != userToKeep?.id } + .forEach { duplicate -> + duplicate.scheduledForDeletion = true + userRepository.updateUser(duplicate) + scheduledCount++ } - } + } + return scheduledCount } - fun updateExternalSignalingServer(id: Long, externalSignalingServer: ExternalSignalingServer): Single = - userRepository.getUserWithId(id).map { user -> - user.externalSignalingServer = externalSignalingServer - userRepository.updateUser(user) - }.toSingle() - - fun updateOrCreateUser(user: User): Single = - Single.fromCallable { - when (user.id) { - null -> userRepository.insertUser(user).toInt() - else -> userRepository.updateUser(user) - } + private suspend fun getAnyUserAndSetAsActive(): User? { + val results = userRepository.getUsersNotScheduledForDeletion() + if (results.isEmpty()) { + return null } + val user = results.first() + return if (setUserAsActiveSuspend(user)) { + userRepository.getActiveUser() + } else { + null + } + } - fun saveUser(user: User): Single = - Single.fromCallable { - userRepository.updateUser(user) + @Deprecated("Use suspend fun updateExternalSignalingServer(id, externalSignalingServer) instead") + fun updateExternalSignalingServer(id: Long, externalSignalingServer: ExternalSignalingServer): Single = + rxSingle { updateExternalSignalingServerSuspend(id, externalSignalingServer) } + + suspend fun updateExternalSignalingServerSuspend(id: Long, externalSignalingServer: ExternalSignalingServer): Int { + val user = userRepository.getUserWithId(id) ?: throw NoSuchElementException() + user.externalSignalingServer = externalSignalingServer + return userRepository.updateUser(user) + } + + @Deprecated("Use suspend fun updateOrCreateUser(user) instead") + fun updateOrCreateUser(user: User): Single = rxSingle { updateOrCreateUserSuspend(user) } + + suspend fun updateOrCreateUserSuspend(user: User): Int = + when (user.id) { + null -> userRepository.insertUser(user).toInt() + else -> userRepository.updateUser(user) } - fun setUserAsActive(user: User): Single { + @Deprecated("Use suspend fun saveUser(user) instead") + fun saveUser(user: User): Single = rxSingle { saveUserSuspend(user) } + + suspend fun saveUserSuspend(user: User): Int = userRepository.updateUser(user) + + @Deprecated("Use suspend fun setUserAsActive(user) instead") + fun setUserAsActive(user: User): Single = rxSingle { setUserAsActiveSuspend(user) } + + suspend fun setUserAsActiveSuspend(user: User): Boolean { Log.d(TAG, "setUserAsActive:" + user.id!!) - return userRepository.setUserAsActiveWithId(user.id!!) - .doOnSuccess { success -> - if (success) { - activeUserSubject.onNext(user) - activeUserStateFlow.value = user - } - } + val success = userRepository.setUserAsActiveWithId(user.id!!) + if (success) { + activeUserSubject.onNext(user) + activeUserStateFlow.value = user + } + return success } + @Deprecated("Use suspend fun storeProfile(username, userAttributes) instead") fun storeProfile(username: String?, userAttributes: UserAttributes): Maybe = - findUser(userAttributes) - .map { user: User? -> - when (user) { - null -> createUser( - username, - userAttributes - ) - else -> { - user.token = userAttributes.token - user.baseUrl = userAttributes.serverUrl - user.current = userAttributes.currentUser - user.userId = userAttributes.userId - user.token = userAttributes.token - user.displayName = userAttributes.displayName - user.clientCertificate = userAttributes.certificateAlias - - updateUserData( - user, - userAttributes - ) - - user - } - } - } - .switchIfEmpty(Maybe.just(createUser(username, userAttributes))) - .map { user -> - userRepository.insertUser(user) - } - .flatMap { id -> - userRepository.getUserWithId(id) + rxMaybe { storeProfileSuspend(username, userAttributes) } + + suspend fun storeProfileSuspend(username: String?, userAttributes: UserAttributes): User? { + val existingUser = findUser(userAttributes) + val user = if (existingUser != null) { + existingUser.apply { + token = userAttributes.token + baseUrl = userAttributes.serverUrl + current = userAttributes.currentUser + userId = userAttributes.userId + token = userAttributes.token + displayName = userAttributes.displayName + clientCertificate = userAttributes.certificateAlias + updateUserData(this, userAttributes) } + } else { + createUser(username, userAttributes) + } + val id = userRepository.insertUser(user) + return userRepository.getUserWithId(id) + } - private fun findUser(userAttributes: UserAttributes): Maybe = + private suspend fun findUser(userAttributes: UserAttributes): User? = if (userAttributes.id != null) { userRepository.getUserWithId(userAttributes.id) } else { - Maybe.empty() + null } private fun updateUserData(user: User, userAttributes: UserAttributes) { @@ -296,7 +328,11 @@ class UserManager internal constructor(private val userRepository: UsersReposito return user } + @Deprecated("Use suspend fun updatePushState(id, state) instead") fun updatePushState(id: Long, state: PushConfigurationState): Single = + rxSingle { updatePushStateSuspend(id, state) } + + suspend fun updatePushStateSuspend(id: Long, state: PushConfigurationState): Int = userRepository.updatePushState(id, state) companion object { diff --git a/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtilsDaos.kt b/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtilsDaos.kt index 842d4bebbc..2000d8e263 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtilsDaos.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtilsDaos.kt @@ -16,9 +16,6 @@ import com.nextcloud.talk.data.database.model.ConversationEntity import com.nextcloud.talk.data.user.UsersDao import com.nextcloud.talk.data.user.model.UserEntity import com.nextcloud.talk.models.json.push.PushConfigurationState -import io.reactivex.Maybe -import io.reactivex.Observable -import io.reactivex.Single import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf @@ -159,7 +156,7 @@ class DummyChatMessagesDaoImpl : ChatMessagesDao { override fun getNumberOfThreadReplies(internalConversationId: String, threadId: Long): Int = 0 } -class DummyUserDaoImpl : UsersDao() { +class DummyUserDaoImpl : UsersDao { private val dummyUsers = mutableListOf( UserEntity(1L, "user1_id", "user1", "server1", "1"), UserEntity(2L, "user2_id", "user2", "server1", "2"), @@ -167,28 +164,24 @@ class DummyUserDaoImpl : UsersDao() { ) private var activeUserId: Long? = 1L - override fun getActiveUser(): Maybe = - Maybe.fromCallable { - dummyUsers.find { it.id == activeUserId && !it.scheduledForDeletion } - } + override suspend fun getActiveUser(): UserEntity? = + dummyUsers.find { it.id == activeUserId && !it.scheduledForDeletion } - override fun getActiveUserObservable(): Observable = - Observable.fromCallable { - dummyUsers.find { it.id == activeUserId && !it.scheduledForDeletion } - } + override fun getActiveUserFlow(): Flow = + flowOf(dummyUsers.find { it.id == activeUserId && !it.scheduledForDeletion }) override fun getActiveUserSynchronously(): UserEntity? = dummyUsers.find { it.id == activeUserId && !it.scheduledForDeletion } - override fun deleteUser(user: UserEntity): Int { + override suspend fun deleteUser(user: UserEntity): Int { val initialSize = dummyUsers.size dummyUsers.removeIf { it.id == user.id } return initialSize - dummyUsers.size } - override fun updateUser(user: UserEntity): Int { + override suspend fun updateUser(user: UserEntity): Int { val index = dummyUsers.indexOfFirst { it.id == user.id } return if (index != -1) { dummyUsers[index] = user @@ -198,59 +191,48 @@ class DummyUserDaoImpl : UsersDao() { } } - override fun saveUser(user: UserEntity): Long { + override suspend fun saveUser(user: UserEntity): Long { val newUser = user.copy(id = dummyUsers.size + 1L) dummyUsers.add(newUser) return newUser.id } - override fun saveUsers(vararg users: UserEntity): List = users.map { saveUser(it) } + override suspend fun saveUsers(vararg users: UserEntity): List = users.map { saveUser(it) } - override fun getUsers(): Single> = Single.just(dummyUsers.filter { !it.scheduledForDeletion }) + override suspend fun getUsers(): List = dummyUsers.filter { !it.scheduledForDeletion } - override fun getUserWithId(id: Long): Maybe = Maybe.fromCallable { dummyUsers.find { it.id == id } } + override suspend fun getUserWithId(id: Long): UserEntity? = dummyUsers.find { it.id == id } - override fun getUserWithIdNotScheduledForDeletion(id: Long): Maybe = - Maybe.fromCallable { - dummyUsers.find { it.id == id && !it.scheduledForDeletion } - } + override suspend fun getUserWithIdNotScheduledForDeletion(id: Long): UserEntity? = + dummyUsers.find { it.id == id && !it.scheduledForDeletion } + + override suspend fun getUserWithUserId(userId: String): UserEntity? = dummyUsers.find { it.userId == userId } - override fun getUserWithUserId(userId: String): Maybe = - Maybe.fromCallable { - dummyUsers.find { it.userId == userId } + override suspend fun getUsersScheduledForDeletion(): List = + dummyUsers.filter { + it.scheduledForDeletion } - override fun getUsersScheduledForDeletion(): Single> = - Single.just( - dummyUsers.filter { - it.scheduledForDeletion - } - ) - - override fun getUsersNotScheduledForDeletion(): Single> = - Single.just( - dummyUsers.filter { - !it.scheduledForDeletion - } - ) - - override fun getUserWithUsernameAndServer(username: String, server: String): Maybe = - Maybe.fromCallable { - dummyUsers.find { it.username == username } + override suspend fun getUsersNotScheduledForDeletion(): List = + dummyUsers.filter { + !it.scheduledForDeletion } - override fun setUserAsActiveWithId(id: Long): Int { + override suspend fun getUserWithUsernameAndServer(username: String, server: String): UserEntity? = + dummyUsers.find { it.username == username } + + override suspend fun setUserAsActiveWithId(id: Long): Int { activeUserId = id return 1 } - override fun updatePushState(id: Long, state: PushConfigurationState): Single { + override suspend fun updatePushState(id: Long, state: PushConfigurationState): Int { val index = dummyUsers.indexOfFirst { it.id == id } return if (index != -1) { dummyUsers[index] = dummyUsers[index] - Single.just(1) + 1 } else { - Single.just(0) + 0 } } } diff --git a/app/src/test/java/com/nextcloud/talk/conversationlist/data/network/ConversationListFreshnessIntegrationTest.kt b/app/src/test/java/com/nextcloud/talk/conversationlist/data/network/ConversationListFreshnessIntegrationTest.kt index f7014dd820..81d1ad9cb3 100644 --- a/app/src/test/java/com/nextcloud/talk/conversationlist/data/network/ConversationListFreshnessIntegrationTest.kt +++ b/app/src/test/java/com/nextcloud/talk/conversationlist/data/network/ConversationListFreshnessIntegrationTest.kt @@ -81,7 +81,9 @@ class ConversationListFreshnessIntegrationTest { db = Room.inMemoryDatabaseBuilder(context, TalkDatabase::class.java) .allowMainThreadQueries() .build() - db.usersDao().saveUser(UserEntity(id = ACCOUNT_ID, userId = "me", username = "me", baseUrl = BASE_URL)) + runBlocking { + db.usersDao().saveUser(UserEntity(id = ACCOUNT_ID, userId = "me", username = "me", baseUrl = BASE_URL)) + } whenever(networkMonitor.isOnline).thenReturn(MutableStateFlow(true)) diff --git a/app/src/test/java/com/nextcloud/talk/conversationlist/data/network/RoomListMessagePrefetchIntegrationTest.kt b/app/src/test/java/com/nextcloud/talk/conversationlist/data/network/RoomListMessagePrefetchIntegrationTest.kt index 20ec0e4890..9a6c862ae3 100644 --- a/app/src/test/java/com/nextcloud/talk/conversationlist/data/network/RoomListMessagePrefetchIntegrationTest.kt +++ b/app/src/test/java/com/nextcloud/talk/conversationlist/data/network/RoomListMessagePrefetchIntegrationTest.kt @@ -71,7 +71,9 @@ class RoomListMessagePrefetchIntegrationTest { db = Room.inMemoryDatabaseBuilder(context, TalkDatabase::class.java) .allowMainThreadQueries() .build() - db.usersDao().saveUser(UserEntity(id = ACCOUNT_ID, userId = "me", username = "me", baseUrl = BASE_URL)) + runBlocking { + db.usersDao().saveUser(UserEntity(id = ACCOUNT_ID, userId = "me", username = "me", baseUrl = BASE_URL)) + } whenever(networkMonitor.isOnline).thenReturn(MutableStateFlow(true)) diff --git a/app/src/test/java/com/nextcloud/talk/users/UserManagerTest.kt b/app/src/test/java/com/nextcloud/talk/users/UserManagerTest.kt index abc4f94b55..9ec71f616d 100644 --- a/app/src/test/java/com/nextcloud/talk/users/UserManagerTest.kt +++ b/app/src/test/java/com/nextcloud/talk/users/UserManagerTest.kt @@ -8,8 +8,7 @@ package com.nextcloud.talk.users import com.nextcloud.talk.data.user.UsersRepository import com.nextcloud.talk.data.user.model.User -import io.reactivex.Maybe -import io.reactivex.Single +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -17,8 +16,9 @@ import org.junit.Before import org.junit.Test import org.mockito.kotlin.mock import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever +import org.mockito.kotlin.wheneverBlocking +@Suppress("DEPRECATION") class UserManagerTest { private val usersRepository: UsersRepository = mock() @@ -32,136 +32,157 @@ class UserManagerTest { // No row resolves as "the" active user unless a test overrides this, so // scheduleDuplicateAccountsForDeletion() falls back to the `current` flag / oldest row, // matching the behavior asserted by the tests below that don't care about this priority. - whenever(usersRepository.getActiveUser()).thenReturn(Maybe.empty()) + wheneverBlocking { usersRepository.getActiveUser() }.thenReturn(null) } @Test - fun `keeps the current user among duplicates and schedules the rest for deletion`() { - val current = user(id = 2, username = "userA", baseUrl = "https://example.com", current = true) - val duplicate = user(id = 1, username = "userA", baseUrl = "https://example.com", current = false) - whenever(usersRepository.getUsers()).thenReturn(Single.just(listOf(current, duplicate))) + fun `keeps the current user among duplicates and schedules the rest for deletion`() = + runTest { + val current = user(id = 2, username = "userA", baseUrl = "https://example.com", current = true) + val duplicate = user(id = 1, username = "userA", baseUrl = "https://example.com", current = false) + wheneverBlocking { usersRepository.getUsers() }.thenReturn(listOf(current, duplicate)) - val scheduledCount = userManager.scheduleDuplicateAccountsForDeletion().blockingGet() + val scheduledCount = userManager.scheduleDuplicateAccountsForDeletionSuspend() - assertEquals(1, scheduledCount) - assertTrue(duplicate.scheduledForDeletion) - assertFalse(current.scheduledForDeletion) - verify(usersRepository).updateUser(duplicate) - } + assertEquals(1, scheduledCount) + assertTrue(duplicate.scheduledForDeletion) + assertFalse(current.scheduledForDeletion) + verify(usersRepository).updateUser(duplicate) + } @Test - fun `keeps the oldest row when none of the duplicates is current`() { - val oldest = user(id = 1, username = "userA", baseUrl = "https://example.com") - val newer = user(id = 2, username = "userA", baseUrl = "https://example.com") - whenever(usersRepository.getUsers()).thenReturn(Single.just(listOf(newer, oldest))) + fun `keeps the oldest row when none of the duplicates is current`() = + runTest { + val oldest = user(id = 1, username = "userA", baseUrl = "https://example.com") + val newer = user(id = 2, username = "userA", baseUrl = "https://example.com") + wheneverBlocking { usersRepository.getUsers() }.thenReturn(listOf(newer, oldest)) - val scheduledCount = userManager.scheduleDuplicateAccountsForDeletion().blockingGet() + val scheduledCount = userManager.scheduleDuplicateAccountsForDeletionSuspend() - assertEquals(1, scheduledCount) - assertTrue(newer.scheduledForDeletion) - assertFalse(oldest.scheduledForDeletion) - } + assertEquals(1, scheduledCount) + assertTrue(newer.scheduledForDeletion) + assertFalse(oldest.scheduledForDeletion) + } @Test - fun `does nothing when there are no duplicates`() { - val userA = user(id = 1, username = "userA", baseUrl = "https://example.com", current = true) - val userB = user(id = 2, username = "userB", baseUrl = "https://example.com") - whenever(usersRepository.getUsers()).thenReturn(Single.just(listOf(userA, userB))) + fun `does nothing when there are no duplicates`() = + runTest { + val userA = user(id = 1, username = "userA", baseUrl = "https://example.com", current = true) + val userB = user(id = 2, username = "userB", baseUrl = "https://example.com") + wheneverBlocking { usersRepository.getUsers() }.thenReturn(listOf(userA, userB)) - val scheduledCount = userManager.scheduleDuplicateAccountsForDeletion().blockingGet() + val scheduledCount = userManager.scheduleDuplicateAccountsForDeletionSuspend() - assertEquals(0, scheduledCount) - assertFalse(userA.scheduledForDeletion) - assertFalse(userB.scheduledForDeletion) - } + assertEquals(0, scheduledCount) + assertFalse(userA.scheduledForDeletion) + assertFalse(userB.scheduledForDeletion) + } @Test - fun `different servers with the same username are not treated as duplicates`() { - val userA = user(id = 1, username = "userA", baseUrl = "https://example.com") - val userB = user(id = 2, username = "userA", baseUrl = "https://other.example.com") - whenever(usersRepository.getUsers()).thenReturn(Single.just(listOf(userA, userB))) + fun `different servers with the same username are not treated as duplicates`() = + runTest { + val userA = user(id = 1, username = "userA", baseUrl = "https://example.com") + val userB = user(id = 2, username = "userA", baseUrl = "https://other.example.com") + wheneverBlocking { usersRepository.getUsers() }.thenReturn(listOf(userA, userB)) - val scheduledCount = userManager.scheduleDuplicateAccountsForDeletion().blockingGet() + val scheduledCount = userManager.scheduleDuplicateAccountsForDeletionSuspend() - assertEquals(0, scheduledCount) - } + assertEquals(0, scheduledCount) + } @Test - fun `rows with a null or blank username or baseUrl are never grouped as duplicates`() { - val nullUsername = user(id = 1, username = "userA", baseUrl = "https://example.com") - .apply { username = null } - val anotherNullUsername = user(id = 2, username = "userA", baseUrl = "https://example.com") - .apply { username = null } - val blankBaseUrl = user(id = 3, username = "userA", baseUrl = "") - val anotherBlankBaseUrl = user(id = 4, username = "userA", baseUrl = "") - whenever(usersRepository.getUsers()).thenReturn( - Single.just(listOf(nullUsername, anotherNullUsername, blankBaseUrl, anotherBlankBaseUrl)) - ) - - val scheduledCount = userManager.scheduleDuplicateAccountsForDeletion().blockingGet() - - assertEquals(0, scheduledCount) - } + fun `rows with a null or blank username or baseUrl are never grouped as duplicates`() = + runTest { + val nullUsername = user(id = 1, username = "userA", baseUrl = "https://example.com") + .apply { username = null } + val anotherNullUsername = user(id = 2, username = "userA", baseUrl = "https://example.com") + .apply { username = null } + val blankBaseUrl = user(id = 3, username = "userA", baseUrl = "") + val anotherBlankBaseUrl = user(id = 4, username = "userA", baseUrl = "") + wheneverBlocking { usersRepository.getUsers() }.thenReturn( + listOf(nullUsername, anotherNullUsername, blankBaseUrl, anotherBlankBaseUrl) + ) + + val scheduledCount = userManager.scheduleDuplicateAccountsForDeletionSuspend() + + assertEquals(0, scheduledCount) + } @Test - fun `keeps only one row out of three or more duplicates`() { - val current = user(id = 3, username = "userA", baseUrl = "https://example.com", current = true) - val duplicate1 = user(id = 1, username = "userA", baseUrl = "https://example.com") - val duplicate2 = user(id = 2, username = "userA", baseUrl = "https://example.com") - whenever(usersRepository.getUsers()).thenReturn(Single.just(listOf(duplicate1, duplicate2, current))) + fun `keeps only one row out of three or more duplicates`() = + runTest { + val current = user(id = 3, username = "userA", baseUrl = "https://example.com", current = true) + val duplicate1 = user(id = 1, username = "userA", baseUrl = "https://example.com") + val duplicate2 = user(id = 2, username = "userA", baseUrl = "https://example.com") + wheneverBlocking { usersRepository.getUsers() }.thenReturn(listOf(duplicate1, duplicate2, current)) - val scheduledCount = userManager.scheduleDuplicateAccountsForDeletion().blockingGet() + val scheduledCount = userManager.scheduleDuplicateAccountsForDeletionSuspend() - assertEquals(2, scheduledCount) - assertTrue(duplicate1.scheduledForDeletion) - assertTrue(duplicate2.scheduledForDeletion) - assertFalse(current.scheduledForDeletion) - } + assertEquals(2, scheduledCount) + assertTrue(duplicate1.scheduledForDeletion) + assertTrue(duplicate2.scheduledForDeletion) + assertFalse(current.scheduledForDeletion) + } @Test - fun `handles multiple independent duplicate groups in one pass`() { - val userACurrent = user(id = 1, username = "userA", baseUrl = "https://example.com", current = true) - val userADuplicate = user(id = 2, username = "userA", baseUrl = "https://example.com") - val userBOldest = user(id = 3, username = "userB", baseUrl = "https://example.com") - val userBNewer = user(id = 4, username = "userB", baseUrl = "https://example.com") - whenever(usersRepository.getUsers()).thenReturn( - Single.just(listOf(userACurrent, userADuplicate, userBNewer, userBOldest)) - ) - - val scheduledCount = userManager.scheduleDuplicateAccountsForDeletion().blockingGet() - - assertEquals(2, scheduledCount) - assertTrue(userADuplicate.scheduledForDeletion) - assertTrue(userBNewer.scheduledForDeletion) - assertFalse(userACurrent.scheduledForDeletion) - assertFalse(userBOldest.scheduledForDeletion) - } + fun `handles multiple independent duplicate groups in one pass`() = + runTest { + val userACurrent = user(id = 1, username = "userA", baseUrl = "https://example.com", current = true) + val userADuplicate = user(id = 2, username = "userA", baseUrl = "https://example.com") + val userBOldest = user(id = 3, username = "userB", baseUrl = "https://example.com") + val userBNewer = user(id = 4, username = "userB", baseUrl = "https://example.com") + wheneverBlocking { usersRepository.getUsers() }.thenReturn( + listOf(userACurrent, userADuplicate, userBNewer, userBOldest) + ) + + val scheduledCount = userManager.scheduleDuplicateAccountsForDeletionSuspend() + + assertEquals(2, scheduledCount) + assertTrue(userADuplicate.scheduledForDeletion) + assertTrue(userBNewer.scheduledForDeletion) + assertFalse(userACurrent.scheduledForDeletion) + assertFalse(userBOldest.scheduledForDeletion) + } @Test - fun `does nothing when there are no users at all`() { - whenever(usersRepository.getUsers()).thenReturn(Single.just(emptyList())) + fun `does nothing when there are no users at all`() = + runTest { + wheneverBlocking { usersRepository.getUsers() }.thenReturn(emptyList()) - val scheduledCount = userManager.scheduleDuplicateAccountsForDeletion().blockingGet() + val scheduledCount = userManager.scheduleDuplicateAccountsForDeletionSuspend() - assertEquals(0, scheduledCount) - } + assertEquals(0, scheduledCount) + } @Test - fun `keeps whichever row getActiveUser resolves to, even over a different row flagged current`() { - // Simulates a past bug leaving two rows marked current=true for the same account: the - // active-user lookup (deterministically) resolves to one of them, but the other still - // carries the current flag too. The actively-resolved row must win, since it may be the - // one a live session/background sync is still bound to. - val staleCurrentFlag = user(id = 1, username = "userA", baseUrl = "https://example.com", current = true) - val actuallyActive = user(id = 2, username = "userA", baseUrl = "https://example.com", current = true) - whenever(usersRepository.getUsers()).thenReturn(Single.just(listOf(staleCurrentFlag, actuallyActive))) - whenever(usersRepository.getActiveUser()).thenReturn(Maybe.just(actuallyActive)) + fun `keeps whichever row getActiveUser resolves to, even over a different row flagged current`() = + runTest { + // Simulates a past bug leaving two rows marked current=true for the same account: the + // active-user lookup (deterministically) resolves to one of them, but the other still + // carries the current flag too. The actively-resolved row must win, since it may be the + // one a live session/background sync is still bound to. + val staleCurrentFlag = user(id = 1, username = "userA", baseUrl = "https://example.com", current = true) + val actuallyActive = user(id = 2, username = "userA", baseUrl = "https://example.com", current = true) + wheneverBlocking { usersRepository.getUsers() }.thenReturn(listOf(staleCurrentFlag, actuallyActive)) + wheneverBlocking { usersRepository.getActiveUser() }.thenReturn(actuallyActive) + + val scheduledCount = userManager.scheduleDuplicateAccountsForDeletionSuspend() + + assertEquals(1, scheduledCount) + assertTrue(staleCurrentFlag.scheduledForDeletion) + assertFalse(actuallyActive.scheduledForDeletion) + verify(usersRepository).updateUser(staleCurrentFlag) + } + + @Test + fun `old RxJava-typed bridge still delegates to the suspend implementation`() { + val current = user(id = 2, username = "userA", baseUrl = "https://example.com", current = true) + val duplicate = user(id = 1, username = "userA", baseUrl = "https://example.com", current = false) + wheneverBlocking { usersRepository.getUsers() }.thenReturn(listOf(current, duplicate)) val scheduledCount = userManager.scheduleDuplicateAccountsForDeletion().blockingGet() assertEquals(1, scheduledCount) - assertTrue(staleCurrentFlag.scheduledForDeletion) - assertFalse(actuallyActive.scheduledForDeletion) - verify(usersRepository).updateUser(staleCurrentFlag) + assertTrue(duplicate.scheduledForDeletion) } }