aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/correct_machine_final_bank_failure_test.go
blob: 5613705d388443c8e47cf614e9c0aeb806358f27 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
package casino

import "testing"

// Regression: when the LAST free spin ends the feature, finishSpin folded
// featureBank into payNow and zeroed the bank BEFORE the wallet save. A
// settlement whose save failed therefore destroyed already-landed free-spin
// winnings with no recovery path and no explicit forfeiture notice, even
// though the triggering stake was correctly kept pending. The bank must stay
// on the machine until its payout is durable, so the shutdown/eviction
// settlement (which pays the bank after refunding the stake) can still
// return it.

// TestMachineFinalFeatureBankSurvivesFailedSettlement: the feature pays as
// one settlement when it ends, but if that payout cannot be persisted the
// banked winnings stay in featureBank instead of vanishing, and the failure
// is explained to the player rather than presented as a mere stake refund.
func TestMachineFinalFeatureBankSurvivesFailedSettlement(t *testing.T) {
	m := newMachine(1, MachineConfig{ID: "slots", Game: GameSlots}, newTestRand())
	wallet := &failingPayoutWallet{testWallet{credits: 100}}
	m.join("alice")
	m.beginSpinLocked(10, wallet, "", "")
	// The final settling spin of a feature: freeSpins has already counted
	// down to 0 while inFreeSpins is still true (the count reached 0 through
	// finishSpin's tail decrement; this spin resolves the feature).
	m.freeSpins = 0
	m.inFreeSpins = true
	m.featureBank = 40 // winnings already landed during earlier free spins
	m.spin = &SlotSpin{Payout: 15, ScatterCount: 3, Wins: nil}

	events := m.finishSpin()

	if m.freeSpins != 0 || m.inFreeSpins {
		t.Fatalf("the feature did not end: freeSpins=%d inFreeSpins=%v", m.freeSpins, m.inFreeSpins)
	}
	if wallet.paid != 0 {
		t.Fatalf("a failed settlement must not be reported as paid: paid=%d", wallet.paid)
	}
	if m.featureBank != 40 {
		t.Fatalf("featureBank=%v, want 40: the banked winnings must survive a failed settlement", m.featureBank)
	}
	if !m.stakeFailed {
		t.Fatal("the triggering stake must stay pending (stakeFailed) after a failed payout")
	}
	if !hasMessage(events, "could not process your payout") {
		t.Fatalf("no failed-payout notice: %+v", events)
	}
	if !hasMessage(events, "banked free-spin winnings") {
		t.Fatalf("the preserved bank was not explained: %+v", events)
	}

	// The stake's pending wager was NOT consumed by the failed settle, so the
	// shutdown refund path still returns it exactly once (the banked settlement
	// fails again on this wallet, which refundAll reports explicitly).
	refundEvents := m.refundAll()
	if len(wallet.open) != 0 {
		t.Fatalf("outstanding stakes after shutdown refund: %+v", wallet.open)
	}
	// After the failed settle: 100 - 10 wagered = 90 credits remain, and the
	// shutdown refund returns the 10-credit stake through the refund path
	// (testWallet records refunds of credits in `paid`). The banked
	// settlement failed again on this wallet, so refundAll reports (never
	// pays) it.
	if got := wallet.credits; got != 90 {
		t.Fatalf("credits after shutdown = %d, want 90", got)
	}
	if got := wallet.paid; got != 10 {
		t.Fatalf("shutdown refund returned %d of the stake, want 10 exactly once", got)
	}
	if !hasMessage(refundEvents, "could not be persisted") {
		t.Fatalf("refundAll did not report the failed bank settlement: %+v", refundEvents)
	}
}

// TestMachineFinalFeatureBankClearedOnSuccessfulSettlement: the ordinary
// path still clears the bank exactly once when the payout persists.
func TestMachineFinalFeatureBankClearedOnSuccessfulSettlement(t *testing.T) {
	m := newMachine(1, MachineConfig{ID: "slots", Game: GameSlots}, newTestRand())
	wallet := &testWallet{credits: 100}
	m.join("alice")
	m.beginSpinLocked(10, wallet, "", "")
	m.freeSpins = 0
	m.inFreeSpins = true
	m.featureBank = 40
	m.spin = &SlotSpin{Payout: 15, ScatterCount: 3}

	events := m.finishSpin()

	if paid := 40 + 15; wallet.paid != paid {
		t.Fatalf("paid=%d, want %d (bank + final free spin)", wallet.paid, paid)
	}
	if m.featureBank != 0 {
		t.Fatalf("featureBank=%v, want 0 after a durable settlement", m.featureBank)
	}
	if len(wallet.open) != 0 {
		t.Fatalf("triggering stake was not consumed with the payout: %+v", wallet.open)
	}
	if !hasMessage(events, "The free spins pay 55 credits") {
		t.Fatalf("missing combined feature payout message: %+v", events)
	}
}