diff --git a/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/db/AppDatabase.kt b/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/db/AppDatabase.kt index 5d0ab9c1a..8961e79ee 100644 --- a/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/db/AppDatabase.kt +++ b/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/db/AppDatabase.kt @@ -29,7 +29,7 @@ import de.tum.informatics.www1.artemis.native_app.feature.push.communication_not CommunicationMessageEntity::class ], exportSchema = true, - version = 10, + version = 11, ) @TypeConverters(RoomTypeConverters::class) abstract class AppDatabase : RoomDatabase() { diff --git a/feature/metis-test/build.gradle.kts b/feature/metis-test/build.gradle.kts index a7c2bfe47..c81a769cf 100644 --- a/feature/metis-test/build.gradle.kts +++ b/feature/metis-test/build.gradle.kts @@ -22,4 +22,5 @@ dependencies { api(libs.koin.test.junit4) api(libs.robolectric) api(libs.koin.android.test) + api(libs.androidx.paging.testing) } diff --git a/feature/metis-test/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metistest/paging_source_util.kt b/feature/metis-test/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metistest/paging_source_util.kt new file mode 100644 index 000000000..880d8ee6d --- /dev/null +++ b/feature/metis-test/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metistest/paging_source_util.kt @@ -0,0 +1,15 @@ +package de.tum.informatics.www1.artemis.native_app.feature.metistest + +import android.annotation.SuppressLint +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingSource +import androidx.paging.testing.asSnapshot + + +@SuppressLint("VisibleForTests") +suspend fun PagingSource.loadAsList(): List { + return Pager(PagingConfig(pageSize = 10), pagingSourceFactory = { this }).flow.asSnapshot { + scrollTo(50) + } +} \ No newline at end of file diff --git a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/post/PostContextBottomSheet.kt b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/post/PostContextBottomSheet.kt index 077fa129c..e10dab188 100644 --- a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/post/PostContextBottomSheet.kt +++ b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/post/PostContextBottomSheet.kt @@ -84,7 +84,8 @@ internal fun PostContextBottomSheet( Column( modifier = Modifier .fillMaxWidth() - .padding(start = Spacings.ScreenHorizontalSpacing, end = Spacings.ScreenHorizontalSpacing, bottom = 40.dp) + .padding(horizontal = Spacings.ScreenHorizontalSpacing) + .padding(bottom = 40.dp) ) { postActions.onClickReaction?.let { onClickReaction -> EmojiReactionBar( diff --git a/feature/metis/shared/build.gradle.kts b/feature/metis/shared/build.gradle.kts index 93f98bfb5..c2c0a841b 100644 --- a/feature/metis/shared/build.gradle.kts +++ b/feature/metis/shared/build.gradle.kts @@ -11,4 +11,5 @@ android { } dependencies { implementation(project(":core:device")) + testImplementation(project(":feature:metis-test")) } diff --git a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/entities/PostReactionEntity.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/entities/PostReactionEntity.kt index ebf795960..b92d11335 100644 --- a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/entities/PostReactionEntity.kt +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/entities/PostReactionEntity.kt @@ -8,6 +8,19 @@ import androidx.room.Index @Entity( tableName = "reactions", primaryKeys = ["post_id", "emoji", "author_id", "server_id"], + foreignKeys = [ + ForeignKey( + entity = BasePostingEntity::class, + parentColumns = ["id"], + childColumns = ["post_id"], + onDelete = ForeignKey.CASCADE + ), + ForeignKey( + entity = MetisUserEntity::class, + parentColumns = ["server_id", "id"], + childColumns = ["server_id", "author_id"] + ) + ], indices = [Index("server_id", "author_id", name = "server_id_author_id_index")] ) data class PostReactionEntity( diff --git a/feature/metis/shared/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/MetisDaoTest.kt b/feature/metis/shared/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/MetisDaoTest.kt new file mode 100644 index 000000000..216ec3504 --- /dev/null +++ b/feature/metis/shared/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/MetisDaoTest.kt @@ -0,0 +1,168 @@ +package de.tum.informatics.www1.artemis.native_app.feature.metis.shared.db + +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.feature.metis.shared.content.dto.UserRole +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.db.entities.BasePostingEntity +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.db.entities.MetisPostContextEntity +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.db.entities.MetisUserEntity +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.db.entities.PostReactionEntity +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.db.entities.StandalonePostingEntity +import de.tum.informatics.www1.artemis.native_app.feature.metistest.MetisDatabaseProviderMock +import de.tum.informatics.www1.artemis.native_app.feature.metistest.MetisTestDatabase +import de.tum.informatics.www1.artemis.native_app.feature.metistest.loadAsList +import kotlinx.coroutines.runBlocking +import kotlinx.datetime.Clock +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.experimental.categories.Category +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +@Category(UnitTest::class) +class MetisDaoTest { + + private val serverId = "host" + private val courseId = 1L + private val conversationId = 1L + private val clientPostId = "clientPostId" + + private val user = MetisUserEntity( + serverId = serverId, + id = 4, + displayName = "User4" + ) + private val basePost = BasePostingEntity( + postId = clientPostId, + serverId = serverId, + postingType = BasePostingEntity.PostingType.STANDALONE, + authorId = user.id, + creationDate = Clock.System.now(), + updatedDate = Clock.System.now(), + content = "post content", + authorRole = UserRole.USER, + ) + private val metisContext = MetisPostContextEntity( + serverId = serverId, + courseId = courseId, + conversationId = conversationId, + serverPostId = 1, + clientPostId = clientPostId, + postingType = BasePostingEntity.PostingType.STANDALONE, + ) + private val post = StandalonePostingEntity( + postId = clientPostId, + title = null, + context = null, + displayPriority = BasePostingEntity.DisplayPriority.NONE, + resolved = false, + liveCreated = false + ) + private val reaction = PostReactionEntity( + postId = clientPostId, + authorId = user.id, + serverId = serverId, + emojiId = "emojiId", + id = 1, + ) + + private lateinit var database: MetisTestDatabase + private lateinit var metisDao: MetisDao + + @Before + fun setup() { + val databaseProviderMock = MetisDatabaseProviderMock(InstrumentationRegistry.getInstrumentation().context) + database = databaseProviderMock.database + metisDao = database.metisDao() + } + + @After + fun teardown() { + database.close() + } + + @Test + fun testAddPost() = runBlocking { + // GIVEN: A inserted post + insertStandalonePost() + + // WHEN: Querying the post + val storedPosts = metisDao.queryCoursePosts( + courseId = courseId, + conversationId = conversationId, + serverId = serverId, + ).loadAsList() + + // THEN: Return post with correct userId and clientPostId + assertEquals(1, storedPosts.size) + val storedPost = storedPosts[0] + assertEquals(user.id, storedPost.authorId) + assertEquals(clientPostId, storedPost.clientPostId) + } + + @Test + fun testDeletePost() = runBlocking { + // GIVEN: A inserted post + insertStandalonePost() + + // WHEN: Deleting the post + metisDao.deletePostingWithClientSideId(clientPostId) + + // THEN: The post is deleted in both tables + database.query("SELECT * FROM standalone_postings", args = null).use { + assertEquals(0, it.count) + } + database.query("SELECT * FROM postings", args = null).use { + assertEquals(0, it.count) + } + + // AND: The post context is deleted + database.query("SELECT * FROM metis_post_context", args = null).use { + assertEquals(0, it.count) + } + } + + @Test + fun testAddPostWithReaction() = runBlocking { + // GIVEN: A inserted post with a reaction + insertStandalonePost() + metisDao.insertReactions(listOf(reaction)) + + // WHEN: Querying the post + val storedPost = metisDao.queryCoursePosts( + courseId = courseId, + conversationId = conversationId, + serverId = serverId, + ).loadAsList()[0] + + // THEN: The reaction is stored + assertEquals(1, storedPost.reactions.size) + assertEquals(reaction.emojiId, storedPost.reactions[0].emojiId) + } + + @Test + fun testDeletePostWithReaction() = runBlocking { + // GIVEN: A inserted post with a reaction + insertStandalonePost() + metisDao.insertReactions(listOf(reaction)) + + // WHEN: Deleting the post + metisDao.deletePostingWithClientSideId(clientPostId) + + // THEN: The reaction is deleted + database.query("SELECT * FROM reactions", args = null).use { + assertEquals(0, it.count) + } + } + + + private suspend fun insertStandalonePost() { + metisDao.insertUser(user) + metisDao.insertBasePost(basePost) + metisDao.insertPost(post) + metisDao.insertPostMetisContext(metisContext) + } +} \ No newline at end of file