Files
mixmaker/backend/internal/adapter/postgres/store_integration_test.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 ""
}