Files
CosmicClash/server/domain/proposal_test.go
T

111 lines
3.9 KiB
Go

package domain
import (
"errors"
"testing"
"time"
)
func TestProposalRequiresUnanimousAcceptance(t *testing.T) {
now := time.Unix(1000, 0)
p, err := NewProposal("proposal-123456789", Casual, []string{"b", "a"}, now)
if err != nil {
t.Fatal(err)
}
if _, err = p.Respond("a", "response-a-123456", true, 0, now); err != nil {
t.Fatal(err)
}
if p.State != Open || p.Revision != 1 {
t.Fatalf("partial acceptance closed proposal: %+v", p)
}
if _, err = p.Respond("b", "response-b-123456", true, 1, now); err != nil {
t.Fatal(err)
}
if p.State != Accepted || p.Revision != 2 {
t.Fatalf("unanimous acceptance not committed: %+v", p)
}
}
func TestProposalResponseReplayIsStableAndPayloadReuseConflicts(t *testing.T) {
now := time.Unix(1000, 0)
p, err := NewProposal("proposal-123456789", Casual, []string{"a", "b"}, now)
if err != nil {
t.Fatal(err)
}
first, err := p.Respond("a", "response-a-123456", true, 0, now)
if err != nil {
t.Fatal(err)
}
replay, err := p.Respond("a", "response-a-123456", true, 0, now.Add(20*time.Second))
if err != nil || replay.Revision != first.Revision {
t.Fatalf("replay = %+v, %v", replay, err)
}
if _, err = p.Respond("a", "response-a-123456", false, 1, now); !errors.Is(err, ErrConflict) {
t.Fatalf("payload reuse error = %v", err)
}
}
func TestReturnedProposalRetainsIdempotencyStateForChainedResponses(t *testing.T) {
now := time.Unix(1000, 0)
p, err := NewProposal("proposal-123456789", Ranked, []string{"a", "b", "c", "d", "e", "f"}, now)
if err != nil {
t.Fatal(err)
}
for _, playerID := range []string{"a", "b", "c", "d", "e", "f"} {
p, err = p.Respond(playerID, "accept-"+playerID+"-123456", true, p.Revision, now)
if err != nil {
t.Fatal(err)
}
}
if p.State != Accepted || p.Revision != 6 {
t.Fatalf("chained responses = %+v", p)
}
}
func TestProposalExpiryTimesOutPendingParticipantsAndClosesRace(t *testing.T) {
now := time.Unix(1000, 0)
p, err := NewProposal("proposal-123456789", Ranked, []string{"a", "b", "c", "d", "e", "f"}, now)
if err != nil {
t.Fatal(err)
}
if !p.Expire(now.Add(ProposalWindow)) || p.State != Expired || p.Revision != 1 {
t.Fatalf("expiry failed: %+v", p)
}
for _, participant := range p.Participants {
if participant.Response != TimedOutResponse {
t.Fatalf("pending participant not timed out: %+v", participant)
}
}
if _, err = p.Respond("a", "late-response-123", true, 1, now.Add(ProposalWindow)); !errors.Is(err, ErrProposalClosed) {
t.Fatalf("late response error = %v", err)
}
}
func TestRankedProposalAndCooldownEscalation(t *testing.T) {
now := time.Unix(1000, 0)
if _, err := NewProposal("proposal-123456789", Ranked, []string{"a", "b"}, now); err == nil {
t.Fatal("ranked proposal accepted fewer than six")
}
if got := CooldownUntil([]CooldownEvent{{At: now, Playlist: Ranked, Kind: DeclinedResponse}}, Ranked, now); !got.Equal(now.Add(2 * time.Minute)) {
t.Fatalf("ranked decline cooldown = %v", got)
}
events := []CooldownEvent{{At: now, Playlist: Ranked, Kind: TimedOutResponse}, {At: now.Add(time.Minute), Playlist: Ranked, Kind: DeclinedResponse}, {At: now.Add(2 * time.Minute), Playlist: Ranked, Kind: TimedOutResponse}}
if got := CooldownUntil(events, Ranked, now.Add(2*time.Minute)); !got.Equal(now.Add(17 * time.Minute)) {
t.Fatalf("ranked escalation cooldown = %v", got)
}
}
func TestCooldownIgnoresFutureForeignAndInvalidEvents(t *testing.T) {
now := time.Unix(10_000, 0)
events := []CooldownEvent{
{At: now.Add(-time.Minute), Playlist: Casual, Kind: DeclinedResponse},
{At: now.Add(time.Hour), Playlist: Ranked, Kind: TimedOutResponse},
{At: now, Playlist: Casual, Kind: TimedOutResponse},
{At: now, Playlist: Ranked, Kind: AcceptedResponse},
{At: now.Add(-31 * time.Minute), Playlist: Ranked, Kind: DeclinedResponse},
}
if got := CooldownUntil(events, Ranked, now); !got.IsZero() {
t.Fatalf("untrusted cooldown events produced %v, want zero", got)
}
}