package service import ( "errors" "strings" "testing" "github.com/freefire/jiang13-bbs/model" "github.com/glebarez/sqlite" "gorm.io/gorm" ) func newSidebarTestDB(t *testing.T) *gorm.DB { 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{}, &model.SidebarWidget{}); err != nil { t.Fatalf("migrate: %v", err) } return db } func TestSidebarGetConfigDefaults(t *testing.T) { db := newSidebarTestDB(t) s := NewSidebarService(db) // 无键 → 默认布局 cfg, err := s.GetConfig() if err != nil { t.Fatalf("GetConfig: %v", err) } def := DefaultSidebarConfig() home := cfg.Scopes["home"] if home.Left == nil || (*home.Left)[0].Key != "board_nav" { t.Fatal("home.left 默认应为 board_nav") } if len(*home.Right) != len(*def.Scopes["home"].Right) { t.Fatalf("home.right 默认长度不符: %d", len(*home.Right)) } // board.left 恒为空 if bl := cfg.Scopes["board"].Left; bl == nil || len(*bl) != 0 { t.Fatal("board.left 默认应为空") } // 坏 JSON → 回落默认 if err := db.Create(&model.SiteSetting{Key: SettingKeySidebarConfig, Value: "{oops"}).Error; err != nil { t.Fatalf("seed bad json: %v", err) } cfg2, err := s.GetConfig() if err != nil { t.Fatalf("GetConfig bad json: %v", err) } if (*cfg2.Scopes["home"].Left)[0].Key != "board_nav" { t.Fatal("坏 JSON 应回落默认布局") } // 显式清空的侧栏要保持为空(非默认回填) empty := []SidebarBlockRef{} saved, err := s.SaveConfig(SidebarConfig{ Version: SidebarConfigVersion, Scopes: map[string]SidebarScopeConfig{ "home": {Right: &empty}, }, }) if err != nil { t.Fatalf("SaveConfig: %v", err) } if saved.Scopes["home"].Right == nil || len(*saved.Scopes["home"].Right) != 0 { t.Fatal("显式清空的 home.right 应保持为空") } cfg3, _ := s.GetConfig() if len(*cfg3.Scopes["home"].Right) != 0 { t.Fatal("读取时显式清空不应被默认回填") } if len(*cfg3.Scopes["post"].Left) == 0 { t.Fatal("未配置的 post.left 应回落默认") } } func TestSidebarSaveConfigValidation(t *testing.T) { db := newSidebarTestDB(t) s := NewSidebarService(db) mk := func(scopes map[string]SidebarScopeConfig) SidebarConfig { return SidebarConfig{Version: SidebarConfigVersion, Scopes: scopes} } side := func(refs []SidebarBlockRef) *[]SidebarBlockRef { return &refs } // 非法 scope if _, err := s.SaveConfig(mk(map[string]SidebarScopeConfig{ "unknown": {Right: side([]SidebarBlockRef{{Key: "pulse"}})}, })); !errors.Is(err, ErrSidebarInvalid) { t.Fatal("非法 scope 应被拒绝") } // 非法 key(board_* 不允许放进 home) if _, err := s.SaveConfig(mk(map[string]SidebarScopeConfig{ "home": {Right: side([]SidebarBlockRef{{Key: "board_hot"}})}, })); !errors.Is(err, ErrSidebarInvalid) { t.Fatal("跨 scope 的内置 key 应被拒绝") } // 引用不存在的自定义工具 if _, err := s.SaveConfig(mk(map[string]SidebarScopeConfig{ "home": {Right: side([]SidebarBlockRef{{WidgetID: 999}})}, })); !errors.Is(err, ErrSidebarInvalid) { t.Fatal("不存在的 widget 引用应被拒绝") } // 同 scope 重复引用 if _, err := s.SaveConfig(mk(map[string]SidebarScopeConfig{ "home": {Right: side([]SidebarBlockRef{{Key: "pulse"}, {Key: "pulse"}})}, })); !errors.Is(err, ErrSidebarInvalid) { t.Fatal("同 scope 重复引用应被拒绝") } // 超 24 块 many := make([]SidebarBlockRef, SidebarMaxBlocksPerSide+1) for i := range many { many[i] = SidebarBlockRef{Key: "pulse"} } if _, err := s.SaveConfig(mk(map[string]SidebarScopeConfig{ "home": {Right: &many}, })); !errors.Is(err, ErrSidebarInvalid) { t.Fatal("超上限应被拒绝") } // key 与 widget 同时出现 if _, err := s.SaveConfig(mk(map[string]SidebarScopeConfig{ "home": {Right: side([]SidebarBlockRef{{Key: "pulse", WidgetID: 1}})}, })); !errors.Is(err, ErrSidebarInvalid) { t.Fatal("key 与 widget 二选一应被强制") } // 空 scopes if _, err := s.SaveConfig(SidebarConfig{Version: 1}); !errors.Is(err, ErrSidebarInvalid) { t.Fatal("空 scopes 应被拒绝") } // board.left 保存后应被丢弃(即使引用合法的 board 块) saved, err := s.SaveConfig(mk(map[string]SidebarScopeConfig{ "board": {Left: side([]SidebarBlockRef{{Key: "board_intro"}}), Right: side([]SidebarBlockRef{{Key: "board_hot"}})}, })) if err != nil { t.Fatalf("board 保存失败: %v", err) } if len(*saved.Scopes["board"].Left) != 0 { t.Fatal("board.left 应被丢弃为空") } } func TestSidebarWidgetCRUDAndRefCleanup(t *testing.T) { db := newSidebarTestDB(t) s := NewSidebarService(db) // 名称校验 w := &model.SidebarWidget{Name: " ", Html: "x"} if err := s.CreateWidget(w); !errors.Is(err, ErrSidebarInvalid) { t.Fatal("空名称应被拒绝") } w = &model.SidebarWidget{Name: strings.Repeat("字", SidebarWidgetMaxNameRunes+1)} if err := s.CreateWidget(w); !errors.Is(err, ErrSidebarInvalid) { t.Fatal("超长名称应被拒绝") } big := strings.Repeat("a", SidebarWidgetMaxCodeBytes+1) if err := s.CreateWidget(&model.SidebarWidget{Name: "t", Html: big}); !errors.Is(err, ErrSidebarInvalid) { t.Fatal("超 64KB 代码应被拒绝") } // 正常创建 + 放入配置 w = &model.SidebarWidget{Name: "跑马灯", Html: "
hi
", Enabled: false} if err := s.CreateWidget(w); err != nil { t.Fatalf("CreateWidget: %v", err) } if w.ID == 0 || !w.Enabled { t.Fatalf("创建后应启用且有 ID: %+v", w) } if _, err := s.SaveConfig(SidebarConfig{Version: SidebarConfigVersion, Scopes: map[string]SidebarScopeConfig{ "home": {Right: func() *[]SidebarBlockRef { r := []SidebarBlockRef{{Key: "pulse"}, {WidgetID: w.ID}} return &r }()}, }}); err != nil { t.Fatalf("SaveConfig with widget: %v", err) } // 更新:显式禁用(布尔零值不能被省略) if err := s.UpdateWidget(w.ID, &model.SidebarWidget{Name: "跑马灯2", Html: "
hi2
", Js: "console.log(1)", Enabled: false}); err != nil { t.Fatalf("UpdateWidget: %v", err) } var after model.SidebarWidget if err := db.First(&after, w.ID).Error; err != nil { t.Fatalf("reload: %v", err) } if after.Enabled || after.Name != "跑马灯2" || after.Js == "" { t.Fatalf("禁用/字段更新未生效: %+v", after) } // 禁用后不出现在公开载荷 payload, err := s.PublicPayload() if err != nil { t.Fatalf("PublicPayload: %v", err) } if len(payload.Widgets) != 0 { t.Fatal("禁用的工具不应出现在公开载荷") } if err := s.SetWidgetEnabled(w.ID, true); err != nil { t.Fatalf("SetWidgetEnabled: %v", err) } payload, _ = s.PublicPayload() if len(payload.Widgets) != 1 || payload.Widgets[0].Html != "
hi2
" { t.Fatalf("启用后应出现在公开载荷: %+v", payload.Widgets) } // 删除工具 → 配置中的引用被清理 if err := s.DeleteWidget(w.ID); err != nil { t.Fatalf("DeleteWidget: %v", err) } cfg, _ := s.GetConfig() for _, r := range *cfg.Scopes["home"].Right { if r.WidgetID == w.ID { t.Fatal("删除后配置中不应残留引用") } } // 重复删除 → NotFound if err := s.DeleteWidget(w.ID); !errors.Is(err, ErrSidebarWidgetNotFound) { t.Fatal("重复删除应返回 NotFound") } }