Files
CosmicClash/server/store/queue_sql_test.go
T
2026-09-02 18:54:10 +01:00

88 lines
3.7 KiB
Go

package store
import (
"github.com/cosmic-clash/cosmic-clash/server/domain"
"testing"
"time"
)
func TestQueueSQLUsesDurableIdempotencyAndOwnerScopedRecovery(t *testing.T) {
for query, fragments := range map[string][]string{
QueueIdempotencyInsertSQL: {"idempotency_keys", "ON CONFLICT (scope, idempotency_key) DO NOTHING", "payload_digest"},
QueueIdempotencySelectSQL: {"scope = $1", "idempotency_key = $2", "FOR UPDATE"},
QueueTicketSelectSQL: {"q.ticket_id = $1", "q.player_id = $2", "proposal_participants", "p.state = 'OPEN'", "match_participants", "participation_active"},
QueueTicketInsertSQL: {"player_id", "playlist", "client_build", "protocol_version"},
QueueTicketHeartbeatSQL: {"player_id = $2", "revision = $3", "expires_at > $4", "RETURNING"},
QueueTicketCancelSQL: {"player_id = $2", "revision = $3", "state NOT IN", "RETURNING"},
QueueCandidateProjectionSQL: {"playlist = $1", "predicted_rtt", "expires_at > $2", "LIMIT $3"},
RankedParticipantSQL: {"steam_id", "player_id = ANY($1)", "ORDER BY player_id"},
ProposalInsertSQL: {"match_region", "match_protocol", "NULLIF($4, '')"},
ProposalParticipantInsertSQL: {"team", "slot", "'PENDING'"},
} {
for _, fragment := range fragments {
if !contains(query, fragment) {
t.Fatalf("query %q missing %q", query, fragment)
}
}
}
}
func TestQueueTicketRecordPreservesRecoveredMatchIdentity(t *testing.T) {
ticket := queueTicketRecordToDomain(queueTicketRecord{TicketID: "ticket-1", PlayerID: "player-1", ProposalID: "proposal-1", MatchID: "match-1", Playlist: string(domain.Casual), State: string(domain.AssignmentReady)})
if ticket.ProposalID != "proposal-1" {
t.Fatalf("recovered proposal ID = %q", ticket.ProposalID)
}
if ticket.MatchID != "match-1" {
t.Fatalf("recovered match ID = %q", ticket.MatchID)
}
if got := queueTicketRecordFromDomain(ticket).MatchID; got != "match-1" {
t.Fatalf("stored match ID = %q", got)
}
if got := queueTicketRecordFromDomain(ticket).ProposalID; got != "proposal-1" {
t.Fatalf("stored proposal ID = %q", got)
}
}
func TestLoadRankedParticipantsRejectsNonSixPlayerLookupsWithoutDatabase(t *testing.T) {
if _, err := LoadRankedParticipants(nil, nil, []string{"player-1"}); err == nil {
t.Fatal("partial ranked identity lookup was accepted")
}
}
func TestListQueuedCandidatesRejectsUnscopedOrUnboundedReads(t *testing.T) {
now := time.Unix(1000, 0)
for _, playlist := range []domain.Playlist{"", "invalid"} {
if _, err := ListQueuedCandidates(nil, nil, playlist, now, 4); err == nil {
t.Fatalf("playlist %q accepted", playlist)
}
}
if _, err := ListQueuedCandidates(nil, nil, domain.Casual, now, 0); err == nil {
t.Fatal("zero limit accepted")
}
if _, err := ListQueuedCandidates(nil, nil, domain.Casual, now, 1001); err == nil {
t.Fatal("unbounded limit accepted")
}
}
func TestQueueMutationAdaptersRejectInvalidArgumentsWithoutDatabase(t *testing.T) {
now := time.Unix(1000, 0)
if _, err := HeartbeatQueueTicket(nil, nil, "player-1", "ticket-1", "short", 0, now); err == nil {
t.Fatal("invalid heartbeat accepted")
}
if _, err := CancelQueueTicket(nil, nil, "player-1", "ticket-1", "short", 0, now); err == nil {
t.Fatal("invalid cancel accepted")
}
}
func TestCreateQueueTicketRejectsInvalidArgumentsWithoutDatabase(t *testing.T) {
if _, err := CreateQueueTicket(nil, nil, "ticket-1", "player-1", "short", domain.QueueSpec{Playlist: domain.Casual, ClientBuild: "build-1", ProtocolVersion: 1}, time.Unix(1000, 0)); err == nil {
t.Fatal("invalid arguments accepted")
}
}
func TestQueueRecoveryRequiresAuthoritativeClock(t *testing.T) {
if _, err := GetQueueTicket(nil, nil, "player-1", "ticket-1", time.Time{}); err == nil {
t.Fatal("recovery without a clock was accepted")
}
}