package allocator import ( "context" "errors" "strings" "testing" "time" "github.com/cosmic-clash/cosmic-clash/server/agones" "github.com/cosmic-clash/cosmic-clash/server/domain" ) type providerSpy struct { calls int result agones.AllocatedServer err error } func (p *providerSpy) Allocate(_ context.Context, _ domain.AllocationRequest, _ map[string]string, _ time.Time) (agones.AllocatedServer, error) { p.calls++ return p.result, p.err } type durableSpy struct { calls int allocation domain.Allocation result domain.Allocation err error } type rosterSpy struct { calls int err error } type quotaSpy struct { calls int err error } func (q *quotaSpy) Consume(context.Context, string, time.Time) error { q.calls++ return q.err } func (r *rosterSpy) PublishRoster(_ context.Context, _ domain.Assignment, _ []domain.SignedJoinAuthorisation, _ func([]byte, []byte) bool) error { r.calls++ return r.err } func (d *durableSpy) RecordProviderAllocation(_ context.Context, allocation domain.Allocation, _ time.Time) (domain.Allocation, error) { d.calls++ d.allocation = allocation if d.result.AllocationID != "" { return d.result, d.err } return allocation, d.err } func TestServiceDurablyRecordsProviderAllocationBeforeReturning(t *testing.T) { request := domain.AllocationRequest{AllocationID: "a", MatchID: "m", Region: "EU", Build: "b", Protocol: 1, Transport: "enet"} provider := &providerSpy{result: agones.AllocatedServer{Allocation: domain.Allocation{AllocationID: "a", MatchID: "m", ServerID: "gs", Region: "EU", Build: "b", Protocol: 1, Transport: "enet", State: domain.ServerAllocated}, Endpoint: "127.0.0.1:7777"}} durable := &durableSpy{} metrics := NewMetrics() service := Service{Provider: provider, Durable: durable, Metrics: metrics, Now: func() time.Time { return time.Unix(1000, 0) }} result, err := service.Allocate(context.Background(), request, map[string]string{"region": "EU"}) if err != nil || result.Endpoint == "" || durable.calls != 1 || durable.allocation.ServerID != "gs" { t.Fatalf("result=%+v err=%v durable=%+v", result, err, durable) } var output strings.Builder if err := metrics.WritePrometheus(&output); err != nil || !strings.Contains(output.String(), `allocations_total{region="EU"} 1`) { t.Fatalf("success metric err=%v output=%s", err, output.String()) } } func TestServiceRejectsMismatchedFreshProviderAllocationBeforePersistence(t *testing.T) { request := domain.AllocationRequest{AllocationID: "allocation-1", MatchID: "match-1", Playlist: domain.Ranked, Region: "EU", Build: "build-1", Protocol: 1, ArenaPath: "res://scenes/arena_01.tscn", Transport: "enet"} base := agones.AllocatedServer{Allocation: domain.Allocation{AllocationID: request.AllocationID, MatchID: request.MatchID, ServerID: "server-1", Region: request.Region, Build: request.Build, Protocol: request.Protocol, ArenaPath: request.ArenaPath, Transport: request.Transport, State: domain.ServerAllocated}, Endpoint: "127.0.0.1:7777"} for name, mutate := range map[string]func(*agones.AllocatedServer){ "allocation id": func(r *agones.AllocatedServer) { r.Allocation.AllocationID = "other" }, "match": func(r *agones.AllocatedServer) { r.Allocation.MatchID = "other" }, "region": func(r *agones.AllocatedServer) { r.Allocation.Region = "NA" }, "build": func(r *agones.AllocatedServer) { r.Allocation.Build = "other" }, "protocol": func(r *agones.AllocatedServer) { r.Allocation.Protocol++ }, "arena": func(r *agones.AllocatedServer) { r.Allocation.ArenaPath = "res://scenes/arena_02.tscn" }, "transport": func(r *agones.AllocatedServer) { r.Allocation.Transport = "steam_sdr" }, "server": func(r *agones.AllocatedServer) { r.Allocation.ServerID = "" }, "state": func(r *agones.AllocatedServer) { r.Allocation.State = domain.ServerReady }, "endpoint": func(r *agones.AllocatedServer) { r.Endpoint = "" }, } { t.Run(name, func(t *testing.T) { result := base mutate(&result) durable := &durableSpy{} service := Service{Provider: &providerSpy{result: result}, Durable: durable, Now: func() time.Time { return time.Unix(1000, 0) }} if _, err := service.Allocate(context.Background(), request, nil); err == nil { t.Fatal("mismatched provider result accepted") } if durable.calls != 0 { t.Fatalf("mismatched result reached durable store %d times", durable.calls) } }) } } func TestServiceReturnsCanonicalDurableAllocation(t *testing.T) { now := time.Unix(1000, 0) request := domain.AllocationRequest{AllocationID: "a", MatchID: "m", Region: "EU", Build: "b", Protocol: 1, Transport: "enet"} providerAllocation := domain.Allocation{AllocationID: "a", MatchID: "m", ServerID: "gs", Region: "EU", Build: "b", Protocol: 1, Transport: "enet", State: domain.ServerAllocated} canonical := providerAllocation canonical.AllocatedAt = now service := Service{ Provider: &providerSpy{result: agones.AllocatedServer{Allocation: providerAllocation, Endpoint: "127.0.0.1:7777"}}, Durable: &durableSpy{result: canonical}, Now: func() time.Time { return now }, } result, err := service.Allocate(context.Background(), request, nil) if err != nil || result.Allocation != canonical { t.Fatalf("result=%+v err=%v, want canonical %+v", result, err, canonical) } } func TestServiceDoesNotReturnProviderResultAfterDurableFailure(t *testing.T) { provider := &providerSpy{result: agones.AllocatedServer{Allocation: domain.Allocation{AllocationID: "a", MatchID: "m", ServerID: "gs", Region: "EU", Build: "b", Protocol: 1, Transport: "enet", State: domain.ServerAllocated}, Endpoint: "127.0.0.1:7777"}} durable := &durableSpy{err: errors.New("database unavailable")} service := Service{Provider: provider, Durable: durable, Now: func() time.Time { return time.Unix(1000, 0) }} result, err := service.Allocate(context.Background(), domain.AllocationRequest{AllocationID: "a", MatchID: "m", Region: "EU", Build: "b", Protocol: 1, Transport: "enet"}, map[string]string{"region": "EU"}) if err == nil || result.Endpoint != "" || durable.calls != 1 { t.Fatalf("result=%+v err=%v calls=%d", result, err, durable.calls) } } func TestServiceConsumesSharedQuotaBeforeFreshProviderCall(t *testing.T) { provider := &providerSpy{result: agones.AllocatedServer{Allocation: domain.Allocation{AllocationID: "a", State: domain.ServerAllocated}, Endpoint: "127.0.0.1:7777"}} quota := "aSpy{err: errors.New("quota exhausted")} metrics := NewMetrics() service := Service{Provider: provider, Durable: &durableSpy{}, Quota: quota, Metrics: metrics, Now: func() time.Time { return time.Unix(1000, 0) }} if _, err := service.Allocate(context.Background(), domain.AllocationRequest{AllocationID: "a", MatchID: "m", Region: "EU", Build: "b", Protocol: 1, Transport: "enet"}, nil); err == nil { t.Fatal("quota rejection was ignored") } if quota.calls != 1 || provider.calls != 0 { t.Fatalf("quota/provider calls = %d/%d, want 1/0", quota.calls, provider.calls) } var output strings.Builder _ = metrics.WritePrometheus(&output) if !strings.Contains(output.String(), `quota_denials_total{region="EU"} 1`) { t.Fatalf("quota denial metric missing: %s", output.String()) } } func TestServiceDoesNotConsumeSharedQuotaWhenReconcilingProviderResult(t *testing.T) { quota := "aSpy{} durable := &durableSpy{} service := Service{Durable: durable, Quota: quota, Now: func() time.Time { return time.Unix(1000, 0) }} result := agones.AllocatedServer{Allocation: domain.Allocation{AllocationID: "a", MatchID: "m", Region: "EU", State: domain.ServerAllocated}, Endpoint: "127.0.0.1:7777"} if _, err := service.RecordProviderAllocation(context.Background(), result, time.Unix(1000, 0)); err != nil { t.Fatalf("reconciliation failed: %v", err) } if quota.calls != 0 || durable.calls != 1 { t.Fatalf("quota/durable calls = %d/%d, want 0/1", quota.calls, durable.calls) } } func TestServiceDoesNotDoubleChargeQuotaAfterProviderResultRecovery(t *testing.T) { quota := "aSpy{} durable := &durableSpy{err: errors.New("recording unavailable")} provider := &providerSpy{result: agones.AllocatedServer{Allocation: domain.Allocation{AllocationID: "a", MatchID: "m", ServerID: "gs", Region: "EU", Build: "b", Protocol: 1, Transport: "enet", State: domain.ServerAllocated}, Endpoint: "127.0.0.1:7777"}} service := Service{Provider: provider, Durable: durable, Quota: quota, Now: func() time.Time { return time.Unix(1000, 0) }} request := domain.AllocationRequest{AllocationID: "a", MatchID: "m", Region: "EU", Build: "b", Protocol: 1, Transport: "enet"} if _, err := service.Allocate(context.Background(), request, nil); err == nil { t.Fatal("durable recording failure was ignored") } durable.err = nil if _, err := service.RecordProviderAllocation(context.Background(), provider.result, time.Unix(1001, 0)); err != nil { t.Fatalf("provider recovery failed: %v", err) } if quota.calls != 1 || durable.calls != 2 { t.Fatalf("quota/durable calls = %d/%d, want 1/2", quota.calls, durable.calls) } } func TestServiceAllocatesOnlyUnanimouslyAcceptedMatchingProposal(t *testing.T) { proposal := domain.Proposal{ ProposalID: "proposal-1", Playlist: domain.Casual, State: domain.Accepted, Participants: []domain.ProposalParticipant{ {PlayerID: "player-a", Response: domain.AcceptedResponse}, {PlayerID: "player-b", Response: domain.AcceptedResponse}, }, } provider := &providerSpy{result: agones.AllocatedServer{Allocation: domain.Allocation{AllocationID: "a", MatchID: "m", ServerID: "gs", Region: "EU", Build: "b", Protocol: 1, Transport: "enet", State: domain.ServerAllocated}, Endpoint: "127.0.0.1:7777"}} durable := &durableSpy{} service := Service{Provider: provider, Durable: durable, Now: func() time.Time { return time.Unix(1000, 0) }} request := domain.AllocationRequest{AllocationID: "a", MatchID: "m", Region: "EU", Build: "b", Protocol: 1, Transport: "enet"} if _, err := service.AllocateAcceptedProposal(context.Background(), proposal, request, domain.Casual, map[string]string{"region": "EU"}); err != nil { t.Fatalf("accepted proposal was rejected: %v", err) } if provider.calls != 1 || durable.calls != 1 { t.Fatalf("provider/durable calls = %d/%d", provider.calls, durable.calls) } for name, mutate := range map[string]func(*domain.Proposal){ "open": func(p *domain.Proposal) { p.State = domain.Open }, "wrong-playlist": func(p *domain.Proposal) { p.Playlist = domain.Ranked }, "pending": func(p *domain.Proposal) { p.Participants[0].Response = domain.Pending }, "duplicate": func(p *domain.Proposal) { p.Participants[1].PlayerID = p.Participants[0].PlayerID }, } { invalid := proposal invalid.Participants = append([]domain.ProposalParticipant(nil), proposal.Participants...) mutate(&invalid) before := provider.calls if _, err := service.AllocateAcceptedProposal(context.Background(), invalid, request, domain.Casual, map[string]string{"region": "EU"}); err == nil { t.Fatalf("%s proposal was accepted", name) } if provider.calls != before { t.Fatalf("%s proposal reached provider", name) } } } func TestServiceRejectsAllocationRequestThatDoesNotMatchAcceptedProposal(t *testing.T) { proposal := domain.Proposal{ ProposalID: "proposal-ranked", Playlist: domain.Ranked, State: domain.Accepted, Region: "EU", Protocol: 1, ArenaPath: "res://scenes/arena_01.tscn", Participants: []domain.ProposalParticipant{ {PlayerID: "player-a", Response: domain.AcceptedResponse}, {PlayerID: "player-b", Response: domain.AcceptedResponse}, {PlayerID: "player-c", Response: domain.AcceptedResponse}, {PlayerID: "player-d", Response: domain.AcceptedResponse}, {PlayerID: "player-e", Response: domain.AcceptedResponse}, {PlayerID: "player-f", Response: domain.AcceptedResponse}, }, } provider := &providerSpy{} service := Service{Provider: provider, Durable: &durableSpy{}, Now: func() time.Time { return time.Unix(1000, 0) }} request := domain.AllocationRequest{AllocationID: "a", MatchID: "m", Playlist: domain.Ranked, Region: "EU", Build: "b", Protocol: 1, ArenaPath: proposal.ArenaPath, Transport: "enet"} for name, mutate := range map[string]func(*domain.AllocationRequest){ "playlist": func(r *domain.AllocationRequest) { r.Playlist = domain.Casual }, "region": func(r *domain.AllocationRequest) { r.Region = "NA" }, "protocol": func(r *domain.AllocationRequest) { r.Protocol = 2 }, "arena": func(r *domain.AllocationRequest) { r.ArenaPath = "res://scenes/arena_02.tscn" }, } { candidate := request mutate(&candidate) if _, err := service.AllocateAcceptedProposal(context.Background(), proposal, candidate, domain.Ranked, map[string]string{"region": "EU"}); err == nil { t.Fatalf("%s mismatch was accepted", name) } } if provider.calls != 0 { t.Fatalf("provider calls=%d, want 0", provider.calls) } } func TestServicePublishesRosterOnlyForAllocatedAssignment(t *testing.T) { roster := &rosterSpy{} service := Service{Roster: roster} assignment := domain.Assignment{Allocation: domain.Allocation{State: domain.ServerAllocated}, Endpoint: "127.0.0.1:7777"} if err := service.PublishRoster(context.Background(), assignment, []domain.SignedJoinAuthorisation{{Signature: []byte("sig")}}, func([]byte, []byte) bool { return true }); err != nil || roster.calls != 1 { t.Fatalf("publish err=%v calls=%d", err, roster.calls) } assignment.Allocation.State = domain.ServerReady if err := service.PublishRoster(context.Background(), assignment, nil, nil); err != domain.ErrManifestRejected || roster.calls != 1 { t.Fatalf("premature publish err=%v calls=%d", err, roster.calls) } }