package systemconfig import ( "encoding/json" "strings" "testing" ) func TestDecodeCP1252Mojibake(t *testing.T) { original := `{"label":"6级子弹","price":"0.26元/发","punct":"(:,)"}` mojibake := encodeCP1252Mojibake(original) decoded, ok := decodeCP1252Mojibake(mojibake) if !ok { t.Fatal("期望识别出 CP1252 乱码") } if decoded != original { t.Fatalf("还原结果 = %q, want %q", decoded, original) } if _, ok := decodeCP1252Mojibake(original); ok { t.Fatal("正常中文不应被误判为乱码") } if _, ok := decodeCP1252Mojibake("plain ascii"); ok { t.Fatal("纯 ASCII 不应被误判为乱码") } } func TestRepairMojibakeJSONRepairsIndividualFields(t *testing.T) { raw := `{"normal":"正常字段","broken":"` + encodeCP1252Mojibake("6级子弹") + `"}` repaired := repairMojibakeJSON(raw) var payload map[string]string if err := json.Unmarshal([]byte(repaired), &payload); err != nil { t.Fatalf("还原后的 JSON 无法解析: %v", err) } if payload["normal"] != "正常字段" { t.Fatalf("正常字段被破坏: %q", payload["normal"]) } if payload["broken"] != "6级子弹" { t.Fatalf("乱码字段未还原: %q", payload["broken"]) } } func TestNormalizePublishOptionsValueMigratesLegacyQuantityItems(t *testing.T) { legacy := `{ "server_options": ["QQ"], "quantity_items": [ {"key": "kit5", "label": "5级全装套数量", "price": "5元/个"}, {"key": "level6Ammo", "label": "6级子弹", "price": "0.26元/发"} ] }` normalized, err := normalizePublishOptionsValue(legacy) if err != nil { t.Fatalf("规范化失败: %v", err) } var options PublishOptionsDTO if err := json.Unmarshal([]byte(normalized), &options); err != nil { t.Fatalf("解析规范化结果失败: %v", err) } if options.SchemaVersion != publishOptionsSchemaVersion { t.Fatalf("schema_version = %d, want %d", options.SchemaVersion, publishOptionsSchemaVersion) } for _, item := range options.QuantityItems { if item.Key == "kit5" { t.Fatal("旧版 kit5 选项应被迁移掉") } } if !hasQuantityItemKey(options.QuantityItems, "level6Ammo") { t.Fatal("迁移后的默认选项缺少 level6Ammo") } } func TestNormalizePublishOptionsValuePreservesCurrentSchema(t *testing.T) { options := DefaultPublishOptions() for index := range options.ScreenshotSlots { if options.ScreenshotSlots[index].Key == "tencentSecurity" { options.ScreenshotSlots[index].Required = true } } options.QuantityItems = append(options.QuantityItems, PublishQuantityItem{ Key: "kit5", Label: "自定义旧选项", Price: "9元/个", }) raw, err := json.Marshal(options) if err != nil { t.Fatalf("构造当前版本配置失败: %v", err) } normalized, err := normalizePublishOptionsValue(string(raw)) if err != nil { t.Fatalf("规范化失败: %v", err) } var got PublishOptionsDTO if err := json.Unmarshal([]byte(normalized), &got); err != nil { t.Fatalf("解析规范化结果失败: %v", err) } if !hasQuantityItemKey(got.QuantityItems, "kit5") { t.Fatal("当前版本配置中的自定义项不应被旧版迁移清掉") } for _, slot := range got.ScreenshotSlots { if slot.Key == "tencentSecurity" && !slot.Required { t.Fatal("当前版本配置中 tencentSecurity.required=true 不应被覆盖") } } } func TestNormalizePublishOptionsValueMigratesSchemaV1Switches(t *testing.T) { legacy := `{"schema_version":1,"next_season_pass_enabled":false,"server_options":["QQ"]}` normalized, err := normalizePublishOptionsValue(legacy) if err != nil { t.Fatalf("规范化失败: %v", err) } var got PublishOptionsDTO if err := json.Unmarshal([]byte(normalized), &got); err != nil { t.Fatalf("解析规范化结果失败: %v", err) } if got.SchemaVersion != publishOptionsSchemaVersion { t.Fatalf("schema_version = %d, want %d", got.SchemaVersion, publishOptionsSchemaVersion) } if got.UnlockDemulanEnabled { t.Fatal("v1 升级后「是否解锁德穆兰」应默认关闭") } if !got.NextSeasonPassEnabled { t.Fatal("v1 升级后「下赛季是否购买通行证」应默认打开") } } func TestNormalizePublishOptionsValueKeepsSchemaV2Switches(t *testing.T) { options := DefaultPublishOptions() options.UnlockDemulanEnabled = true options.NextSeasonPassEnabled = false raw, err := json.Marshal(options) if err != nil { t.Fatalf("构造配置失败: %v", err) } normalized, err := normalizePublishOptionsValue(string(raw)) if err != nil { t.Fatalf("规范化失败: %v", err) } var got PublishOptionsDTO if err := json.Unmarshal([]byte(normalized), &got); err != nil { t.Fatalf("解析规范化结果失败: %v", err) } if !got.UnlockDemulanEnabled { t.Fatal("v2 配置中后台手动开启的德穆兰开关不应被覆盖") } if got.NextSeasonPassEnabled { t.Fatal("v2 配置中后台手动关闭的通行证开关不应被覆盖") } } func TestNormalizePublishOptionsValueRepairsMojibake(t *testing.T) { options := DefaultPublishOptions() raw, err := json.Marshal(options) if err != nil { t.Fatalf("构造配置失败: %v", err) } mojibake := encodeCP1252Mojibake(string(raw)) normalized, err := normalizePublishOptionsValue(mojibake) if err != nil { t.Fatalf("规范化乱码配置失败: %v", err) } var got PublishOptionsDTO if err := json.Unmarshal([]byte(normalized), &got); err != nil { t.Fatalf("解析规范化结果失败: %v", err) } if !containsString(got.ServerOptions, "微信") { t.Fatalf("乱码未恢复,server_options = %#v", got.ServerOptions) } if got.SchemaVersion != publishOptionsSchemaVersion { t.Fatalf("schema_version = %d, want %d", got.SchemaVersion, publishOptionsSchemaVersion) } } func encodeCP1252Mojibake(value string) string { var builder strings.Builder for _, item := range []byte(value) { if item >= 0x80 && item <= 0x9F { builder.WriteRune(cp1252HighRunes[item-0x80]) continue } builder.WriteRune(rune(item)) } return builder.String() } func hasQuantityItemKey(items []PublishQuantityItem, key string) bool { for _, item := range items { if item.Key == key { return true } } return false } func containsString(items []string, target string) bool { for _, item := range items { if item == target { return true } } return false }