package com.rssuper.integration
import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.rssuper.database.RssDatabase
import com.rssuper.parsing.FeedParser
import com.rssuper.parsing.ParseResult
import com.rssuper.services.FeedFetcher
import com.rssuper.services.HTTPAuthCredentials
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest
import org.junit.Assert.*
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.io.File
import java.io.FileReader
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.test.runTest
/**
* Integration tests for cross-platform feed functionality.
*
* These tests verify the complete feed fetch → parse → store flow
* across the Android platform using real network calls and database operations.
*/
@RunWith(AndroidJUnit4::class)
class FeedIntegrationTest {
private lateinit var context: Context
private lateinit var database: RssDatabase
private lateinit var feedFetcher: FeedFetcher
private lateinit var feedParser: FeedParser
private lateinit var mockServer: MockWebServer
// Sample RSS feed content embedded directly
private val sampleRssContent = """
Test RSS Feed
https://example.com
Test feed
-
Article 1
https://example.com/1
Content 1
Mon, 31 Mar 2026 10:00:00 GMT
-
Article 2
https://example.com/2
Content 2
Mon, 31 Mar 2026 11:00:00 GMT
-
Article 3
https://example.com/3
Content 3
Mon, 31 Mar 2026 12:00:00 GMT
""".trimIndent()
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
// Use in-memory database for isolation
database = Room.inMemoryDatabaseBuilder(context, RssDatabase::class.java)
.allowMainThreadQueries()
.build()
feedFetcher = FeedFetcher(timeoutMs = 10000)
feedParser = FeedParser()
mockServer = MockWebServer()
mockServer.start(8080)
}
@After
fun tearDown() {
database.close()
mockServer.shutdown()
}
@Test
fun testFetchParseAndStoreFlow() = runBlockingTest {
// Setup mock server to return sample RSS feed
mockServer.enqueue(MockResponse().setBody(sampleRssContent).setResponseCode(200))
val feedUrl = mockServer.url("/feed.xml").toString()
// 1. Fetch the feed
val fetchResult = feedFetcher.fetch(feedUrl)
assertTrue("Fetch should succeed", fetchResult.isSuccess())
assertNotNull("Fetch result should not be null", fetchResult.getOrNull())
// 2. Parse the feed
val parseResult = feedParser.parse(fetchResult.getOrNull()!!.feedXml, feedUrl)
assertNotNull("Parse result should not be null", parseResult)
// 3. Store the subscription
database.subscriptionDao().insert(parseResult.feed.subscription)
// 4. Store the feed items
parseResult.feed.items.forEach { item ->
database.feedItemDao().insert(item)
}
// 5. Verify items were stored
val storedItems = database.feedItemDao().getAll()
assertEquals("Should have 3 feed items", 3, storedItems.size)
val storedSubscription = database.subscriptionDao().getAll().first()
assertEquals("Subscription title should match", parseResult.feed.subscription.title, storedSubscription.title)
}
@Test
fun testSearchEndToEnd() = runBlockingTest {
// Create test subscription
val subscription = database.subscriptionDao().insert(
com.rssuper.database.entities.SubscriptionEntity(
id = "test-search-sub",
url = "https://example.com/feed.xml",
title = "Test Search Feed"
)
)
// Create test feed items with searchable content
val item1 = com.rssuper.database.entities.FeedItemEntity(
id = "test-item-1",
title = "Hello World Article",
content = "This is a test article about programming",
subscriptionId = subscription.id,
publishedAt = System.currentTimeMillis()
)
val item2 = com.rssuper.database.entities.FeedItemEntity(
id = "test-item-2",
title = "Another Article",
content = "This article is about technology and software",
subscriptionId = subscription.id,
publishedAt = System.currentTimeMillis()
)
database.feedItemDao().insert(item1)
database.feedItemDao().insert(item2)
// Perform search
val searchResults = database.feedItemDao().search("%test%", limit = 10)
// Verify results
assertTrue("Should find at least one result", searchResults.size >= 1)
assertTrue("Should find items with 'test' in content",
searchResults.any { it.content.contains("test", ignoreCase = true) })
}
@Test
fun testBackgroundSyncIntegration() = runBlockingTest {
// Setup mock server with multiple feeds
mockServer.enqueue(MockResponse().setBody(sampleRssContent).setResponseCode(200))
mockServer.enqueue(MockResponse().setBody(sampleRssContent).setResponseCode(200))
val feed1Url = mockServer.url("/feed1.xml").toString()
val feed2Url = mockServer.url("/feed2.xml").toString()
// Insert subscriptions
database.subscriptionDao().insert(
com.rssuper.database.entities.SubscriptionEntity(
id = "sync-feed-1",
url = feed1Url,
title = "Sync Test Feed 1"
)
)
database.subscriptionDao().insert(
com.rssuper.database.entities.SubscriptionEntity(
id = "sync-feed-2",
url = feed2Url,
title = "Sync Test Feed 2"
)
)
// Simulate sync by fetching and parsing both feeds
feed1Url.let { url ->
val result = feedFetcher.fetchAndParse(url)
assertTrue("First feed fetch should succeed or fail gracefully",
result.isSuccess() || result.isFailure())
}
feed2Url.let { url ->
val result = feedFetcher.fetchAndParse(url)
assertTrue("Second feed fetch should succeed or fail gracefully",
result.isSuccess() || result.isFailure())
}
// Verify subscriptions exist
val subscriptions = database.subscriptionDao().getAll()
assertEquals("Should have 2 subscriptions", 2, subscriptions.size)
}
@Test
fun testNotificationDelivery() = runBlockingTest {
// Create subscription
val subscription = database.subscriptionDao().insert(
com.rssuper.database.entities.SubscriptionEntity(
id = "test-notification-sub",
url = "https://example.com/feed.xml",
title = "Test Notification Feed"
)
)
// Create feed item
val item = com.rssuper.database.entities.FeedItemEntity(
id = "test-notification-item",
title = "Test Notification Article",
content = "This article should trigger a notification",
subscriptionId = subscription.id,
publishedAt = System.currentTimeMillis()
)
database.feedItemDao().insert(item)
// Verify item was created
val storedItem = database.feedItemDao().getById(item.id)
assertNotNull("Item should be stored", storedItem)
assertEquals("Title should match", item.title, storedItem?.title)
}
@Test
fun testSettingsPersistence() = runBlockingTest {
// Test notification preferences
val preferences = com.rssuper.database.entities.NotificationPreferencesEntity(
id = 1,
enabled = true,
sound = true,
vibration = true,
light = true,
channel = "rssuper_notifications"
)
database.notificationPreferencesDao().insert(preferences)
val stored = database.notificationPreferencesDao().get()
assertNotNull("Preferences should be stored", stored)
assertTrue("Notifications should be enabled", stored.enabled)
}
@Test
fun testBookmarkCRUD() = runBlockingTest {
// Create subscription and feed item
val subscription = database.subscriptionDao().insert(
com.rssuper.database.entities.SubscriptionEntity(
id = "test-bookmark-sub",
url = "https://example.com/feed.xml",
title = "Test Bookmark Feed"
)
)
val item = com.rssuper.database.entities.FeedItemEntity(
id = "test-bookmark-item",
title = "Test Bookmark Article",
content = "This article will be bookmarked",
subscriptionId = subscription.id,
publishedAt = System.currentTimeMillis()
)
database.feedItemDao().insert(item)
// Create bookmark
val bookmark = com.rssuper.database.entities.BookmarkEntity(
id = "bookmark-1",
feedItemId = item.id,
title = item.title,
link = "https://example.com/article1",
description = item.content,
content = item.content,
createdAt = System.currentTimeMillis()
)
database.bookmarkDao().insert(bookmark)
// Verify bookmark was created
val storedBookmarks = database.bookmarkDao().getAll()
assertEquals("Should have 1 bookmark", 1, storedBookmarks.size)
assertEquals("Bookmark title should match", bookmark.title, storedBookmarks.first().title)
// Update bookmark
val updatedBookmark = bookmark.copy(description = "Updated description")
database.bookmarkDao().update(updatedBookmark)
val reloaded = database.bookmarkDao().getById(bookmark.id)
assertEquals("Bookmark description should be updated",
updatedBookmark.description, reloaded?.description)
// Delete bookmark
database.bookmarkDao().delete(bookmark.id)
val deleted = database.bookmarkDao().getById(bookmark.id)
assertNull("Bookmark should be deleted", deleted)
}
@Test
fun testErrorRecoveryNetworkFailure() = runBlockingTest {
// Setup mock server to fail
mockServer.enqueue(MockResponse().setResponseCode(500))
mockServer.enqueue(MockResponse().setResponseCode(500))
mockServer.enqueue(MockResponse().setBody("Success").setResponseCode(200))
val feedUrl = mockServer.url("/feed.xml").toString()
// Should fail on first two attempts (mocked in FeedFetcher with retries)
val result = feedFetcher.fetch(feedUrl)
// After 3 retries, should eventually succeed or fail
assertTrue("Should complete after retries", result.isSuccess() || result.isFailure())
}
@Test
fun testErrorRecoveryParseError() = runBlockingTest {
// Setup mock server with invalid XML
mockServer.enqueue(MockResponse().setBody(" runBlockingTest(block: suspend () -> T): T {
return kotlinx.coroutines.test.runTest { block() }
}
}