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
418 lines
16 KiB
Kotlin
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() }
|
|
}
|
|
}
|