Refactor series handling in backend to include player context in Toss, Ban, Pick, and Record actions. Update service methods to accept player parameters, enhancing authorization checks and ensuring proper team actions. Modify integration tests to reflect new method signatures and improve tournament hydration logic in the store. Enhance frontend components to support new series features and improve user experience with live match navigation.
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / compose (push) Has been cancelled

This commit is contained in:
2026-07-19 03:22:32 +03:00
parent 4b8218ec14
commit 5d3e66d2f1
16 changed files with 449 additions and 62 deletions

View File

@@ -135,20 +135,20 @@ func TestFullScrimPipeline(t *testing.T) {
t.Fatalf("start failed: %#v, %v", started, err)
}
seriesIDs = append(seriesIDs, started.Series.ID)
series, err := service.TossSeriesCoin(ctx, admin, started.Series.ID, "integration-seed", started.Series.Version)
series, err := service.TossSeriesCoin(ctx, admin, domain.Player{}, started.Series.ID, "integration-seed", started.Series.TeamAID, started.Series.Version)
if err != nil {
t.Fatal(err)
}
for mapNumber := 0; mapNumber < 2; mapNumber++ {
if series.Phase == domain.MapPickPhase {
series, err = service.PickSeriesMap(ctx, admin, series.ID, series.NextMapPickerID, series.AvailableMaps[0], series.Version)
series, err = service.PickSeriesMap(ctx, admin, domain.Player{}, series.ID, series.NextMapPickerID, series.AvailableMaps[0], series.Version)
if err != nil {
t.Fatal(err)
}
}
for series.Phase != domain.HeroBanPhase {
name := firstUnbanned(series.MapDraft.Pool, series.MapDraft.Banned)
series, err = service.BanSeriesMap(ctx, admin, series.ID, series.MapDraft.NextTeam(), name, series.Version)
series, err = service.BanSeriesMap(ctx, admin, domain.Player{}, series.ID, series.MapDraft.NextTeam(), name, series.Version)
if err != nil {
t.Fatal(err)
}
@@ -156,12 +156,12 @@ func TestFullScrimPipeline(t *testing.T) {
for series.Phase != domain.PlayingPhase {
teamID := series.HeroDraft.NextTeam()
hero := firstAvailableHero(series.HeroDraft, teamID)
series, err = service.BanSeriesHero(ctx, admin, series.ID, teamID, hero, series.Version)
series, err = service.BanSeriesHero(ctx, admin, domain.Player{}, series.ID, teamID, hero, series.Version)
if err != nil {
t.Fatal(err)
}
}
series, err = service.RecordSeriesResult(ctx, admin, series.ID, domain.TeamAWin, series.Version)
series, err = service.RecordSeriesResult(ctx, admin, domain.Player{}, series.ID, series.TeamAID, domain.TeamAWin, series.Version)
if err != nil {
t.Fatal(err)
}
@@ -175,6 +175,97 @@ func TestFullScrimPipeline(t *testing.T) {
}
}
func TestParallelTournamentHydratesLiveSeriesAndFinal(t *testing.T) {
databaseURL := os.Getenv("TEST_DATABASE_URL")
if databaseURL == "" {
t.Skip("TEST_DATABASE_URL is not configured")
}
ctx := context.Background()
if err := Migrate(ctx, databaseURL, "../../../migrations"); err != nil {
t.Fatal(err)
}
store, err := Open(ctx, databaseURL)
if err != nil {
t.Fatal(err)
}
defer store.Close()
suffix := fmt.Sprintf("%d", time.Now().UnixNano())
accountID, eventID, tournamentID := "parallel-admin-"+suffix, "parallel-event-"+suffix, "parallel-tournament-"+suffix
_, err = store.pool.Exec(ctx, `INSERT INTO accounts(id,discord_id,username,avatar_url,role,created_at) VALUES($1,$1,'Parallel Admin','','admin',now())`, accountID)
if err != nil {
t.Fatal(err)
}
defer func() {
_, _ = store.pool.Exec(ctx, `DELETE FROM series WHERE tournament_id=$1`, tournamentID)
_, _ = store.pool.Exec(ctx, `DELETE FROM events WHERE id=$1`, eventID)
_, _ = store.pool.Exec(ctx, `DELETE FROM accounts WHERE id=$1`, accountID)
}()
service := application.New(store, discardPublisher{})
event, err := service.CreateEvent(ctx, domain.Account{ID: accountID, Role: domain.RoleAdmin}, domain.Event{
ID: eventID, Name: "Parallel tournament", StartsAt: time.Now().Add(time.Hour), EndsAt: time.Now().Add(3 * time.Hour),
RegistrationDeadline: time.Now(), RulesetID: "standard-control-hybrid-control",
})
if err != nil {
t.Fatal(err)
}
eventID = event.ID
rules, err := store.GetRuleset(ctx, event.RulesetID)
if err != nil {
t.Fatal(err)
}
tournament, err := domain.NewTournament(tournamentID, eventID, event.Name, []string{"team-a", "team-b", "team-c", "team-d"})
if err != nil {
t.Fatal(err)
}
if _, err = store.SaveTournament(ctx, *tournament); err != nil {
t.Fatal(err)
}
for match := range tournament.Rounds[0] {
slot := tournament.Rounds[0][match]
series, createErr := domain.NewSeries(slot.ID, eventID, tournamentID, [2]string{slot.TeamAID, slot.TeamBID}, rules)
if createErr != nil {
t.Fatal(createErr)
}
if match == 0 {
if createErr = series.Toss("parallel-seed", accountID, time.Now().UTC(), rules); createErr != nil {
t.Fatal(createErr)
}
}
if _, createErr = store.SaveSeries(ctx, *series); createErr != nil {
t.Fatal(createErr)
}
}
hydrated, err := store.GetTournamentByEvent(ctx, eventID)
if err != nil {
t.Fatal(err)
}
if hydrated.Rounds[0][0].Phase != domain.MapBanPhase || hydrated.Rounds[0][1].Phase != domain.CoinTossPending {
t.Fatalf("parallel series were not hydrated independently: %s / %s", hydrated.Rounds[0][0].Phase, hydrated.Rounds[0][1].Phase)
}
for match := range hydrated.Rounds[0] {
hydrated.Rounds[0][match].WinnerTeamID = hydrated.Rounds[0][match].TeamAID
hydrated.Rounds[0][match].Phase = domain.SeriesComplete
if err = hydrated.Advance(0, match); err != nil {
t.Fatal(err)
}
}
if _, err = store.SaveTournament(ctx, hydrated); err != nil {
t.Fatal(err)
}
finalSlot := hydrated.Rounds[1][0]
final, err := domain.NewSeries(finalSlot.ID, eventID, tournamentID, [2]string{finalSlot.TeamAID, finalSlot.TeamBID}, rules)
if err != nil {
t.Fatal(err)
}
if _, err = store.SaveSeries(ctx, *final); err != nil {
t.Fatal(err)
}
hydrated, err = store.GetTournamentByEvent(ctx, eventID)
if err != nil || hydrated.Rounds[1][0].Phase != domain.CoinTossPending {
t.Fatalf("final was not persisted and hydrated: phase=%s err=%v", hydrated.Rounds[1][0].Phase, err)
}
}
func firstUnbanned(pool, banned []string) string {
for _, name := range pool {
found := false