package dto import ( "encoding/json" "testing" "git.yeij.top/AskaEth/Scoreboard/server/internal/domain" ) // 非 admin 会话只能看到大屏渲染所需字段:admin-only 字段必须清零, // 且序列化后仍是显式的 null / 0,不能变成字段缺失。 func TestEventSettingsWithoutAdminFields(t *testing.T) { profileID := int64(3) full := EventSettings{ Event: EventSettingsEvent{Name: "运动会"}, Display: EventSettingsDisplay{Title: "大屏", RotateSecond: 15}, Standings: EventSettingsStandings{IndividualToTeamWeight: 0.5, RankingMode: "points", ShowIndividualBoard: true, ShowIndividualMedals: false}, DefaultProfileID: &profileID, RequireApproval: true, } got := full.WithoutAdminFields() if got.DefaultProfileID != nil { t.Fatalf("defaultProfileId 应置空,得到 %v", got.DefaultProfileID) } if got.RequireApproval { t.Fatalf("requireApproval 应清零") } if got.Standings.IndividualToTeamWeight != 0 { t.Fatalf("individualToTeamWeight 应清零,得到 %v", got.Standings.IndividualToTeamWeight) } if got.Event.Name != "运动会" || got.Display.Title != "大屏" || got.Display.RotateSecond != 15 { t.Fatalf("event/display 不应被裁剪: %+v", got) } if got.Standings.RankingMode != "points" || !got.Standings.ShowIndividualBoard || got.Standings.ShowIndividualMedals { t.Fatalf("榜单开关不应被裁剪: %+v", got.Standings) } raw, err := json.Marshal(got) if err != nil { t.Fatalf("序列化: %v", err) } var top map[string]json.RawMessage if err := json.Unmarshal(raw, &top); err != nil { t.Fatalf("解析: %v", err) } if string(top["defaultProfileId"]) != "null" { t.Fatalf("defaultProfileId 应显式序列化为 null: %s", raw) } if string(top["requireApproval"]) != "false" { t.Fatalf("requireApproval 应显式序列化为 false: %s", raw) } var standings map[string]json.RawMessage if err := json.Unmarshal(top["standings"], &standings); err != nil { t.Fatalf("解析 standings: %v", err) } if string(standings["individualToTeamWeight"]) != "0" { t.Fatalf("individualToTeamWeight 应显式序列化为 0: %s", raw) } } // 公告是给大屏底栏用的展示字段,非 admin 的快照也必须带上它。 func TestEventSettingsWithoutAdminFieldsKeepsAnnounce(t *testing.T) { announce := "决赛推迟到 15:00" full := EventSettings{Display: EventSettingsDisplay{Announce: &announce}} got := full.WithoutAdminFields() if got.Display.Announce == nil || *got.Display.Announce != announce { t.Fatalf("非 admin 快照应保留公告,得到 %v", got.Display.Announce) } } // 公告正文取自 settings 表的 display.announce_text(由大屏指令写入); // 空串表示无公告,必须下发显式 null,底栏才能回落到「暂无公告」。 func TestParseEventSettingsAnnounce(t *testing.T) { withText := ParseEventSettings(map[string]string{"display.announce_text": "决赛推迟到 15:00"}) if withText.Display.Announce == nil || *withText.Display.Announce != "决赛推迟到 15:00" { t.Fatalf("公告应随 settings 下发,得到 %v", withText.Display.Announce) } empty := ParseEventSettings(map[string]string{"display.announce_text": ""}) if empty.Display.Announce != nil { t.Fatalf("空串表示无公告,应解析为 null,得到 %q", *empty.Display.Announce) } raw, err := json.Marshal(empty) if err != nil { t.Fatalf("序列化: %v", err) } var display map[string]json.RawMessage if err := json.Unmarshal(mustField(t, raw, "display"), &display); err != nil { t.Fatalf("解析 display: %v", err) } if string(display["announce"]) != "null" { t.Fatalf("无公告时应显式序列化为 null: %s", raw) } } // 字体 id 是给大屏渲染用的展示字段,非 admin 的快照也必须带上它, // 否则 guest/recorder 打开的大屏只会用默认字体。 func TestEventSettingsWithoutAdminFieldsKeepsFonts(t *testing.T) { title := "builtin:noto-serif" body := "custom:思源宋体.ttf" full := EventSettings{Display: EventSettingsDisplay{FontTitle: &title, FontBody: &body}} got := full.WithoutAdminFields() if got.Display.FontTitle == nil || *got.Display.FontTitle != title { t.Fatalf("非 admin 快照应保留标题字体,得到 %v", got.Display.FontTitle) } if got.Display.FontBody == nil || *got.Display.FontBody != body { t.Fatalf("非 admin 快照应保留正文字体,得到 %v", got.Display.FontBody) } raw, err := json.Marshal(got) if err != nil { t.Fatalf("序列化: %v", err) } var display map[string]json.RawMessage if err := json.Unmarshal(mustField(t, raw, "display"), &display); err != nil { t.Fatalf("解析 display: %v", err) } if string(display["fontTitle"]) != `"`+title+`"` || string(display["fontBody"]) != `"`+body+`"` { t.Fatalf("字体应随 display 一起下发: %s", raw) } } // 字体取自 settings 表的 display.font_title / display.font_body; // 空串与缺键都表示用默认字体,必须下发显式 null。 func TestParseEventSettingsFonts(t *testing.T) { withFonts := ParseEventSettings(map[string]string{ "display.font_title": "builtin:misans", "display.font_body": "custom:my-font.ttf", }) if withFonts.Display.FontTitle == nil || *withFonts.Display.FontTitle != "builtin:misans" { t.Fatalf("标题字体应随 settings 下发,得到 %v", withFonts.Display.FontTitle) } if withFonts.Display.FontBody == nil || *withFonts.Display.FontBody != "custom:my-font.ttf" { t.Fatalf("正文字体应随 settings 下发,得到 %v", withFonts.Display.FontBody) } empty := ParseEventSettings(map[string]string{"display.font_title": "", "display.font_body": ""}) if empty.Display.FontTitle != nil || empty.Display.FontBody != nil { t.Fatalf("空串表示默认字体,应解析为 null,得到 %v / %v", empty.Display.FontTitle, empty.Display.FontBody) } raw, err := json.Marshal(empty) if err != nil { t.Fatalf("序列化: %v", err) } var display map[string]json.RawMessage if err := json.Unmarshal(mustField(t, raw, "display"), &display); err != nil { t.Fatalf("解析 display: %v", err) } if string(display["fontTitle"]) != "null" || string(display["fontBody"]) != "null" { t.Fatalf("未配置字体时应显式序列化为 null: %s", raw) } } // mustField 取出顶层字段的原始 JSON,供字段级断言使用。 func mustField(t *testing.T, raw []byte, key string) []byte { t.Helper() var top map[string]json.RawMessage if err := json.Unmarshal(raw, &top); err != nil { t.Fatalf("解析 %s: %v (%s)", key, err, raw) } return top[key] } // 单位/小项的 DTO 必须把可空列写成显式 null,不能沿用 domain 的 omitempty。 func TestTeamAndEventFromDomainNullableFields(t *testing.T) { teamRaw, err := json.Marshal(TeamFromDomain(domain.Team{ID: 1, Code: "T1", Name: "第一单位"})) if err != nil { t.Fatalf("序列化单位: %v", err) } var team map[string]json.RawMessage if err := json.Unmarshal(teamRaw, &team); err != nil { t.Fatalf("解析单位: %v", err) } for _, key := range []string{"shortName", "logoPath", "color"} { if got, ok := team[key]; !ok || string(got) != "null" { t.Fatalf("单位字段 %s 应为显式 null: %s", key, teamRaw) } } eventRaw, err := json.Marshal(EventFromDomain(domain.Event{ID: 1, Code: "E1", Name: "100米"})) if err != nil { t.Fatalf("序列化小项: %v", err) } var event map[string]json.RawMessage if err := json.Unmarshal(eventRaw, &event); err != nil { t.Fatalf("解析小项: %v", err) } for _, key := range []string{"scheduledAt", "venue", "remark"} { if got, ok := event[key]; !ok || string(got) != "null" { t.Fatalf("小项字段 %s 应为显式 null: %s", key, eventRaw) } } if string(event["sortOrder"]) != "0" { t.Fatalf("sortOrder 应保留 0: %s", eventRaw) } }