mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
86 lines
3.6 KiB
Go
86 lines
3.6 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/cosmic-clash/cosmic-clash/server/domain"
|
|
)
|
|
|
|
func TestInitialConnectSQLPreservesAtomicNoShowReconciliation(t *testing.T) {
|
|
if !contains(initialConnectCandidatesSQL, "ASSIGNMENT_READY") || !contains(initialConnectCandidatesSQL, "initial_connect_ready_at") || !contains(initialConnectCandidatesSQL, "LIMIT $1") {
|
|
t.Fatal("initial-connect sweep is not bounded to pre-live matches")
|
|
}
|
|
for query, fragments := range map[string][]string{
|
|
initialConnectIdempotencyInsertSQL: {"ON CONFLICT", "payload_digest"},
|
|
initialConnectMatchLockSQL: {"FOR UPDATE", "match_id = $1"},
|
|
initialConnectParticipantsSQL: {"participation_active", "FOR UPDATE"},
|
|
initialConnectDeactivateSQL: {"abandoned_at", "participation_active = FALSE"},
|
|
initialConnectReleaseAllSQL: {"match_id = $1", "participation_active = FALSE"},
|
|
initialConnectPenaltySQL: {"INITIAL_CONNECT_NO_SHOW", "ON CONFLICT"},
|
|
initialConnectOutboxSQL: {"state_changed", "revision", "ON CONFLICT"},
|
|
} {
|
|
for _, fragment := range fragments {
|
|
if !contains(query, fragment) {
|
|
t.Fatalf("query %q missing %q", query, fragment)
|
|
}
|
|
}
|
|
}
|
|
for _, fragment := range []string{"INITIAL_CONNECT_NO_SHOW", "MATCH_ABANDONED"} {
|
|
if !contains(initialConnectHistorySQL, fragment) {
|
|
t.Fatalf("initial-connect abandon history missing %q", fragment)
|
|
}
|
|
if !contains(QueueCooldownSelectSQL, fragment) {
|
|
t.Fatalf("queue cooldown fence missing %q", fragment)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInitialConnectPlanValidationRejectsIncompleteOrForgedPlans(t *testing.T) {
|
|
participants := []initialConnectParticipant{
|
|
{PlayerID: "p0", TicketID: "t0", Team: 0, Slot: 0, ConnectedAt: validTime(100), Active: true},
|
|
{PlayerID: "p1", TicketID: "t1", Team: 1, Slot: 3, Active: true},
|
|
}
|
|
plan := domain.InitialConnectPlan{
|
|
Action: domain.InitialConnectStartWithBot, MatchState: domain.Live,
|
|
Connected: []string{"p0"},
|
|
NoShows: []domain.Abandonment{{PlayerID: "p1", Cooldown: time.Minute, AbandonedAt: time.Unix(100, 0)}},
|
|
CasualLineup: []domain.CasualSlot{
|
|
{Slot: 0, Team: 0, PlayerID: "p0"}, {Slot: 1, Team: 0, PlayerID: "bot-1", IsBot: true},
|
|
{Slot: 2, Team: 0, PlayerID: "bot-2", IsBot: true}, {Slot: 3, Team: 1, PlayerID: "bot-3", IsBot: true},
|
|
{Slot: 4, Team: 1, PlayerID: "bot-4", IsBot: true}, {Slot: 5, Team: 1, PlayerID: "bot-5", IsBot: true},
|
|
},
|
|
}
|
|
if err := validateInitialConnectPlan(plan, participants, domain.Casual); err != nil {
|
|
t.Fatalf("valid plan rejected: %v", err)
|
|
}
|
|
plan.CasualLineup[1].Team = 1
|
|
if err := validateInitialConnectPlan(plan, participants, domain.Casual); err == nil {
|
|
t.Fatal("team-swapped lineup accepted")
|
|
}
|
|
}
|
|
|
|
func TestInitialConnectPlanValidationRequiresCompleteRosterToStart(t *testing.T) {
|
|
participants := []initialConnectParticipant{
|
|
{PlayerID: "p0", TicketID: "t0", Team: 0, Slot: 0, ConnectedAt: validTime(100), Active: true},
|
|
{PlayerID: "p1", TicketID: "t1", Team: 1, Slot: 3, ConnectedAt: validTime(100), Active: true},
|
|
}
|
|
plan := domain.InitialConnectPlan{
|
|
Action: domain.InitialConnectStart, MatchState: domain.Live, Connected: []string{"p0", "p1"},
|
|
}
|
|
if err := validateInitialConnectPlan(plan, participants, domain.Ranked); err != nil {
|
|
t.Fatalf("valid complete start rejected: %v", err)
|
|
}
|
|
participants[1].ConnectedAt = sql.NullTime{}
|
|
if err := validateInitialConnectPlan(plan, participants, domain.Ranked); err == nil {
|
|
t.Fatal("start with disconnected participant accepted")
|
|
}
|
|
}
|
|
|
|
func validTime(unix int64) (result sql.NullTime) {
|
|
result.Time = time.Unix(unix, 0)
|
|
result.Valid = true
|
|
return result
|
|
}
|