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() } } }