feat: implement Android database layer with Room
- Add SubscriptionEntity, FeedItemEntity, SearchHistoryEntity - Create SubscriptionDao, FeedItemDao, SearchHistoryDao with CRUD operations - Implement RssDatabase with FTS5 virtual table for full-text search - Add type converters for Date, String lists, and FeedItem lists - Implement cascade delete for feed items when subscription is removed - Add comprehensive unit tests for all DAOs - Add database integration tests for entity round-trips and FTS - Configure Room testing dependencies
This commit is contained in:
@@ -48,4 +48,10 @@ dependencies {
|
||||
testImplementation("com.squareup.moshi:moshi-kotlin-reflect:1.15.1")
|
||||
testImplementation("org.mockito:mockito-core:5.7.0")
|
||||
testImplementation("org.mockito:mockito-inline:5.2.0")
|
||||
testImplementation("androidx.room:room-testing:2.6.1")
|
||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
|
||||
testImplementation("androidx.arch.core:core-testing:2.2.0")
|
||||
testImplementation("androidx.test:core:1.5.0")
|
||||
testImplementation("androidx.test.ext:junit:1.1.5")
|
||||
testImplementation("androidx.test:runner:1.5.2")
|
||||
}
|
||||
|
||||
18
native-route/android/settings.gradle.kts
Normal file
18
native-route/android/settings.gradle.kts
Normal file
@@ -0,0 +1,18 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "rssuper-android"
|
||||
include(":android")
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.rssuper.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Database
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.rssuper.converters.DateConverter
|
||||
import com.rssuper.converters.FeedItemListConverter
|
||||
import com.rssuper.converters.StringListConverter
|
||||
import com.rssuper.database.daos.FeedItemDao
|
||||
import com.rssuper.database.daos.SearchHistoryDao
|
||||
import com.rssuper.database.daos.SubscriptionDao
|
||||
import com.rssuper.database.entities.FeedItemEntity
|
||||
import com.rssuper.database.entities.SearchHistoryEntity
|
||||
import com.rssuper.database.entities.SubscriptionEntity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Date
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
SubscriptionEntity::class,
|
||||
FeedItemEntity::class,
|
||||
SearchHistoryEntity::class
|
||||
],
|
||||
version = 1,
|
||||
exportSchema = true
|
||||
)
|
||||
@TypeConverters(DateConverter::class, StringListConverter::class, FeedItemListConverter::class)
|
||||
abstract class RssDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun subscriptionDao(): SubscriptionDao
|
||||
abstract fun feedItemDao(): FeedItemDao
|
||||
abstract fun searchHistoryDao(): SearchHistoryDao
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var INSTANCE: RssDatabase? = null
|
||||
|
||||
fun getDatabase(context: Context): RssDatabase {
|
||||
return INSTANCE ?: synchronized(this) {
|
||||
val instance = Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
RssDatabase::class.java,
|
||||
"rss_database"
|
||||
)
|
||||
.addCallback(DatabaseCallback())
|
||||
.build()
|
||||
INSTANCE = instance
|
||||
instance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DatabaseCallback : RoomDatabase.Callback() {
|
||||
override fun onCreate(db: SupportSQLiteDatabase) {
|
||||
super.onCreate(db)
|
||||
INSTANCE?.let { database ->
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
createFTSVirtualTable(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOpen(db: SupportSQLiteDatabase) {
|
||||
super.onOpen(db)
|
||||
createFTSVirtualTable(db)
|
||||
}
|
||||
|
||||
private fun createFTSVirtualTable(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("""
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS feed_items_fts USING fts5(
|
||||
title,
|
||||
description,
|
||||
content,
|
||||
author,
|
||||
content='feed_items',
|
||||
contentless_delete=true
|
||||
)
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.rssuper.database.daos
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import com.rssuper.database.entities.FeedItemEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.util.Date
|
||||
|
||||
@Dao
|
||||
interface FeedItemDao {
|
||||
@Query("SELECT * FROM feed_items WHERE subscriptionId = :subscriptionId ORDER BY published DESC")
|
||||
fun getItemsBySubscription(subscriptionId: String): Flow<List<FeedItemEntity>>
|
||||
|
||||
@Query("SELECT * FROM feed_items WHERE id = :id")
|
||||
suspend fun getItemById(id: String): FeedItemEntity?
|
||||
|
||||
@Query("SELECT * FROM feed_items WHERE subscriptionId IN (:subscriptionIds) ORDER BY published DESC")
|
||||
fun getItemsBySubscriptions(subscriptionIds: List<String>): Flow<List<FeedItemEntity>>
|
||||
|
||||
@Query("SELECT * FROM feed_items WHERE isRead = 0 ORDER BY published DESC")
|
||||
fun getUnreadItems(): Flow<List<FeedItemEntity>>
|
||||
|
||||
@Query("SELECT * FROM feed_items WHERE isStarred = 1 ORDER BY published DESC")
|
||||
fun getStarredItems(): Flow<List<FeedItemEntity>>
|
||||
|
||||
@Query("SELECT * FROM feed_items WHERE published > :date ORDER BY published DESC")
|
||||
fun getItemsAfterDate(date: Date): Flow<List<FeedItemEntity>>
|
||||
|
||||
@Query("SELECT * FROM feed_items WHERE subscriptionId = :subscriptionId AND published > :date ORDER BY published DESC")
|
||||
fun getSubscriptionItemsAfterDate(subscriptionId: String, date: Date): Flow<List<FeedItemEntity>>
|
||||
|
||||
@Query("SELECT COUNT(*) FROM feed_items WHERE subscriptionId = :subscriptionId AND isRead = 0")
|
||||
fun getUnreadCount(subscriptionId: String): Flow<Int>
|
||||
|
||||
@Query("SELECT COUNT(*) FROM feed_items WHERE isRead = 0")
|
||||
fun getTotalUnreadCount(): Flow<Int>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertItem(item: FeedItemEntity): Long
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertItems(items: List<FeedItemEntity>): List<Long>
|
||||
|
||||
@Update
|
||||
suspend fun updateItem(item: FeedItemEntity): Int
|
||||
|
||||
@Delete
|
||||
suspend fun deleteItem(item: FeedItemEntity): Int
|
||||
|
||||
@Query("DELETE FROM feed_items WHERE id = :id")
|
||||
suspend fun deleteItemById(id: String): Int
|
||||
|
||||
@Query("DELETE FROM feed_items WHERE subscriptionId = :subscriptionId")
|
||||
suspend fun deleteItemsBySubscription(subscriptionId: String): Int
|
||||
|
||||
@Query("UPDATE feed_items SET isRead = 1 WHERE id = :id")
|
||||
suspend fun markAsRead(id: String): Int
|
||||
|
||||
@Query("UPDATE feed_items SET isRead = 0 WHERE id = :id")
|
||||
suspend fun markAsUnread(id: String): Int
|
||||
|
||||
@Query("UPDATE feed_items SET isStarred = 1 WHERE id = :id")
|
||||
suspend fun markAsStarred(id: String): Int
|
||||
|
||||
@Query("UPDATE feed_items SET isStarred = 0 WHERE id = :id")
|
||||
suspend fun markAsUnstarred(id: String): Int
|
||||
|
||||
@Query("UPDATE feed_items SET isRead = 1 WHERE subscriptionId = :subscriptionId")
|
||||
suspend fun markAllAsRead(subscriptionId: String): Int
|
||||
|
||||
@Query("SELECT * FROM feed_items WHERE subscriptionId = :subscriptionId LIMIT :limit OFFSET :offset")
|
||||
suspend fun getItemsPaginated(subscriptionId: String, limit: Int, offset: Int): List<FeedItemEntity>
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.rssuper.database.daos
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import com.rssuper.database.entities.SearchHistoryEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface SearchHistoryDao {
|
||||
@Query("SELECT * FROM search_history ORDER BY timestamp DESC")
|
||||
fun getAllSearchHistory(): Flow<List<SearchHistoryEntity>>
|
||||
|
||||
@Query("SELECT * FROM search_history WHERE id = :id")
|
||||
suspend fun getSearchHistoryById(id: String): SearchHistoryEntity?
|
||||
|
||||
@Query("SELECT * FROM search_history WHERE query LIKE '%' || :query || '%' ORDER BY timestamp DESC")
|
||||
fun searchHistory(query: String): Flow<List<SearchHistoryEntity>>
|
||||
|
||||
@Query("SELECT * FROM search_history ORDER BY timestamp DESC LIMIT :limit")
|
||||
fun getRecentSearches(limit: Int): Flow<List<SearchHistoryEntity>>
|
||||
|
||||
@Query("SELECT COUNT(*) FROM search_history")
|
||||
fun getSearchHistoryCount(): Flow<Int>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertSearchHistory(search: SearchHistoryEntity): Long
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertSearchHistories(searches: List<SearchHistoryEntity>): List<Long>
|
||||
|
||||
@Update
|
||||
suspend fun updateSearchHistory(search: SearchHistoryEntity): Int
|
||||
|
||||
@Delete
|
||||
suspend fun deleteSearchHistory(search: SearchHistoryEntity): Int
|
||||
|
||||
@Query("DELETE FROM search_history WHERE id = :id")
|
||||
suspend fun deleteSearchHistoryById(id: String): Int
|
||||
|
||||
@Query("DELETE FROM search_history")
|
||||
suspend fun deleteAllSearchHistory(): Int
|
||||
|
||||
@Query("DELETE FROM search_history WHERE timestamp < :timestamp")
|
||||
suspend fun deleteSearchHistoryOlderThan(timestamp: Long): Int
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.rssuper.database.daos
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import com.rssuper.database.entities.SubscriptionEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.util.Date
|
||||
|
||||
@Dao
|
||||
interface SubscriptionDao {
|
||||
@Query("SELECT * FROM subscriptions ORDER BY title ASC")
|
||||
fun getAllSubscriptions(): Flow<List<SubscriptionEntity>>
|
||||
|
||||
@Query("SELECT * FROM subscriptions WHERE id = :id")
|
||||
suspend fun getSubscriptionById(id: String): SubscriptionEntity?
|
||||
|
||||
@Query("SELECT * FROM subscriptions WHERE url = :url")
|
||||
suspend fun getSubscriptionByUrl(url: String): SubscriptionEntity?
|
||||
|
||||
@Query("SELECT * FROM subscriptions WHERE enabled = 1 ORDER BY title ASC")
|
||||
fun getEnabledSubscriptions(): Flow<List<SubscriptionEntity>>
|
||||
|
||||
@Query("SELECT * FROM subscriptions WHERE category = :category ORDER BY title ASC")
|
||||
fun getSubscriptionsByCategory(category: String): Flow<List<SubscriptionEntity>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertSubscription(subscription: SubscriptionEntity): Long
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertSubscriptions(subscriptions: List<SubscriptionEntity>): List<Long>
|
||||
|
||||
@Update
|
||||
suspend fun updateSubscription(subscription: SubscriptionEntity): Int
|
||||
|
||||
@Delete
|
||||
suspend fun deleteSubscription(subscription: SubscriptionEntity): Int
|
||||
|
||||
@Query("DELETE FROM subscriptions WHERE id = :id")
|
||||
suspend fun deleteSubscriptionById(id: String): Int
|
||||
|
||||
@Query("SELECT COUNT(*) FROM subscriptions")
|
||||
fun getSubscriptionCount(): Flow<Int>
|
||||
|
||||
@Query("UPDATE subscriptions SET error = :error WHERE id = :id")
|
||||
suspend fun updateError(id: String, error: String?)
|
||||
|
||||
@Query("UPDATE subscriptions SET lastFetchedAt = :lastFetchedAt, error = NULL WHERE id = :id")
|
||||
suspend fun updateLastFetchedAt(id: String, lastFetchedAt: Date)
|
||||
|
||||
@Query("UPDATE subscriptions SET nextFetchAt = :nextFetchAt WHERE id = :id")
|
||||
suspend fun updateNextFetchAt(id: String, nextFetchAt: Date)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.rssuper.database.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import java.util.Date
|
||||
|
||||
@Entity(
|
||||
tableName = "feed_items",
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["subscriptionId"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
],
|
||||
indices = [
|
||||
Index(value = ["subscriptionId"]),
|
||||
Index(value = ["published"])
|
||||
]
|
||||
)
|
||||
data class FeedItemEntity(
|
||||
@PrimaryKey
|
||||
val id: String,
|
||||
|
||||
val subscriptionId: String,
|
||||
|
||||
val title: String,
|
||||
|
||||
val link: String? = null,
|
||||
|
||||
val description: String? = null,
|
||||
|
||||
val content: String? = null,
|
||||
|
||||
val author: String? = null,
|
||||
|
||||
val published: Date? = null,
|
||||
|
||||
val updated: Date? = null,
|
||||
|
||||
val categories: String? = null,
|
||||
|
||||
val enclosureUrl: String? = null,
|
||||
|
||||
val enclosureType: String? = null,
|
||||
|
||||
val enclosureLength: Long? = null,
|
||||
|
||||
val guid: String? = null,
|
||||
|
||||
val isRead: Boolean = false,
|
||||
|
||||
val isStarred: Boolean = false
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.rssuper.database.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import java.util.Date
|
||||
|
||||
@Entity(
|
||||
tableName = "search_history",
|
||||
indices = [Index(value = ["timestamp"])]
|
||||
)
|
||||
data class SearchHistoryEntity(
|
||||
@PrimaryKey
|
||||
val id: String,
|
||||
|
||||
val query: String,
|
||||
|
||||
val timestamp: Date
|
||||
)
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.rssuper.database.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import com.rssuper.models.HttpAuth
|
||||
import java.util.Date
|
||||
|
||||
@Entity(
|
||||
tableName = "subscriptions",
|
||||
indices = [Index(value = ["url"], unique = true)]
|
||||
)
|
||||
data class SubscriptionEntity(
|
||||
@PrimaryKey
|
||||
val id: String,
|
||||
|
||||
val url: String,
|
||||
|
||||
val title: String,
|
||||
|
||||
val category: String? = null,
|
||||
|
||||
val enabled: Boolean = true,
|
||||
|
||||
val fetchInterval: Long = 3600000,
|
||||
|
||||
val createdAt: Date,
|
||||
|
||||
val updatedAt: Date,
|
||||
|
||||
val lastFetchedAt: Date? = null,
|
||||
|
||||
val nextFetchAt: Date? = null,
|
||||
|
||||
val error: String? = null,
|
||||
|
||||
val httpAuthUsername: String? = null,
|
||||
|
||||
val httpAuthPassword: String? = null
|
||||
) {
|
||||
fun toHttpAuth(): HttpAuth? {
|
||||
return if (httpAuthUsername != null && httpAuthPassword != null) {
|
||||
HttpAuth(httpAuthUsername, httpAuthPassword)
|
||||
} else null
|
||||
}
|
||||
|
||||
fun fromHttpAuth(auth: HttpAuth?): SubscriptionEntity {
|
||||
return copy(
|
||||
httpAuthUsername = auth?.username,
|
||||
httpAuthPassword = auth?.password
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
package com.rssuper.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.rssuper.database.daos.FeedItemDao
|
||||
import com.rssuper.database.entities.FeedItemEntity
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.util.Date
|
||||
|
||||
class FeedItemDaoTest {
|
||||
|
||||
private lateinit var database: RssDatabase
|
||||
private lateinit var dao: FeedItemDao
|
||||
|
||||
@Before
|
||||
fun createDb() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
database = Room.inMemoryDatabaseBuilder(
|
||||
context,
|
||||
RssDatabase::class.java
|
||||
)
|
||||
.allowMainThreadQueries()
|
||||
.build()
|
||||
dao = database.feedItemDao()
|
||||
}
|
||||
|
||||
@After
|
||||
fun closeDb() {
|
||||
database.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun insertAndGetItem() = runTest {
|
||||
val item = createTestItem("1", "sub1")
|
||||
|
||||
dao.insertItem(item)
|
||||
|
||||
val result = dao.getItemById("1")
|
||||
assertNotNull(result)
|
||||
assertEquals("1", result?.id)
|
||||
assertEquals("Test Item", result?.title)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getItemsBySubscription() = runTest {
|
||||
val item1 = createTestItem("1", "sub1")
|
||||
val item2 = createTestItem("2", "sub1")
|
||||
val item3 = createTestItem("3", "sub2")
|
||||
|
||||
dao.insertItems(listOf(item1, item2, item3))
|
||||
|
||||
val result = dao.getItemsBySubscription("sub1").first()
|
||||
assertEquals(2, result.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getItemsBySubscriptions() = runTest {
|
||||
val item1 = createTestItem("1", "sub1")
|
||||
val item2 = createTestItem("2", "sub2")
|
||||
val item3 = createTestItem("3", "sub3")
|
||||
|
||||
dao.insertItems(listOf(item1, item2, item3))
|
||||
|
||||
val result = dao.getItemsBySubscriptions(listOf("sub1", "sub2")).first()
|
||||
assertEquals(2, result.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getUnreadItems() = runTest {
|
||||
val unread = createTestItem("1", "sub1", isRead = false)
|
||||
val read = createTestItem("2", "sub1", isRead = true)
|
||||
|
||||
dao.insertItems(listOf(unread, read))
|
||||
|
||||
val result = dao.getUnreadItems().first()
|
||||
assertEquals(1, result.size)
|
||||
assertEquals("1", result[0].id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getStarredItems() = runTest {
|
||||
val starred = createTestItem("1", "sub1", isStarred = true)
|
||||
val notStarred = createTestItem("2", "sub1", isStarred = false)
|
||||
|
||||
dao.insertItems(listOf(starred, notStarred))
|
||||
|
||||
val result = dao.getStarredItems().first()
|
||||
assertEquals(1, result.size)
|
||||
assertEquals("1", result[0].id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getItemsAfterDate() = runTest {
|
||||
val oldDate = Date(System.currentTimeMillis() - 86400000 * 2)
|
||||
val newDate = Date(System.currentTimeMillis() - 86400000)
|
||||
val today = Date()
|
||||
|
||||
val oldItem = createTestItem("1", "sub1", published = oldDate)
|
||||
val newItem = createTestItem("2", "sub1", published = newDate)
|
||||
val todayItem = createTestItem("3", "sub1", published = today)
|
||||
|
||||
dao.insertItems(listOf(oldItem, newItem, todayItem))
|
||||
|
||||
val result = dao.getItemsAfterDate(newDate).first()
|
||||
assertEquals(1, result.size)
|
||||
assertEquals("3", result[0].id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getUnreadCount() = runTest {
|
||||
val unread1 = createTestItem("1", "sub1", isRead = false)
|
||||
val unread2 = createTestItem("2", "sub1", isRead = false)
|
||||
val read = createTestItem("3", "sub1", isRead = true)
|
||||
|
||||
dao.insertItems(listOf(unread1, unread2, read))
|
||||
|
||||
val count = dao.getUnreadCount("sub1").first()
|
||||
assertEquals(2, count)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getTotalUnreadCount() = runTest {
|
||||
val unread1 = createTestItem("1", "sub1", isRead = false)
|
||||
val unread2 = createTestItem("2", "sub2", isRead = false)
|
||||
val read = createTestItem("3", "sub1", isRead = true)
|
||||
|
||||
dao.insertItems(listOf(unread1, unread2, read))
|
||||
|
||||
val count = dao.getTotalUnreadCount().first()
|
||||
assertEquals(2, count)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updateItem() = runTest {
|
||||
val item = createTestItem("1", "sub1")
|
||||
|
||||
dao.insertItem(item)
|
||||
|
||||
val updated = item.copy(title = "Updated Title")
|
||||
dao.updateItem(updated)
|
||||
|
||||
val result = dao.getItemById("1")
|
||||
assertEquals("Updated Title", result?.title)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteItem() = runTest {
|
||||
val item = createTestItem("1", "sub1")
|
||||
|
||||
dao.insertItem(item)
|
||||
dao.deleteItem(item)
|
||||
|
||||
val result = dao.getItemById("1")
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteItemById() = runTest {
|
||||
val item = createTestItem("1", "sub1")
|
||||
|
||||
dao.insertItem(item)
|
||||
dao.deleteItemById("1")
|
||||
|
||||
val result = dao.getItemById("1")
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteItemsBySubscription() = runTest {
|
||||
val item1 = createTestItem("1", "sub1")
|
||||
val item2 = createTestItem("2", "sub1")
|
||||
val item3 = createTestItem("3", "sub2")
|
||||
|
||||
dao.insertItems(listOf(item1, item2, item3))
|
||||
dao.deleteItemsBySubscription("sub1")
|
||||
|
||||
val sub1Items = dao.getItemsBySubscription("sub1").first()
|
||||
val sub2Items = dao.getItemsBySubscription("sub2").first()
|
||||
|
||||
assertEquals(0, sub1Items.size)
|
||||
assertEquals(1, sub2Items.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun markAsRead() = runTest {
|
||||
val item = createTestItem("1", "sub1", isRead = false)
|
||||
|
||||
dao.insertItem(item)
|
||||
dao.markAsRead("1")
|
||||
|
||||
val result = dao.getItemById("1")
|
||||
assertEquals(true, result?.isRead)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun markAsUnread() = runTest {
|
||||
val item = createTestItem("1", "sub1", isRead = true)
|
||||
|
||||
dao.insertItem(item)
|
||||
dao.markAsUnread("1")
|
||||
|
||||
val result = dao.getItemById("1")
|
||||
assertEquals(false, result?.isRead)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun markAsStarred() = runTest {
|
||||
val item = createTestItem("1", "sub1", isStarred = false)
|
||||
|
||||
dao.insertItem(item)
|
||||
dao.markAsStarred("1")
|
||||
|
||||
val result = dao.getItemById("1")
|
||||
assertEquals(true, result?.isStarred)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun markAsUnstarred() = runTest {
|
||||
val item = createTestItem("1", "sub1", isStarred = true)
|
||||
|
||||
dao.insertItem(item)
|
||||
dao.markAsUnstarred("1")
|
||||
|
||||
val result = dao.getItemById("1")
|
||||
assertEquals(false, result?.isStarred)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun markAllAsRead() = runTest {
|
||||
val item1 = createTestItem("1", "sub1", isRead = false)
|
||||
val item2 = createTestItem("2", "sub1", isRead = false)
|
||||
val item3 = createTestItem("3", "sub2", isRead = false)
|
||||
|
||||
dao.insertItems(listOf(item1, item2, item3))
|
||||
dao.markAllAsRead("sub1")
|
||||
|
||||
val sub1Items = dao.getItemsBySubscription("sub1").first()
|
||||
val sub2Items = dao.getItemsBySubscription("sub2").first()
|
||||
|
||||
assertEquals(true, sub1Items[0].isRead)
|
||||
assertEquals(true, sub1Items[1].isRead)
|
||||
assertEquals(false, sub2Items[0].isRead)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getItemsPaginated() = runTest {
|
||||
for (i in 1..10) {
|
||||
val item = createTestItem(i.toString(), "sub1")
|
||||
dao.insertItem(item)
|
||||
}
|
||||
|
||||
val firstPage = dao.getItemsPaginated("sub1", 5, 0)
|
||||
val secondPage = dao.getItemsPaginated("sub1", 5, 5)
|
||||
|
||||
assertEquals(5, firstPage.size)
|
||||
assertEquals(5, secondPage.size)
|
||||
}
|
||||
|
||||
private fun createTestItem(
|
||||
id: String,
|
||||
subscriptionId: String,
|
||||
title: String = "Test Item",
|
||||
isRead: Boolean = false,
|
||||
isStarred: Boolean = false,
|
||||
published: Date = Date()
|
||||
): FeedItemEntity {
|
||||
return FeedItemEntity(
|
||||
id = id,
|
||||
subscriptionId = subscriptionId,
|
||||
title = title,
|
||||
link = "https://example.com/$id",
|
||||
description = "Test description",
|
||||
content = "Test content",
|
||||
author = "Test Author",
|
||||
published = published,
|
||||
updated = published,
|
||||
categories = "Tech,News",
|
||||
enclosureUrl = null,
|
||||
enclosureType = null,
|
||||
enclosureLength = null,
|
||||
guid = "guid-$id",
|
||||
isRead = isRead,
|
||||
isStarred = isStarred
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package com.rssuper.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.rssuper.database.entities.FeedItemEntity
|
||||
import com.rssuper.database.entities.SearchHistoryEntity
|
||||
import com.rssuper.database.entities.SubscriptionEntity
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.util.Date
|
||||
import java.util.UUID
|
||||
|
||||
class RssDatabaseTest {
|
||||
|
||||
private lateinit var database: RssDatabase
|
||||
|
||||
@Before
|
||||
fun createDb() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
database = Room.inMemoryDatabaseBuilder(
|
||||
context,
|
||||
RssDatabase::class.java
|
||||
)
|
||||
.allowMainThreadQueries()
|
||||
.build()
|
||||
}
|
||||
|
||||
@After
|
||||
fun closeDb() {
|
||||
database.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun databaseConstruction() {
|
||||
assertNotNull(database.subscriptionDao())
|
||||
assertNotNull(database.feedItemDao())
|
||||
assertNotNull(database.searchHistoryDao())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ftsVirtualTableExists() {
|
||||
val cursor = database.run {
|
||||
openHelper.writableDatabase.rawQuery(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='feed_items_fts'",
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(true, cursor.moveToFirst())
|
||||
cursor.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun subscriptionEntityRoundTrip() = runTest {
|
||||
val now = Date()
|
||||
val subscription = SubscriptionEntity(
|
||||
id = UUID.randomUUID().toString(),
|
||||
url = "https://example.com/feed",
|
||||
title = "Test Feed",
|
||||
category = "Tech",
|
||||
enabled = true,
|
||||
fetchInterval = 3600000,
|
||||
createdAt = now,
|
||||
updatedAt = now,
|
||||
lastFetchedAt = null,
|
||||
nextFetchAt = null,
|
||||
error = null,
|
||||
httpAuthUsername = null,
|
||||
httpAuthPassword = null
|
||||
)
|
||||
|
||||
database.subscriptionDao().insertSubscription(subscription)
|
||||
|
||||
val result = database.subscriptionDao().getSubscriptionById(subscription.id)
|
||||
assertNotNull(result)
|
||||
assertEquals(subscription.id, result?.id)
|
||||
assertEquals(subscription.title, result?.title)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun feedItemEntityRoundTrip() = runTest {
|
||||
val now = Date()
|
||||
val subscription = SubscriptionEntity(
|
||||
id = "sub1",
|
||||
url = "https://example.com/feed",
|
||||
title = "Test Feed",
|
||||
category = "Tech",
|
||||
enabled = true,
|
||||
fetchInterval = 3600000,
|
||||
createdAt = now,
|
||||
updatedAt = now,
|
||||
lastFetchedAt = null,
|
||||
nextFetchAt = null,
|
||||
error = null,
|
||||
httpAuthUsername = null,
|
||||
httpAuthPassword = null
|
||||
)
|
||||
database.subscriptionDao().insertSubscription(subscription)
|
||||
|
||||
val item = FeedItemEntity(
|
||||
id = UUID.randomUUID().toString(),
|
||||
subscriptionId = "sub1",
|
||||
title = "Test Item",
|
||||
link = "https://example.com/item",
|
||||
description = "Test description",
|
||||
content = "Test content",
|
||||
author = "Test Author",
|
||||
published = now,
|
||||
updated = now,
|
||||
categories = "Tech",
|
||||
enclosureUrl = null,
|
||||
enclosureType = null,
|
||||
enclosureLength = null,
|
||||
guid = "guid-1",
|
||||
isRead = false,
|
||||
isStarred = false
|
||||
)
|
||||
|
||||
database.feedItemDao().insertItem(item)
|
||||
|
||||
val result = database.feedItemDao().getItemById(item.id)
|
||||
assertNotNull(result)
|
||||
assertEquals(item.id, result?.id)
|
||||
assertEquals(item.title, result?.title)
|
||||
assertEquals("sub1", result?.subscriptionId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun searchHistoryEntityRoundTrip() = runTest {
|
||||
val now = Date()
|
||||
val search = SearchHistoryEntity(
|
||||
id = UUID.randomUUID().toString(),
|
||||
query = "kotlin coroutines",
|
||||
timestamp = now
|
||||
)
|
||||
|
||||
database.searchHistoryDao().insertSearchHistory(search)
|
||||
|
||||
val result = database.searchHistoryDao().getSearchHistoryById(search.id)
|
||||
assertNotNull(result)
|
||||
assertEquals(search.id, result?.id)
|
||||
assertEquals(search.query, result?.query)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cascadeDeleteFeedItems() = runTest {
|
||||
val now = Date()
|
||||
val subscription = SubscriptionEntity(
|
||||
id = "sub1",
|
||||
url = "https://example.com/feed",
|
||||
title = "Test Feed",
|
||||
category = "Tech",
|
||||
enabled = true,
|
||||
fetchInterval = 3600000,
|
||||
createdAt = now,
|
||||
updatedAt = now,
|
||||
lastFetchedAt = null,
|
||||
nextFetchAt = null,
|
||||
error = null,
|
||||
httpAuthUsername = null,
|
||||
httpAuthPassword = null
|
||||
)
|
||||
database.subscriptionDao().insertSubscription(subscription)
|
||||
|
||||
val item = FeedItemEntity(
|
||||
id = "item1",
|
||||
subscriptionId = "sub1",
|
||||
title = "Test Item",
|
||||
link = "https://example.com/item",
|
||||
description = "Test description",
|
||||
content = "Test content",
|
||||
author = "Test Author",
|
||||
published = now,
|
||||
updated = now,
|
||||
categories = "Tech",
|
||||
enclosureUrl = null,
|
||||
enclosureType = null,
|
||||
enclosureLength = null,
|
||||
guid = "guid-1",
|
||||
isRead = false,
|
||||
isStarred = false
|
||||
)
|
||||
database.feedItemDao().insertItem(item)
|
||||
|
||||
database.subscriptionDao().deleteSubscription(subscription)
|
||||
|
||||
val items = database.feedItemDao().getItemsBySubscription("sub1").first()
|
||||
assertEquals(0, items.size)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package com.rssuper.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.rssuper.database.daos.SearchHistoryDao
|
||||
import com.rssuper.database.entities.SearchHistoryEntity
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.util.Date
|
||||
|
||||
class SearchHistoryDaoTest {
|
||||
|
||||
private lateinit var database: RssDatabase
|
||||
private lateinit var dao: SearchHistoryDao
|
||||
|
||||
@Before
|
||||
fun createDb() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
database = Room.inMemoryDatabaseBuilder(
|
||||
context,
|
||||
RssDatabase::class.java
|
||||
)
|
||||
.allowMainThreadQueries()
|
||||
.build()
|
||||
dao = database.searchHistoryDao()
|
||||
}
|
||||
|
||||
@After
|
||||
fun closeDb() {
|
||||
database.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun insertAndGetSearchHistory() = runTest {
|
||||
val search = createTestSearch("1", "kotlin")
|
||||
|
||||
dao.insertSearchHistory(search)
|
||||
|
||||
val result = dao.getSearchHistoryById("1")
|
||||
assertNotNull(result)
|
||||
assertEquals("1", result?.id)
|
||||
assertEquals("kotlin", result?.query)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getAllSearchHistory() = runTest {
|
||||
val search1 = createTestSearch("1", "kotlin")
|
||||
val search2 = createTestSearch("2", "android")
|
||||
val search3 = createTestSearch("3", "room database")
|
||||
|
||||
dao.insertSearchHistories(listOf(search1, search2, search3))
|
||||
|
||||
val result = dao.getAllSearchHistory().first()
|
||||
assertEquals(3, result.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun searchHistory() = runTest {
|
||||
val search1 = createTestSearch("1", "kotlin coroutines")
|
||||
val search2 = createTestSearch("2", "android kotlin")
|
||||
val search3 = createTestSearch("3", "java")
|
||||
|
||||
dao.insertSearchHistories(listOf(search1, search2, search3))
|
||||
|
||||
val result = dao.searchHistory("kotlin").first()
|
||||
assertEquals(2, result.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRecentSearches() = runTest {
|
||||
val search1 = createTestSearch("1", "query1", timestamp = Date(System.currentTimeMillis() - 300000))
|
||||
val search2 = createTestSearch("2", "query2", timestamp = Date(System.currentTimeMillis() - 200000))
|
||||
val search3 = createTestSearch("3", "query3", timestamp = Date(System.currentTimeMillis() - 100000))
|
||||
|
||||
dao.insertSearchHistories(listOf(search1, search2, search3))
|
||||
|
||||
val result = dao.getRecentSearches(2).first()
|
||||
assertEquals(2, result.size)
|
||||
assertEquals("3", result[0].id)
|
||||
assertEquals("2", result[1].id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getSearchHistoryCount() = runTest {
|
||||
val search1 = createTestSearch("1", "query1")
|
||||
val search2 = createTestSearch("2", "query2")
|
||||
val search3 = createTestSearch("3", "query3")
|
||||
|
||||
dao.insertSearchHistories(listOf(search1, search2, search3))
|
||||
|
||||
val count = dao.getSearchHistoryCount().first()
|
||||
assertEquals(3, count)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updateSearchHistory() = runTest {
|
||||
val search = createTestSearch("1", "old query")
|
||||
|
||||
dao.insertSearchHistory(search)
|
||||
|
||||
val updated = search.copy(query = "new query")
|
||||
dao.updateSearchHistory(updated)
|
||||
|
||||
val result = dao.getSearchHistoryById("1")
|
||||
assertEquals("new query", result?.query)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteSearchHistory() = runTest {
|
||||
val search = createTestSearch("1", "kotlin")
|
||||
|
||||
dao.insertSearchHistory(search)
|
||||
dao.deleteSearchHistory(search)
|
||||
|
||||
val result = dao.getSearchHistoryById("1")
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteSearchHistoryById() = runTest {
|
||||
val search = createTestSearch("1", "kotlin")
|
||||
|
||||
dao.insertSearchHistory(search)
|
||||
dao.deleteSearchHistoryById("1")
|
||||
|
||||
val result = dao.getSearchHistoryById("1")
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteAllSearchHistory() = runTest {
|
||||
val search1 = createTestSearch("1", "query1")
|
||||
val search2 = createTestSearch("2", "query2")
|
||||
|
||||
dao.insertSearchHistories(listOf(search1, search2))
|
||||
dao.deleteAllSearchHistory()
|
||||
|
||||
val result = dao.getAllSearchHistory().first()
|
||||
assertEquals(0, result.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteSearchHistoryOlderThan() = runTest {
|
||||
val oldSearch = createTestSearch("1", "old query", timestamp = Date(System.currentTimeMillis() - 86400000 * 2))
|
||||
val recentSearch = createTestSearch("2", "recent query", timestamp = Date(System.currentTimeMillis() - 86400000))
|
||||
|
||||
dao.insertSearchHistories(listOf(oldSearch, recentSearch))
|
||||
dao.deleteSearchHistoryOlderThan(System.currentTimeMillis() - 86400000)
|
||||
|
||||
val result = dao.getAllSearchHistory().first()
|
||||
assertEquals(1, result.size)
|
||||
assertEquals("2", result[0].id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun insertSearchHistoryWithConflict() = runTest {
|
||||
val search = createTestSearch("1", "kotlin")
|
||||
|
||||
dao.insertSearchHistory(search)
|
||||
|
||||
val duplicate = search.copy(query = "android")
|
||||
val result = dao.insertSearchHistory(duplicate)
|
||||
|
||||
assertEquals(-1L, result)
|
||||
|
||||
val dbSearch = dao.getSearchHistoryById("1")
|
||||
assertEquals("kotlin", dbSearch?.query)
|
||||
}
|
||||
|
||||
private fun createTestSearch(
|
||||
id: String,
|
||||
query: String,
|
||||
timestamp: Date = Date()
|
||||
): SearchHistoryEntity {
|
||||
return SearchHistoryEntity(
|
||||
id = id,
|
||||
query = query,
|
||||
timestamp = timestamp
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package com.rssuper.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.rssuper.database.daos.SubscriptionDao
|
||||
import com.rssuper.database.entities.SubscriptionEntity
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.util.Date
|
||||
|
||||
class SubscriptionDaoTest {
|
||||
|
||||
private lateinit var database: RssDatabase
|
||||
private lateinit var dao: SubscriptionDao
|
||||
|
||||
@Before
|
||||
fun createDb() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
database = Room.inMemoryDatabaseBuilder(
|
||||
context,
|
||||
RssDatabase::class.java
|
||||
)
|
||||
.allowMainThreadQueries()
|
||||
.build()
|
||||
dao = database.subscriptionDao()
|
||||
}
|
||||
|
||||
@After
|
||||
fun closeDb() {
|
||||
database.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun insertAndGetSubscription() = runTest {
|
||||
val subscription = createTestSubscription("1")
|
||||
|
||||
dao.insertSubscription(subscription)
|
||||
|
||||
val result = dao.getSubscriptionById("1")
|
||||
assertNotNull(result)
|
||||
assertEquals("1", result?.id)
|
||||
assertEquals("Test Feed", result?.title)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getSubscriptionByUrl() = runTest {
|
||||
val subscription = createTestSubscription("1", url = "https://example.com/feed")
|
||||
|
||||
dao.insertSubscription(subscription)
|
||||
|
||||
val result = dao.getSubscriptionByUrl("https://example.com/feed")
|
||||
assertNotNull(result)
|
||||
assertEquals("1", result?.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getAllSubscriptions() = runTest {
|
||||
val subscription1 = createTestSubscription("1")
|
||||
val subscription2 = createTestSubscription("2")
|
||||
|
||||
dao.insertSubscriptions(listOf(subscription1, subscription2))
|
||||
|
||||
val result = dao.getAllSubscriptions().first()
|
||||
assertEquals(2, result.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getEnabledSubscriptions() = runTest {
|
||||
val enabled = createTestSubscription("1", enabled = true)
|
||||
val disabled = createTestSubscription("2", enabled = false)
|
||||
|
||||
dao.insertSubscriptions(listOf(enabled, disabled))
|
||||
|
||||
val result = dao.getEnabledSubscriptions().first()
|
||||
assertEquals(1, result.size)
|
||||
assertEquals("1", result[0].id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updateSubscription() = runTest {
|
||||
val subscription = createTestSubscription("1")
|
||||
|
||||
dao.insertSubscription(subscription)
|
||||
|
||||
val updated = subscription.copy(title = "Updated Title")
|
||||
dao.updateSubscription(updated)
|
||||
|
||||
val result = dao.getSubscriptionById("1")
|
||||
assertEquals("Updated Title", result?.title)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteSubscription() = runTest {
|
||||
val subscription = createTestSubscription("1")
|
||||
|
||||
dao.insertSubscription(subscription)
|
||||
dao.deleteSubscription(subscription)
|
||||
|
||||
val result = dao.getSubscriptionById("1")
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteSubscriptionById() = runTest {
|
||||
val subscription = createTestSubscription("1")
|
||||
|
||||
dao.insertSubscription(subscription)
|
||||
dao.deleteSubscriptionById("1")
|
||||
|
||||
val result = dao.getSubscriptionById("1")
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getSubscriptionCount() = runTest {
|
||||
val subscription1 = createTestSubscription("1")
|
||||
val subscription2 = createTestSubscription("2")
|
||||
|
||||
dao.insertSubscriptions(listOf(subscription1, subscription2))
|
||||
|
||||
val count = dao.getSubscriptionCount().first()
|
||||
assertEquals(2, count)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updateError() = runTest {
|
||||
val subscription = createTestSubscription("1")
|
||||
|
||||
dao.insertSubscription(subscription)
|
||||
dao.updateError("1", "Feed not found")
|
||||
|
||||
val result = dao.getSubscriptionById("1")
|
||||
assertEquals("Feed not found", result?.error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updateLastFetchedAt() = runTest {
|
||||
val subscription = createTestSubscription("1")
|
||||
val now = Date()
|
||||
|
||||
dao.insertSubscription(subscription)
|
||||
dao.updateLastFetchedAt("1", now)
|
||||
|
||||
val result = dao.getSubscriptionById("1")
|
||||
assertEquals(now, result?.lastFetchedAt)
|
||||
assertNull(result?.error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updateNextFetchAt() = runTest {
|
||||
val subscription = createTestSubscription("1")
|
||||
val future = Date(System.currentTimeMillis() + 3600000)
|
||||
|
||||
dao.insertSubscription(subscription)
|
||||
dao.updateNextFetchAt("1", future)
|
||||
|
||||
val result = dao.getSubscriptionById("1")
|
||||
assertEquals(future, result?.nextFetchAt)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun insertSubscriptionWithConflict() = runTest {
|
||||
val subscription = createTestSubscription("1")
|
||||
|
||||
dao.insertSubscription(subscription)
|
||||
|
||||
val updated = subscription.copy(title = "Updated")
|
||||
dao.insertSubscription(updated)
|
||||
|
||||
val result = dao.getSubscriptionById("1")
|
||||
assertEquals("Updated", result?.title)
|
||||
}
|
||||
|
||||
private fun createTestSubscription(
|
||||
id: String,
|
||||
url: String = "https://example.com/feed/$id",
|
||||
title: String = "Test Feed",
|
||||
enabled: Boolean = true
|
||||
): SubscriptionEntity {
|
||||
val now = Date()
|
||||
return SubscriptionEntity(
|
||||
id = id,
|
||||
url = url,
|
||||
title = title,
|
||||
category = "Tech",
|
||||
enabled = enabled,
|
||||
fetchInterval = 3600000,
|
||||
createdAt = now,
|
||||
updatedAt = now,
|
||||
lastFetchedAt = null,
|
||||
nextFetchAt = null,
|
||||
error = null,
|
||||
httpAuthUsername = null,
|
||||
httpAuthPassword = null
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user