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("com.squareup.moshi:moshi-kotlin-reflect:1.15.1")
|
||||||
testImplementation("org.mockito:mockito-core:5.7.0")
|
testImplementation("org.mockito:mockito-core:5.7.0")
|
||||||
testImplementation("org.mockito:mockito-inline:5.2.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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
5
native-route/build.gradle.kts
Normal file
5
native-route/build.gradle.kts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application") version "8.2.0" apply false
|
||||||
|
id("com.android.library") version "8.2.0" apply false
|
||||||
|
id("org.jetbrains.kotlin.android") version "1.9.20" apply false
|
||||||
|
}
|
||||||
18
native-route/settings.gradle.kts
Normal file
18
native-route/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 = "native-route"
|
||||||
|
include(":android")
|
||||||
Reference in New Issue
Block a user