aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/correct_casino_refund_bank_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/casino/correct_casino_refund_bank_test.go')
-rw-r--r--internal/casino/correct_casino_refund_bank_test.go101
1 files changed, 101 insertions, 0 deletions
diff --git a/internal/casino/correct_casino_refund_bank_test.go b/internal/casino/correct_casino_refund_bank_test.go
new file mode 100644
index 0000000..fd90ae1
--- /dev/null
+++ b/internal/casino/correct_casino_refund_bank_test.go
@@ -0,0 +1,101 @@
+package casino
+
+import "testing"
+
+// Free-spin winnings live only in Machine.featureBank until the feature ends,
+// so a graceful shutdown (or room eviction) that zeroes the bank without
+// paying it would destroy winnings that had already landed. refundAll must
+// settle the bank through the wallet in addition to refunding the triggering
+// stake, and must keep the stake refundable (not double-paid) when either
+// side of that settlement fails.
+
+// TestMachineRefundAllPreservesBankedFeatureWinnings: a nonzero banked bonus
+// survives RefundAll while the triggering stake is refunded exactly once.
+func TestMachineRefundAllPreservesBankedFeatureWinnings(t *testing.T) {
+ m := newMachine(1, MachineConfig{ID: "slots", Game: GameSlots}, newTestRand())
+ wallet := &testWallet{credits: 100}
+ m.join("alice")
+ m.beginSpinLocked(10, wallet, "", "")
+ m.spin = &SlotSpin{Payout: 25, ScatterCount: 3}
+ m.finishSpin() // triggers the feature: 25 banked, the stake stays open
+ if m.featureBank != 25 || !m.inFreeSpins {
+ t.Fatalf("setup: featureBank=%v inFreeSpins=%v, want 25/true", m.featureBank, m.inFreeSpins)
+ }
+ if wallet.paid != 0 {
+ t.Fatalf("setup: a payout was journaled mid-feature (%d)", wallet.paid)
+ }
+
+ events := m.refundAll()
+ // 10 for the refunded stake plus the 25 banked credits.
+ if wallet.paid != 35 {
+ t.Fatalf("refundAll paid %d, want 35 (10 stake + 25 banked winnings)", wallet.paid)
+ }
+ if got := wallet.credits + wallet.paid; got != 125 {
+ t.Fatalf("banked winnings were destroyed: balance %d, want 125", got)
+ }
+ if !hasMessage(events, "25") {
+ t.Fatalf("missing banked-winnings settlement message: %+v", events)
+ }
+ if len(wallet.open) != 0 {
+ t.Fatalf("stake still outstanding after shutdown: %+v", wallet.open)
+ }
+}
+
+// TestMachineRefundAllFailedBankedPayoutKeepsStakeRefunded: if the banked
+// settlement cannot be persisted, the triggering stake is still refunded
+// exactly once (not paid twice) and the failure is reported rather than
+// silently swallowing the win.
+func TestMachineRefundAllFailedBankedPayoutKeepsStakeRefunded(t *testing.T) {
+ m := newMachine(1, MachineConfig{ID: "slots", Game: GameSlots}, newTestRand())
+ wallet := &failingPayoutWallet{testWallet{credits: 100}}
+ m.join("alice")
+ m.beginSpinLocked(10, wallet, "", "")
+ m.featureBank = 25
+
+ events := m.refundAll()
+ if wallet.paid != 10 {
+ t.Fatalf("stake refund paid %d, want 10 once", wallet.paid)
+ }
+ if len(wallet.open) != 0 {
+ t.Fatalf("stake still outstanding: %+v", wallet.open)
+ }
+ if !hasMessage(events, "could not be persisted") {
+ t.Fatalf("a failed banked payout was not reported: %+v", events)
+ }
+}
+
+// failingRefundWallet rejects every refund, mirroring the production wallet's
+// persistence-failure path: the stakes stay open for recovery.
+type failingRefundWallet struct {
+ testWallet
+}
+
+func (w *failingRefundWallet) RefundOutstanding() (int, int, error) {
+ return 0, 0, ErrWalletPersistence
+}
+
+// TestMachineRefundAllFailedRefundKeepsStakeOpenForRecovery pins the
+// load-bearing stakeFailed guard in refundAll: resetSeat must not resolve
+// (forfeit) the stake RefundOutstanding left open, or the recovery refund
+// contract is broken. Deleting `m.stakeFailed = err != nil` fails this test.
+func TestMachineRefundAllFailedRefundKeepsStakeOpenForRecovery(t *testing.T) {
+ m := newMachine(1, MachineConfig{ID: "slots", Game: GameSlots}, newTestRand())
+ wallet := &failingRefundWallet{testWallet{credits: 100}}
+ m.join("alice")
+ m.beginSpinLocked(10, wallet, "", "")
+ if len(wallet.open) != 1 {
+ t.Fatalf("setup: the stake must be open, got %+v", wallet.open)
+ }
+ m.featureBank = 25
+
+ events := m.refundAll()
+ if len(wallet.open) != 1 {
+ t.Fatalf("a failed refund forfeited the open stake; want it preserved for recovery: %+v", wallet.open)
+ }
+ if wallet.paid != 0 {
+ t.Fatalf("a failed refund paid %d; want 0", wallet.paid)
+ }
+ if !hasMessage(events, "recorded for recovery") {
+ t.Fatalf("the failed refund was not reported: %+v", events)
+ }
+}