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