//go:build integration package allocator import ( "context" "database/sql" "encoding/json" "fmt" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "time" "github.com/cosmic-clash/cosmic-clash/server/agones" "github.com/cosmic-clash/cosmic-clash/server/migrations" "github.com/cosmic-clash/cosmic-clash/server/store" _ "github.com/jackc/pgx/v5/stdlib" ) func TestRealAllocatorWorkerReconcilesAgonesAllocationAndBindsMatch(t *testing.T) { 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.Fatal(err) } defer db.Close() ctx := context.Background() if err := db.PingContext(ctx); err != nil { t.Fatal(err) } if _, err := db.ExecContext(ctx, `DROP TABLE IF EXISTS schema_migrations, assignments, audit_events, outbox, result_receipts, ranked_season_rollovers, penalties, seasons, ratings, match_participants, matches, proposal_participants, proposals, allocations, game_servers, queue_tickets, idempotency_keys, sessions, identities CASCADE`); err != nil { t.Fatal(err) } if err := migrations.Apply(ctx, db, filepath.Join("..", "migrations")); err != nil { t.Fatal(err) } now := time.Now().UTC().Truncate(time.Microsecond) for index, player := range []string{"allocator-worker-a", "allocator-worker-b"} { if _, err := db.ExecContext(ctx, `INSERT INTO identities (player_id, steam_id) VALUES ($1, $1)`, player); err != nil { t.Fatal(err) } if _, err := db.ExecContext(ctx, `INSERT INTO queue_tickets (ticket_id, player_id, playlist, state, client_build, protocol_version, enqueued_at, expires_at) VALUES ($1, $2, 'casual', 'ACCEPTED', 'build-1', 1, $3, $4)`, fmt.Sprintf("allocator-worker-ticket-%d", index), player, now, now.Add(time.Minute)); err != nil { t.Fatal(err) } } if _, err := db.ExecContext(ctx, `INSERT INTO matches (match_id, playlist, state, region, protocol_version) VALUES ('allocator-worker-match', 'casual', 'ALLOCATING', 'EU', 1)`); err != nil { t.Fatal(err) } for index, player := range []string{"allocator-worker-a", "allocator-worker-b"} { if _, err := db.ExecContext(ctx, `INSERT INTO match_participants (match_id, player_id, ticket_id, slot, team) VALUES ('allocator-worker-match', $1, $2, $3, $4)`, player, fmt.Sprintf("allocator-worker-ticket-%d", index), index, index); err != nil { t.Fatal(err) } } var allocationCalls int provider := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { _, _ = w.Write([]byte(`{"items":[{"metadata":{"name":"agones-ready-1","labels":{"cosmic-clash.io/region":"EU","cosmic-clash.io/build":"build-1","cosmic-clash.io/protocol":"1","cosmic-clash.io/transport":"enet"}},"status":{"state":"Ready"}}]}`)) return } if r.Method != http.MethodPost { t.Fatalf("provider method = %s", r.Method) } allocationCalls++ var body map[string]any if err := json.NewDecoder(r.Body).Decode(&body); err != nil { t.Fatal(err) } if body["kind"] != "GameServerAllocation" { t.Fatalf("provider body kind = %v", body["kind"]) } w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"status":{"state":"Allocated","gameServerName":"agones-ready-1","address":"127.0.0.1","ports":[{"name":"default","port":7777}]}}`)) })) defer provider.Close() agonesClient := agones.Client{BaseURL: provider.URL, Namespace: "games", HTTP: provider.Client()} ready, err := agonesClient.ListReadyServers(ctx) if err != nil || len(ready) != 1 { t.Fatalf("ready projection = %+v, err=%v", ready, err) } if err := store.RegisterReadyServer(ctx, db, ready[0], now); err != nil { t.Fatal(err) } worker := Worker{ Claims: store.AllocatingMatchClaims{DB: db, Transport: "enet"}, Service: Service{Provider: agonesClient, Durable: store.AllocationRegistry{DB: db}, Now: func() time.Time { return now }}, Now: func() time.Time { return now }, } processed, err := worker.RunOnce(ctx) if err != nil || !processed || allocationCalls != 1 { t.Fatalf("worker processed=%t err=%v provider calls=%d", processed, err, allocationCalls) } var serverID, matchState, ticketState string if err := db.QueryRowContext(ctx, `SELECT server_id, state FROM matches WHERE match_id = 'allocator-worker-match'`).Scan(&serverID, &matchState); err != nil { t.Fatal(err) } if err := db.QueryRowContext(ctx, `SELECT state FROM queue_tickets WHERE ticket_id = 'allocator-worker-ticket-0'`).Scan(&ticketState); err != nil { t.Fatal(err) } if serverID != "agones-ready-1" || matchState != "ALLOCATING" || ticketState != "ALLOCATING" { t.Fatalf("durable lifecycle server=%q match=%q ticket=%q", serverID, matchState, ticketState) } var recorded int if err := db.QueryRowContext(ctx, `SELECT count(*) FROM allocations WHERE allocation_id = 'allocation-allocator-worker-match' AND server_id = 'agones-ready-1' AND state = 'ALLOCATED'`).Scan(&recorded); err != nil || recorded != 1 { t.Fatalf("recorded allocations=%d err=%v", recorded, err) } }