//go:build integration package store import ( "context" "database/sql" "os" "path/filepath" "testing" "time" "github.com/cosmic-clash/cosmic-clash/server/domain" _ "github.com/jackc/pgx/v5/stdlib" ) // This binary is deliberately opt-in. It requires a disposable PostgreSQL // instance supplied by scripts/run_postgres_integration.sh. func openIntegrationPostgres(t *testing.T) *sql.DB { t.Helper() dsn := os.Getenv("COSMIC_CLASH_POSTGRES_DSN") if dsn == "" { t.Skip("COSMIC_CLASH_POSTGRES_DSN is not set") } db, err := sql.Open("pgx", dsn) if err != nil { t.Fatalf("open PostgreSQL: %v", err) } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := db.PingContext(ctx); err != nil { db.Close() t.Fatalf("ping PostgreSQL: %v", err) } t.Cleanup(func() { db.Close() }) return db } func applyIntegrationMigrations(t *testing.T, db *sql.DB) { t.Helper() if _, err := db.ExecContext(context.Background(), `DROP TABLE IF EXISTS assignments, audit_events, outbox, result_receipts, ranked_season_rollovers, penalties, seasons, ratings, match_participants, matches, proposal_participants, proposals, queue_tickets, idempotency_keys, sessions, identities CASCADE`); err != nil { t.Fatalf("reset PostgreSQL schema: %v", err) } for _, name := range []string{"0001_initial.sql", "0002_assignments.sql"} { path := filepath.Join("..", "migrations", name) sqlBytes, err := os.ReadFile(path) if err != nil { t.Fatal(err) } if _, err := db.ExecContext(context.Background(), string(sqlBytes)); err != nil { t.Fatalf("apply %s: %v", name, err) } } } func TestPostgreSQLQueueAdapterAgainstRealDatabase(t *testing.T) { db := openIntegrationPostgres(t) applyIntegrationMigrations(t, db) now := time.Now().UTC().Truncate(time.Microsecond) ctx := context.Background() if _, err := db.ExecContext(ctx, `INSERT INTO identities (player_id, steam_id) VALUES ('integration-player', 'integration-steam')`); err != nil { t.Fatal(err) } spec := domain.QueueSpec{Playlist: domain.Casual, ClientBuild: "integration-build", ProtocolVersion: 1} ticket, err := CreateQueueTicket(ctx, db, "integration-ticket", "integration-player", "integration-create-0001", spec, now) if err != nil { t.Fatalf("create queue ticket: %v", err) } if ticket.State != domain.Queued || ticket.Revision != 0 { t.Fatalf("unexpected ticket: %+v", ticket) } replay, err := CreateQueueTicket(ctx, db, "integration-ticket", "integration-player", "integration-create-0001", spec, now.Add(time.Second)) if err != nil { t.Fatalf("idempotent queue replay: %v", err) } if replay.TicketID != ticket.TicketID || !replay.ExpiresAt.Equal(ticket.ExpiresAt) { t.Fatalf("replay changed durable result: %+v vs %+v", replay, ticket) } if _, err := CreateQueueTicket(ctx, db, "integration-ticket-2", "integration-player", "integration-create-0002", spec, now); err == nil { t.Fatal("second active player ticket was accepted") } if _, err := GetQueueTicket(ctx, db, "integration-player", "integration-ticket", now); err != nil { t.Fatalf("owner recovery: %v", err) } if _, err := GetQueueTicket(ctx, db, "other-player", "integration-ticket", now); err == nil { t.Fatal("non-owner recovered queue ticket") } } func TestPostgreSQLMigrationsAreForwardExecutable(t *testing.T) { db := openIntegrationPostgres(t) applyIntegrationMigrations(t, db) var tableCount int if err := db.QueryRow(`SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'assignments'`).Scan(&tableCount); err != nil { t.Fatal(err) } if tableCount != 1 { t.Fatal("assignments migration did not create its table") } }