Files
bd2/go/internal/server/commerce/entitlements_test.go
T
Flechazo 924a42c32e fix(all): complete activity calendars, shop purchases and reward dispatch
Restore event and mini hubs, current shop and hunt calendars, and regular seasons; preserve all published windows. Implement story/quiz routes, native shop purchases, avatar sets, currencies, rank-change items and permanent buffs with rollback and reward coverage tests. Refresh client buff stats and update Cash Shop to 0.1.1.
2026-10-05 15:49:50 +08:00

144 lines
5.1 KiB
Go

package commerce
import (
"bd2server/internal/server/gamedata"
"bd2server/internal/server/player"
"bd2server/internal/server/stateio"
"bd2server/internal/server/wire"
"bytes"
"testing"
"time"
)
type entitlementGraph struct{ Calls int }
func (g *entitlementGraph) ResolveGranted(r []gamedata.BattleReward) ([]gamedata.BattleReward, error) {
g.Calls++
return r, nil
}
type entitlementBase struct {
Calls int
Rewards []gamedata.Reward
}
func (b *entitlementBase) Apply(_ string, _ []gamedata.Reward, r []gamedata.Reward) ([]byte, error) {
b.Calls++
b.Rewards = append(b.Rewards, r...)
return wire.AppendVarint(nil, 7, 1), nil
}
func entitlementFixture(t *testing.T) (*EntitlementEconomy, *player.Inventory, *entitlementBase, *entitlementGraph, *time.Time) {
t.Helper()
store := stateio.NewMemory()
items, err := player.OpenInventory(store, &player.Starter{Version: "2.35.10"})
if err != nil {
t.Fatal(err)
}
d := &gamedata.CashEntitlementDesign{AvatarSets: map[uint64]bool{10: true}, TicketTypes: map[uint64]uint64{38: 2, 99: 3}, Attendance: map[uint64][]gamedata.CashAttendanceReward{38: {{ID: 1, Reward: gamedata.BattleReward{Type: 3, Count: 1}}, {ID: 2, Reward: gamedata.BattleReward{Type: 3, Count: 5}}}, 99: {{ID: 1, Reward: gamedata.BattleReward{Type: 3, Count: 1}}, {ID: 2, Reward: gamedata.BattleReward{Type: 3, Count: 7}}}}, AttendanceTypes: map[uint64]uint64{38: 1}}
base := &entitlementBase{}
graph := &entitlementGraph{}
e, err := NewEntitlementEconomy(store, base, graph, items, d)
if err != nil {
t.Fatal(err)
}
now := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC)
e.SetClock(func() time.Time { return now }, 0)
return e, items, base, graph, &now
}
func TestEntitlementTypedSkinExpiryRetryAndRenewal(t *testing.T) {
e, items, base, graph, now := entitlementFixture(t)
rewards := []gamedata.Reward{{Type: 62, ID: 10, Count: 1}, {Type: 19, ID: 38, Count: 1}, {Type: 45, ID: 999, Count: 1}}
first, err := e.Apply("buy1", nil, rewards)
if err != nil {
t.Fatal(err)
}
if items.ContentTicketExpiry(38) != uint64(now.UnixMilli()+30*86400000) || len(base.Rewards) != 3 || base.Rewards[0].Type != 62 || base.Rewards[1].Type != 45 {
t.Fatal("expiry or prestige dispatch invalid")
}
for _, item := range items.All() {
if item.Type == 62 {
t.Fatal("commerce stored synthetic avatar set")
}
}
again, err := e.Apply("buy1", nil, rewards)
if err != nil || !bytes.Equal(first, again) || graph.Calls != 2 || base.Calls != 1 {
t.Fatal("retry rerolled or double granted")
}
if _, err = e.Apply("buy1", nil, nil); err == nil {
t.Fatal("identity reuse accepted")
}
if _, err = e.Apply("invalid", nil, []gamedata.Reward{{Type: 62, ID: 888, Count: 1}}); err == nil || base.Calls != 1 {
t.Fatal("unknown skin partially applied")
}
if _, err = e.Apply("buy2", nil, []gamedata.Reward{{Type: 19, ID: 38, Count: 1}}); err != nil {
t.Fatal(err)
}
if items.ContentTicketExpiry(38) != uint64(now.UnixMilli()+60*86400000) {
t.Fatal("renewal lost prior period")
}
merged, err := e.MergeAttendance(nil)
if err != nil {
t.Fatal(err)
}
if _, ok, _ := wire.Bytes(merged, 3); !ok {
t.Fatal("subscription info missing")
}
if _, ok, _ := wire.Bytes(merged, 7); !ok {
t.Fatal("monthly reward map missing")
}
}
func TestSubscriptionDailyClaimResetExpiryAndPermanentCompletion(t *testing.T) {
e, _, base, _, now := entitlementFixture(t)
_, err := e.Apply("buy", nil, []gamedata.Reward{{Type: 19, ID: 38, Count: 1}, {Type: 19, ID: 99, Count: 1}})
if err != nil {
t.Fatal(err)
}
calls := base.Calls
if _, err = e.ClaimSubscriptions("login"); err != nil || base.Calls != calls {
t.Fatal("purchase day double claim")
}
*now = now.Add(24 * time.Hour)
if _, err = e.ClaimSubscriptions("login2"); err != nil || base.Calls != calls+2 {
t.Fatalf("daily claims %v calls%d", err, base.Calls)
}
calls = base.Calls
if _, err = e.ClaimSubscriptions("retry"); err != nil || base.Calls != calls {
t.Fatal("daily retry grants again")
}
*now = now.Add(31 * 24 * time.Hour)
if _, err = e.ClaimSubscriptions("expired"); err != nil || base.Calls != calls {
t.Fatal("expired or completed claim")
}
merged, err := e.MergeAttendance(nil)
if err != nil {
t.Fatal(err)
}
if _, ok, _ := wire.Bytes(merged, 4); !ok {
t.Fatal("permanent attendance markers missing")
}
}
func TestSubscriptionFirstRewardAndLostReply(t *testing.T) {
e, _, base, _, now := entitlementFixture(t)
_, err := e.Apply("covered", nil, []gamedata.Reward{{Type: 19, ID: 38, Count: 1}, {Type: 3, Count: 1}})
if err != nil {
t.Fatal(err)
}
if len(base.Rewards) != 1 || base.Rewards[0].Count != 1 {
t.Fatalf("first-day reward duplicated: %+v", base.Rewards)
}
*now = now.Add(24 * time.Hour)
first, err := e.ClaimSubscriptions("same-session-request")
if err != nil || len(first) == 0 {
t.Fatalf("claim %x %v", first, err)
}
calls := base.Calls
replay, err := e.ClaimSubscriptions("same-session-request")
if err != nil || !bytes.Equal(first, replay) || base.Calls != calls {
t.Fatal("lost reply not replayed exactly")
}
fresh, err := e.ClaimSubscriptions("new-request-same-day")
if err != nil || len(fresh) != 0 || base.Calls != calls {
t.Fatal("new same-day request repeated award")
}
}