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:
@@ -2,10 +2,19 @@ package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"mixmaker/backend/internal/application"
|
||||
"mixmaker/backend/internal/domain"
|
||||
)
|
||||
|
||||
type discardPublisher struct{}
|
||||
|
||||
func (discardPublisher) Publish(string, any) {}
|
||||
|
||||
func TestMigrationsAndReadiness(t *testing.T) {
|
||||
databaseURL := os.Getenv("TEST_DATABASE_URL")
|
||||
if databaseURL == "" {
|
||||
@@ -23,4 +32,177 @@ func TestMigrationsAndReadiness(t *testing.T) {
|
||||
if err := store.Ready(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var workflowColumns int
|
||||
if err := store.pool.QueryRow(ctx, `SELECT count(*) FROM information_schema.columns
|
||||
WHERE table_name='events' AND column_name IN ('state','version','ruleset_id','active_series_id','tournament_id')`).Scan(&workflowColumns); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if workflowColumns != 5 {
|
||||
t.Fatalf("workflow migration is incomplete: found %d event columns", workflowColumns)
|
||||
}
|
||||
var rosterTable bool
|
||||
if err := store.pool.QueryRow(ctx, `SELECT to_regclass('public.event_rosters') IS NOT NULL`).Scan(&rosterTable); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !rosterTable {
|
||||
t.Fatal("event_rosters table is missing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFullScrimPipeline(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 := "pipeline-admin-"+suffix, "pipeline-event-"+suffix
|
||||
playerIDs, seriesIDs := make([]string, 0, 10), make([]string, 0, 1)
|
||||
_, err = store.pool.Exec(ctx, `INSERT INTO accounts(id,discord_id,username,avatar_url,role,created_at)
|
||||
VALUES($1,$2,'Pipeline Admin','','admin',now())`, accountID, accountID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
_, _ = store.pool.Exec(ctx, `DELETE FROM audit_log WHERE actor_account_id=$1`, accountID)
|
||||
_, _ = store.pool.Exec(ctx, `DELETE FROM events WHERE id=$1`, eventID)
|
||||
for _, seriesID := range seriesIDs {
|
||||
_, _ = store.pool.Exec(ctx, `DELETE FROM series WHERE id=$1`, seriesID)
|
||||
}
|
||||
for _, playerID := range playerIDs {
|
||||
_, _ = store.pool.Exec(ctx, `DELETE FROM players WHERE id=$1`, playerID)
|
||||
}
|
||||
_, _ = store.pool.Exec(ctx, `DELETE FROM accounts WHERE id=$1`, accountID)
|
||||
}()
|
||||
|
||||
service := application.New(store, discardPublisher{})
|
||||
admin := domain.Account{ID: accountID, Role: domain.RoleAdmin}
|
||||
now := time.Now().UTC()
|
||||
event, err := service.CreateEvent(ctx, admin, domain.Event{
|
||||
ID: eventID, Name: "Pipeline", StartsAt: now.Add(24 * time.Hour), EndsAt: now.Add(28 * time.Hour),
|
||||
RegistrationDeadline: now.Add(23 * time.Hour), RulesetID: "standard-control-hybrid-control",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// CreateEvent owns the identifier, so use the returned value from here onward.
|
||||
eventID = event.ID
|
||||
for i := 0; i < 10; i++ {
|
||||
player, _, createErr := service.CreateParticipant(ctx, admin, eventID, fmt.Sprintf("Player %d", i+1),
|
||||
domain.Ratings{Tank: 18 + i%5, Damage: 19 + i%5, Support: 20 + i%5}, domain.Going)
|
||||
if createErr != nil {
|
||||
t.Fatal(createErr)
|
||||
}
|
||||
playerIDs = append(playerIDs, player.ID)
|
||||
}
|
||||
event, err = service.CloseRegistration(ctx, admin, eventID, event.RulesetID, event.Version)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
balance, err := service.GenerateBalance(ctx, admin, eventID, event.Version)
|
||||
if err != nil || len(balance.Candidates) == 0 {
|
||||
t.Fatalf("balance failed: candidates=%d err=%v", len(balance.Candidates), err)
|
||||
}
|
||||
roster, err := service.SelectWorkflowBalance(ctx, admin, eventID, balance.Candidates[0], balance.Event.Version)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, team := range roster.Teams {
|
||||
roster, err = service.SetRosterCaptain(ctx, admin, eventID, team.ID, team.Slots[0].PlayerID, roster.Version)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
event, err = store.GetEvent(ctx, eventID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
event, err = service.ConfirmRosters(ctx, admin, eventID, event.Version, roster.Version)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
started, err := service.StartScrim(ctx, admin, eventID, event.Version)
|
||||
if err != nil || started.Series == nil {
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
series, err = service.RecordSeriesResult(ctx, admin, series.ID, domain.TeamAWin, series.Version)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if series.Phase != domain.SeriesComplete {
|
||||
t.Fatalf("series did not complete: %s", series.Phase)
|
||||
}
|
||||
event, err = store.GetEvent(ctx, eventID)
|
||||
if err != nil || event.State != domain.Completed {
|
||||
t.Fatalf("event did not complete: state=%s err=%v", event.State, err)
|
||||
}
|
||||
}
|
||||
|
||||
func firstUnbanned(pool, banned []string) string {
|
||||
for _, name := range pool {
|
||||
found := false
|
||||
for _, item := range banned {
|
||||
found = found || item == name
|
||||
}
|
||||
if !found {
|
||||
return name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func firstAvailableHero(draft *domain.HeroDraft, teamID string) string {
|
||||
for _, hero := range draft.Heroes {
|
||||
usedRole, usedHero, ownRepeat := false, false, false
|
||||
for _, role := range draft.CurrentRoles[teamID] {
|
||||
usedRole = usedRole || role == hero.Role
|
||||
}
|
||||
for _, ban := range draft.CurrentBans {
|
||||
usedHero = usedHero || ban.Value == hero.Name
|
||||
}
|
||||
for _, name := range draft.SeriesBans[teamID] {
|
||||
ownRepeat = ownRepeat || name == hero.Name
|
||||
}
|
||||
if !usedRole && !usedHero && !ownRepeat {
|
||||
return hero.Name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user