mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
111 lines
3.9 KiB
Go
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)
|
|
}
|
|
}
|