Files
bd2/go/internal/server/commerce/installed_audit_test.go
T

256 lines
8.1 KiB
Go

package commerce
import (
"bd2server/internal/server/calendar"
"bd2server/internal/server/events"
"bd2server/internal/server/gameconfig"
"bd2server/internal/server/gamedata"
"bd2server/internal/server/hunting"
"bd2server/internal/server/player"
"bd2server/internal/server/readonly"
"bd2server/internal/server/stateio"
"bd2server/internal/server/wire"
"bytes"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestInstalledCashProductRewardCoverage23510(t *testing.T) {
root := os.Getenv("BD2_REAL_GAMEDATA")
if root == "" {
t.Skip("BD2_REAL_GAMEDATA not configured")
}
catalog, err := gamedata.LoadCashCatalog(root, "20260923193640")
if err != nil {
t.Fatal(err)
}
resolver, err := gamedata.LoadCashRewardResolver(root, "20260923193640")
if err != nil {
t.Fatal(err)
}
entitlements, err := gamedata.LoadCashEntitlementDesign(root, "20260923193640")
if err != nil {
t.Fatal(err)
}
// Infinite draw products grant the user's confirmed preview, rather than
// the placeholder cash box. Validate their dedicated design and route.
db, release, err := gamedata.OpenDatabase(root, "20260923193640", "common")
if err != nil {
t.Fatal(err)
}
rowsDB, err := db.Query("SELECT id,ProtoBuf FROM GachaGroupTable")
if err != nil {
release()
t.Fatal(err)
}
type drawGroup struct{ id, gacha uint64 }
var groups []drawGroup
for rowsDB.Next() {
var id uint64
var raw []byte
if err := rowsDB.Scan(&id, &raw); err != nil {
t.Fatal(err)
}
typ, _, _ := wire.Varint(raw, 17)
sub, _, _ := wire.Varint(raw, 16)
if typ == 1 && sub == 5 {
gacha, _, _ := wire.Varint(raw, 33)
groups = append(groups, drawGroup{id, gacha})
}
}
if err := rowsDB.Err(); err != nil {
t.Fatal(err)
}
rowsDB.Close()
release()
policy, err := NewCatalog("2.35.10", catalog, gameconfig.Default().Purchases)
if err != nil {
t.Fatal(err)
}
shop, err := NewService(policy, stateio.NewMemory(), &purchaseEconomy{})
if err != nil {
t.Fatal(err)
}
seed, err := readonly.Load(filepath.Join("..", "..", "..", "seed", "v2_35_10", "readonly.json"))
if err != nil {
t.Fatal(err)
}
calendars, err := calendar.LoadDirectory(filepath.Join("..", "..", "..", "..", "schedules"), "2.35.10", "20260923193640")
if err != nil {
t.Fatal(err)
}
seed, err = calendars.ApplyReadonly(seed)
if err != nil {
t.Fatal(err)
}
shop.SetClock(func() time.Time { return time.Date(2026, 10, 5, 5, 0, 0, 0, time.UTC) }, 0)
if err := shop.AttachShopSeed(seed); err != nil {
t.Fatal(err)
}
graph, err := gamedata.LoadRewardGraph(root, "20260923193640")
if err != nil {
t.Fatal(err)
}
costumes, err := gamedata.LoadRewardCostumeCatalog(root, "20260923193640")
if err != nil {
t.Fatal(err)
}
equipment, err := gamedata.LoadRewardEquipmentCatalog(root, "20260923193640")
if err != nil {
t.Fatal(err)
}
skins, err := gamedata.LoadPrestigeSkins(root, "20260923193640")
if err != nil {
t.Fatal(err)
}
ownedDesign, err := gamedata.LoadOwnedEventItemDesign(root, "20260923193640")
if err != nil {
t.Fatal(err)
}
newEconomy := func() (*EntitlementEconomy, *player.Wallet) {
store := stateio.NewMemory()
items, err := player.OpenInventory(store, &player.Starter{Version: "2.35.10"})
if err != nil {
t.Fatal(err)
}
wallet, err := player.OpenWallet(store, player.Currency{Jewelry: 100000000, FreeJewelry: 100000000, Gold: 100000000})
if err != nil {
t.Fatal(err)
}
collection, err := player.OpenCollectionStore(store, nil)
if err != nil {
t.Fatal(err)
}
ownedEquipment, err := player.OpenEquipmentInventory(store)
if err != nil {
t.Fatal(err)
}
for _, persist := range []func() error{items.EnsurePersisted, wallet.EnsurePersisted, collection.EnsurePersisted, ownedEquipment.EnsurePersisted} {
if err := persist(); err != nil {
t.Fatal(err)
}
}
base, err := events.NewEconomy(store, items, wallet, collection, ownedEquipment, costumes, equipment, graph, nil)
if err != nil {
t.Fatal(err)
}
base.AttachPrestigeSkins(skins)
base.AttachOwnedItemDesign(ownedDesign)
ap, err := hunting.Open(store, root, "20260923193640", items, wallet, func() (int, error) { return 21, nil }, 0, 0)
if err != nil {
t.Fatal(err)
}
base.AttachHuntingAP(ap)
e, err := NewEntitlementEconomy(store, base, resolver, items, entitlements)
if err != nil {
t.Fatal(err)
}
return e, wallet
}
delegated := map[gamedata.CashProductKey]bool{}
retired := map[gamedata.CashProductKey]bool{}
for _, group := range groups {
d, err := gamedata.LoadInfiniteGachaForSchedules(root, "20260923193640", []uint64{group.id})
if err != nil && strings.Contains(err.Error(), "infinite cash product missing") {
// Early infinite draws retain placeholder gold/count=0 boxes in this
// version. They are historical timed products, not grantable purchases.
found := false
for _, p := range catalog.Products {
if p.Key.ProductID != group.gacha {
continue
}
found = true
if p.TimeLimitType == 0 || shop.IsAvailable(p.Key) {
t.Fatalf("retired infinite product %+v is purchasable without a valid preview design", p.Key)
}
retired[p.Key] = true
}
if !found {
t.Fatalf("infinite group=%d has no matching retired product", group.id)
}
continue
}
if err != nil || d == nil || d.Count == 0 {
t.Fatalf("infinite draw group=%d cannot grant confirmed preview: %v", group.id, err)
}
delegated[gamedata.CashProductKey{GroupID: d.ProductGroupID, ProductID: d.ProductID, SaleGroup: d.SaleGroup}] = true
}
cash, recharge, delegateCount, retiredCount := 0, 0, 0, 0
for _, p := range catalog.Products {
if p.PriceType != 1 {
continue
}
cash++
if p.Recharge {
recharge++
}
if delegated[p.Key] {
delegateCount++
continue
}
if retired[p.Key] {
retiredCount++
continue
}
if p.RandomBoxID == 0 {
t.Errorf("cash product %+v has no reward box", p.Key)
continue
}
for _, box := range []uint64{p.RandomBoxID, p.BonusRandomBoxID} {
if box == 0 {
continue
}
leaves, err := resolver.ResolveGranted([]gamedata.BattleReward{{Type: 9, ID: box, Count: 1}})
if err != nil || len(leaves) == 0 {
t.Errorf("cash product %+v box=%d cannot grant rewards: %v", p.Key, box, err)
continue
}
for _, r := range leaves {
if r.Type == 19 && entitlements.TicketTypes[r.ID] == 0 || r.Type == 62 && !entitlements.AvatarSets[r.ID] {
t.Errorf("cash product %+v has unknown entitlement %+v", p.Key, r)
}
}
}
// Exercise the same grant and debit modules used by CashShopBuy against
// isolated in-memory accounts; no real save or authenticated API is touched.
q, err := policy.Quote(p.Key, 1)
if err != nil {
t.Fatal(err)
}
var costs []gamedata.Reward
if q.Cost > 0 {
costs = []gamedata.Reward{{Type: q.ItemType, Count: q.Cost}}
}
rewards := []gamedata.Reward{{Type: 9, ID: p.RandomBoxID, Count: 1}}
if p.BonusRandomBoxID != 0 {
rewards = append(rewards, gamedata.Reward{Type: 9, ID: p.BonusRandomBoxID, Count: 1})
}
e, wallet := newEconomy()
bundle, err := e.Apply("installed-audit", costs, rewards)
if err != nil || len(bundle) == 0 {
t.Errorf("cash product %+v failed real debit/grant: %v", p.Key, err)
continue
}
balance := wallet.Snapshot()
replay, err := e.Apply("installed-audit", costs, rewards)
if err != nil || !bytes.Equal(replay, bundle) || wallet.Snapshot() != balance {
t.Errorf("cash product %+v replay repeated debit/grant: %v", p.Key, err)
}
}
if cash != 614 || recharge != 14 || delegateCount != 3 || retiredCount != 5 {
t.Fatalf("cash coverage changed: cash=%d recharge=%d delegated=%d retired=%d", cash, recharge, delegateCount, retiredCount)
}
bonus, err := gamedata.LoadCashBonusCatalog(root, "20260923193640")
if err != nil {
t.Fatal(err)
}
rows := bonus.Groups[1]
if len(rows) != 3 || rows[0].RequireCount != 2 || rows[0].Reward.Count != 2 || rows[1].RequireCount != 6 || rows[1].Reward.Count != 15 || rows[2].RequireCount != 8 || rows[2].Reward.Count != 30 {
t.Fatalf("cash bonus thresholds or rewards changed: %+v", rows)
}
t.Logf("validated %d cash rows: %d recharge variants, %d preview designs, %d unavailable historical draws; checked other reward boxes and typed ticket/skin entitlements", cash, recharge, delegateCount, retiredCount)
}