package rss import ( "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // Helper feed for build tests func newTestFeed() *Feed { return &Feed{ Format: "rss2.0", Title: "Test Blog", Link: "https://example.com", Description: "A test blog", Language: "en", Updated: "Mon, 01 Jan 2024 00:00:00 +0000", Items: []FeedItem{ { Title: "First Post", Link: "https://example.com/post-1", Description: "Summary of first post", Content: "

Full content

", Author: "Alice", Published: "Sun, 31 Dec 2023 12:00:00 +0000", GUID: "https://example.com/post-1", Categories: []string{"Tech", "Go"}, Enclosures: []Enclosure{ {URL: "https://example.com/audio.mp3", Type: "audio/mpeg", Length: "12345678"}, }, }, { Title: "Second Post", Link: "https://example.com/post-2", Published: "Mon, 01 Jan 2024 00:00:00 +0000", GUID: "https://example.com/post-2", }, }, } } func newTestPodcastFeed() *Feed { feed := &Feed{ Format: "rss2.0", Title: "My Podcast", Link: "https://podcast.example.com", Description: "A tech podcast", Language: "en", Updated: "Wed, 15 Nov 2023 08:00:00 +0000", Podcast: &Podcast{ Author: "Jane Doe", Summary: "Weekly tech discussions", Image: "https://podcast.example.com/cover.jpg", Explicit: false, Type: "episodic", Owner: &Owner{Name: "Jane Doe", Email: "jane@example.com"}, Category: []string{"Technology", "Technology > Podcasting", "Education"}, }, Items: []FeedItem{ { Title: "Episode 1", Link: "https://podcast.example.com/ep1", Description: "Our first episode", Published: "Wed, 15 Nov 2023 08:00:00 +0000", GUID: "https://podcast.example.com/ep1", Enclosures: []Enclosure{ {URL: "https://podcast.example.com/ep1.mp3", Type: "audio/mpeg", Length: "50000000"}, }, Episode: &Episode{ Duration: "01:23:45", Season: 1, Number: 1, Type: "full", Explicit: false, Image: "https://podcast.example.com/ep1-cover.jpg", Summary: "Getting started with podcasting", }, }, }, } return feed } // --- RSS Build tests --- func TestBuild_RSS_Basic(t *testing.T) { feed := newTestFeed() xml, err := Build(feed, "rss") require.NoError(t, err) assert.Contains(t, xml, ``) assert.Contains(t, xml, `Test Blog`) assert.Contains(t, xml, `https://example.com`) assert.Contains(t, xml, `A test blog`) assert.Contains(t, xml, `en`) assert.Contains(t, xml, `First Post`) assert.Contains(t, xml, `Second Post`) assert.Contains(t, xml, `Alice`) assert.Contains(t, xml, `Tech`) assert.Contains(t, xml, `Go`) assert.Contains(t, xml, `url="https://example.com/audio.mp3"`) assert.Contains(t, xml, `type="audio/mpeg"`) // Should NOT contain itunes namespace since no podcast data assert.NotContains(t, xml, "itunes") } func TestBuild_RSS_DefaultFormat(t *testing.T) { feed := newTestFeed() xml, err := Build(feed, "") require.NoError(t, err) assert.Contains(t, xml, `Jane Doe`) assert.Contains(t, xml, `Weekly tech discussions`) assert.Contains(t, xml, `href="https://podcast.example.com/cover.jpg"`) assert.Contains(t, xml, `Jane Doe`) assert.Contains(t, xml, `jane@example.com`) assert.Contains(t, xml, `no`) assert.Contains(t, xml, `episodic`) // Categories assert.Contains(t, xml, `text="Technology"`) assert.Contains(t, xml, `text="Podcasting"`) assert.Contains(t, xml, `text="Education"`) // Episode metadata assert.Contains(t, xml, `01:23:45`) assert.Contains(t, xml, `1`) assert.Contains(t, xml, `1`) assert.Contains(t, xml, `full`) } func TestBuild_RSS_ContentEncoded(t *testing.T) { feed := newTestFeed() xml, err := Build(feed, "rss") require.NoError(t, err) assert.Contains(t, xml, `xmlns:content=`) assert.Contains(t, xml, ``) assert.Contains(t, xml, `

Full content

`) } // --- Atom Build tests --- func TestBuild_Atom_Basic(t *testing.T) { feed := newTestFeed() xml, err := Build(feed, "atom") require.NoError(t, err) assert.Contains(t, xml, ``) assert.Contains(t, xml, `Test Blog`) assert.Contains(t, xml, `A test blog`) assert.Contains(t, xml, `href="https://example.com"`) assert.Contains(t, xml, `First Post`) assert.Contains(t, xml, `Alice`) assert.Contains(t, xml, `term="Tech"`) assert.Contains(t, xml, `term="Go"`) // Atom should NOT have iTunes namespace assert.NotContains(t, xml, "itunes") } func TestBuild_Atom_PodcastIgnored(t *testing.T) { feed := newTestPodcastFeed() xml, err := Build(feed, "atom") require.NoError(t, err) // Podcast metadata should be ignored in Atom output assert.NotContains(t, xml, "itunes") assert.Contains(t, xml, `My Podcast`) } func TestBuild_Atom_MultipleAuthors(t *testing.T) { feed := &Feed{ Title: "Multi Author", Items: []FeedItem{ { Title: "Post", Author: "Alice, Bob", GUID: "1", }, }, } xml, err := Build(feed, "atom") require.NoError(t, err) assert.Contains(t, xml, `Alice`) assert.Contains(t, xml, `Bob`) } // --- Round-trip test --- func TestBuild_RoundTrip_RSS(t *testing.T) { // Parse → Build → Parse and verify consistency feed1, err := Parse(testRSS) require.NoError(t, err) xmlOut, err := Build(feed1, "rss") require.NoError(t, err) feed2, err := Parse(xmlOut) require.NoError(t, err) assert.Equal(t, feed1.Title, feed2.Title) assert.Equal(t, feed1.Link, feed2.Link) assert.Equal(t, feed1.Description, feed2.Description) assert.Len(t, feed2.Items, len(feed1.Items)) assert.Equal(t, feed1.Items[0].Title, feed2.Items[0].Title) } func TestBuild_RoundTrip_Podcast(t *testing.T) { feed1, err := Parse(testPodcast) require.NoError(t, err) xmlOut, err := Build(feed1, "rss") require.NoError(t, err) feed2, err := Parse(xmlOut) require.NoError(t, err) assert.Equal(t, feed1.Title, feed2.Title) require.NotNil(t, feed2.Podcast) assert.Equal(t, feed1.Podcast.Author, feed2.Podcast.Author) assert.Equal(t, feed1.Podcast.Summary, feed2.Podcast.Summary) assert.Len(t, feed2.Items, len(feed1.Items)) require.NotNil(t, feed2.Items[0].Episode) assert.Equal(t, feed1.Items[0].Episode.Duration, feed2.Items[0].Episode.Duration) } func TestBuild_RoundTrip_Atom(t *testing.T) { feed1, err := Parse(testAtom) require.NoError(t, err) xmlOut, err := Build(feed1, "atom") require.NoError(t, err) feed2, err := Parse(xmlOut) require.NoError(t, err) assert.Equal(t, feed1.Title, feed2.Title) assert.Len(t, feed2.Items, len(feed1.Items)) assert.Equal(t, feed1.Items[0].Title, feed2.Items[0].Title) } // --- Edge cases --- func TestBuild_NilFeed(t *testing.T) { _, err := Build(nil, "rss") assert.Error(t, err) assert.Contains(t, err.Error(), "nil") } func TestBuild_UnsupportedFormat(t *testing.T) { feed := newTestFeed() _, err := Build(feed, "json") assert.Error(t, err) assert.Contains(t, err.Error(), "unsupported") } func TestBuild_EmptyFeed(t *testing.T) { feed := &Feed{Title: "Empty"} xml, err := Build(feed, "rss") require.NoError(t, err) assert.Contains(t, xml, `Empty`) assert.NotContains(t, xml, "") } // --- mapToFeed tests --- func TestMapToFeed_DirectFeed(t *testing.T) { original := newTestFeed() result, err := mapToFeed(original) require.NoError(t, err) assert.Equal(t, original, result) } func TestMapToFeed_FromMap(t *testing.T) { m := map[string]interface{}{ "title": "From Map", "link": "https://example.com", "description": "Converted from map", "items": []interface{}{ map[string]interface{}{ "title": "Item 1", "link": "https://example.com/1", }, }, } feed, err := mapToFeed(m) require.NoError(t, err) assert.Equal(t, "From Map", feed.Title) require.Len(t, feed.Items, 1) assert.Equal(t, "Item 1", feed.Items[0].Title) } func TestMapToFeed_Nil(t *testing.T) { _, err := mapToFeed(nil) assert.Error(t, err) } // --- intToStr test --- func TestIntToStr(t *testing.T) { assert.Equal(t, "1", intToStr(1)) assert.Equal(t, "42", intToStr(42)) assert.Equal(t, "123", intToStr(123)) assert.Equal(t, "", intToStr(0)) } // --- Build output well-formedness --- func TestBuild_RSS_WellFormedXML(t *testing.T) { feed := newTestPodcastFeed() xmlStr, err := Build(feed, "rss") require.NoError(t, err) // Should start with XML declaration assert.True(t, strings.HasPrefix(xmlStr, "