diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt index 2d70e26e..9977e1d5 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt @@ -4,10 +4,12 @@ import android.content.Context import com.squareup.moshi.Moshi import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R +import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.dao.MessageAttachmentDao import io.github.wulkanowy.data.db.dao.MessagesDao import io.github.wulkanowy.data.db.entities.Message +import io.github.wulkanowy.data.db.entities.MessageWithAttachment import io.github.wulkanowy.data.db.entities.Recipient import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student @@ -48,22 +50,54 @@ class MessageRepository @Inject constructor( private val cacheKey = "message" @Suppress("UNUSED_PARAMETER") - fun getMessages(student: Student, semester: Semester, folder: MessageFolder, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource( + fun getMessages( + student: Student, semester: Semester, + folder: MessageFolder, forceRefresh: Boolean, notify: Boolean = false + ): Flow>> = networkBoundResource( mutex = saveFetchResultMutex, - shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student, folder)) }, + shouldFetch = { + it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed( + getRefreshKey(cacheKey, student, folder) + ) + }, query = { messagesDb.loadAll(student.id.toInt(), folder.id) }, - fetch = { sdk.init(student).getMessages(Folder.valueOf(folder.name), now().minusMonths(3), now()).mapToEntities(student) }, + fetch = { + sdk.init(student).getMessages(Folder.valueOf(folder.name), now().minusMonths(3), now()) + .mapToEntities(student) + }, saveFetchResult = { old, new -> messagesDb.deleteAll(old uniqueSubtract new) messagesDb.insertAll((new uniqueSubtract old).onEach { it.isNotified = !notify }) + messagesDb.updateAll(getMessagesWithReadByChange(old, new, !notify)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student, folder)) } ) - fun getMessage(student: Student, message: Message, markAsRead: Boolean = false) = networkBoundResource( + private fun getMessagesWithReadByChange( + old: List, new: List, + setNotified: Boolean + ): List { + val oldMeta = old.map { Triple(it, it.readBy, it.unreadBy) } + val newMeta = new.map { Triple(it, it.readBy, it.unreadBy) } + + val updatedItems = newMeta uniqueSubtract oldMeta + + return updatedItems.map { + val oldItem = old.find { item -> item.messageId == it.first.messageId } + it.first.apply { + id = oldItem?.id ?: 0 + isNotified = oldItem?.isNotified ?: setNotified + content = oldItem?.content.orEmpty() + } + } + } + + fun getMessage( + student: Student, message: Message, markAsRead: Boolean = false + ): Flow> = networkBoundResource( shouldFetch = { checkNotNull(it, { "This message no longer exist!" }) Timber.d("Message content in db empty: ${it.message.content.isEmpty()}") @@ -71,7 +105,12 @@ class MessageRepository @Inject constructor( }, query = { messagesDb.loadMessageWithAttachment(student.id.toInt(), message.messageId) }, fetch = { - sdk.init(student).getMessageDetails(it!!.message.messageId, message.folderId, markAsRead, message.realId).let { details -> + sdk.init(student).getMessageDetails( + messageId = it!!.message.messageId, + folderId = message.folderId, + read = markAsRead, + id = message.realId + ).let { details -> details.content to details.attachments.mapToEntities() } }, @@ -95,26 +134,34 @@ class MessageRepository @Inject constructor( return messagesDb.updateAll(messages) } - suspend fun sendMessage(student: Student, subject: String, content: String, recipients: List): SentMessage { - return sdk.init(student).sendMessage( - subject = subject, - content = content, - recipients = recipients.mapFromEntities() - ) - } + suspend fun sendMessage( + student: Student, subject: String, content: String, + recipients: List + ): SentMessage = sdk.init(student).sendMessage( + subject = subject, + content = content, + recipients = recipients.mapFromEntities() + ) suspend fun deleteMessage(student: Student, message: Message) { - val isDeleted = sdk.init(student).deleteMessages(listOf(message.messageId), message.folderId) + val isDeleted = sdk.init(student).deleteMessages( + messages = listOf(message.messageId), message.folderId + ) - if (message.folderId != MessageFolder.TRASHED.id) { - if (isDeleted) messagesDb.updateAll(listOf(message.copy(folderId = MessageFolder.TRASHED.id).apply { + if (message.folderId != MessageFolder.TRASHED.id && isDeleted) { + val deletedMessage = message.copy(folderId = MessageFolder.TRASHED.id).apply { id = message.id content = message.content - })) + } + messagesDb.updateAll(listOf(deletedMessage)) } else messagesDb.deleteAll(listOf(message)) } var draftMessage: MessageDraft? - get() = sharedPrefProvider.getString(context.getString(R.string.pref_key_message_send_draft))?.let { MessageDraftJsonAdapter(moshi).fromJson(it) } - set(value) = sharedPrefProvider.putString(context.getString(R.string.pref_key_message_send_draft), value?.let { MessageDraftJsonAdapter(moshi).toJson(it) }) + get() = sharedPrefProvider.getString(context.getString(R.string.pref_key_message_send_draft)) + ?.let { MessageDraftJsonAdapter(moshi).fromJson(it) } + set(value) = sharedPrefProvider.putString( + context.getString(R.string.pref_key_message_send_draft), + value?.let { MessageDraftJsonAdapter(moshi).toJson(it) } + ) } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt index 8b72479f..cadc4225 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt @@ -8,19 +8,25 @@ import io.github.wulkanowy.data.db.dao.MessageAttachmentDao import io.github.wulkanowy.data.db.dao.MessagesDao import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.MessageWithAttachment +import io.github.wulkanowy.data.enums.MessageFolder +import io.github.wulkanowy.getSemesterEntity import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.sdk.pojo.Folder import io.github.wulkanowy.sdk.pojo.MessageDetails +import io.github.wulkanowy.sdk.pojo.Sender import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.toFirstResult import io.mockk.MockKAnnotations import io.mockk.Runs +import io.mockk.checkEquals import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.SpyK import io.mockk.just +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking @@ -29,6 +35,7 @@ import org.junit.Before import org.junit.Test import java.net.UnknownHostException import java.time.LocalDateTime +import kotlin.test.assertTrue class MessageRepositoryTest { @@ -52,7 +59,9 @@ class MessageRepositoryTest { private val student = getStudentEntity() - private lateinit var messageRepository: MessageRepository + private val semester = getSemesterEntity() + + private lateinit var repository: MessageRepository @MockK private lateinit var moshi: Moshi @@ -62,15 +71,92 @@ class MessageRepositoryTest { MockKAnnotations.init(this) every { refreshHelper.isShouldBeRefreshed(any()) } returns false - messageRepository = MessageRepository(messageDb, messageAttachmentDao, sdk, context, refreshHelper, sharedPrefProvider, moshi) + repository = MessageRepository( + messagesDb = messageDb, + messageAttachmentDao = messageAttachmentDao, + sdk = sdk, + context = context, + refreshHelper = refreshHelper, + sharedPrefProvider = sharedPrefProvider, + moshi = moshi, + ) + } + + @Test + fun `get messages when read by values was changed on already read message`() = runBlocking { + every { messageDb.loadAll(any(), any()) } returns flow { + val dbMessage = getMessageEntity(3, "", false).apply { + unreadBy = 10 + readBy = 5 + isNotified = true + } + emit(listOf(dbMessage)) + } + coEvery { sdk.getMessages(Folder.RECEIVED, any(), any()) } returns listOf( + getMessageDto(messageId = 3, content = "", unread = false).copy( + unreadBy = 5, + readBy = 10, + ) + ) + coEvery { messageDb.deleteAll(any()) } just Runs + coEvery { messageDb.insertAll(any()) } returns listOf() + + repository.getMessages( + student = student, + semester = semester, + folder = MessageFolder.RECEIVED, + forceRefresh = true, + notify = true, // all new messages will be marked as not notified + ).toFirstResult().data.orEmpty() + + coVerify(exactly = 1) { messageDb.deleteAll(emptyList()) } + coVerify(exactly = 1) { messageDb.insertAll(emptyList()) } + coVerify(exactly = 1) { + messageDb.updateAll(withArg { + assertEquals(1, it.size) + assertEquals(5, it.single().unreadBy) + assertEquals(10, it.single().readBy) + }) + } + } + + @Test + fun `get messages when fetched completely new message without notify`() = runBlocking { + every { messageDb.loadAll(any(), any()) } returns flowOf(emptyList()) + coEvery { sdk.getMessages(Folder.RECEIVED, any(), any()) } returns listOf( + getMessageDto(messageId = 4, content = "Test", unread = true).copy( + unreadBy = 5, + readBy = 10, + ) + ) + coEvery { messageDb.deleteAll(any()) } just Runs + coEvery { messageDb.insertAll(any()) } returns listOf() + + repository.getMessages( + student = student, + semester = semester, + folder = MessageFolder.RECEIVED, + forceRefresh = true, + notify = false, + ).toFirstResult().data.orEmpty() + + coVerify(exactly = 1) { messageDb.deleteAll(withArg { checkEquals(emptyList()) }) } + coVerify { + messageDb.insertAll(withArg { + assertEquals(4, it.single().messageId) + assertTrue(it.single().isNotified) + }) + } } @Test(expected = NoSuchElementException::class) fun `throw error when message is not in the db`() { val testMessage = getMessageEntity(1, "", false) - coEvery { messageDb.loadMessageWithAttachment(1, 1) } throws NoSuchElementException("No message in database") + coEvery { + messageDb.loadMessageWithAttachment(1, 1) + } throws NoSuchElementException("No message in database") - runBlocking { messageRepository.getMessage(student, testMessage).toFirstResult() } + runBlocking { repository.getMessage(student, testMessage).toFirstResult() } } @Test @@ -78,9 +164,11 @@ class MessageRepositoryTest { val testMessage = getMessageEntity(123, "Test", false) val messageWithAttachment = MessageWithAttachment(testMessage, emptyList()) - coEvery { messageDb.loadMessageWithAttachment(1, testMessage.messageId) } returns flowOf(messageWithAttachment) + coEvery { messageDb.loadMessageWithAttachment(1, testMessage.messageId) } returns flowOf( + messageWithAttachment + ) - val res = runBlocking { messageRepository.getMessage(student, testMessage).toFirstResult() } + val res = runBlocking { repository.getMessage(student, testMessage).toFirstResult() } assertEquals(null, res.error) assertEquals(Status.SUCCESS, res.status) @@ -95,12 +183,24 @@ class MessageRepositoryTest { val mWa = MessageWithAttachment(testMessage, emptyList()) val mWaWithContent = MessageWithAttachment(testMessageWithContent, emptyList()) - coEvery { messageDb.loadMessageWithAttachment(1, testMessage.messageId) } returnsMany listOf(flowOf(mWa), flowOf(mWaWithContent)) - coEvery { sdk.getMessageDetails(testMessage.messageId, 1, false, testMessage.realId) } returns MessageDetails("Test", emptyList()) + coEvery { + messageDb.loadMessageWithAttachment( + 1, + testMessage.messageId + ) + } returnsMany listOf(flowOf(mWa), flowOf(mWaWithContent)) + coEvery { + sdk.getMessageDetails( + messageId = testMessage.messageId, + folderId = 1, + read = false, + id = testMessage.realId + ) + } returns MessageDetails("Test", emptyList()) coEvery { messageDb.updateAll(any()) } just Runs coEvery { messageAttachmentDao.insertAttachments(any()) } returns listOf(1) - val res = runBlocking { messageRepository.getMessage(student, testMessage).toFirstResult() } + val res = runBlocking { repository.getMessage(student, testMessage).toFirstResult() } assertEquals(null, res.error) assertEquals(Status.SUCCESS, res.status) @@ -112,18 +212,22 @@ class MessageRepositoryTest { fun `get message when content in db is empty and there is no internet connection`() { val testMessage = getMessageEntity(123, "", false) - coEvery { messageDb.loadMessageWithAttachment(1, testMessage.messageId) } throws UnknownHostException() + coEvery { + messageDb.loadMessageWithAttachment(1, testMessage.messageId) + } throws UnknownHostException() - runBlocking { messageRepository.getMessage(student, testMessage).toFirstResult() } + runBlocking { repository.getMessage(student, testMessage).toFirstResult() } } @Test(expected = UnknownHostException::class) fun `get message when content in db is empty, unread and there is no internet connection`() { val testMessage = getMessageEntity(123, "", true) - coEvery { messageDb.loadMessageWithAttachment(1, testMessage.messageId) } throws UnknownHostException() + coEvery { + messageDb.loadMessageWithAttachment(1, testMessage.messageId) + } throws UnknownHostException() - runBlocking { messageRepository.getMessage(student, testMessage).toList()[1] } + runBlocking { repository.getMessage(student, testMessage).toList()[1] } } private fun getMessageEntity( @@ -135,10 +239,10 @@ class MessageRepositoryTest { realId = 1, messageId = messageId, sender = "", - senderId = 1, - recipient = "", + senderId = 0, + recipient = "Wielu adresatów", subject = "", - date = LocalDateTime.now(), + date = LocalDateTime.MAX, folderId = 1, unread = unread, removed = false, @@ -148,4 +252,24 @@ class MessageRepositoryTest { unreadBy = 1 readBy = 1 } + + private fun getMessageDto( + messageId: Int, + content: String, + unread: Boolean, + ) = io.github.wulkanowy.sdk.pojo.Message( + id = 1, + messageId = messageId, + sender = Sender("", "", 0, 0, 0, ""), + recipients = listOf(), + subject = "", + content = content, + date = LocalDateTime.MAX, + folderId = 1, + unread = unread, + unreadBy = 0, + readBy = 0, + removed = false, + hasAttachments = false, + ) }