209 lines
6.5 KiB
Go
209 lines
6.5 KiB
Go
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 == "" {
|
|
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()
|
|
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 ""
|
|
}
|