package model
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestLoadFeed_Success(t *testing.T) {
// Mock RSS feed
rssFeed := `
Test Podcast
This is a test podcast
http://example.com
http://example.com/image.jpg
Test Copyright
en
-
Episode 1
http://example.com/episode1
Episode 1 description
`
// Create a test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(rssFeed))
}))
defer server.Close()
// Call LoadFeed
podcast, err := LoadFeed(server.URL, "", 10*time.Second)
// Assertions
assert.NoError(t, err)
assert.NotNil(t, podcast)
assert.Equal(t, "Test Podcast", podcast.Title)
assert.Equal(t, "This is a test podcast", podcast.Description)
assert.Equal(t, "http://example.com", podcast.Link)
assert.Equal(t, "http://example.com/image.jpg", podcast.Image)
assert.Equal(t, "Test Copyright", podcast.Copyright)
assert.Equal(t, "en", podcast.Language)
assert.Len(t, podcast.Episodes, 1)
assert.Equal(t, "Episode 1", podcast.Episodes[0].Title)
}
func TestLoadFeed_Timeout(t *testing.T) {
// Create a test server that delays response
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Second)
w.Write([]byte(""))
}))
defer server.Close()
// Call LoadFeed with a short timeout
_, err := LoadFeed(server.URL, "", 1*time.Second)
// Assertions
assert.Error(t, err)
assert.Contains(t, err.Error(), "context deadline exceeded")
}
func TestLoadFeed_InvalidURL(t *testing.T) {
// Call LoadFeed with an invalid URL
_, err := LoadFeed("http://invalid-url", "", 10*time.Second)
// Assertions
assert.Error(t, err)
assert.Contains(t, err.Error(), "解析播客订阅失败")
}
func TestLoadFeed_EmptyFeed(t *testing.T) {
// Create a test server with an empty feed
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(""))
}))
defer server.Close()
// Call LoadFeed
_, err := LoadFeed(server.URL, "", 10*time.Second)
// Assertions
assert.Error(t, err)
assert.Contains(t, err.Error(), "解析播客订阅失败")
}
// UA 是这个库唯一会影响「能不能抓到」的可配项:实测有源站按 UA 黑名单直接 403
// (feed.xyzfm.space 对 gofeed 出厂的 "Gofeed/1.0" 返回 denied by UA ACL = blacklist),
// 且无 UA 的请求比任何 UA 都更容易被拒。故这三条都必须钉死在测试里。
func TestLoadFeedWithOptions_UserAgent(t *testing.T) {
cases := []struct {
name string
give string
want string
}{
{"显式指定的 UA 逐字节发出", "txpodcast/1.0", "txpodcast/1.0"},
{"空串回落默认而不是发空头", "", DefaultUserAgent},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// buffered chan 而非裸变量:handler 在另一个 goroutine 里写,-race 会告警。
got := make(chan string, 1)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
got <- r.Header.Get("User-Agent")
_, _ = w.Write([]byte(`t`))
}))
defer srv.Close()
_, err := LoadFeedWithOptions(context.Background(), srv.URL,
FeedOptions{UserAgent: tc.give, Timeout: 10 * time.Second})
assert.NoError(t, err)
assert.Equal(t, tc.want, <-got)
})
}
}
// 旧签名必须继续发默认 UA:它是 LoadFeedWithOptions 的包装,委托时若漏传就会退化成空头。
func TestLoadFeedContext_KeepsDefaultUserAgent(t *testing.T) {
got := make(chan string, 1)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
got <- r.Header.Get("User-Agent")
_, _ = w.Write([]byte(`t`))
}))
defer srv.Close()
_, err := LoadFeedContext(context.Background(), srv.URL, "", 10*time.Second)
assert.NoError(t, err)
assert.Equal(t, DefaultUserAgent, <-got)
}
// timeout <= 0 回落 30 秒的既有语义不能因为改走 FeedOptions 而丢失。
func TestLoadFeedWithOptions_ZeroTimeoutFallsBack(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`t`))
}))
defer srv.Close()
// 0 若被原样当成超时,请求会立刻 context deadline exceeded。
_, err := LoadFeedWithOptions(context.Background(), srv.URL, FeedOptions{})
assert.NoError(t, err)
}