diff --git a/native-route/android/build.gradle.kts b/native-route/android/build.gradle.kts new file mode 100644 index 0000000..ac637cc --- /dev/null +++ b/native-route/android/build.gradle.kts @@ -0,0 +1,51 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("kotlin-parcelize") + id("kotlin-kapt") +} + +android { + namespace = "com.rssuper" + compileSdk = 34 + + defaultConfig { + minSdk = 24 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } +} + +dependencies { + // AndroidX + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.room:room-runtime:2.6.1") + implementation("androidx.room:room-ktx:2.6.1") + kapt("androidx.room:room-compiler:2.6.1") + + // Moshi for JSON serialization + implementation("com.squareup.moshi:moshi-kotlin:1.15.1") + kapt("com.squareup.moshi:moshi-kotlin-codegen:1.15.1") + implementation("com.squareup.moshi:moshi-kotlin-reflect:1.15.1") + + // Testing + testImplementation("junit:junit:4.13.2") + testImplementation("com.squareup.moshi:moshi-kotlin: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-inline:5.2.0") +} diff --git a/native-route/android/src/main/java/com/rssuper/converters/DateConverter.kt b/native-route/android/src/main/java/com/rssuper/converters/DateConverter.kt new file mode 100644 index 0000000..4e31266 --- /dev/null +++ b/native-route/android/src/main/java/com/rssuper/converters/DateConverter.kt @@ -0,0 +1,16 @@ +package com.rssuper.converters + +import androidx.room.TypeConverter +import java.util.Date + +class DateConverter { + @TypeConverter + fun fromTimestamp(value: Long?): Date? { + return value?.let { Date(it) } + } + + @TypeConverter + fun dateToTimestamp(date: Date?): Long? { + return date?.time + } +} diff --git a/native-route/android/src/main/java/com/rssuper/converters/FeedItemListConverter.kt b/native-route/android/src/main/java/com/rssuper/converters/FeedItemListConverter.kt new file mode 100644 index 0000000..49342b9 --- /dev/null +++ b/native-route/android/src/main/java/com/rssuper/converters/FeedItemListConverter.kt @@ -0,0 +1,23 @@ +package com.rssuper.converters + +import androidx.room.TypeConverter +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import com.rssuper.models.FeedItem + +class FeedItemListConverter { + private val moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + private val adapter = moshi.adapter(List::class.java) + + @TypeConverter + fun fromFeedItemList(value: List?): String? { + return value?.let { adapter.toJson(it) } + } + + @TypeConverter + fun toFeedItemList(value: String?): List? { + return value?.let { adapter.fromJson(it) } + } +} diff --git a/native-route/android/src/main/java/com/rssuper/converters/StringListConverter.kt b/native-route/android/src/main/java/com/rssuper/converters/StringListConverter.kt new file mode 100644 index 0000000..c295fb8 --- /dev/null +++ b/native-route/android/src/main/java/com/rssuper/converters/StringListConverter.kt @@ -0,0 +1,15 @@ +package com.rssuper.converters + +import androidx.room.TypeConverter + +class StringListConverter { + @TypeConverter + fun fromStringList(value: List?): String? { + return value?.joinToString(",") + } + + @TypeConverter + fun toStringList(value: String?): List? { + return value?.split(",")?.filter { it.isNotEmpty() } + } +} diff --git a/native-route/android/src/main/java/com/rssuper/models/Feed.kt b/native-route/android/src/main/java/com/rssuper/models/Feed.kt new file mode 100644 index 0000000..e5f87bc --- /dev/null +++ b/native-route/android/src/main/java/com/rssuper/models/Feed.kt @@ -0,0 +1,60 @@ +package com.rssuper.models + +import android.os.Parcelable +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.TypeConverters +import com.rssuper.converters.DateConverter +import com.rssuper.converters.FeedItemListConverter +import kotlinx.parcelize.Parcelize +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.util.Date + +@JsonClass(generateAdapter = true) +@Parcelize +@TypeConverters(DateConverter::class, FeedItemListConverter::class) +@Entity(tableName = "feeds") +data class Feed( + @PrimaryKey + val id: String, + + @Json(name = "title") + val title: String, + + @Json(name = "link") + val link: String? = null, + + @Json(name = "description") + val description: String? = null, + + @Json(name = "subtitle") + val subtitle: String? = null, + + @Json(name = "language") + val language: String? = null, + + @Json(name = "lastBuildDate") + val lastBuildDate: Date? = null, + + @Json(name = "updated") + val updated: Date? = null, + + @Json(name = "generator") + val generator: String? = null, + + @Json(name = "ttl") + val ttl: Int? = null, + + @Json(name = "items") + val items: List = emptyList(), + + @Json(name = "rawUrl") + val rawUrl: String, + + @Json(name = "lastFetchedAt") + val lastFetchedAt: Date? = null, + + @Json(name = "nextFetchAt") + val nextFetchAt: Date? = null +) : Parcelable diff --git a/native-route/android/src/main/java/com/rssuper/models/FeedItem.kt b/native-route/android/src/main/java/com/rssuper/models/FeedItem.kt new file mode 100644 index 0000000..890631c --- /dev/null +++ b/native-route/android/src/main/java/com/rssuper/models/FeedItem.kt @@ -0,0 +1,67 @@ +package com.rssuper.models + +import android.os.Parcelable +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.TypeConverters +import com.rssuper.converters.DateConverter +import com.rssuper.converters.StringListConverter +import kotlinx.parcelize.Parcelize +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.util.Date + +@JsonClass(generateAdapter = true) +@Parcelize +@TypeConverters(DateConverter::class, StringListConverter::class) +@Entity(tableName = "feed_items") +data class FeedItem( + @PrimaryKey + val id: String, + + @Json(name = "title") + val title: String, + + @Json(name = "link") + val link: String? = null, + + @Json(name = "description") + val description: String? = null, + + @Json(name = "content") + val content: String? = null, + + @Json(name = "author") + val author: String? = null, + + @Json(name = "published") + val published: Date? = null, + + @Json(name = "updated") + val updated: Date? = null, + + @Json(name = "categories") + val categories: List? = null, + + @Json(name = "enclosure") + val enclosure: Enclosure? = null, + + @Json(name = "guid") + val guid: String? = null, + + @Json(name = "subscriptionTitle") + val subscriptionTitle: String? = null +) : Parcelable + +@JsonClass(generateAdapter = true) +@Parcelize +data class Enclosure( + @Json(name = "url") + val url: String, + + @Json(name = "type") + val type: String, + + @Json(name = "length") + val length: Long? = null +) : Parcelable diff --git a/native-route/android/src/main/java/com/rssuper/models/FeedSubscription.kt b/native-route/android/src/main/java/com/rssuper/models/FeedSubscription.kt new file mode 100644 index 0000000..89e14e8 --- /dev/null +++ b/native-route/android/src/main/java/com/rssuper/models/FeedSubscription.kt @@ -0,0 +1,63 @@ +package com.rssuper.models + +import android.os.Parcelable +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.TypeConverters +import com.rssuper.converters.DateConverter +import kotlinx.parcelize.Parcelize +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.util.Date + +@JsonClass(generateAdapter = true) +@Parcelize +@TypeConverters(DateConverter::class) +@Entity(tableName = "feed_subscriptions") +data class FeedSubscription( + @PrimaryKey + val id: String, + + @Json(name = "url") + val url: String, + + @Json(name = "title") + val title: String, + + @Json(name = "category") + val category: String? = null, + + @Json(name = "enabled") + val enabled: Boolean = true, + + @Json(name = "fetchInterval") + val fetchInterval: Long, + + @Json(name = "createdAt") + val createdAt: Date, + + @Json(name = "updatedAt") + val updatedAt: Date, + + @Json(name = "lastFetchedAt") + val lastFetchedAt: Date? = null, + + @Json(name = "nextFetchAt") + val nextFetchAt: Date? = null, + + @Json(name = "error") + val error: String? = null, + + @Json(name = "httpAuth") + val httpAuth: HttpAuth? = null +) : Parcelable + +@JsonClass(generateAdapter = true) +@Parcelize +data class HttpAuth( + @Json(name = "username") + val username: String, + + @Json(name = "password") + val password: String +) : Parcelable diff --git a/native-route/android/src/main/java/com/rssuper/models/NotificationPreferences.kt b/native-route/android/src/main/java/com/rssuper/models/NotificationPreferences.kt new file mode 100644 index 0000000..359514a --- /dev/null +++ b/native-route/android/src/main/java/com/rssuper/models/NotificationPreferences.kt @@ -0,0 +1,34 @@ +package com.rssuper.models + +import android.os.Parcelable +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.parcelize.Parcelize +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +@Parcelize +@Entity(tableName = "notification_preferences") +data class NotificationPreferences( + @PrimaryKey + val id: String = "default", + + @Json(name = "newArticles") + val newArticles: Boolean = true, + + @Json(name = "episodeReleases") + val episodeReleases: Boolean = true, + + @Json(name = "customAlerts") + val customAlerts: Boolean = false, + + @Json(name = "badgeCount") + val badgeCount: Boolean = true, + + @Json(name = "sound") + val sound: Boolean = true, + + @Json(name = "vibration") + val vibration: Boolean = true +) : Parcelable diff --git a/native-route/android/src/main/java/com/rssuper/models/ReadingPreferences.kt b/native-route/android/src/main/java/com/rssuper/models/ReadingPreferences.kt new file mode 100644 index 0000000..66c9df4 --- /dev/null +++ b/native-route/android/src/main/java/com/rssuper/models/ReadingPreferences.kt @@ -0,0 +1,59 @@ +package com.rssuper.models + +import android.os.Parcelable +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.parcelize.Parcelize +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +@Parcelize +@Entity(tableName = "reading_preferences") +data class ReadingPreferences( + @PrimaryKey + val id: String = "default", + + @Json(name = "fontSize") + val fontSize: FontSize = FontSize.MEDIUM, + + @Json(name = "lineHeight") + val lineHeight: LineHeight = LineHeight.NORMAL, + + @Json(name = "showTableOfContents") + val showTableOfContents: Boolean = false, + + @Json(name = "showReadingTime") + val showReadingTime: Boolean = true, + + @Json(name = "showAuthor") + val showAuthor: Boolean = true, + + @Json(name = "showDate") + val showDate: Boolean = true +) : Parcelable + +sealed class FontSize(val value: String) { + @Json(name = "small") + data object SMALL : FontSize("small") + + @Json(name = "medium") + data object MEDIUM : FontSize("medium") + + @Json(name = "large") + data object LARGE : FontSize("large") + + @Json(name = "xlarge") + data object XLARGE : FontSize("xlarge") +} + +sealed class LineHeight(val value: String) { + @Json(name = "normal") + data object NORMAL : LineHeight("normal") + + @Json(name = "relaxed") + data object RELAXED : LineHeight("relaxed") + + @Json(name = "loose") + data object LOOSE : LineHeight("loose") +} diff --git a/native-route/android/src/main/java/com/rssuper/models/SearchFilters.kt b/native-route/android/src/main/java/com/rssuper/models/SearchFilters.kt new file mode 100644 index 0000000..3e4f79f --- /dev/null +++ b/native-route/android/src/main/java/com/rssuper/models/SearchFilters.kt @@ -0,0 +1,73 @@ +package com.rssuper.models + +import android.os.Parcelable +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.TypeConverters +import com.rssuper.converters.DateConverter +import com.rssuper.converters.StringListConverter +import kotlinx.parcelize.Parcelize +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.util.Date + +@JsonClass(generateAdapter = true) +@Parcelize +@TypeConverters(DateConverter::class, StringListConverter::class) +@Entity(tableName = "search_filters") +data class SearchFilters( + @PrimaryKey + val id: String = "default", + + @Json(name = "dateFrom") + val dateFrom: Date? = null, + + @Json(name = "dateTo") + val dateTo: Date? = null, + + @Json(name = "feedIds") + val feedIds: List? = null, + + @Json(name = "authors") + val authors: List? = null, + + @Json(name = "contentType") + val contentType: ContentType? = null, + + @Json(name = "sortOption") + val sortOption: SearchSortOption = SearchSortOption.RELEVANCE +) : Parcelable + +sealed class ContentType(val value: String) { + @Json(name = "article") + data object ARTICLE : ContentType("article") + + @Json(name = "audio") + data object AUDIO : ContentType("audio") + + @Json(name = "video") + data object VIDEO : ContentType("video") +} + +sealed class SearchSortOption(val value: String) { + @Json(name = "relevance") + data object RELEVANCE : SearchSortOption("relevance") + + @Json(name = "date_desc") + data object DATE_DESC : SearchSortOption("date_desc") + + @Json(name = "date_asc") + data object DATE_ASC : SearchSortOption("date_asc") + + @Json(name = "title_asc") + data object TITLE_ASC : SearchSortOption("title_asc") + + @Json(name = "title_desc") + data object TITLE_DESC : SearchSortOption("title_desc") + + @Json(name = "feed_asc") + data object FEED_ASC : SearchSortOption("feed_asc") + + @Json(name = "feed_desc") + data object FEED_DESC : SearchSortOption("feed_desc") +} diff --git a/native-route/android/src/main/java/com/rssuper/models/SearchResult.kt b/native-route/android/src/main/java/com/rssuper/models/SearchResult.kt new file mode 100644 index 0000000..3869ea4 --- /dev/null +++ b/native-route/android/src/main/java/com/rssuper/models/SearchResult.kt @@ -0,0 +1,49 @@ +package com.rssuper.models + +import android.os.Parcelable +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.TypeConverters +import com.rssuper.converters.DateConverter +import kotlinx.parcelize.Parcelize +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.util.Date + +@JsonClass(generateAdapter = true) +@Parcelize +@TypeConverters(DateConverter::class) +@Entity(tableName = "search_results") +data class SearchResult( + @PrimaryKey + val id: String, + + @Json(name = "type") + val type: SearchResultType, + + @Json(name = "title") + val title: String, + + @Json(name = "snippet") + val snippet: String? = null, + + @Json(name = "link") + val link: String? = null, + + @Json(name = "feedTitle") + val feedTitle: String? = null, + + @Json(name = "published") + val published: Date? = null, + + @Json(name = "score") + val score: Double? = null +) : Parcelable + +enum class SearchResultType { + @Json(name = "article") + ARTICLE, + + @Json(name = "feed") + FEED +} diff --git a/native-route/android/src/test/java/com/rssuper/models/FeedItemTest.kt b/native-route/android/src/test/java/com/rssuper/models/FeedItemTest.kt new file mode 100644 index 0000000..19fd06b --- /dev/null +++ b/native-route/android/src/test/java/com/rssuper/models/FeedItemTest.kt @@ -0,0 +1,134 @@ +package com.rssuper.models + +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +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 FeedItemTest { + + private lateinit var moshi: Moshi + private lateinit var adapter: com.squareup.moshi.JsonAdapter + + @Before + fun setup() { + moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + adapter = moshi.adapter(FeedItem::class.java) + } + + @Test + fun testSerialization() { + val feedItem = FeedItem( + id = "item-1", + title = "Test Article", + link = "https://example.com/article", + description = "Short description", + content = "Full content here", + author = "John Doe", + published = Date(1672531200000), + categories = listOf("Tech", "News"), + guid = "guid-123", + subscriptionTitle = "Tech News" + ) + + val json = adapter.toJson(feedItem) + assertNotNull(json) + } + + @Test + fun testDeserialization() { + val json = """{ + "id": "item-1", + "title": "Test Article", + "link": "https://example.com/article", + "description": "Short description", + "author": "John Doe", + "published": 1672531200000, + "categories": ["Tech", "News"], + "guid": "guid-123", + "subscriptionTitle": "Tech News" + }""" + + val feedItem = adapter.fromJson(json) + assertNotNull(feedItem) + assertEquals("item-1", feedItem?.id) + assertEquals("Test Article", feedItem?.title) + assertEquals("John Doe", feedItem?.author) + } + + @Test + fun testOptionalFieldsNull() { + val json = """{ + "id": "item-1", + "title": "Test Article" + }""" + + val feedItem = adapter.fromJson(json) + assertNotNull(feedItem) + assertNull(feedItem?.link) + assertNull(feedItem?.description) + assertNull(feedItem?.author) + } + + @Test + fun testEnclosureSerialization() { + val feedItem = FeedItem( + id = "item-1", + title = "Podcast Episode", + enclosure = Enclosure( + url = "https://example.com/episode.mp3", + type = "audio/mpeg", + length = 12345678 + ) + ) + + val json = adapter.toJson(feedItem) + assertNotNull(json) + } + + @Test + fun testCopy() { + val original = FeedItem( + id = "item-1", + title = "Original Title", + author = "Original Author" + ) + + val modified = original.copy(title = "Modified Title") + + assertEquals("item-1", modified.id) + assertEquals("Modified Title", modified.title) + assertEquals("Original Author", modified.author) + } + + @Test + fun testEqualsAndHashCode() { + val item1 = FeedItem(id = "item-1", title = "Test") + val item2 = FeedItem(id = "item-1", title = "Test") + val item3 = FeedItem(id = "item-2", title = "Test") + + assertEquals(item1, item2) + assertEquals(item1.hashCode(), item2.hashCode()) + assert(item1 != item3) + } + + @Test + fun testToString() { + val feedItem = FeedItem( + id = "item-1", + title = "Test Article", + author = "John Doe" + ) + + val toString = feedItem.toString() + assertNotNull(toString) + assert(toString.contains("id=item-1")) + assert(toString.contains("title=Test Article")) + } +} diff --git a/native-route/android/src/test/java/com/rssuper/models/FeedSubscriptionTest.kt b/native-route/android/src/test/java/com/rssuper/models/FeedSubscriptionTest.kt new file mode 100644 index 0000000..e9e20ef --- /dev/null +++ b/native-route/android/src/test/java/com/rssuper/models/FeedSubscriptionTest.kt @@ -0,0 +1,199 @@ +package com.rssuper.models + +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +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 FeedSubscriptionTest { + + private lateinit var moshi: Moshi + private lateinit var adapter: com.squareup.moshi.JsonAdapter + + @Before + fun setup() { + moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + adapter = moshi.adapter(FeedSubscription::class.java) + } + + @Test + fun testSerialization() { + val subscription = FeedSubscription( + id = "sub-1", + url = "https://example.com/feed.xml", + title = "Tech News", + category = "Technology", + enabled = true, + fetchInterval = 60, + createdAt = Date(1672531200000), + updatedAt = Date(1672617600000) + ) + + val json = adapter.toJson(subscription) + assertNotNull(json) + } + + @Test + fun testDeserialization() { + val json = """{ + "id": "sub-1", + "url": "https://example.com/feed.xml", + "title": "Tech News", + "category": "Technology", + "enabled": true, + "fetchInterval": 60, + "createdAt": 1672531200000, + "updatedAt": 1672617600000 + }""" + + val subscription = adapter.fromJson(json) + assertNotNull(subscription) + assertEquals("sub-1", subscription?.id) + assertEquals("https://example.com/feed.xml", subscription?.url) + assertEquals("Tech News", subscription?.title) + assertEquals("Technology", subscription?.category) + assertEquals(true, subscription?.enabled) + assertEquals(60, subscription?.fetchInterval) + } + + @Test + fun testOptionalFieldsNull() { + val json = """{ + "id": "sub-1", + "url": "https://example.com/feed.xml", + "title": "Tech News", + "enabled": true, + "fetchInterval": 60, + "createdAt": 1672531200000, + "updatedAt": 1672617600000 + }""" + + val subscription = adapter.fromJson(json) + assertNotNull(subscription) + assertNull(subscription?.category) + assertNull(subscription?.error) + assertNull(subscription?.httpAuth) + } + + @Test + fun testHttpAuthSerialization() { + val subscription = FeedSubscription( + id = "sub-1", + url = "https://example.com/feed.xml", + title = "Private Feed", + enabled = true, + fetchInterval = 60, + createdAt = Date(1672531200000), + updatedAt = Date(1672617600000), + httpAuth = HttpAuth( + username = "user123", + password = "pass456" + ) + ) + + val json = adapter.toJson(subscription) + assertNotNull(json) + } + + @Test + fun testHttpAuthDeserialization() { + val json = """{ + "id": "sub-1", + "url": "https://example.com/feed.xml", + "title": "Private Feed", + "enabled": true, + "fetchInterval": 60, + "createdAt": 1672531200000, + "updatedAt": 1672617600000, + "httpAuth": { + "username": "user123", + "password": "pass456" + } + }""" + + val subscription = adapter.fromJson(json) + assertNotNull(subscription) + assertNotNull(subscription?.httpAuth) + assertEquals("user123", subscription?.httpAuth?.username) + assertEquals("pass456", subscription?.httpAuth?.password) + } + + @Test + fun testCopy() { + val original = FeedSubscription( + id = "sub-1", + url = "https://example.com/feed.xml", + title = "Original Title", + enabled = true, + fetchInterval = 60, + createdAt = Date(1672531200000), + updatedAt = Date(1672617600000) + ) + + val modified = original.copy(title = "Modified Title", enabled = false) + + assertEquals("sub-1", modified.id) + assertEquals("Modified Title", modified.title) + assertEquals(false, modified.enabled) + assertEquals(60, modified.fetchInterval) + } + + @Test + fun testEqualsAndHashCode() { + val sub1 = FeedSubscription( + id = "sub-1", + url = "https://example.com", + title = "Test", + enabled = true, + fetchInterval = 60, + createdAt = Date(1672531200000), + updatedAt = Date(1672617600000) + ) + val sub2 = FeedSubscription( + id = "sub-1", + url = "https://example.com", + title = "Test", + enabled = true, + fetchInterval = 60, + createdAt = Date(1672531200000), + updatedAt = Date(1672617600000) + ) + val sub3 = FeedSubscription( + id = "sub-2", + url = "https://example.com", + title = "Test", + enabled = true, + fetchInterval = 60, + createdAt = Date(1672531200000), + updatedAt = Date(1672617600000) + ) + + assertEquals(sub1, sub2) + assertEquals(sub1.hashCode(), sub2.hashCode()) + assert(sub1 != sub3) + } + + @Test + fun testToString() { + val subscription = FeedSubscription( + id = "sub-1", + url = "https://example.com/feed.xml", + title = "Tech News", + enabled = true, + fetchInterval = 60, + createdAt = Date(1672531200000), + updatedAt = Date(1672617600000) + ) + + val toString = subscription.toString() + assertNotNull(toString) + assert(toString.contains("id=sub-1")) + assert(toString.contains("title=Tech News")) + } +} diff --git a/native-route/android/src/test/java/com/rssuper/models/FeedTest.kt b/native-route/android/src/test/java/com/rssuper/models/FeedTest.kt new file mode 100644 index 0000000..f5b9426 --- /dev/null +++ b/native-route/android/src/test/java/com/rssuper/models/FeedTest.kt @@ -0,0 +1,139 @@ +package com.rssuper.models + +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.util.Date + +class FeedTest { + + private lateinit var moshi: Moshi + private lateinit var adapter: com.squareup.moshi.JsonAdapter + + @Before + fun setup() { + moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + adapter = moshi.adapter(Feed::class.java) + } + + @Test + fun testSerialization() { + val feed = Feed( + id = "feed-1", + title = "Tech News", + link = "https://example.com", + description = "Technology news feed", + subtitle = "Daily tech updates", + language = "en", + rawUrl = "https://example.com/feed.xml", + ttl = 60, + items = listOf( + FeedItem(id = "item-1", title = "Article 1"), + FeedItem(id = "item-2", title = "Article 2") + ) + ) + + val json = adapter.toJson(feed) + assertNotNull(json) + } + + @Test + fun testDeserialization() { + val json = """{ + "id": "feed-1", + "title": "Tech News", + "link": "https://example.com", + "description": "Technology news feed", + "subtitle": "Daily tech updates", + "language": "en", + "rawUrl": "https://example.com/feed.xml", + "ttl": 60, + "items": [ + {"id": "item-1", "title": "Article 1"}, + {"id": "item-2", "title": "Article 2"} + ] + }""" + + val feed = adapter.fromJson(json) + assertNotNull(feed) + assertEquals("feed-1", feed?.id) + assertEquals("Tech News", feed?.title) + assertEquals(2, feed?.items?.size) + } + + @Test + fun testOptionalFieldsNull() { + val json = """{ + "id": "feed-1", + "title": "Tech News", + "rawUrl": "https://example.com/feed.xml" + }""" + + val feed = adapter.fromJson(json) + assertNotNull(feed) + assertNull(feed?.link) + assertNull(feed?.description) + assertNull(feed?.language) + } + + @Test + fun testEmptyItemsList() { + val json = """{ + "id": "feed-1", + "title": "Tech News", + "rawUrl": "https://example.com/feed.xml", + "items": [] + }""" + + val feed = adapter.fromJson(json) + assertNotNull(feed) + assertTrue(feed?.items?.isEmpty() == true) + } + + @Test + fun testCopy() { + val original = Feed( + id = "feed-1", + title = "Original Title", + rawUrl = "https://example.com/feed.xml" + ) + + val modified = original.copy(title = "Modified Title") + + assertEquals("feed-1", modified.id) + assertEquals("Modified Title", modified.title) + assertEquals("https://example.com/feed.xml", modified.rawUrl) + } + + @Test + fun testEqualsAndHashCode() { + val feed1 = Feed(id = "feed-1", title = "Test", rawUrl = "https://example.com") + val feed2 = Feed(id = "feed-1", title = "Test", rawUrl = "https://example.com") + val feed3 = Feed(id = "feed-2", title = "Test", rawUrl = "https://example.com") + + assertEquals(feed1, feed2) + assertEquals(feed1.hashCode(), feed2.hashCode()) + assert(feed1 != feed3) + } + + @Test + fun testToString() { + val feed = Feed( + id = "feed-1", + title = "Tech News", + rawUrl = "https://example.com/feed.xml" + ) + + val toString = feed.toString() + assertNotNull(toString) + assert(toString.contains("id=feed-1")) + assert(toString.contains("title=Tech News")) + } +} diff --git a/native-route/android/src/test/java/com/rssuper/models/NotificationPreferencesTest.kt b/native-route/android/src/test/java/com/rssuper/models/NotificationPreferencesTest.kt new file mode 100644 index 0000000..da33037 --- /dev/null +++ b/native-route/android/src/test/java/com/rssuper/models/NotificationPreferencesTest.kt @@ -0,0 +1,108 @@ +package com.rssuper.models + +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.Test + +class NotificationPreferencesTest { + + private lateinit var moshi: Moshi + private lateinit var adapter: com.squareup.moshi.JsonAdapter + + @Before + fun setup() { + moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + adapter = moshi.adapter(NotificationPreferences::class.java) + } + + @Test + fun testSerialization() { + val preferences = NotificationPreferences( + newArticles = true, + episodeReleases = true, + customAlerts = false, + badgeCount = true, + sound = true, + vibration = false + ) + + val json = adapter.toJson(preferences) + assertNotNull(json) + } + + @Test + fun testDeserialization() { + val json = """{ + "newArticles": true, + "episodeReleases": true, + "customAlerts": false, + "badgeCount": true, + "sound": true, + "vibration": false + }""" + + val preferences = adapter.fromJson(json) + assertNotNull(preferences) + assertEquals(true, preferences?.newArticles) + assertEquals(true, preferences?.episodeReleases) + assertEquals(false, preferences?.customAlerts) + assertEquals(true, preferences?.badgeCount) + assertEquals(true, preferences?.sound) + assertEquals(false, preferences?.vibration) + } + + @Test + fun testDefaultValues() { + val preferences = NotificationPreferences() + + assertEquals(true, preferences.newArticles) + assertEquals(true, preferences.episodeReleases) + assertEquals(false, preferences.customAlerts) + assertEquals(true, preferences.badgeCount) + assertEquals(true, preferences.sound) + assertEquals(true, preferences.vibration) + } + + @Test + fun testCopy() { + val original = NotificationPreferences( + newArticles = true, + sound = true + ) + + val modified = original.copy(newArticles = false, sound = false) + + assertEquals(false, modified.newArticles) + assertEquals(false, modified.sound) + assertEquals(true, modified.episodeReleases) + } + + @Test + fun testEqualsAndHashCode() { + val pref1 = NotificationPreferences(newArticles = true, sound = true) + val pref2 = NotificationPreferences(newArticles = true, sound = true) + val pref3 = NotificationPreferences(newArticles = false, sound = true) + + assertEquals(pref1, pref2) + assertEquals(pref1.hashCode(), pref2.hashCode()) + assert(pref1 != pref3) + } + + @Test + fun testToString() { + val preferences = NotificationPreferences( + newArticles = true, + sound = true + ) + + val toString = preferences.toString() + assertNotNull(toString) + assert(toString.contains("newArticles")) + assert(toString.contains("sound")) + } +} diff --git a/native-route/android/src/test/java/com/rssuper/models/ReadingPreferencesTest.kt b/native-route/android/src/test/java/com/rssuper/models/ReadingPreferencesTest.kt new file mode 100644 index 0000000..c3e1209 --- /dev/null +++ b/native-route/android/src/test/java/com/rssuper/models/ReadingPreferencesTest.kt @@ -0,0 +1,141 @@ +package com.rssuper.models + +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.Test + +class ReadingPreferencesTest { + + private lateinit var moshi: Moshi + private lateinit var adapter: com.squareup.moshi.JsonAdapter + + @Before + fun setup() { + moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + adapter = moshi.adapter(ReadingPreferences::class.java) + } + + @Test + fun testSerialization() { + val preferences = ReadingPreferences( + fontSize = FontSize.LARGE, + lineHeight = LineHeight.RELAXED, + showTableOfContents = true, + showReadingTime = true, + showAuthor = false, + showDate = true + ) + + val json = adapter.toJson(preferences) + assertNotNull(json) + } + + @Test + fun testDeserialization() { + val json = """{ + "fontSize": "large", + "lineHeight": "relaxed", + "showTableOfContents": true, + "showReadingTime": true, + "showAuthor": false, + "showDate": true + }""" + + val preferences = adapter.fromJson(json) + assertNotNull(preferences) + assertEquals(FontSize.LARGE, preferences?.fontSize) + assertEquals(LineHeight.RELAXED, preferences?.lineHeight) + assertEquals(true, preferences?.showTableOfContents) + assertEquals(true, preferences?.showReadingTime) + assertEquals(false, preferences?.showAuthor) + assertEquals(true, preferences?.showDate) + } + + @Test + fun testFontSizeOptions() { + val fontSizes = listOf( + "small" to FontSize.SMALL, + "medium" to FontSize.MEDIUM, + "large" to FontSize.LARGE, + "xlarge" to FontSize.XLARGE + ) + + for ((jsonValue, expectedEnum) in fontSizes) { + val json = """{"fontSize": "$jsonValue"}""" + val preferences = adapter.fromJson(json) + assertNotNull("Failed for fontSize: $jsonValue", preferences) + assertEquals("Failed for fontSize: $jsonValue", expectedEnum, preferences?.fontSize) + } + } + + @Test + fun testLineHeightOptions() { + val lineHeights = listOf( + "normal" to LineHeight.NORMAL, + "relaxed" to LineHeight.RELAXED, + "loose" to LineHeight.LOOSE + ) + + for ((jsonValue, expectedEnum) in lineHeights) { + val json = """{"lineHeight": "$jsonValue"}""" + val preferences = adapter.fromJson(json) + assertNotNull("Failed for lineHeight: $jsonValue", preferences) + assertEquals("Failed for lineHeight: $jsonValue", expectedEnum, preferences?.lineHeight) + } + } + + @Test + fun testDefaultValues() { + val preferences = ReadingPreferences() + + assertEquals(FontSize.MEDIUM, preferences.fontSize) + assertEquals(LineHeight.NORMAL, preferences.lineHeight) + assertEquals(false, preferences.showTableOfContents) + assertEquals(true, preferences.showReadingTime) + assertEquals(true, preferences.showAuthor) + assertEquals(true, preferences.showDate) + } + + @Test + fun testCopy() { + val original = ReadingPreferences( + fontSize = FontSize.MEDIUM, + showReadingTime = true + ) + + val modified = original.copy(fontSize = FontSize.XLARGE, showReadingTime = false) + + assertEquals(FontSize.XLARGE, modified.fontSize) + assertEquals(false, modified.showReadingTime) + assertEquals(LineHeight.NORMAL, modified.lineHeight) + } + + @Test + fun testEqualsAndHashCode() { + val pref1 = ReadingPreferences(fontSize = FontSize.MEDIUM, showReadingTime = true) + val pref2 = ReadingPreferences(fontSize = FontSize.MEDIUM, showReadingTime = true) + val pref3 = ReadingPreferences(fontSize = FontSize.LARGE, showReadingTime = true) + + assertEquals(pref1, pref2) + assertEquals(pref1.hashCode(), pref2.hashCode()) + assert(pref1 != pref3) + } + + @Test + fun testToString() { + val preferences = ReadingPreferences( + fontSize = FontSize.LARGE, + showReadingTime = true + ) + + val toString = preferences.toString() + assertNotNull(toString) + assert(toString.contains("fontSize")) + assert(toString.contains("showReadingTime")) + } +} diff --git a/native-route/android/src/test/java/com/rssuper/models/SearchFiltersTest.kt b/native-route/android/src/test/java/com/rssuper/models/SearchFiltersTest.kt new file mode 100644 index 0000000..38f3f55 --- /dev/null +++ b/native-route/android/src/test/java/com/rssuper/models/SearchFiltersTest.kt @@ -0,0 +1,156 @@ +package com.rssuper.models + +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +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 SearchFiltersTest { + + private lateinit var moshi: Moshi + private lateinit var adapter: com.squareup.moshi.JsonAdapter + + @Before + fun setup() { + moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + adapter = moshi.adapter(SearchFilters::class.java) + } + + @Test + fun testSerialization() { + val filters = SearchFilters( + dateFrom = Date(1672531200000), + dateTo = Date(1672617600000), + feedIds = listOf("feed-1", "feed-2"), + authors = listOf("John Doe", "Jane Smith"), + contentType = ContentType.ARTICLE, + sortOption = SearchSortOption.DATE_DESC + ) + + val json = adapter.toJson(filters) + assertNotNull(json) + } + + @Test + fun testDeserialization() { + val json = """{ + "dateFrom": 1672531200000, + "dateTo": 1672617600000, + "feedIds": ["feed-1", "feed-2"], + "authors": ["John Doe", "Jane Smith"], + "contentType": "article", + "sortOption": "date_desc" + }""" + + val filters = adapter.fromJson(json) + assertNotNull(filters) + assertNotNull(filters?.dateFrom) + assertNotNull(filters?.dateTo) + assertEquals(2, filters?.feedIds?.size) + assertEquals(2, filters?.authors?.size) + assertEquals(ContentType.ARTICLE, filters?.contentType) + assertEquals(SearchSortOption.DATE_DESC, filters?.sortOption) + } + + @Test + fun testContentTypeAudio() { + val json = """{ + "contentType": "audio" + }""" + + val filters = adapter.fromJson(json) + assertNotNull(filters) + assertEquals(ContentType.AUDIO, filters?.contentType) + } + + @Test + fun testContentTypeVideo() { + val json = """{ + "contentType": "video" + }""" + + val filters = adapter.fromJson(json) + assertNotNull(filters) + assertEquals(ContentType.VIDEO, filters?.contentType) + } + + @Test + fun testSortOptions() { + val sortOptions = listOf( + "relevance" to SearchSortOption.RELEVANCE, + "date_desc" to SearchSortOption.DATE_DESC, + "date_asc" to SearchSortOption.DATE_ASC, + "title_asc" to SearchSortOption.TITLE_ASC, + "title_desc" to SearchSortOption.TITLE_DESC, + "feed_asc" to SearchSortOption.FEED_ASC, + "feed_desc" to SearchSortOption.FEED_DESC + ) + + for ((jsonValue, expectedEnum) in sortOptions) { + val json = """{"sortOption": "$jsonValue"}""" + val filters = adapter.fromJson(json) + assertNotNull("Failed for sortOption: $jsonValue", filters) + assertEquals("Failed for sortOption: $jsonValue", expectedEnum, filters?.sortOption) + } + } + + @Test + fun testOptionalFieldsNull() { + val json = "{}" + + val filters = adapter.fromJson(json) + assertNotNull(filters) + assertNull(filters?.dateFrom) + assertNull(filters?.dateTo) + assertNull(filters?.feedIds) + assertNull(filters?.authors) + assertNull(filters?.contentType) + assertEquals(SearchSortOption.RELEVANCE, filters?.sortOption) + } + + @Test + fun testCopy() { + val original = SearchFilters( + feedIds = listOf("feed-1"), + sortOption = SearchSortOption.RELEVANCE + ) + + val modified = original.copy( + feedIds = listOf("feed-1", "feed-2"), + sortOption = SearchSortOption.DATE_DESC + ) + + assertEquals(2, modified.feedIds?.size) + assertEquals(SearchSortOption.DATE_DESC, modified.sortOption) + } + + @Test + fun testEqualsAndHashCode() { + val filters1 = SearchFilters(feedIds = listOf("feed-1"), sortOption = SearchSortOption.RELEVANCE) + val filters2 = SearchFilters(feedIds = listOf("feed-1"), sortOption = SearchSortOption.RELEVANCE) + val filters3 = SearchFilters(feedIds = listOf("feed-2"), sortOption = SearchSortOption.RELEVANCE) + + assertEquals(filters1, filters2) + assertEquals(filters1.hashCode(), filters2.hashCode()) + assert(filters1 != filters3) + } + + @Test + fun testToString() { + val filters = SearchFilters( + feedIds = listOf("feed-1"), + sortOption = SearchSortOption.DATE_DESC + ) + + val toString = filters.toString() + assertNotNull(toString) + assert(toString.contains("feedIds")) + assert(toString.contains("sortOption")) + } +} diff --git a/native-route/android/src/test/java/com/rssuper/models/SearchResultTest.kt b/native-route/android/src/test/java/com/rssuper/models/SearchResultTest.kt new file mode 100644 index 0000000..bbd6982 --- /dev/null +++ b/native-route/android/src/test/java/com/rssuper/models/SearchResultTest.kt @@ -0,0 +1,153 @@ +package com.rssuper.models + +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +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 SearchResultTest { + + private lateinit var moshi: Moshi + private lateinit var adapter: com.squareup.moshi.JsonAdapter + + @Before + fun setup() { + moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + adapter = moshi.adapter(SearchResult::class.java) + } + + @Test + fun testArticleSerialization() { + val result = SearchResult( + id = "article-1", + type = SearchResultType.ARTICLE, + title = "Test Article", + snippet = "This is a snippet", + link = "https://example.com/article", + feedTitle = "Tech News", + published = Date(1672531200000), + score = 0.95 + ) + + val json = adapter.toJson(result) + assertNotNull(json) + } + + @Test + fun testFeedSerialization() { + val result = SearchResult( + id = "feed-1", + type = SearchResultType.FEED, + title = "Tech News Feed", + snippet = "Technology news and updates", + link = "https://example.com", + score = 0.85 + ) + + val json = adapter.toJson(result) + assertNotNull(json) + } + + @Test + fun testArticleDeserialization() { + val json = """{ + "id": "article-1", + "type": "article", + "title": "Test Article", + "snippet": "This is a snippet", + "link": "https://example.com/article", + "feedTitle": "Tech News", + "published": 1672531200000, + "score": 0.95 + }""" + + val result = adapter.fromJson(json) + assertNotNull(result) + assertEquals("article-1", result?.id) + assertEquals(SearchResultType.ARTICLE, result?.type) + assertEquals("Test Article", result?.title) + assertEquals("This is a snippet", result?.snippet) + } + + @Test + fun testFeedDeserialization() { + val json = """{ + "id": "feed-1", + "type": "feed", + "title": "Tech News Feed", + "snippet": "Technology news and updates", + "link": "https://example.com", + "score": 0.85 + }""" + + val result = adapter.fromJson(json) + assertNotNull(result) + assertEquals("feed-1", result?.id) + assertEquals(SearchResultType.FEED, result?.type) + } + + @Test + fun testOptionalFieldsNull() { + val json = """{ + "id": "article-1", + "type": "article", + "title": "Test Article" + }""" + + val result = adapter.fromJson(json) + assertNotNull(result) + assertNull(result?.snippet) + assertNull(result?.link) + assertNull(result?.feedTitle) + assertNull(result?.published) + assertNull(result?.score) + } + + @Test + fun testCopy() { + val original = SearchResult( + id = "article-1", + type = SearchResultType.ARTICLE, + title = "Original Title" + ) + + val modified = original.copy(title = "Modified Title", score = 0.99) + + assertEquals("article-1", modified.id) + assertEquals(SearchResultType.ARTICLE, modified.type) + assertEquals("Modified Title", modified.title) + assertEquals(0.99, modified.score, 0.001) + } + + @Test + fun testEqualsAndHashCode() { + val result1 = SearchResult(id = "article-1", type = SearchResultType.ARTICLE, title = "Test") + val result2 = SearchResult(id = "article-1", type = SearchResultType.ARTICLE, title = "Test") + val result3 = SearchResult(id = "article-2", type = SearchResultType.ARTICLE, title = "Test") + + assertEquals(result1, result2) + assertEquals(result1.hashCode(), result2.hashCode()) + assert(result1 != result3) + } + + @Test + fun testToString() { + val result = SearchResult( + id = "article-1", + type = SearchResultType.ARTICLE, + title = "Test Article", + score = 0.95 + ) + + val toString = result.toString() + assertNotNull(toString) + assert(toString.contains("id=article-1")) + assert(toString.contains("title=Test Article")) + } +}