Files
CosmicClash/server/allocator/service_test.go
T

181 lines
8.4 KiB
Go

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
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
return allocation, d.err
}
func TestServiceDurablyRecordsProviderAllocationBeforeReturning(t *testing.T) {
provider := &providerSpy{result: agones.AllocatedServer{Allocation: domain.Allocation{AllocationID: "a", MatchID: "m", ServerID: "gs", 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(), 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 || 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 TestServiceDoesNotReturnProviderResultAfterDurableFailure(t *testing.T) {
provider := &providerSpy{result: agones.AllocatedServer{Allocation: domain.Allocation{AllocationID: "a", 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 := &quotaSpy{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 := &quotaSpy{}
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 := &quotaSpy{}
durable := &durableSpy{err: errors.New("recording unavailable")}
provider := &providerSpy{result: agones.AllocatedServer{Allocation: domain.Allocation{AllocationID: "a", MatchID: "m", Region: "EU", 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", 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 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)
}
}