新增模块: - 友链(friendlink)后端处理 + 前端管理页与友链板 - 书库(library):导入、章节、封面、阅读页、横竖版自适应 AdaptiveCoverSlot - 顶栏导航(header_nav)配置与 MobileTabBar/MobileRailDrawers 移动端抽屉 - 排行榜 service 测试、站点页面测试、时间线发布(timeline_release) 其它改动: - 后端 handlers/services 全量小幅调整 - 前端组件、库函数、URL/品牌/站点 URL 工具更新 - Lightbox 图片、MdEntries 条目卡、coverColor 派生色相等前端能力
165 lines
5.2 KiB
Go
165 lines
5.2 KiB
Go
package service
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/freefire/jiang13-bbs/model"
|
|
"github.com/glebarez/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func newHeaderNavTestService(t *testing.T) *SettingService {
|
|
t.Helper()
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("open sqlite: %v", err)
|
|
}
|
|
if err := db.AutoMigrate(&model.SiteSetting{}); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
return NewSettingService(db)
|
|
}
|
|
|
|
func TestHeaderNavDefaultWhenMissing(t *testing.T) {
|
|
s := newHeaderNavTestService(t)
|
|
got := s.HeaderNav()
|
|
if len(got) != 3 ||
|
|
got[0] != (HeaderNavItem{Type: HeaderNavItemBuiltin, Key: "home"}) ||
|
|
got[1] != (HeaderNavItem{Type: HeaderNavItemBuiltin, Key: "leaderboard"}) ||
|
|
got[2] != (HeaderNavItem{Type: HeaderNavItemBuiltin, Key: "library"}) {
|
|
t.Fatalf("missing row should fall back to builtin defaults, got %+v", got)
|
|
}
|
|
pub, err := s.Public()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(pub.HeaderNav) != 3 {
|
|
t.Fatalf("Public().HeaderNav len = %d, want 3", len(pub.HeaderNav))
|
|
}
|
|
}
|
|
|
|
func TestSetHeaderNavRoundTrip(t *testing.T) {
|
|
s := newHeaderNavTestService(t)
|
|
want := []HeaderNavItem{
|
|
{Type: HeaderNavItemBuiltin, Key: "library"},
|
|
{Type: HeaderNavItemCustom, Label: "关于", URL: "/p/about", NewTab: false},
|
|
{Type: HeaderNavItemBuiltin, Key: "home"},
|
|
{Type: HeaderNavItemCustom, Label: "官网", URL: "https://example.com", NewTab: true},
|
|
}
|
|
if err := s.SetHeaderNav(want); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := s.HeaderNav()
|
|
if len(got) != len(want) {
|
|
t.Fatalf("len = %d, want %d (%+v)", len(got), len(want), got)
|
|
}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Fatalf("item %d = %+v, want %+v", i, got[i], want[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSetHeaderNavEmptyHidesAll(t *testing.T) {
|
|
s := newHeaderNavTestService(t)
|
|
if err := s.SetHeaderNav(nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := s.HeaderNav(); len(got) != 0 {
|
|
t.Fatalf("explicit empty list should hide all menus, got %+v", got)
|
|
}
|
|
var rows int64
|
|
s.db.Where("key = ?", SettingKeyHeaderNav).Model(&model.SiteSetting{}).Count(&rows)
|
|
if rows != 1 {
|
|
t.Fatalf("empty list should be persisted, rows=%d", rows)
|
|
}
|
|
}
|
|
|
|
func TestSetHeaderNavDefaultDeletesKey(t *testing.T) {
|
|
s := newHeaderNavTestService(t)
|
|
if err := s.SetHeaderNav([]HeaderNavItem{{Type: HeaderNavItemCustom, Label: "x", URL: "/x"}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.SetHeaderNav(DefaultHeaderNav()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var rows int64
|
|
s.db.Where("key = ?", SettingKeyHeaderNav).Model(&model.SiteSetting{}).Count(&rows)
|
|
if rows != 0 {
|
|
t.Fatalf("default config should delete the row, rows=%d", rows)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeHeaderNavRejectsInvalid(t *testing.T) {
|
|
longLabel := strings.Repeat("字", MaxHeaderNavLabelRunes+1)
|
|
cases := [][]HeaderNavItem{
|
|
{{Type: HeaderNavItemBuiltin, Key: "unknown"}},
|
|
{{Type: "weird", Key: "home"}},
|
|
{{Type: HeaderNavItemCustom, Label: "", URL: "/x"}},
|
|
{{Type: HeaderNavItemCustom, Label: longLabel, URL: "/x"}},
|
|
{{Type: HeaderNavItemCustom, Label: "x", URL: "javascript:alert(1)"}},
|
|
{{Type: HeaderNavItemCustom, Label: "x", URL: "relative"}},
|
|
{{Type: HeaderNavItemCustom, Label: "x", URL: "//evil.com/x"}},
|
|
}
|
|
for i, items := range cases {
|
|
if _, err := NormalizeHeaderNav(items); !errors.Is(err, ErrInvalidSiteSetting) {
|
|
t.Fatalf("case %d should fail, got %v", i, err)
|
|
}
|
|
}
|
|
tooMany := make([]HeaderNavItem, MaxHeaderNavItems+1)
|
|
for i := range tooMany {
|
|
tooMany[i] = HeaderNavItem{Type: HeaderNavItemCustom, Label: "x", URL: "/x"}
|
|
}
|
|
if _, err := NormalizeHeaderNav(tooMany); !errors.Is(err, ErrInvalidSiteSetting) {
|
|
t.Fatalf("over-cap list should fail, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeHeaderNavDedupesBuiltins(t *testing.T) {
|
|
got, err := NormalizeHeaderNav([]HeaderNavItem{
|
|
{Type: HeaderNavItemBuiltin, Key: "home"},
|
|
{Type: HeaderNavItemBuiltin, Key: " HOME "},
|
|
{Type: HeaderNavItemBuiltin, Key: "library"},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got) != 2 || got[0].Key != "home" || got[1].Key != "library" {
|
|
t.Fatalf("builtin duplicates should be deduped, got %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestParseHeaderNavJSONTolerant(t *testing.T) {
|
|
// 坏 JSON 回落默认
|
|
if got := parseHeaderNavJSON("{bad"); len(got) != 3 {
|
|
t.Fatalf("bad json should fall back to defaults, got %+v", got)
|
|
}
|
|
// 空数组 = 全部隐藏
|
|
if got := parseHeaderNavJSON("[]"); len(got) != 0 {
|
|
t.Fatalf("empty array should be respected, got %+v", got)
|
|
}
|
|
// 单条非法跳过;内置重复跳过;外链 new_tab 缺省为 true
|
|
got := parseHeaderNavJSON(`[
|
|
{"type":"builtin","key":"home"},
|
|
{"type":"builtin","key":"nope"},
|
|
{"type":"builtin","key":"home"},
|
|
{"type":"custom","label":"","url":"/x"},
|
|
{"type":"custom","label":"外链","url":"https://example.com"},
|
|
{"type":"custom","label":"站内","url":"/p/about","new_tab":true}
|
|
]`)
|
|
if len(got) != 3 {
|
|
t.Fatalf("want 3 valid items, got %+v", got)
|
|
}
|
|
if got[0] != (HeaderNavItem{Type: HeaderNavItemBuiltin, Key: "home"}) {
|
|
t.Fatalf("item0 = %+v", got[0])
|
|
}
|
|
if got[1].Type != HeaderNavItemCustom || got[1].Label != "外链" || !got[1].NewTab {
|
|
t.Fatalf("external custom item should default new_tab=true, got %+v", got[1])
|
|
}
|
|
if got[2].Label != "站内" || !got[2].NewTab {
|
|
t.Fatalf("explicit new_tab=true should be kept, got %+v", got[2])
|
|
}
|
|
}
|