Files
podcast-search-go/model/podcast_test.go
T
ansandClaude Fable 5 f8bdd7f08f feat: feed 回源支持指定 User-Agent(FeedOptions)
新增 LoadFeedWithOptions(ctx, feedURL, FeedOptions{Proxy, Timeout, UserAgent}),
LoadFeed / LoadFeedContext 签名一字不改、降级为它的包装——库自身 main.go 与四个
既有测试的 5 处调用因此零改动。

动机:调用方要把「回源 feed」和「可达性探测」用同一个 UA 发出去,两者视角不一致
则打标不可信;而此前 UA 写死在函数体里、无从覆盖。

空 UA 不原样发出而是回落默认:gofeed 无条件 Header.Set("User-Agent", …),net/http
见到空值会把整条头省掉,而不发 UA 比发任何 UA 都更容易被拒(实测有源站对无 UA
请求直接 403)。

默认值保持原样并提为导出常量 DefaultUserAgent。顺带把它的 doc comment 从「不知道
当年为什么设成 Chrome」改成实测得出的可检验规则:约束是「不得是 HTTP 库出厂 UA、
不得为空」,而非「必须是浏览器」——feed.xyzfm.space 对 "Gofeed/1.0"(ec79b8b 之前
本函数实际发出的值)与 "Go-http-client/1.1" 返回 403 且响应头写明
X-Tengine-Error: denied by UA ACL = blacklist,而 Chrome、txpodcast/1.0、
podcast-search-go/0.1 一律放行。

补 UA 断言测试(此前四个测试从未断言过 UA):显式 UA 逐字节发出、空串回落默认、
旧签名仍发默认、timeout<=0 仍回落 30s。已验证注入回归时会红。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 11:51:23 +08:00

153 lines
4.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 := `
<rss version="2.0">
<channel>
<title>Test Podcast</title>
<description>This is a test podcast</description>
<link>http://example.com</link>
<image>
<url>http://example.com/image.jpg</url>
</image>
<copyright>Test Copyright</copyright>
<language>en</language>
<item>
<title>Episode 1</title>
<link>http://example.com/episode1</link>
<description>Episode 1 description</description>
</item>
</channel>
</rss>
`
// 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("<rss></rss>"))
}))
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(`<rss version="2.0"><channel><title>t</title></channel></rss>`))
}))
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(`<rss version="2.0"><channel><title>t</title></channel></rss>`))
}))
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(`<rss version="2.0"><channel><title>t</title></channel></rss>`))
}))
defer srv.Close()
// 0 若被原样当成超时,请求会立刻 context deadline exceeded。
_, err := LoadFeedWithOptions(context.Background(), srv.URL, FeedOptions{})
assert.NoError(t, err)
}