- Add FeedParser.kt with automatic feed type detection - Add RSSParser.kt for RSS 2.0 feeds - Add AtomParser.kt for Atom 1.0 feeds - Add comprehensive unit tests for both parsers - Support iTunes namespace and enclosures - Fix pre-existing compilation issues in the codebase - Update build.gradle.kts with proper dependencies and AGP 8.5.0
256 lines
8.8 KiB
Kotlin
256 lines
8.8 KiB
Kotlin
package com.rssuper.parsing
|
|
|
|
import com.rssuper.models.Enclosure
|
|
import com.rssuper.models.Feed
|
|
import org.junit.Assert.assertEquals
|
|
import org.junit.Assert.assertNotNull
|
|
import org.junit.Assert.assertNull
|
|
import org.junit.Test
|
|
import org.junit.runner.RunWith
|
|
import org.robolectric.RobolectricTestRunner
|
|
import org.robolectric.annotation.Config
|
|
|
|
@RunWith(RobolectricTestRunner::class)
|
|
@Config(sdk = [24])
|
|
class RSSParserTest {
|
|
|
|
@Test
|
|
fun testParseBasicRSS() {
|
|
val xml = """
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<rss version="2.0">
|
|
<channel>
|
|
<title>Test Feed</title>
|
|
<link>https://example.com</link>
|
|
<description>A test feed</description>
|
|
<language>en-us</language>
|
|
<lastBuildDate>Mon, 01 Jan 2024 12:00:00 GMT</lastBuildDate>
|
|
<generator>RSS Generator</generator>
|
|
<ttl>60</ttl>
|
|
<item>
|
|
<title>Item 1</title>
|
|
<link>https://example.com/item1</link>
|
|
<description>Description of item 1</description>
|
|
<guid isPermaLink="true">https://example.com/item1</guid>
|
|
<pubDate>Mon, 01 Jan 2024 10:00:00 GMT</pubDate>
|
|
</item>
|
|
<item>
|
|
<title>Item 2</title>
|
|
<link>https://example.com/item2</link>
|
|
<description>Description of item 2</description>
|
|
<guid>item-2-guid</guid>
|
|
<pubDate>Sun, 31 Dec 2023 10:00:00 GMT</pubDate>
|
|
</item>
|
|
</channel>
|
|
</rss>
|
|
""".trimIndent()
|
|
|
|
val feed = RSSParser.parse(xml, "https://example.com/feed.xml")
|
|
|
|
assertNotNull(feed)
|
|
assertEquals("Test Feed", feed.title)
|
|
assertEquals("https://example.com", feed.link)
|
|
assertEquals("A test feed", feed.description)
|
|
assertEquals("en-us", feed.language)
|
|
assertEquals(60, feed.ttl)
|
|
assertEquals(2, feed.items.size)
|
|
|
|
val item1 = feed.items[0]
|
|
assertEquals("Item 1", item1.title)
|
|
assertEquals("https://example.com/item1", item1.link)
|
|
assertEquals("Description of item 1", item1.description)
|
|
assertNotNull(item1.published)
|
|
}
|
|
|
|
@Test
|
|
fun testParseRSSWithiTunesNamespace() {
|
|
val xml = """
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
|
|
<channel>
|
|
<title>Podcast Feed</title>
|
|
<link>https://example.com/podcast</link>
|
|
<description>My podcast</description>
|
|
<itunes:subtitle>Podcast subtitle</itunes:subtitle>
|
|
<itunes:author>Author Name</itunes:author>
|
|
<item>
|
|
<title>Episode 1</title>
|
|
<link>https://example.com/episode1</link>
|
|
<description>Episode description</description>
|
|
<itunes:duration>01:30:00</itunes:duration>
|
|
<enclosure url="https://example.com/ep1.mp3" type="audio/mpeg" length="12345678"/>
|
|
</item>
|
|
</channel>
|
|
</rss>
|
|
""".trimIndent()
|
|
|
|
val feed = RSSParser.parse(xml, "https://example.com/feed.xml")
|
|
|
|
assertNotNull(feed)
|
|
assertEquals("Podcast Feed", feed.title)
|
|
|
|
val item = feed.items[0]
|
|
assertEquals("Episode 1", item.title)
|
|
assertNotNull(item.enclosure)
|
|
assertEquals("https://example.com/ep1.mp3", item.enclosure?.url)
|
|
assertEquals("audio/mpeg", item.enclosure?.type)
|
|
assertEquals(12345678L, item.enclosure?.length)
|
|
}
|
|
|
|
@Test
|
|
fun testParseRSSWithContentNamespace() {
|
|
val xml = """
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
|
|
<channel>
|
|
<title>Feed with Content</title>
|
|
<item>
|
|
<title>Item with Content</title>
|
|
<description>Short description</description>
|
|
<content:encoded><![CDATA[<p>Full content here</p>]]></content:encoded>
|
|
</item>
|
|
</channel>
|
|
</rss>
|
|
""".trimIndent()
|
|
|
|
val feed = RSSParser.parse(xml, "https://example.com/feed.xml")
|
|
|
|
assertNotNull(feed)
|
|
assertEquals(1, feed.items.size)
|
|
assertEquals("Item with Content", feed.items[0].title)
|
|
assertEquals("<p>Full content here</p>", feed.items[0].content)
|
|
}
|
|
|
|
@Test
|
|
fun testParseRSSWithCategories() {
|
|
val xml = """
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<rss version="2.0">
|
|
<channel>
|
|
<title>Categorized Feed</title>
|
|
<item>
|
|
<title>Tech Article</title>
|
|
<category>Technology</category>
|
|
<category>Programming</category>
|
|
</item>
|
|
</channel>
|
|
</rss>
|
|
""".trimIndent()
|
|
|
|
val feed = RSSParser.parse(xml, "https://example.com/feed.xml")
|
|
|
|
assertNotNull(feed)
|
|
val item = feed.items[0]
|
|
assertEquals(2, item.categories?.size)
|
|
assertEquals("Technology", item.categories?.get(0))
|
|
assertEquals("Programming", item.categories?.get(1))
|
|
}
|
|
|
|
@Test
|
|
fun testParseRSSWithAuthor() {
|
|
val xml = """
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<rss version="2.0">
|
|
<channel>
|
|
<title>Author Feed</title>
|
|
<item>
|
|
<title>Article by Author</title>
|
|
<author>author@example.com (John Doe)</author>
|
|
</item>
|
|
</channel>
|
|
</rss>
|
|
""".trimIndent()
|
|
|
|
val feed = RSSParser.parse(xml, "https://example.com/feed.xml")
|
|
|
|
assertNotNull(feed)
|
|
val item = feed.items[0]
|
|
assertEquals("author@example.com (John Doe)", item.author)
|
|
}
|
|
|
|
@Test
|
|
fun testParseRSSWithGuid() {
|
|
val xml = """
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<rss version="2.0">
|
|
<channel>
|
|
<title>Guid Feed</title>
|
|
<item>
|
|
<title>Item</title>
|
|
<guid>custom-guid-12345</guid>
|
|
</item>
|
|
</channel>
|
|
</rss>
|
|
""".trimIndent()
|
|
|
|
val feed = RSSParser.parse(xml, "https://example.com/feed.xml")
|
|
|
|
assertNotNull(feed)
|
|
assertEquals("custom-guid-12345", feed.items[0].guid)
|
|
}
|
|
|
|
@Test
|
|
fun testParseRSSWithEmptyChannel() {
|
|
val xml = """
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<rss version="2.0">
|
|
<channel>
|
|
<title>Minimal Feed</title>
|
|
</channel>
|
|
</rss>
|
|
""".trimIndent()
|
|
|
|
val feed = RSSParser.parse(xml, "https://example.com/feed.xml")
|
|
|
|
assertNotNull(feed)
|
|
assertEquals("Minimal Feed", feed.title)
|
|
assertEquals(0, feed.items.size)
|
|
}
|
|
|
|
@Test
|
|
fun testParseRSSWithMissingFields() {
|
|
val xml = """
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<rss version="2.0">
|
|
<channel>
|
|
<item>
|
|
<title>Only Title</title>
|
|
</item>
|
|
</channel>
|
|
</rss>
|
|
""".trimIndent()
|
|
|
|
val feed = RSSParser.parse(xml, "https://example.com/feed.xml")
|
|
|
|
assertNotNull(feed)
|
|
assertEquals("Untitled Feed", feed.title)
|
|
assertEquals(1, feed.items.size)
|
|
assertEquals("Only Title", feed.items[0].title)
|
|
assertNull(feed.items[0].link)
|
|
}
|
|
|
|
@Test
|
|
fun testParseRSSWithCDATA() {
|
|
val xml = """
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<rss version="2.0">
|
|
<channel>
|
|
<title><![CDATA[CDATA Title]]></title>
|
|
<description><![CDATA[<p>HTML <strong>content</strong></p>]]></description>
|
|
<item>
|
|
<title>CDATA Item</title>
|
|
<description><![CDATA[Item content]]></description>
|
|
</item>
|
|
</channel>
|
|
</rss>
|
|
""".trimIndent()
|
|
|
|
val feed = RSSParser.parse(xml, "https://example.com/feed.xml")
|
|
|
|
assertNotNull(feed)
|
|
assertEquals("CDATA Title", feed.title)
|
|
assertEquals("<p>HTML <strong>content</strong></p>", feed.description)
|
|
assertEquals("Item content", feed.items[0].description)
|
|
}
|
|
}
|