新增模块: - 友链(friendlink)后端处理 + 前端管理页与友链板 - 书库(library):导入、章节、封面、阅读页、横竖版自适应 AdaptiveCoverSlot - 顶栏导航(header_nav)配置与 MobileTabBar/MobileRailDrawers 移动端抽屉 - 排行榜 service 测试、站点页面测试、时间线发布(timeline_release) 其它改动: - 后端 handlers/services 全量小幅调整 - 前端组件、库函数、URL/品牌/站点 URL 工具更新 - Lightbox 图片、MdEntries 条目卡、coverColor 派生色相等前端能力
171 lines
5.0 KiB
Go
171 lines
5.0 KiB
Go
package service
|
||
|
||
import (
|
||
"errors"
|
||
"os"
|
||
"testing"
|
||
)
|
||
|
||
func TestOpenBrandFile(t *testing.T) {
|
||
s, dir := newMediaLibraryService(t)
|
||
png := "/uploads/brand/0123456789abcdef0123456789abcdef.png"
|
||
if _, err := s.OpenBrandFile(png); !errors.Is(err, os.ErrNotExist) {
|
||
t.Fatalf("missing file want ErrNotExist, got %v", err)
|
||
}
|
||
writeMediaFile(t, dir, "brand/0123456789abcdef0123456789abcdef.png", []byte("png"))
|
||
f, err := s.OpenBrandFile(png)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
f.Close()
|
||
for _, bad := range []string{
|
||
"",
|
||
"/uploads/images/0123456789abcdef0123456789abcdef.png",
|
||
"/uploads/brand/../avatars/x.png",
|
||
"http://evil.test/x.png",
|
||
} {
|
||
if _, err := s.OpenBrandFile(bad); !errors.Is(err, os.ErrNotExist) {
|
||
t.Fatalf("should reject %q, got %v", bad, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestNormalizeBrandURL(t *testing.T) {
|
||
png := "/uploads/brand/0123456789abcdef0123456789abcdef.png"
|
||
got, ok := NormalizeBrandURL(BrandSlotLight, png)
|
||
if !ok || got != png {
|
||
t.Fatalf("logo png want %s got %q ok=%v", png, got, ok)
|
||
}
|
||
ico := "/uploads/brand/0123456789abcdef0123456789abcdef.ico"
|
||
if _, ok := NormalizeBrandURL(BrandSlotLight, ico); ok {
|
||
t.Fatal("logo should reject ico")
|
||
}
|
||
got, ok = NormalizeBrandURL(BrandSlotFavicon, ico)
|
||
if !ok || got != ico {
|
||
t.Fatalf("favicon ico want %s got %q ok=%v", ico, got, ok)
|
||
}
|
||
empty, ok := NormalizeBrandURL(BrandSlotDark, " ")
|
||
if !ok || empty != "" {
|
||
t.Fatalf("empty should clear, got %q ok=%v", empty, ok)
|
||
}
|
||
rejects := []string{
|
||
"http://evil.test/x.png",
|
||
"/uploads/brand/../avatars/x.png",
|
||
"/uploads/backgrounds/0123456789abcdef0123456789abcdef.png",
|
||
"/uploads/brand/short.png",
|
||
"javascript:alert(1)",
|
||
}
|
||
for _, u := range rejects {
|
||
if _, ok := NormalizeBrandURL(BrandSlotFavicon, u); ok {
|
||
t.Fatalf("should reject %q", u)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestNormalizeKeywords(t *testing.T) {
|
||
got, err := NormalizeKeywords([]string{" Go , gin ", "论坛,Go"})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(got) != 3 || got[0] != "Go" || got[1] != "gin" || got[2] != "论坛" {
|
||
t.Fatalf("unexpected %v", got)
|
||
}
|
||
long := ""
|
||
for i := 0; i < MaxKeywordRunes+1; i++ {
|
||
long += "字"
|
||
}
|
||
if _, err := NormalizeKeywords([]string{long}); err == nil {
|
||
t.Fatal("overlong keyword should fail")
|
||
}
|
||
many := make([]string, MaxKeywordCount+1)
|
||
for i := range many {
|
||
many[i] = string(rune('a' + i))
|
||
}
|
||
if _, err := NormalizeKeywords(many); err == nil {
|
||
t.Fatal("too many keywords should fail")
|
||
}
|
||
}
|
||
|
||
func TestNormalizeFooterLinks(t *testing.T) {
|
||
got, err := NormalizeFooterLinks([]FooterLink{
|
||
{Label: "备案", URL: "https://beian.miit.gov.cn/", NewTab: true, Align: FooterLinkAlignRight},
|
||
{Label: "关于", URL: "/about", NewTab: false, Align: FooterLinkAlignLeft},
|
||
})
|
||
if err != nil || len(got) != 2 || got[1].URL != "/about" || got[1].Align != FooterLinkAlignLeft {
|
||
t.Fatalf("got %+v err=%v", got, err)
|
||
}
|
||
bad := []FooterLink{
|
||
{Label: "x", URL: "javascript:alert(1)"},
|
||
{Label: "x", URL: "//evil.test"},
|
||
{Label: "", URL: "/a"},
|
||
{Label: "x", URL: "ftp://files.test/a"},
|
||
{Label: "x", URL: "/ok", Align: "middle"},
|
||
}
|
||
for _, item := range bad {
|
||
if _, err := NormalizeFooterLinks([]FooterLink{item}); err == nil {
|
||
t.Fatalf("should reject %+v", item)
|
||
}
|
||
}
|
||
raw := `[{"label":"ICP","url":"https://beian.miit.gov.cn/"}]`
|
||
parsed, err := parseFooterLinksJSON(raw)
|
||
if err != nil || len(parsed) != 1 || !parsed[0].NewTab || parsed[0].Align != FooterLinkAlignRight {
|
||
t.Fatalf("external default new tab + right align, got %+v err=%v", parsed, err)
|
||
}
|
||
}
|
||
|
||
func TestNormalizeFooterLinkAlign(t *testing.T) {
|
||
cases := []struct {
|
||
in string
|
||
want string
|
||
ok bool
|
||
}{
|
||
{"", FooterLinkAlignRight, true},
|
||
{" right ", FooterLinkAlignRight, true},
|
||
{"left", FooterLinkAlignLeft, true},
|
||
{"center", FooterLinkAlignCenter, true},
|
||
{"middle", "", false},
|
||
}
|
||
for _, tc := range cases {
|
||
got, ok := NormalizeFooterLinkAlign(tc.in)
|
||
if ok != tc.ok || got != tc.want {
|
||
t.Fatalf("in=%q got=%q ok=%v want=%q/%v", tc.in, got, ok, tc.want, tc.ok)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestNormalizeBrandMarkAndSize(t *testing.T) {
|
||
mark, ok := NormalizeBrandMark(" image ")
|
||
if !ok || mark != BrandMarkImage {
|
||
t.Fatalf("mark got %q ok=%v", mark, ok)
|
||
}
|
||
mark, ok = NormalizeBrandMark("")
|
||
if !ok || mark != BrandMarkImageText {
|
||
t.Fatalf("empty mark should default, got %q", mark)
|
||
}
|
||
if _, ok := NormalizeBrandMark("banner"); ok {
|
||
t.Fatal("unknown mark should fail")
|
||
}
|
||
size, ok := NormalizeBrandLogoSize("4x1")
|
||
if !ok || size != BrandLogoSize4x1 {
|
||
t.Fatalf("size got %q", size)
|
||
}
|
||
size, ok = NormalizeBrandLogoSize("")
|
||
if !ok || size != BrandLogoSizeSQ {
|
||
t.Fatalf("empty size should default, got %q", size)
|
||
}
|
||
if _, ok := NormalizeBrandLogoSize("xl"); ok {
|
||
t.Fatal("old height preset should fail")
|
||
}
|
||
fit, ok := NormalizeBrandLogoFit("repeat")
|
||
if !ok || fit != BrandLogoFitRepeat {
|
||
t.Fatalf("fit got %q", fit)
|
||
}
|
||
fit, ok = NormalizeBrandLogoFit("")
|
||
if !ok || fit != BrandLogoFitContain {
|
||
t.Fatalf("empty fit should default, got %q", fit)
|
||
}
|
||
if _, ok := NormalizeBrandLogoFit("zoom"); ok {
|
||
t.Fatal("unknown fit should fail")
|
||
}
|
||
}
|