package sdkserver import ( "encoding/base64" "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" "gac-hkrpg-go/internal/common" goproto "gac-hkrpg-go/protocol/proto" "google.golang.org/protobuf/proto" ) func TestLiveBattleReportRoundTripAndExpiry(t *testing.T) { configPath := filepath.Join(t.TempDir(), "server.json") config := `{"account_name":"fixture","player_uid":52022,"activity_schedule":{"begin_time":0,"end_time":100},"srtool":{"lease_file":"fixture-session.json","lease_seconds":30},"battle_diagnostics":{"live_report_stale_seconds":10},"client_version":"fixture","game_host":"localhost","game_port":32101,"sdk_host":"localhost","sdk_port":32102,"public_game_host":"localhost","public_sdk_url":"http://localhost:32102"}` if err := os.WriteFile(configPath, []byte(config), 0600); err != nil { t.Fatal(err) } t.Setenv("GAC_CONFIG", configPath) t.Setenv("GAC_LIVE_BATTLE_STALE_SECONDS", "10") defer clearLiveBattleReport() statistics := &goproto.BattleStatistics{ TotalBattleTurns: 2, BattleAvatarList: []*goproto.AvatarBattleInfo{{ Id: 1204, StageId: 1052106, TotalDamage: 400, AttackTypeDamage: []*goproto.AttackDamageProperty{{AttackType: "Normal", Damage: 400}}, }}, MonsterList: []*goproto.MonsterBattleInfo{{MonsterId: 5035011, TotalDamageTaken: 400}}, } wire, err := proto.Marshal(statistics) if err != nil { t.Fatal(err) } requestBody, err := json.Marshal(liveBattleReportRequest{ Active: true, StatisticsBase64: base64.StdEncoding.EncodeToString(wire), }) if err != nil { t.Fatal(err) } post := httptest.NewRequest(http.MethodPost, "/api/battle_report/live", strings.NewReader(string(requestBody))) postResponse := httptest.NewRecorder() apiBattleReportLive(postResponse, post) if postResponse.Code != http.StatusOK { t.Fatalf("live POST status = %d, body = %s", postResponse.Code, postResponse.Body.String()) } getResponse := httptest.NewRecorder() apiBattleReport(getResponse, httptest.NewRequest(http.MethodGet, "/api/battle_report", nil)) var result struct { IsLive bool `json:"is_live"` Revision int64 `json:"revision"` Report common.BattleReport `json:"report"` } if err := json.Unmarshal(getResponse.Body.Bytes(), &result); err != nil { t.Fatal(err) } if !result.IsLive || result.Report.EndStatus != "LIVE" || result.Report.StageID != 1052106 { t.Fatalf("unexpected live response: %+v", result) } if result.Report.TeamTotalDamage != 400 || result.Report.Avatars[0].DamagePercent != 100 || result.Revision == 0 { t.Fatalf("unexpected live damage values: %+v", result.Report) } avatarSnapshots := []*goproto.AvatarBattleInfo{ { Id: 1409, StageId: 1052106, TotalDamage: 600, TotalTurns: 2, AttackTypeDamage: []*goproto.AttackDamageProperty{{AttackType: "BPSkill", Damage: 600}}, }, { Id: 1507, StageId: 1052106, TotalDamage: 400, TotalTurns: 1, AttackTypeDamage: []*goproto.AttackDamageProperty{{AttackType: "Normal", Damage: 400}}, }, } avatarStatisticsBase64 := make([]string, 0, len(avatarSnapshots)) for _, avatar := range avatarSnapshots { wire, err := proto.Marshal(avatar) if err != nil { t.Fatal(err) } avatarStatisticsBase64 = append(avatarStatisticsBase64, base64.StdEncoding.EncodeToString(wire)) } requestBody, err = json.Marshal(liveBattleReportRequest{ Active: true, AvatarStatisticsBase64: avatarStatisticsBase64, TotalBattleTurns: 3, }) if err != nil { t.Fatal(err) } post = httptest.NewRequest(http.MethodPost, "/api/battle_report/live", strings.NewReader(string(requestBody))) postResponse = httptest.NewRecorder() apiBattleReportLive(postResponse, post) if postResponse.Code != http.StatusOK { t.Fatalf("avatar snapshot POST status = %d, body = %s", postResponse.Code, postResponse.Body.String()) } getResponse = httptest.NewRecorder() apiBattleReport(getResponse, httptest.NewRequest(http.MethodGet, "/api/battle_report", nil)) if err := json.Unmarshal(getResponse.Body.Bytes(), &result); err != nil { t.Fatal(err) } if !result.IsLive || result.Report.TotalBattleTurns != 3 || result.Report.TeamTotalDamage != 1000 { t.Fatalf("unexpected avatar-only live report: %+v", result) } if len(result.Report.Avatars) != 2 || result.Report.Avatars[0].DamagePercent != 60 || result.Report.Avatars[1].DamagePercent != 40 { t.Fatalf("unexpected avatar-only live shares: %+v", result.Report.Avatars) } if result.Report.Avatars[0].AttackTypeDamage["BPSkill"] != 600 || result.Report.Avatars[1].AttackTypeDamage["Normal"] != 400 { t.Fatalf("unexpected avatar-only live breakdown: %+v", result.Report.Avatars) } servantWire, _ := proto.Marshal(&goproto.BattleAvatarServantInfo{AvatarId: avatarSnapshots[1].Id, EntityId: 88, TotalDamage: 1000}) requestBody, _ = json.Marshal(liveBattleReportRequest{ Active: true, AvatarStatisticsBase64: avatarStatisticsBase64, ServantStatisticsBase64: []string{base64.StdEncoding.EncodeToString(servantWire)}, ServantBreakdowns: []liveAvatarBreakdown{{Damage: map[string]float64{"BPSkill": 900}}}, }) postResponse = httptest.NewRecorder() apiBattleReportLive(postResponse, httptest.NewRequest(http.MethodPost, "/api/battle_report/live", strings.NewReader(string(requestBody)))) if postResponse.Code != http.StatusOK { t.Fatalf("servant POST: %s", postResponse.Body.String()) } getResponse = httptest.NewRecorder() apiBattleReport(getResponse, httptest.NewRequest(http.MethodGet, "/api/battle_report", nil)) if err := json.Unmarshal(getResponse.Body.Bytes(), &result); err != nil { t.Fatal(err) } owner := result.Report.Avatars[1] if !result.IsLive || result.Report.TeamTotalDamage != 2000 || owner.TotalDamage != 1400 || owner.MemospriteDamage != 1000 || owner.DamagePercent != 70 || owner.AttackTypeDamage["BPSkill"] != 900 { t.Fatalf("servant data lost through live API: report=%+v owner=%+v", result.Report, owner) } liveBattleReportState.Lock() liveBattleReportState.received = time.Now().Add(-11 * time.Second) liveBattleReportState.Unlock() if _, _, live, err := latestLiveBattleReport(); err != nil || live { t.Fatalf("expired live report should be ignored, live=%t err=%v", live, err) } }