Files
jiang13-bbs/backend/service/sidebar_test.go
freefire 7b327bc8cc feat: 站点访问统计与可配置侧边栏组件
- 新增访问统计:visitstats 服务与查询、visit_capture 中间件、爬虫/来源识别、管理端 analytics 页面
- 新增侧边栏:sidebar 服务与 handler、管理端 sidebar 配置页、前端侧边栏组件
- 新增 widget 嵌入代码生成(widgetCode)与 widgetApi
- 遥测逻辑迁移至 handler 层,删除 service/telemetry
- env 示例补充 Docker 部署可信代理 IP 说明
2026-09-24 18:53:45 +08:00

225 lines
7.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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: "<b>x</b>"}
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: "<div>hi</div>", 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: "<div>hi2</div>", 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 != "<div>hi2</div>" {
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")
}
}