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]) } }