package realtime import ( "context" "database/sql" "encoding/json" "strings" "testing" "git.yeij.top/AskaEth/Scoreboard/server/internal/domain" ) // 快照必须与数据库同源:版本号取自 change_log,各列表取真实表, // 且空库时序列化为 [] 而不是 null,避免前端判空逻辑踩坑。 func TestSnapshotContents(t *testing.T) { env := newTestEnv(t) db := env.db mustExec(t, db, `UPDATE settings SET value = ? WHERE key = 'event.name'`, `"测试运动会"`) mustExec(t, db, `INSERT INTO settings(key, value) VALUES ('display.theme', 'dark')`) mustExec(t, db, `INSERT INTO teams(code, name, short_name, sort_order, active) VALUES ('T1', '第一单位', '一队', 1, 1)`) mustExec(t, db, `INSERT INTO sports(code, name) VALUES ('S1', '田径')`) mustExec(t, db, `INSERT INTO events(sport_id, code, name, event_type, result_mode, result_type, compare, status) VALUES (1, 'E1', '100米', 'individual', 'auto', 'time', 'asc', 'published')`) mustExec(t, db, `INSERT INTO event_categories(event_id, name) VALUES (1, '男子')`) mustExec(t, db, `INSERT INTO athletes(name, gender, team_id) VALUES ('张三', '男', 1)`) mustExec(t, db, `INSERT INTO entries(event_id, category_id, team_id, athlete_id, bib) VALUES (1, 1, 1, 1, '101')`) mustExec(t, db, `INSERT INTO results(event_id, entry_id, rank, result_text, result_value, unit, state, status) VALUES (1, 1, 1, '11.58', 11.58, 's', 'published', 'ok')`) mustExec(t, db, `INSERT INTO score_ledger(team_id, athlete_id, event_id, entry_id, result_id, source_type, source_id, points) VALUES (1, 1, 1, 1, 1, 'result', 1, 9)`) mustExec(t, db, `INSERT INTO records(event_id, level, result_value, result_text, holder_name, holder_team_id, achieved_at, is_current, source) VALUES (1, 'meet', 11.2, '11.20', '李四', 1, '2025-09-01', 1, 'history')`) version := insertChange(t, db, "result", 1, ActionUpsert, `{"rank":1}`) snap, err := env.hub.Snapshot(context.Background()) if err != nil { t.Fatalf("构建快照: %v", err) } if snap.Version != version { t.Fatalf("快照版本应为 %d,得到 %d", version, snap.Version) } if got := string(snap.Settings["event.name"]); got != `"测试运动会"` { t.Fatalf("settings JSON 值应透传,得到 %s", got) } if got := string(snap.Settings["display.theme"]); got != `"dark"` { t.Fatalf("settings 纯文本应编码为 JSON 字符串,得到 %s", got) } if len(snap.Teams) != 1 || snap.Teams[0].Code != "T1" || !snap.Teams[0].Active { t.Fatalf("单位不符: %+v", snap.Teams) } if len(snap.Events) != 1 { t.Fatalf("小项数应为 1,得到 %d", len(snap.Events)) } ev := snap.Events[0] if ev.Name != "100米" || ev.EventType != domain.EventTypeIndividual || !ev.Scored || ev.Compare != domain.CompareAsc { t.Fatalf("小项不符: %+v", ev) } if len(snap.Standings) != 1 { t.Fatalf("榜单行数应为 1,得到 %d", len(snap.Standings)) } row := snap.Standings[0] if row.TeamID != 1 || row.Points != 9 || row.Gold != 1 || row.Rank != 1 { t.Fatalf("榜单不符: %+v", row) } if len(snap.Records) != 1 || snap.Records[0].HolderName != "李四" || snap.Records[0].ResultValue == nil { t.Fatalf("纪录不符: %+v", snap.Records) } if len(snap.LatestResults) != 1 { t.Fatalf("最新成绩数应为 1,得到 %d", len(snap.LatestResults)) } lr := snap.LatestResults[0] if lr.ResultText != "11.58" || lr.TeamID != 1 || lr.AthleteID != 1 || lr.Rank != 1 || lr.State != "published" { t.Fatalf("最新成绩不符: %+v", lr) } } func TestEmptySnapshotMarshalsEmptyLists(t *testing.T) { env := newTestEnv(t) snap, err := env.hub.Snapshot(context.Background()) if err != nil { t.Fatalf("构建空快照: %v", err) } raw, err := json.Marshal(snap) if err != nil { t.Fatalf("序列化快照: %v", err) } for _, want := range []string{`"teams":[]`, `"events":[]`, `"standings":[]`, `"records":[]`, `"latestResults":[]`} { if !strings.Contains(string(raw), want) { t.Fatalf("空快照应包含 %s,实际 %s", want, raw) } } } func mustExec(t *testing.T, db *sql.DB, q string, args ...any) { t.Helper() if _, err := db.Exec(q, args...); err != nil { t.Fatalf("执行 SQL %q: %v", q, err) } }