Add event management features to API, including event cancellation, deletion, registration closure, and workflow balance generation. Introduce new roster management endpoints and enhance participant handling with avoided player preferences. Update database schema and service logic to support these changes.
This commit is contained in:
127
backend/internal/domain/workflow_test.go
Normal file
127
backend/internal/domain/workflow_test.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestEventWorkflowRejectsStaleVersion(t *testing.T) {
|
||||
event := Event{State: RegistrationOpen, Version: 2}
|
||||
if err := event.Transition([]EventState{RegistrationOpen}, RegistrationClosed, 1); !errors.Is(err, ErrConflict) {
|
||||
t.Fatalf("expected stale version conflict, got %v", err)
|
||||
}
|
||||
if event.State != RegistrationOpen || event.Version != 2 {
|
||||
t.Fatal("stale transition mutated the event")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRosterSwapAndReserveKeepRoleShape(t *testing.T) {
|
||||
roster := testRoster()
|
||||
if err := roster.Swap("a", "a-d1", "b", "b-s1"); !errors.Is(err, ErrInvalid) {
|
||||
t.Fatalf("expected cross-role swap rejection, got %v", err)
|
||||
}
|
||||
if err := roster.Swap("a", "a-d1", "b", "b-d1"); err != nil {
|
||||
t.Fatalf("same-role swap failed: %v", err)
|
||||
}
|
||||
if err := roster.Substitute("a", "b-d1", "reserve", 30); err != nil {
|
||||
t.Fatalf("reserve substitution failed: %v", err)
|
||||
}
|
||||
if roster.Reserve[0] != "b-d1" {
|
||||
t.Fatalf("displaced player was not returned to reserve: %#v", roster.Reserve)
|
||||
}
|
||||
if err := roster.Validate(false); err != nil {
|
||||
t.Fatalf("valid 1/2/2 roster rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeriesFSMRunsCoinBansAndResult(t *testing.T) {
|
||||
rules := testRules()
|
||||
series, err := NewSeries("series", "event", "", [2]string{"a", "b"}, rules)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
now := time.Unix(100, 0).UTC()
|
||||
if err = series.Toss("seed", "admin", now, rules); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for !func() bool { _, selected := series.MapDraft.Selected(); return selected }() {
|
||||
name := firstAvailableMap(series.MapDraft)
|
||||
if err = series.BanMap(series.MapDraft.NextTeam(), name, "admin", now, rules); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if series.Phase != HeroBanPhase {
|
||||
t.Fatalf("expected hero bans, got %s", series.Phase)
|
||||
}
|
||||
for _, hero := range []string{"Tank A", "Tank B", "Damage A", "Damage B"} {
|
||||
if err = series.BanHero(series.HeroDraft.NextTeam(), hero, "admin", now); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if series.Phase != PlayingPhase {
|
||||
t.Fatalf("expected playing phase, got %s", series.Phase)
|
||||
}
|
||||
if err = series.RecordCurrentMap(TeamAWin, "admin", now, rules); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if series.Phase != MapPickPhase || len(series.PlayedMaps) != 1 || series.NextMapPickerID != "b" {
|
||||
t.Fatalf("next map was not prepared: phase=%s maps=%v", series.Phase, series.PlayedMaps)
|
||||
}
|
||||
if contains(series.AvailableMaps, series.PlayedMaps[0]) {
|
||||
t.Fatal("played map remained available in a later pool")
|
||||
}
|
||||
nextMap := series.AvailableMaps[0]
|
||||
if err = series.PickMap("a", nextMap, "admin", now, rules); !errors.Is(err, ErrConflict) {
|
||||
t.Fatalf("winner was allowed to pick the next map: %v", err)
|
||||
}
|
||||
if err = series.PickMap("b", nextMap, "admin", now, rules); err != nil || series.Phase != HeroBanPhase {
|
||||
t.Fatalf("loser could not pick the next map: phase=%s err=%v", series.Phase, err)
|
||||
}
|
||||
}
|
||||
|
||||
func testRoster() RosterDraft {
|
||||
teams := make([]Team, 2)
|
||||
for teamIndex, id := range []string{"a", "b"} {
|
||||
slots := []Slot{{PlayerID: id + "-t", Role: Tank, Rating: 20}}
|
||||
for i := 1; i <= 2; i++ {
|
||||
slots = append(slots,
|
||||
Slot{PlayerID: fmt.Sprintf("%s-d%d", id, i), Role: Damage, Rating: 20},
|
||||
Slot{PlayerID: fmt.Sprintf("%s-s%d", id, i), Role: Support, Rating: 20},
|
||||
)
|
||||
}
|
||||
teams[teamIndex] = Team{ID: id, EventID: "event", Name: id, Slots: slots}
|
||||
}
|
||||
return RosterDraft{EventID: "event", Teams: teams, Reserve: []string{"reserve"}}
|
||||
}
|
||||
|
||||
func testRules() Ruleset {
|
||||
return Ruleset{
|
||||
ID: "rules", BestOf: 3, HeroBansPerTeam: 2,
|
||||
MapPools: [][]string{{"Map 1", "Map 2", "Map 3"}, {"Map 1", "Map 4", "Map 5"}, {"Map 1", "Map 2", "Map 6"}},
|
||||
Heroes: []Hero{
|
||||
{Name: "Tank A", Role: Tank}, {Name: "Tank B", Role: Tank},
|
||||
{Name: "Damage A", Role: Damage}, {Name: "Damage B", Role: Damage},
|
||||
{Name: "Support A", Role: Support}, {Name: "Support B", Role: Support},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func firstAvailableMap(draft *MapDraft) string {
|
||||
for _, name := range draft.Pool {
|
||||
if !contains(draft.Banned, name) {
|
||||
return name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func contains(values []string, value string) bool {
|
||||
for _, item := range values {
|
||||
if item == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user