125 lines
5.2 KiB
Go

package luckygift
import (
"strings"
"testing"
domain "hyapp/services/lucky-gift-service/internal/domain/luckygift"
)
func TestDefaultRuleConfigUsesSafeDynamicV3Draft(t *testing.T) {
config := DefaultRuleConfig("lalu", "lucky")
if config.StrategyVersion != domain.StrategyDynamicV3 || config.Enabled {
t.Fatalf("default strategy=%q enabled=%v, want disabled dynamic_v3 draft", config.StrategyVersion, config.Enabled)
}
if config.TargetRTPPPM != 980_000 || config.PoolRatePPM != 980_000 || config.ProfitRatePPM != 10_000 || config.AnchorRatePPM != 10_000 {
t.Fatalf("default RTP/fund split mismatch: %+v", config)
}
if config.ControlBandPPM != 30_000 || config.InitialPoolCoins != 0 || config.LossStreakGuarantee != 5 {
t.Fatalf("default control values mismatch: %+v", config)
}
if config.LowWatermarkCoins != 10_000_000 || config.LowWaterNonzeroFactorPPM != 700_000 ||
config.HighWatermarkCoins != 20_000_000 || config.HighWaterNonzeroFactorPPM != 1_300_000 {
t.Fatalf("default water controls mismatch: %+v", config)
}
if config.RechargeBoostWindowMS != 300_000 || config.RechargeBoostFactorPPM != 1_100_000 {
t.Fatalf("default recharge boost mismatch: %+v", config)
}
if config.JackpotGlobalRTPMaxPPM != 980_000 || config.JackpotUserDayRTPMaxPPM != 960_000 ||
config.JackpotUser72hRTPMaxPPM != 960_000 || config.MaxJackpotHitsPerUserDay != 5 {
t.Fatalf("default jackpot controls mismatch: %+v", config)
}
if config.JackpotSpendThresholdCoins != 0 || config.MaxSinglePayout != 0 || config.AnchorDailyPayoutCap != 0 {
t.Fatalf("App-specific monetary limits must stay unset in disabled draft: %+v", config)
}
wantThresholds := [][2]int64{{0, 0}, {0, 1}, {1, 1}}
for index, stage := range config.Stages {
if got := [2]int64{stage.MinRecharge7DCoins, stage.MinRecharge30DCoins}; got != wantThresholds[index] {
t.Fatalf("stage %s thresholds=%v, want %v", stage.Stage, got, wantThresholds[index])
}
var expectedRTPPPM int64
for _, tier := range stage.Tiers {
if tier.Enabled {
expectedRTPPPM += tier.MultiplierPPM * tier.BaseWeightPPM / ppmScale
}
}
if expectedRTPPPM != 980_000 {
t.Fatalf("stage %s static expected RTP=%d, want safe 980000", stage.Stage, expectedRTPPPM)
}
}
if err := validateRuleConfig(config); err != nil {
t.Fatalf("disabled dynamic_v3 draft should validate before App-specific monetary limits are known: %v", err)
}
}
func TestValidateDynamicV3RejectsImplicitInitialFunding(t *testing.T) {
config := DefaultRuleConfig("lalu", "lucky")
config.InitialPoolCoins = 1_000_000
if err := validateRuleConfig(config); err == nil || !strings.Contains(err.Error(), "audited credit adjustment") {
t.Fatalf("implicit V3 seed error=%v, want audited credit guidance", err)
}
}
func TestValidateDynamicV3RequiresExplicitMonetaryRiskLimitsWhenEnabled(t *testing.T) {
config := DefaultRuleConfig("lalu", "lucky")
config.Enabled = true
if err := validateRuleConfig(config); err == nil || !strings.Contains(err.Error(), "USD 50 equivalent") {
t.Fatalf("enabled draft error=%v, want explicit USD 50 equivalent coin threshold guidance", err)
}
config.JackpotSpendThresholdCoins = 5_000
config.MaxSinglePayout = 10_000
config.UserHourlyPayoutCap = 20_000
config.UserDailyPayoutCap = 30_000
config.DeviceDailyPayoutCap = 40_000
config.RoomHourlyPayoutCap = 50_000
config.AnchorDailyPayoutCap = 60_000
if err := validateRuleConfig(config); err != nil {
t.Fatalf("fully configured dynamic_v3 should validate: %v", err)
}
config.RoomHourlyPayoutCap = 0
if err := validateRuleConfig(config); err == nil || !strings.Contains(err.Error(), "room_hourly_payout_cap") {
t.Fatalf("zero six-dimensional cap error=%v, want the missing cap name", err)
}
}
func TestValidateDynamicV3RechargeStagesAreStrictlyMonotonic(t *testing.T) {
config := DefaultRuleConfig("lalu", "lucky")
config.Stages[2].MinRecharge7DCoins = config.Stages[1].MinRecharge7DCoins
config.Stages[2].MinRecharge30DCoins = config.Stages[1].MinRecharge30DCoins
if err := validateRuleConfig(config); err == nil || !strings.Contains(err.Error(), "exceed normal") {
t.Fatalf("equal normal/advanced thresholds error=%v, want strict stage separation", err)
}
}
func TestValidateRuleConfigKeepsLegacyFixedV2Compatible(t *testing.T) {
config := DefaultRuleConfig("lalu", "lucky")
config.StrategyVersion = ""
config.Enabled = true
// 升级前请求没有任何 dynamic_v3 字段;空 strategy_version 必须按 fixed_v2 解释,不能要求 50 美元门槛或六维 cap。
config.ProfitRatePPM = 0
config.AnchorRatePPM = 0
config.InitialPoolCoins = 0
config.LossStreakGuarantee = 0
config.LowWatermarkCoins = 0
config.LowWaterNonzeroFactorPPM = 0
config.HighWatermarkCoins = 0
config.HighWaterNonzeroFactorPPM = 0
config.RechargeBoostWindowMS = 0
config.RechargeBoostFactorPPM = 0
config.JackpotMultiplierPPMs = nil
config.JackpotGlobalRTPMaxPPM = 0
config.JackpotUserDayRTPMaxPPM = 0
config.JackpotUser72hRTPMaxPPM = 0
config.MaxJackpotHitsPerUserDay = 0
if err := validateRuleConfig(config); err != nil {
t.Fatalf("legacy fixed_v2 config was broken by dynamic validation: %v", err)
}
normalized := normalizeRuleConfig(config)
if normalized.StrategyVersion != domain.StrategyFixedV2 {
t.Fatalf("legacy empty strategy normalized to %q, want fixed_v2", normalized.StrategyVersion)
}
}