Skip to content

Commit

Permalink
Fix answer post got duplicated (#96)
Browse files Browse the repository at this point in the history
  • Loading branch information
FelberMartin authored Nov 20, 2024
1 parent ea12b02 commit 5e0e776
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 7 deletions.
8 changes: 4 additions & 4 deletions feature/metis/conversation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dependencies {
implementation(project(":core:device"))

implementation(project(":feature:metis:shared"))
testImplementation(project(":feature:metis-test"))

implementation(libs.androidx.paging.runtime)
implementation(libs.androidx.paging.compose)
Expand All @@ -33,10 +34,9 @@ dependencies {
implementation(libs.androidx.work.runtime.ktx)

implementation(libs.androidx.dataStore.preferences)

testImplementation(project(":feature:metis-test"))
implementation("androidx.paging:paging-common:3.2.1")

implementation(libs.androidx.paging.common)

testImplementation(libs.androidx.paging.testing)
testImplementation(libs.mockk.android)
testImplementation(libs.mockk.agent)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.se

import androidx.paging.PagingSource
import androidx.room.withTransaction
import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.MetisContext
import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.service.storage.MetisStorageService
import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.MetisDatabaseProvider
import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.MetisContext
import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.AnswerPost
import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.BasePost
import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.CourseWideContext
Expand Down Expand Up @@ -336,6 +336,19 @@ internal class MetisStorageServiceImpl(
val metisDao = databaseProvider.metisDao

databaseProvider.database.withTransaction {
val doesPostAnswerAlreadyExist = metisDao.isPostPresentInContext(
serverId = host,
serverPostId = post.id ?: return@withTransaction,
courseId = metisContext.courseId,
conversationId = metisContext.conversationId
)

// In rare cases, the websocket connection already inserted the post answer. In that case, we can delete the client side post.
if (doesPostAnswerAlreadyExist) {
metisDao.deletePostingWithClientSideId(clientPostId = clientSidePostId)
return@withTransaction
}

metisDao.upgradePost(
clientSidePostId = clientSidePostId,
serverSidePostId = post.id ?: return@withTransaction
Expand Down Expand Up @@ -502,7 +515,6 @@ internal class MetisStorageServiceImpl(
answerServerIds = sp.answers.orEmpty().mapNotNull { it.id }
)
}

for (ap in sp.answers.orEmpty()) {
val answerPostId = ap.id ?: continue

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.service.storage.impl

import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingSource
import androidx.paging.testing.asSnapshot
import androidx.test.platform.app.InstrumentationRegistry
import de.tum.informatics.www1.artemis.native_app.core.common.test.UnitTest
import de.tum.informatics.www1.artemis.native_app.core.model.account.User
import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.MetisContext
import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.AnswerPost
import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.StandalonePost
import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.UserRole
import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.OneToOneChat
import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.db.pojo.AnswerPostPojo
import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.db.pojo.PostPojo
import de.tum.informatics.www1.artemis.native_app.feature.metistest.MetisDatabaseProviderMock
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Clock
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.experimental.categories.Category
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@Category(UnitTest::class)
@RunWith(RobolectricTestRunner::class)
class MetisStorageServiceImplUpgradeLocalAnswerPostTest {

private val databaseProviderMock = MetisDatabaseProviderMock(InstrumentationRegistry.getInstrumentation().context)
private val sut = MetisStorageServiceImpl(databaseProviderMock)

private val host = "host"

private val author = User(id = 20, name = "AuthorName")
private val parentClientPostId = "parent-client-id-0"
private val answerClientPostId = "answer-client-id-0"

private val course: MetisContext.Course = MetisContext.Course(courseId = 1)
private val conversation = OneToOneChat(id = 2)
private val metisContext = MetisContext.Conversation(course.courseId, conversation.id)

private val localAnswerPojo = AnswerPostPojo(
parentPostId = parentClientPostId,
postId = answerClientPostId,
resolvesPost = false,
basePostingCache = AnswerPostPojo.BasePostingCache(
serverPostId = 0,
authorId = author.id,
creationDate = Clock.System.now(),
updatedDate = null,
content = "Answer post content 0",
authorRole = UserRole.USER,
authorName = author.name!!
),
reactions = emptyList(),
serverPostIdCache = AnswerPostPojo.ServerPostIdCache(
serverPostId = null // Only local answer post, no server id
)
)

private val basePostPojo = PostPojo(
clientPostId = parentClientPostId,
serverPostId = 0,
content = "Base post content",
resolved = false,
updatedDate = null,
creationDate = Clock.System.now(),
authorId = author.id,
title = null,
authorName = author.name!!,
authorRole = UserRole.USER,
courseWideContext = null,
tags = emptyList(),
answers = emptyList(),
reactions = emptyList()
)

private val basePost = StandalonePost(basePostPojo, conversation)
private val localAnswer = AnswerPost(localAnswerPojo, basePost)

private lateinit var basePostUpdated: StandalonePost
private lateinit var answerUpdated: AnswerPost

@Test
fun testInsertClientSidePost() = runTest {
// GIVEN: A base post
sut.insertOrUpdatePosts(
host = host,
metisContext = metisContext,
posts = listOf(basePost),
)

// WHEN: Inserting a client side answer post
sut.insertClientSidePost(
host = host,
metisContext = metisContext,
post = localAnswer,
clientSidePostId = answerClientPostId
)

// THEN: Both the base post and the answer post are stored
assertStoredContentIsTheSame()
}

@Test
fun testUpgradeClientSideAnswerPost() = runTest {
// GIVEN: A post with a new only local answer post
setupPostWithLocalAnswer()

// WHEN: insertOrUpdatePosts is called before upgradeClientSideAnswerPost.
updateAnswerPostWithServerId()

// Called by the WebSocket
sut.updatePost(
host = host,
metisContext = metisContext,
post = basePostUpdated
)

// Called by SendConversationPostWorker
sut.upgradeClientSideAnswerPost(
host = host,
metisContext = metisContext,
clientSidePostId = answerClientPostId,
post = answerUpdated
)

// THEN: Content stays the same and the upgrade is successful
assertStoredContentIsTheSame()
assertUpgradeSuccessful()
}

@Test
fun testUpgradeClientSideAnswerPost2() = runTest {
// GIVEN: A post with a new only local answer post
setupPostWithLocalAnswer()

// WHEN: upgradeClientSideAnswerPost is called before updatePost.
updateAnswerPostWithServerId()

// Called by SendConversationPostWorker
sut.upgradeClientSideAnswerPost(
host = host,
metisContext = metisContext,
clientSidePostId = answerClientPostId,
post = answerUpdated
)

// Called by the WebSocket
sut.updatePost(
host = host,
metisContext = metisContext,
post = basePostUpdated
)

// THEN: Content stays the same and the upgrade is successful
assertStoredContentIsTheSame()
assertUpgradeSuccessful()
}

private suspend fun setupPostWithLocalAnswer() {
sut.insertOrUpdatePosts(
host = host,
metisContext = metisContext,
posts = listOf(basePost)
)
sut.insertClientSidePost(
host = host,
metisContext = metisContext,
clientSidePostId = answerClientPostId,
post = localAnswer
)
}


private fun updateAnswerPostWithServerId() {
val answerPojoUpdated = localAnswerPojo.copy(serverPostIdCache = localAnswerPojo.serverPostIdCache.copy(serverPostId = 1))
basePostUpdated = StandalonePost(basePostPojo, conversation)
answerUpdated = AnswerPost(answerPojoUpdated, basePostUpdated)
basePostUpdated = basePostUpdated.copy(answers = listOf(answerUpdated))
}

private suspend fun assertStoredContentIsTheSame() {
val posts = getStoredPosts()
assertEquals(1, posts.size)
assertEquals(basePostPojo.content, posts.first().content)
assertEquals(1, posts.first().answers.size)
assertEquals(localAnswerPojo.content, posts.first().answers.first().content)
}

private suspend fun assertUpgradeSuccessful() {
val posts = getStoredPosts()
assertEquals(answerUpdated.serverPostId, posts.first().answers.first().serverPostId)
}

private suspend fun getStoredPosts() = sut.getStoredPosts(
serverId = host,
metisContext = metisContext
).loadAsList()

private suspend fun <T : Any> PagingSource<Int, T>.loadAsList(): List<T> {
return Pager(PagingConfig(pageSize = 10), pagingSourceFactory = { this }).flow.asSnapshot {
scrollTo(50)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ interface MetisDao {
postingType: BasePostingEntity.PostingType = BasePostingEntity.PostingType.STANDALONE
)

@Query("delete from metis_post_context where client_post_id = :clientPostId")
@Query("delete from postings where id = :clientPostId")
suspend fun deletePostingWithClientSideId(
clientPostId: String
)
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "li
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" }
androidx-paging-runtime = { group = "androidx.paging", name = "paging-runtime", version.ref = "androidxPaging" }
androidx-paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "androidxPagingCompose" }
androidx-paging-common = { group = "androidx.paging", name = "paging-common", version.ref = "androidxPaging" }
androidx-paging-testing = { group = "androidx.paging", name = "paging-testing", version.ref = "androidxPaging" }
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
androidx-room-paging = { group = "androidx.room", name = "room-paging", version.ref = "room" }
Expand Down

0 comments on commit 5e0e776

Please sign in to comment.