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, "