aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/correct_casino_refund_bank_test.go
blob: fd90ae17f944b5d34bf6faa124d9e36ec45dd883 (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"

// 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)
	}
}