Files
RSSuper/android/src/androidTest/java/com/rssuper/integration/FeedIntegrationTest.kt
Michael Freno b79e6e7aa2
Some checks failed
CI - Multi-Platform Native / Build iOS (RSSuper) (push) Has been cancelled
CI - Multi-Platform Native / Build macOS (push) Has been cancelled
CI - Multi-Platform Native / Build Android (push) Has been cancelled
CI - Multi-Platform Native / Build Linux (push) Has been cancelled
CI - Multi-Platform Native / Integration Tests (push) Has been cancelled
CI - Multi-Platform Native / Build Summary (push) Has been cancelled
fix readme repo diagram, add agents.md
2026-03-31 12:54:01 -04:00

418 lines
16 KiB
Kotlin

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 = """
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>Test RSS Feed</title>
<link>https://example.com</link>
<description>Test feed</description>
<item>
<title>Article 1</title>
<link>https://example.com/1</link>
<description>Content 1</description>
<pubDate>Mon, 31 Mar 2026 10:00:00 GMT</pubDate>
</item>
<item>
<title>Article 2</title>
<link>https://example.com/2</link>
<description>Content 2</description>
<pubDate>Mon, 31 Mar 2026 11:00:00 GMT</pubDate>
</item>
<item>
<title>Article 3</title>
<link>https://example.com/3</link>
<description>Content 3</description>
<pubDate>Mon, 31 Mar 2026 12:00:00 GMT</pubDate>
</item>
</channel>
</rss>
""".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("<invalid xml").setResponseCode(200))
val feedUrl = mockServer.url("/feed.xml").toString()
val fetchResult = feedFetcher.fetch(feedUrl)
assertTrue("Fetch should succeed", fetchResult.isSuccess())
try {
feedParser.parse(fetchResult.getOrNull()!!.feedXml, feedUrl)
fail("Parsing invalid XML should throw exception")
} catch (e: Exception) {
}
}
@Test
fun testCrossPlatformDataConsistency() = runBlockingTest {
// Verify data structures are consistent across platforms
// This test verifies that the same data can be created and retrieved
// Create subscription
val subscription = database.subscriptionDao().insert(
com.rssuper.database.entities.SubscriptionEntity(
id = "cross-platform-test",
url = "https://example.com/feed.xml",
title = "Cross Platform Test"
)
)
// Create feed item
val item = com.rssuper.database.entities.FeedItemEntity(
id = "cross-platform-item",
title = "Cross Platform Item",
content = "Testing cross-platform data consistency",
subscriptionId = subscription.id,
publishedAt = System.currentTimeMillis()
)
database.feedItemDao().insert(item)
// Verify data integrity
val storedItem = database.feedItemDao().getById(item.id)
assertNotNull("Item should be retrievable", storedItem)
assertEquals("Title should match", item.title, storedItem?.title)
assertEquals("Content should match", item.content, storedItem?.content)
}
@Test
fun testHTTPAuthCredentials() = runBlockingTest {
// Test HTTP authentication integration
val auth = HTTPAuthCredentials("testuser", "testpass")
val credentials = auth.toCredentials()
assertTrue("Credentials should start with Basic", credentials.startsWith("Basic "))
// Setup mock server with auth
mockServer.enqueue(MockResponse().setResponseCode(401))
mockServer.enqueue(MockResponse().setBody("Success").setResponseCode(200)
.addHeader("WWW-Authenticate", "Basic realm=\"test\""))
val feedUrl = mockServer.url("/feed.xml").toString()
val result = feedFetcher.fetch(feedUrl, httpAuth = auth)
assertTrue("Should handle auth", result.isSuccess() || result.isFailure())
}
@Test
fun testCacheControl() = runBlockingTest {
// Test ETag and If-Modified-Since headers
val etag = "test-etag-123"
val lastModified = "Mon, 01 Jan 2024 00:00:00 GMT"
// First request
mockServer.enqueue(MockResponse().setBody("Feed 1").setResponseCode(200)
.addHeader("ETag", etag)
.addHeader("Last-Modified", lastModified))
// Second request with If-None-Match
mockServer.enqueue(MockResponse().setResponseCode(304))
val feedUrl = mockServer.url("/feed.xml").toString()
// First fetch
val result1 = feedFetcher.fetch(feedUrl)
assertTrue("First fetch should succeed", result1.isSuccess())
// Second fetch with ETag
val result2 = feedFetcher.fetch(feedUrl, ifNoneMatch = etag)
assertTrue("Second fetch should complete", result2.isSuccess() || result2.isFailure())
}
private suspend fun <T> runBlockingTest(block: suspend () -> T): T {
return kotlinx.coroutines.test.runTest { block() }
}
}