@@ -165,6 +165,84 @@ func TestGenerateRSS_RendersEchoImages(t *testing.T) {
165165 assert .Contains (t , atom , "look at this" )
166166}
167167
168+ // TestGenerateRSS_RendersMediaByCategory 附件按 Category 分流渲染:
169+ // video → <video controls>、audio → <audio controls>(均内嵌 <a> 链接兜底),
170+ // 其它类型(pdf/file)→ 📎 下载链接;image 仍是 <img>。
171+ func TestGenerateRSS_RendersMediaByCategory (t * testing.T ) {
172+ repo := commonmock .NewMockCommonRepository (t )
173+ svc := commonService .NewCommonService (repo , newFakeCache ())
174+
175+ echos := []echoModel.Echo {
176+ {
177+ ID : "echo-media" ,
178+ Username : "carol" ,
179+ Content : "mixed media" ,
180+ CreatedAt : time .Now ().UTC ().Unix (),
181+ EchoFiles : []echoModel.EchoFile {
182+ {File : fileModel.File {Category : "image" , URL : "http://example.com/files/pic.png" }},
183+ {File : fileModel.File {Category : "video" , URL : "http://example.com/files/clip.mp4" }},
184+ {File : fileModel.File {Category : "audio" , URL : "http://example.com/files/song.mp3" }},
185+ {File : fileModel.File {Category : "pdf" , URL : "http://example.com/files/doc.pdf" , Name : "doc.pdf" }},
186+ },
187+ },
188+ }
189+
190+ repo .EXPECT ().GetAllEchos (mock .Anything , false ).Return (echos , nil ).Once ()
191+ repo .EXPECT ().TrackRSSCacheKey (mock .Anything ).Return ().Once ()
192+
193+ ctx := newRSSContext (t , "example.com" )
194+ atom , err := svc .GenerateRSS (ctx )
195+ require .NoError (t , err )
196+
197+ // summary 内容会被 feeds 库再做一层 XML 转义(< → <," → "),故断言转义后的形态。
198+ // image → <img>
199+ assert .Contains (t , atom , `<img src="http://example.com/files/pic.png"` , "图片应渲染为 <img>" )
200+ // video → <video controls> 且内嵌 <a> 链接兜底
201+ assert .Contains (t , atom , `<video controls src="http://example.com/files/clip.mp4"` , "视频应渲染为 <video controls>" )
202+ assert .Contains (t , atom , `<a href="http://example.com/files/clip.mp4">打开视频</a>` , "视频应内嵌链接兜底" )
203+ // audio → <audio controls> 且内嵌 <a> 链接兜底
204+ assert .Contains (t , atom , `<audio controls src="http://example.com/files/song.mp3"` , "音频应渲染为 <audio controls>" )
205+ assert .Contains (t , atom , `<a href="http://example.com/files/song.mp3">打开音频</a>` , "音频应内嵌链接兜底" )
206+ // pdf/其它 → 📎 下载链接(文件名作为链接文本)
207+ assert .Contains (t , atom , `<a href="http://example.com/files/doc.pdf">doc.pdf</a>` , "普通文件应渲染为下载链接" )
208+ }
209+
210+ // TestGenerateRSS_MediaFieldEscaping 绑定 GHSA-3v85-fqvh-7rxf 同类注入:
211+ // external 附件的 URL / 文件名是用户可控字段,进入 <summary type="html"> 前必须 HTML 实体转义,
212+ // 不得让原始引号/尖括号突破属性或标签上下文。
213+ func TestGenerateRSS_MediaFieldEscaping (t * testing.T ) {
214+ repo := commonmock .NewMockCommonRepository (t )
215+ svc := commonService .NewCommonService (repo , newFakeCache ())
216+
217+ echos := []echoModel.Echo {
218+ {
219+ ID : "echo-evil" ,
220+ Username : "mallory" ,
221+ Content : "benign" ,
222+ CreatedAt : time .Now ().UTC ().Unix (),
223+ EchoFiles : []echoModel.EchoFile {
224+ {File : fileModel.File {
225+ Category : "file" ,
226+ URL : `http://x/"><script>alert(1)</script>` ,
227+ Name : `<script>alert(2)</script>` ,
228+ }},
229+ },
230+ },
231+ }
232+
233+ repo .EXPECT ().GetAllEchos (mock .Anything , false ).Return (echos , nil ).Once ()
234+ repo .EXPECT ().TrackRSSCacheKey (mock .Anything ).Return ().Once ()
235+
236+ ctx := newRSSContext (t , "example.com" )
237+ atom , err := svc .GenerateRSS (ctx )
238+ require .NoError (t , err )
239+
240+ // 不得出现由 URL/文件名注入的原始 <script>。
241+ assert .NotContains (t , atom , "<script>" , "URL/文件名注入的原始 script 标签不得出现" )
242+ // 单层转义形态(<script>)也不应出现——须先 HTML 实体转义再经 Atom 的 XML 序列化,呈双层转义。
243+ assert .NotContains (t , atom , "<script>" , "媒体字段必须先做 HTML 实体转义,杜绝单层转义形态" )
244+ }
245+
168246// TestGenerateRSS_ReadThrough 读穿透:相同 host 第二次调用命中缓存,不再回源仓库。
169247func TestGenerateRSS_ReadThrough (t * testing.T ) {
170248 repo := commonmock .NewMockCommonRepository (t )
0 commit comments